Update the EH search engine to fix issues with the current search features

This commit is contained in:
Jobobby04 2020-08-03 17:21:10 -04:00
parent fb19f6b860
commit 4f803494ff
5 changed files with 127 additions and 80 deletions

View File

@ -58,11 +58,11 @@ class LibraryCategoryAdapter(view: LibraryCategoryView) :
* *
* @param list the list to set. * @param list the list to set.
*/ */
suspend fun setItems(cScope: CoroutineScope, list: List<LibraryItem>) { suspend fun setItems(scope: CoroutineScope, list: List<LibraryItem>) {
// A copy of manga always unfiltered. // A copy of manga always unfiltered.
mangas = list.toList() mangas = list.toList()
performFilter(cScope) performFilter(scope)
} }
/** /**
@ -78,12 +78,12 @@ class LibraryCategoryAdapter(view: LibraryCategoryView) :
// Note that we cannot use FlexibleAdapter's built in filtering system as we cannot cancel it // Note that we cannot use FlexibleAdapter's built in filtering system as we cannot cancel it
// (well technically we can cancel it by invoking filterItems again but that doesn't work when // (well technically we can cancel it by invoking filterItems again but that doesn't work when
// we want to perform a no-op filter) // we want to perform a no-op filter)
suspend fun performFilter(cScope: CoroutineScope) { suspend fun performFilter(scope: CoroutineScope) {
lastFilterJob?.cancel() lastFilterJob?.cancel()
if (mangas.isNotEmpty() && searchText.isNotBlank()) { if (mangas.isNotEmpty() && searchText.isNotBlank()) {
val savedSearchText = searchText val savedSearchText = searchText
val job = cScope.launch(Dispatchers.IO) { val job = scope.launch(Dispatchers.IO) {
val newManga = try { val newManga = try {
// Prepare filter object // Prepare filter object
val parsedQuery = searchEngine.parseQuery(savedSearchText) val parsedQuery = searchEngine.parseQuery(savedSearchText)
@ -134,11 +134,11 @@ class LibraryCategoryAdapter(view: LibraryCategoryView) :
// Check if this manga even has metadata // Check if this manga even has metadata
if (mangaWithMetaIds.binarySearch(mangaId) < 0) { if (mangaWithMetaIds.binarySearch(mangaId) < 0) {
// No meta? Filter using title // No meta? Filter using title
item.filter(savedSearchText) item.filter(savedSearchText to true)
} else false } else item.filter(savedSearchText to false)
} else true } else true
} else { } else {
item.filter(savedSearchText) item.filter(savedSearchText to true)
} }
}.toList() }.toList()
} catch (e: Exception) { } catch (e: Exception) {

View File

@ -86,7 +86,7 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
// EXH --> // EXH -->
private var initialLoadHandle: LoadingHandle? = null private var initialLoadHandle: LoadingHandle? = null
lateinit var scope2: CoroutineScope private lateinit var supervisorScope: CoroutineScope
private fun newScope() = object : CoroutineScope { private fun newScope() = object : CoroutineScope {
override val coroutineContext = SupervisorJob() + Dispatchers.Main override val coroutineContext = SupervisorJob() + Dispatchers.Main
@ -150,7 +150,7 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
// SY <-- // SY <--
// EXH --> // EXH -->
scope2 = newScope() supervisorScope = newScope()
initialLoadHandle = controller.loaderManager.openProgressBar() initialLoadHandle = controller.loaderManager.openProgressBar()
// EXH <-- // EXH <--
@ -161,7 +161,7 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe { .subscribe {
// EXH --> // EXH -->
scope2.launch { supervisorScope.launch {
val handle = controller.loaderManager.openProgressBar() val handle = controller.loaderManager.openProgressBar()
try { try {
// EXH <-- // EXH <--
@ -177,7 +177,7 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
subscriptions += controller.libraryMangaRelay subscriptions += controller.libraryMangaRelay
.subscribe { .subscribe {
// EXH --> // EXH -->
scope2.launch { supervisorScope.launch {
try { try {
// EXH <-- // EXH <--
onNextLibraryManga(this, it) onNextLibraryManga(this, it)
@ -249,7 +249,7 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
fun unsubscribe() { fun unsubscribe() {
subscriptions.clear() subscriptions.clear()
// EXH --> // EXH -->
scope2.cancel() supervisorScope.cancel()
controller.loaderManager.closeProgressBar(initialLoadHandle) controller.loaderManager.closeProgressBar(initialLoadHandle)
// EXH <-- // EXH <--
} }

View File

@ -19,18 +19,26 @@ import eu.kanade.tachiyomi.data.preference.PreferenceValues.DisplayMode
import eu.kanade.tachiyomi.data.track.TrackManager import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.widget.AutofitRecyclerView import eu.kanade.tachiyomi.widget.AutofitRecyclerView
import exh.isNamespaceSource
import exh.metadata.metadata.base.RaisedTag
import exh.util.SourceTagsUtil.Companion.TAG_TYPE_EXCLUDE
import exh.util.SourceTagsUtil.Companion.getRaisedTags
import exh.util.SourceTagsUtil.Companion.parseTag
import kotlinx.android.synthetic.main.source_compact_grid_item.view.card import kotlinx.android.synthetic.main.source_compact_grid_item.view.card
import kotlinx.android.synthetic.main.source_compact_grid_item.view.gradient import kotlinx.android.synthetic.main.source_compact_grid_item.view.gradient
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
class LibraryItem(val manga: LibraryManga, private val libraryDisplayMode: Preference<DisplayMode>) : class LibraryItem(val manga: LibraryManga, private val libraryDisplayMode: Preference<DisplayMode>) :
AbstractFlexibleItem<LibraryHolder>(), IFilterable<String> { AbstractFlexibleItem<LibraryHolder>(), IFilterable<Pair<String, Boolean>> {
private val sourceManager: SourceManager = Injekt.get() private val sourceManager: SourceManager = Injekt.get()
// SY --> // SY -->
private val trackManager: TrackManager = Injekt.get() private val trackManager: TrackManager = Injekt.get()
private val db: DatabaseHelper = Injekt.get() private val db: DatabaseHelper = Injekt.get()
private val source by lazy {
sourceManager.get(manga.source)
}
// SY <-- // SY <--
var downloadCount = -1 var downloadCount = -1
@ -96,38 +104,13 @@ class LibraryItem(val manga: LibraryManga, private val libraryDisplayMode: Prefe
* @param constraint the query to apply. * @param constraint the query to apply.
* @return true if the manga should be included, false otherwise. * @return true if the manga should be included, false otherwise.
*/ */
override fun filter(constraint: String): Boolean { override fun filter(constraint: Pair<String, Boolean>): Boolean {
return manga.title.contains(constraint, true) || return manga.title.contains(constraint.first, true) ||
(manga.author?.contains(constraint, true) ?: false) || (manga.author?.contains(constraint.first, true) ?: false) ||
(manga.artist?.contains(constraint, true) ?: false) || (manga.artist?.contains(constraint.first, true) ?: false) ||
sourceManager.getOrStub(manga.source).name.contains(constraint, true) || (source?.name?.contains(constraint.first, true) ?: false) ||
(Injekt.get<TrackManager>().hasLoggedServices() && filterTracks(constraint, db.getTracks(manga).executeAsBlocking())) || (Injekt.get<TrackManager>().hasLoggedServices() && filterTracks(constraint.first, db.getTracks(manga).executeAsBlocking())) ||
if (constraint.contains(" ") || constraint.contains("\"")) { constraint.second && ehContainsGenre(constraint.first)
val genres = manga.genre?.split(", ")?.map {
it.drop(it.indexOfFirst { it == ':' } + 1).toLowerCase().trim() // tachiEH tag namespaces
}
var clean_constraint = ""
var ignorespace = false
for (i in constraint.trim().toLowerCase()) {
if (i == ' ') {
if (!ignorespace) {
clean_constraint = clean_constraint + ","
} else {
clean_constraint = clean_constraint + " "
}
} else if (i == '"') {
ignorespace = !ignorespace
} else {
clean_constraint = clean_constraint + Character.toString(i)
}
}
clean_constraint.split(",").all { containsGenre(it.trim(), genres) }
} else containsGenre(
constraint,
manga.genre?.split(", ")?.map {
it.drop(it.indexOfFirst { it == ':' } + 1).toLowerCase().trim() // tachiEH tag namespaces
}
)
} }
private fun filterTracks(constraint: String, tracks: List<Track>): Boolean { private fun filterTracks(constraint: String, tracks: List<Track>): Boolean {
@ -141,6 +124,54 @@ class LibraryItem(val manga: LibraryManga, private val libraryDisplayMode: Prefe
return@any false return@any false
} }
} }
private fun ehContainsGenre(constraint: String): Boolean {
val genres = manga.getGenres()
val raisedTags = if (source?.isNamespaceSource() == true) {
manga.getRaisedTags(genres)
} else null
return if (constraint.contains(" ") || constraint.contains("\"")) {
var cleanConstraint = ""
var ignoreSpace = false
for (i in constraint.trim().toLowerCase()) {
when (i) {
' ' -> {
cleanConstraint = if (!ignoreSpace) {
"$cleanConstraint,"
} else {
"$cleanConstraint "
}
}
'"' -> {
ignoreSpace = !ignoreSpace
}
else -> {
cleanConstraint += i.toString()
}
}
}
cleanConstraint.split(",").all {
if (raisedTags == null) containsGenre(it.trim(), genres) else containsRaisedGenre(
parseTag(it.trim()), raisedTags
)
}
} else if (raisedTags == null) {
containsGenre(constraint, genres)
} else {
containsRaisedGenre(parseTag(constraint), raisedTags)
}
}
private fun containsRaisedGenre(tag: RaisedTag, genres: List<RaisedTag>): Boolean {
val genre = genres.find {
(it.namespace?.toLowerCase() == tag.namespace?.toLowerCase() && it.name.toLowerCase() == tag.name.toLowerCase())
}
return if (tag.type == TAG_TYPE_EXCLUDE) {
genre == null
} else {
genre != null
}
}
// SY <-- // SY <--
private fun containsGenre(tag: String, genres: List<String>?): Boolean { private fun containsGenre(tag: String, genres: List<String>?): Boolean {

View File

@ -7,7 +7,7 @@ import exh.metadata.sql.tables.SearchTitleTable
class SearchEngine { class SearchEngine {
private val queryCache = mutableMapOf<String, List<QueryComponent>>() private val queryCache = mutableMapOf<String, List<QueryComponent>>()
fun textToSubQueries( private fun textToSubQueries(
namespace: String?, namespace: String?,
component: Text? component: Text?
): Pair<String, List<String>>? { ): Pair<String, List<String>>? {
@ -20,12 +20,13 @@ class SearchEngine {
} }
val componentTagQuery = maybeLenientComponent?.let { val componentTagQuery = maybeLenientComponent?.let {
val params = mutableListOf<String>() val params = mutableListOf<String>()
it.map { q -> it.joinToString(separator = " OR ", prefix = "(", postfix = ")") { q ->
params += q params += q
"${SearchTagTable.TABLE}.${SearchTagTable.COL_NAME} LIKE ?" "${SearchTagTable.TABLE}.${SearchTagTable.COL_NAME} LIKE ?"
}.joinToString(separator = " OR ", prefix = "(", postfix = ")") to params } to params
} }
return if (namespace != null) { return when {
namespace != null -> {
var query = var query =
""" """
(SELECT ${SearchTagTable.COL_MANGA_ID} AS $COL_MANGA_ID FROM ${SearchTagTable.TABLE} (SELECT ${SearchTagTable.COL_MANGA_ID} AS $COL_MANGA_ID FROM ${SearchTagTable.TABLE}
@ -39,7 +40,8 @@ class SearchEngine {
} }
"$query)" to params "$query)" to params
} else if (component != null) { }
component != null -> {
// Match title + tags // Match title + tags
val tagQuery = val tagQuery =
""" """
@ -55,7 +57,9 @@ class SearchEngine {
"(${tagQuery.first} UNION ${titleQuery.first})".trimIndent() to "(${tagQuery.first} UNION ${titleQuery.first})".trimIndent() to
(tagQuery.second + titleQuery.second) (tagQuery.second + titleQuery.second)
} else null }
else -> null
}
} }
fun queryToSql(q: List<QueryComponent>): Pair<String, List<String>> { fun queryToSql(q: List<QueryComponent>): Pair<String, List<String>> {
@ -158,7 +162,7 @@ class SearchEngine {
} }
} }
for (char in query.toLowerCase()) { query.toLowerCase().forEach { char ->
if (char == '"') { if (char == '"') {
inQuotes = !inQuotes inQuotes = !inQuotes
} else if (enableWildcard && (char == '?' || char == '_')) { } else if (enableWildcard && (char == '?' || char == '_')) {
@ -167,7 +171,7 @@ class SearchEngine {
} else if (enableWildcard && (char == '*' || char == '%')) { } else if (enableWildcard && (char == '*' || char == '%')) {
flushText() flushText()
queuedText.add(MultiWildcard(char.toString())) queuedText.add(MultiWildcard(char.toString()))
} else if (char == '-') { } else if (char == '-' && !inQuotes && (queuedRawText.isBlank() || queuedRawText.last() == ' ')) {
nextIsExcluded = true nextIsExcluded = true
} else if (char == '$') { } else if (char == '$') {
nextIsExact = true nextIsExact = true

View File

@ -48,9 +48,21 @@ class SourceTagsUtil {
"$namespace:$tag" "$namespace:$tag"
} }
companion object { companion object {
fun Manga.getRaisedTags(): List<RaisedTag>? = this.getGenres()?.map { parseTag(it) } fun Manga.getRaisedTags(genres: List<String>? = null): List<RaisedTag>? = (genres ?: this.getGenres())?.map { parseTag(it) }
fun parseTag(tag: String) = RaisedTag(tag.substringBefore(':').trimOrNull(), (tag.substringAfter(':').trimOrNull() ?: tag), TAG_TYPE_DEFAULT) fun parseTag(tag: String) = RaisedTag(
(
if (tag.startsWith("-")) {
tag.substringAfter("-")
} else {
tag
}
).substringBefore(':', missingDelimiterValue = "").trimOrNull(),
tag.substringAfter(':', missingDelimiterValue = tag).trim(),
if (tag.startsWith("-")) TAG_TYPE_EXCLUDE else TAG_TYPE_DEFAULT
)
const val TAG_TYPE_EXCLUDE = 69 // why not
const val DOUJINSHI_COLOR = "#f44336" const val DOUJINSHI_COLOR = "#f44336"
const val MANGA_COLOR = "#ff9800" const val MANGA_COLOR = "#ff9800"