From 4f803494ff188a2a748bbda90f19d910d9ddfaa8 Mon Sep 17 00:00:00 2001 From: Jobobby04 Date: Mon, 3 Aug 2020 17:21:10 -0400 Subject: [PATCH] Update the EH search engine to fix issues with the current search features --- .../ui/library/LibraryCategoryAdapter.kt | 14 +-- .../ui/library/LibraryCategoryView.kt | 10 +- .../tachiyomi/ui/library/LibraryItem.kt | 97 ++++++++++++------- app/src/main/java/exh/search/SearchEngine.kt | 70 ++++++------- app/src/main/java/exh/util/SourceTagsUtil.kt | 16 ++- 5 files changed, 127 insertions(+), 80 deletions(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryAdapter.kt index 1bd4a1cde..d11bb512c 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryAdapter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryAdapter.kt @@ -58,11 +58,11 @@ class LibraryCategoryAdapter(view: LibraryCategoryView) : * * @param list the list to set. */ - suspend fun setItems(cScope: CoroutineScope, list: List) { + suspend fun setItems(scope: CoroutineScope, list: List) { // A copy of manga always unfiltered. 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 // (well technically we can cancel it by invoking filterItems again but that doesn't work when // we want to perform a no-op filter) - suspend fun performFilter(cScope: CoroutineScope) { + suspend fun performFilter(scope: CoroutineScope) { lastFilterJob?.cancel() if (mangas.isNotEmpty() && searchText.isNotBlank()) { val savedSearchText = searchText - val job = cScope.launch(Dispatchers.IO) { + val job = scope.launch(Dispatchers.IO) { val newManga = try { // Prepare filter object val parsedQuery = searchEngine.parseQuery(savedSearchText) @@ -134,11 +134,11 @@ class LibraryCategoryAdapter(view: LibraryCategoryView) : // Check if this manga even has metadata if (mangaWithMetaIds.binarySearch(mangaId) < 0) { // No meta? Filter using title - item.filter(savedSearchText) - } else false + item.filter(savedSearchText to true) + } else item.filter(savedSearchText to false) } else true } else { - item.filter(savedSearchText) + item.filter(savedSearchText to true) } }.toList() } catch (e: Exception) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryView.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryView.kt index f5a03864c..3289ad3e8 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryView.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryView.kt @@ -86,7 +86,7 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att // EXH --> private var initialLoadHandle: LoadingHandle? = null - lateinit var scope2: CoroutineScope + private lateinit var supervisorScope: CoroutineScope private fun newScope() = object : CoroutineScope { override val coroutineContext = SupervisorJob() + Dispatchers.Main @@ -150,7 +150,7 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att // SY <-- // EXH --> - scope2 = newScope() + supervisorScope = newScope() initialLoadHandle = controller.loaderManager.openProgressBar() // EXH <-- @@ -161,7 +161,7 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att .observeOn(AndroidSchedulers.mainThread()) .subscribe { // EXH --> - scope2.launch { + supervisorScope.launch { val handle = controller.loaderManager.openProgressBar() try { // EXH <-- @@ -177,7 +177,7 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att subscriptions += controller.libraryMangaRelay .subscribe { // EXH --> - scope2.launch { + supervisorScope.launch { try { // EXH <-- onNextLibraryManga(this, it) @@ -249,7 +249,7 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att fun unsubscribe() { subscriptions.clear() // EXH --> - scope2.cancel() + supervisorScope.cancel() controller.loaderManager.closeProgressBar(initialLoadHandle) // EXH <-- } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryItem.kt index fbef84813..8f89b79d5 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryItem.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryItem.kt @@ -19,18 +19,26 @@ import eu.kanade.tachiyomi.data.preference.PreferenceValues.DisplayMode import eu.kanade.tachiyomi.data.track.TrackManager import eu.kanade.tachiyomi.source.SourceManager 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.gradient import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get class LibraryItem(val manga: LibraryManga, private val libraryDisplayMode: Preference) : - AbstractFlexibleItem(), IFilterable { + AbstractFlexibleItem(), IFilterable> { private val sourceManager: SourceManager = Injekt.get() // SY --> private val trackManager: TrackManager = Injekt.get() private val db: DatabaseHelper = Injekt.get() + private val source by lazy { + sourceManager.get(manga.source) + } // SY <-- var downloadCount = -1 @@ -96,38 +104,13 @@ class LibraryItem(val manga: LibraryManga, private val libraryDisplayMode: Prefe * @param constraint the query to apply. * @return true if the manga should be included, false otherwise. */ - override fun filter(constraint: String): Boolean { - return manga.title.contains(constraint, true) || - (manga.author?.contains(constraint, true) ?: false) || - (manga.artist?.contains(constraint, true) ?: false) || - sourceManager.getOrStub(manga.source).name.contains(constraint, true) || - (Injekt.get().hasLoggedServices() && filterTracks(constraint, db.getTracks(manga).executeAsBlocking())) || - if (constraint.contains(" ") || constraint.contains("\"")) { - 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 - } - ) + override fun filter(constraint: Pair): Boolean { + return manga.title.contains(constraint.first, true) || + (manga.author?.contains(constraint.first, true) ?: false) || + (manga.artist?.contains(constraint.first, true) ?: false) || + (source?.name?.contains(constraint.first, true) ?: false) || + (Injekt.get().hasLoggedServices() && filterTracks(constraint.first, db.getTracks(manga).executeAsBlocking())) || + constraint.second && ehContainsGenre(constraint.first) } private fun filterTracks(constraint: String, tracks: List): Boolean { @@ -141,6 +124,54 @@ class LibraryItem(val manga: LibraryManga, private val libraryDisplayMode: Prefe 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): 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 <-- private fun containsGenre(tag: String, genres: List?): Boolean { diff --git a/app/src/main/java/exh/search/SearchEngine.kt b/app/src/main/java/exh/search/SearchEngine.kt index ff723676f..161af8806 100755 --- a/app/src/main/java/exh/search/SearchEngine.kt +++ b/app/src/main/java/exh/search/SearchEngine.kt @@ -7,7 +7,7 @@ import exh.metadata.sql.tables.SearchTitleTable class SearchEngine { private val queryCache = mutableMapOf>() - fun textToSubQueries( + private fun textToSubQueries( namespace: String?, component: Text? ): Pair>? { @@ -20,42 +20,46 @@ class SearchEngine { } val componentTagQuery = maybeLenientComponent?.let { val params = mutableListOf() - it.map { q -> + it.joinToString(separator = " OR ", prefix = "(", postfix = ")") { q -> params += q "${SearchTagTable.TABLE}.${SearchTagTable.COL_NAME} LIKE ?" - }.joinToString(separator = " OR ", prefix = "(", postfix = ")") to params + } to params } - return if (namespace != null) { - var query = - """ - (SELECT ${SearchTagTable.COL_MANGA_ID} AS $COL_MANGA_ID FROM ${SearchTagTable.TABLE} - WHERE ${SearchTagTable.COL_NAMESPACE} IS NOT NULL - AND ${SearchTagTable.COL_NAMESPACE} LIKE ? - """.trimIndent() - val params = mutableListOf(escapeLike(namespace)) - if (componentTagQuery != null) { - query += "\n AND ${componentTagQuery.first}" - params += componentTagQuery.second + return when { + namespace != null -> { + var query = + """ + (SELECT ${SearchTagTable.COL_MANGA_ID} AS $COL_MANGA_ID FROM ${SearchTagTable.TABLE} + WHERE ${SearchTagTable.COL_NAMESPACE} IS NOT NULL + AND ${SearchTagTable.COL_NAMESPACE} LIKE ? + """.trimIndent() + val params = mutableListOf(escapeLike(namespace)) + if (componentTagQuery != null) { + query += "\n AND ${componentTagQuery.first}" + params += componentTagQuery.second + } + + "$query)" to params } + component != null -> { + // Match title + tags + val tagQuery = + """ + SELECT ${SearchTagTable.COL_MANGA_ID} AS $COL_MANGA_ID FROM ${SearchTagTable.TABLE} + WHERE ${componentTagQuery!!.first} + """.trimIndent() to componentTagQuery.second - "$query)" to params - } else if (component != null) { - // Match title + tags - val tagQuery = - """ - SELECT ${SearchTagTable.COL_MANGA_ID} AS $COL_MANGA_ID FROM ${SearchTagTable.TABLE} - WHERE ${componentTagQuery!!.first} - """.trimIndent() to componentTagQuery.second + val titleQuery = + """ + SELECT ${SearchTitleTable.COL_MANGA_ID} AS $COL_MANGA_ID FROM ${SearchTitleTable.TABLE} + WHERE ${SearchTitleTable.COL_TITLE} LIKE ? + """.trimIndent() to listOf(component.asLenientTitleQuery()) - val titleQuery = - """ - SELECT ${SearchTitleTable.COL_MANGA_ID} AS $COL_MANGA_ID FROM ${SearchTitleTable.TABLE} - WHERE ${SearchTitleTable.COL_TITLE} LIKE ? - """.trimIndent() to listOf(component.asLenientTitleQuery()) - - "(${tagQuery.first} UNION ${titleQuery.first})".trimIndent() to - (tagQuery.second + titleQuery.second) - } else null + "(${tagQuery.first} UNION ${titleQuery.first})".trimIndent() to + (tagQuery.second + titleQuery.second) + } + else -> null + } } fun queryToSql(q: List): Pair> { @@ -158,7 +162,7 @@ class SearchEngine { } } - for (char in query.toLowerCase()) { + query.toLowerCase().forEach { char -> if (char == '"') { inQuotes = !inQuotes } else if (enableWildcard && (char == '?' || char == '_')) { @@ -167,7 +171,7 @@ class SearchEngine { } else if (enableWildcard && (char == '*' || char == '%')) { flushText() queuedText.add(MultiWildcard(char.toString())) - } else if (char == '-') { + } else if (char == '-' && !inQuotes && (queuedRawText.isBlank() || queuedRawText.last() == ' ')) { nextIsExcluded = true } else if (char == '$') { nextIsExact = true diff --git a/app/src/main/java/exh/util/SourceTagsUtil.kt b/app/src/main/java/exh/util/SourceTagsUtil.kt index 48c5dc639..1d0bba9b3 100644 --- a/app/src/main/java/exh/util/SourceTagsUtil.kt +++ b/app/src/main/java/exh/util/SourceTagsUtil.kt @@ -48,9 +48,21 @@ class SourceTagsUtil { "$namespace:$tag" } companion object { - fun Manga.getRaisedTags(): List? = this.getGenres()?.map { parseTag(it) } + fun Manga.getRaisedTags(genres: List? = null): List? = (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 MANGA_COLOR = "#ff9800"