feat(sanitizer): Sanitize manga titles for special characters before searching (#956)

This commit is contained in:
Cuong-Tran 2025-05-28 17:37:13 +07:00 committed by GitHub
parent b1773eb08f
commit aaa1d855ff
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 75 additions and 10 deletions

View file

@ -32,6 +32,7 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import mihon.domain.manga.model.toDomainManga import mihon.domain.manga.model.toDomainManga
import tachiyomi.core.common.util.QuerySanitizer.sanitize
import tachiyomi.core.common.util.lang.launchIO import tachiyomi.core.common.util.lang.launchIO
import tachiyomi.core.common.util.lang.launchNonCancellable import tachiyomi.core.common.util.lang.launchNonCancellable
import tachiyomi.core.common.util.lang.withIOContext import tachiyomi.core.common.util.lang.withIOContext
@ -287,7 +288,7 @@ open class FeedScreenModel(
} else { } else {
itemUI.source.getSearchManga( itemUI.source.getSearchManga(
1, 1,
itemUI.savedSearch.query.orEmpty(), itemUI.savedSearch.query?.sanitize().orEmpty(),
getFilterList(itemUI.savedSearch, itemUI.source), getFilterList(itemUI.savedSearch, itemUI.source),
) )
} }

View file

@ -42,6 +42,7 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import mihon.domain.manga.model.toDomainManga import mihon.domain.manga.model.toDomainManga
import tachiyomi.core.common.util.QuerySanitizer.sanitize
import tachiyomi.core.common.util.lang.launchIO import tachiyomi.core.common.util.lang.launchIO
import tachiyomi.core.common.util.lang.launchNonCancellable import tachiyomi.core.common.util.lang.launchNonCancellable
import tachiyomi.core.common.util.lang.withIOContext import tachiyomi.core.common.util.lang.withIOContext
@ -238,7 +239,7 @@ open class SourceFeedScreenModel(
is SourceFeedUI.Latest -> source.getLatestUpdates(1) is SourceFeedUI.Latest -> source.getLatestUpdates(1)
is SourceFeedUI.SourceSavedSearch -> source.getSearchManga( is SourceFeedUI.SourceSavedSearch -> source.getSearchManga(
page = 1, page = 1,
query = sourceFeed.savedSearch.query.orEmpty(), query = sourceFeed.savedSearch.query?.sanitize().orEmpty(),
filters = getFilterList(sourceFeed.savedSearch, source), filters = getFilterList(sourceFeed.savedSearch, source),
) )
} }

View file

