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.
*/
suspend fun setItems(cScope: CoroutineScope, list: List<LibraryItem>) {
suspend fun setItems(scope: CoroutineScope, list: List<LibraryItem>) {
// 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) {

View File

@ -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 <--
}

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.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<DisplayMode>) :
AbstractFlexibleItem<LibraryHolder>(), IFilterable<String> {
AbstractFlexibleItem<LibraryHolder>(), IFilterable<Pair<String, Boolean>> {
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<TrackManager>().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<String, Boolean>): 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<TrackManager>().hasLoggedServices() && filterTracks(constraint.first, db.getTracks(manga).executeAsBlocking())) ||
constraint.second && ehContainsGenre(constraint.first)
}
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
}
}
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 <--
private fun containsGenre(tag: String, genres: List<String>?): Boolean {

View File

@ -7,7 +7,7 @@ import exh.metadata.sql.tables.SearchTitleTable
class SearchEngine {
private val queryCache = mutableMapOf<String, List<QueryComponent>>()
fun textToSubQueries(
private fun textToSubQueries(
namespace: String?,
component: Text?
): Pair<String, List<String>>? {
@ -20,42 +20,46 @@ class SearchEngine {
}
val componentTagQuery = maybeLenientComponent?.let {
val params = mutableListOf<String>()
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<QueryComponent>): Pair<String, List<String>> {
@ -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

View File

@ -48,9 +48,21 @@ class SourceTagsUtil {
"$namespace:$tag"
}
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 MANGA_COLOR = "#ff9800"