@ -19,13 +19,13 @@ import kotlinx.coroutines.asCoroutineDispatcher
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.isActive import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import mihon.domain.manga.model.toDomainManga import mihon.domain.manga.model.toDomainManga
import tachiyomi.core.common.preference.toggle import tachiyomi.core.common.preference.toggle
import tachiyomi.core.common.util.QuerySanitizer.sanitize
import tachiyomi.domain.manga.interactor.GetManga import tachiyomi.domain.manga.interactor.GetManga
import tachiyomi.domain.manga.interactor.NetworkToLocalManga import tachiyomi.domain.manga.interactor.NetworkToLocalManga
import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.manga.model.Manga
@ -186,7 +186,7 @@ abstract class SearchScreenModel(
try { try {
val page = withContext(coroutineDispatcher) { val page = withContext(coroutineDispatcher) {
source.getSearchManga(1, query, source.getFilterList()) source.getSearchManga(1, query.sanitize(), source.getFilterList())
} }
val titles = page.mangas val titles = page.mangas

View file

@ -66,6 +66,7 @@ import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import logcat.LogPriority import logcat.LogPriority
import tachiyomi.core.common.i18n.stringResource import tachiyomi.core.common.i18n.stringResource
import tachiyomi.core.common.util.QuerySanitizer.sanitize
import tachiyomi.core.common.util.lang.launchNonCancellable import tachiyomi.core.common.util.lang.launchNonCancellable
import tachiyomi.core.common.util.lang.withIOContext import tachiyomi.core.common.util.lang.withIOContext
import tachiyomi.core.common.util.lang.withUIContext import tachiyomi.core.common.util.lang.withUIContext
@ -714,7 +715,7 @@ data class TrackerSearchScreen(
val result = withIOContext { val result = withIOContext {
try { try {
val results = tracker.search(query) val results = tracker.search(query.sanitize())
Result.success(results) Result.success(results)
} catch (e: Throwable) { } catch (e: Throwable) {
Result.failure(e) Result.failure(e)

View file

@ -8,6 +8,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.supervisorScope import kotlinx.coroutines.supervisorScope
import mihon.domain.manga.model.toDomainManga import mihon.domain.manga.model.toDomainManga
import tachiyomi.core.common.util.QuerySanitizer.sanitize
import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.manga.model.Manga
import java.util.Locale import java.util.Locale
@ -30,7 +31,7 @@ class SmartSearchEngine(
query query
} }
val searchResults = source.getSearchManga(1, builtQuery, FilterList()) val searchResults = source.getSearchManga(1, builtQuery.sanitize(), FilterList())
searchResults.mangas.map { searchResults.mangas.map {
val cleanedMangaTitle = cleanSmartSearchTitle(it.originalTitle) val cleanedMangaTitle = cleanSmartSearchTitle(it.originalTitle)
@ -53,7 +54,7 @@ class SmartSearchEngine(
} else { } else {
title title
} }
val searchResults = source.getSearchManga(1, searchQuery, FilterList()) val searchResults = source.getSearchManga(1, searchQuery.sanitize(), FilterList())
if (searchResults.mangas.size == 1) { if (searchResults.mangas.size == 1) {
return@supervisorScope listOf(SearchEntry(searchResults.mangas.first(), 0.0)) return@supervisorScope listOf(SearchEntry(searchResults.mangas.first(), 0.0))

View file

@ -0,0 +1,58 @@
package tachiyomi.core.common.util
object QuerySanitizer {
fun String.sanitize(removePrefix: String = ""): String {
return trim()
.removePrefix(removePrefix)
.trim(*CHARACTER_TRIM_CHARS)
.replaceSpecialChar()
}
// Replaces special characters with their standard equivalents.
private fun String.replaceSpecialChar(): String {
return replace("", "'")
.replace("", "'")
.replace("", "\"")
.replace("", "\"")
.replace("", "-")
.replace("", "-")
.replace("", "...")
}
private val CHARACTER_TRIM_CHARS = arrayOf(
// Whitespace
' ',
'\u0009',
'\u000A',
'\u000B',
'\u000C',
'\u000D',
'\u0020',
'\u0085',
'\u00A0',
'\u1680',
'\u2000',
'\u2001',
'\u2002',
'\u2003',
'\u2004',
'\u2005',
'\u2006',
'\u2007',
'\u2008',
'\u2009',
'\u200A',
'\u2028',
'\u2029',
'\u202F',
'\u205F',
'\u3000',
// Separators
'-',
'_',
',',
':',
).toCharArray()
}

View file

@ -6,6 +6,7 @@ import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.MetadataMangasPage import eu.kanade.tachiyomi.source.model.MetadataMangasPage
import exh.metadata.metadata.RaisedSearchMetadata import exh.metadata.metadata.RaisedSearchMetadata
import mihon.domain.manga.model.toDomainManga import mihon.domain.manga.model.toDomainManga
import tachiyomi.core.common.util.QuerySanitizer.sanitize
import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.manga.model.Manga
abstract class EHentaiPagingSource( abstract class EHentaiPagingSource(
@ -40,7 +41,7 @@ class EHentaiSearchPagingSource(
val filters: FilterList, val filters: FilterList,
) : EHentaiPagingSource(source) { ) : EHentaiPagingSource(source) {
override suspend fun requestNextPage(currentPage: Int): MangasPage { override suspend fun requestNextPage(currentPage: Int): MangasPage {
return source.getSearchManga(currentPage, query, filters) return source.getSearchManga(currentPage, query.sanitize(), filters)
} }
} }

View file

@ -7,6 +7,7 @@ import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.MetadataMangasPage import eu.kanade.tachiyomi.source.model.MetadataMangasPage
import exh.metadata.metadata.RaisedSearchMetadata import exh.metadata.metadata.RaisedSearchMetadata
import mihon.domain.manga.model.toDomainManga import mihon.domain.manga.model.toDomainManga
import tachiyomi.core.common.util.QuerySanitizer.sanitize
import tachiyomi.core.common.util.lang.withIOContext import tachiyomi.core.common.util.lang.withIOContext
import tachiyomi.domain.manga.interactor.NetworkToLocalManga import tachiyomi.domain.manga.interactor.NetworkToLocalManga
import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.manga.model.Manga
@ -20,7 +21,7 @@ class SourceSearchPagingSource(
private val filters: FilterList, private val filters: FilterList,
) : BaseSourcePagingSource(source) { ) : BaseSourcePagingSource(source) {
override suspend fun requestNextPage(currentPage: Int): MangasPage { override suspend fun requestNextPage(currentPage: Int): MangasPage {
return source?.getSearchManga(currentPage, query, filters) return source?.getSearchManga(currentPage, query.sanitize(), filters)
// KMK --> // KMK -->
?: MangasPage(emptyList(), false) ?: MangasPage(emptyList(), false)
// KMK <-- // KMK <--

View file

@ -8,6 +8,7 @@ import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.supervisorScope import kotlinx.coroutines.supervisorScope
import logcat.LogPriority import logcat.LogPriority
import tachiyomi.core.common.util.QuerySanitizer.sanitize
import tachiyomi.core.common.util.system.logcat import tachiyomi.core.common.util.system.logcat
/** /**
@ -187,7 +188,7 @@ interface CatalogueSource : Source {
words.map { keyword -> words.map { keyword ->
launch { launch {
runCatching { runCatching {
getSearchManga(1, keyword, FilterList()).mangas getSearchManga(1, keyword.sanitize(), FilterList()).mangas
} }
.onSuccess { if (it.isNotEmpty()) pushResults(Pair(keyword, it), false) } .onSuccess { if (it.isNotEmpty()) pushResults(Pair(keyword, it), false) }
.onFailure { e -> .onFailure { e ->