From aaa1d855ffd3214008fb8291ffe98def925a77ab Mon Sep 17 00:00:00 2001 From: Cuong-Tran Date: Wed, 28 May 2025 17:37:13 +0700 Subject: [PATCH] feat(sanitizer): Sanitize manga titles for special characters before searching (#956) --- .../ui/browse/feed/FeedScreenModel.kt | 3 +- .../source/feed/SourceFeedScreenModel.kt | 3 +- .../source/globalsearch/SearchScreenModel.kt | 4 +- .../ui/manga/track/TrackInfoDialog.kt | 3 +- .../java/exh/smartsearch/SmartSearchEngine.kt | 5 +- .../core/common/util/QuerySanitizer.kt | 58 +++++++++++++++++++ .../data/source/EHentaiPagingSource.kt | 3 +- .../data/source/SourcePagingSource.kt | 3 +- .../tachiyomi/source/CatalogueSource.kt | 3 +- 9 files changed, 75 insertions(+), 10 deletions(-) create mode 100644 core/common/src/main/kotlin/tachiyomi/core/common/util/QuerySanitizer.kt diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedScreenModel.kt index 40ac6669a..c97e9f807 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedScreenModel.kt @@ -32,6 +32,7 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import kotlinx.serialization.json.Json 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.launchNonCancellable import tachiyomi.core.common.util.lang.withIOContext @@ -287,7 +288,7 @@ open class FeedScreenModel( } else { itemUI.source.getSearchManga( 1, - itemUI.savedSearch.query.orEmpty(), + itemUI.savedSearch.query?.sanitize().orEmpty(), getFilterList(itemUI.savedSearch, itemUI.source), ) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/feed/SourceFeedScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/feed/SourceFeedScreenModel.kt index 598e495f2..1b3754769 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/feed/SourceFeedScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/feed/SourceFeedScreenModel.kt @@ -42,6 +42,7 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import kotlinx.serialization.json.Json 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.launchNonCancellable import tachiyomi.core.common.util.lang.withIOContext @@ -238,7 +239,7 @@ open class SourceFeedScreenModel( is SourceFeedUI.Latest -> source.getLatestUpdates(1) is SourceFeedUI.SourceSavedSearch -> source.getSearchManga( page = 1, - query = sourceFeed.savedSearch.query.orEmpty(), + query = sourceFeed.savedSearch.query?.sanitize().orEmpty(), filters = getFilterList(sourceFeed.savedSearch, source), ) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/SearchScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/SearchScreenModel.kt index 39694034a..0781f0c48 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/SearchScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/SearchScreenModel.kt @@ -19,13 +19,13 @@ import kotlinx.coroutines.asCoroutineDispatcher import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.update import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import mihon.domain.manga.model.toDomainManga import tachiyomi.core.common.preference.toggle +import tachiyomi.core.common.util.QuerySanitizer.sanitize import tachiyomi.domain.manga.interactor.GetManga import tachiyomi.domain.manga.interactor.NetworkToLocalManga import tachiyomi.domain.manga.model.Manga @@ -186,7 +186,7 @@ abstract class SearchScreenModel( try { val page = withContext(coroutineDispatcher) { - source.getSearchManga(1, query, source.getFilterList()) + source.getSearchManga(1, query.sanitize(), source.getFilterList()) } val titles = page.mangas diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackInfoDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackInfoDialog.kt index 06a4c8378..594222c70 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackInfoDialog.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackInfoDialog.kt @@ -66,6 +66,7 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import logcat.LogPriority 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.withIOContext import tachiyomi.core.common.util.lang.withUIContext @@ -714,7 +715,7 @@ data class TrackerSearchScreen( val result = withIOContext { try { - val results = tracker.search(query) + val results = tracker.search(query.sanitize()) Result.success(results) } catch (e: Throwable) { Result.failure(e) diff --git a/app/src/main/java/exh/smartsearch/SmartSearchEngine.kt b/app/src/main/java/exh/smartsearch/SmartSearchEngine.kt index 0bb6844e5..c56038f73 100644 --- a/app/src/main/java/exh/smartsearch/SmartSearchEngine.kt +++ b/app/src/main/java/exh/smartsearch/SmartSearchEngine.kt @@ -8,6 +8,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async import kotlinx.coroutines.supervisorScope import mihon.domain.manga.model.toDomainManga +import tachiyomi.core.common.util.QuerySanitizer.sanitize import tachiyomi.domain.manga.model.Manga import java.util.Locale @@ -30,7 +31,7 @@ class SmartSearchEngine( query } - val searchResults = source.getSearchManga(1, builtQuery, FilterList()) + val searchResults = source.getSearchManga(1, builtQuery.sanitize(), FilterList()) searchResults.mangas.map { val cleanedMangaTitle = cleanSmartSearchTitle(it.originalTitle) @@ -53,7 +54,7 @@ class SmartSearchEngine( } else { title } - val searchResults = source.getSearchManga(1, searchQuery, FilterList()) + val searchResults = source.getSearchManga(1, searchQuery.sanitize(), FilterList()) if (searchResults.mangas.size == 1) { return@supervisorScope listOf(SearchEntry(searchResults.mangas.first(), 0.0)) diff --git a/core/common/src/main/kotlin/tachiyomi/core/common/util/QuerySanitizer.kt b/core/common/src/main/kotlin/tachiyomi/core/common/util/QuerySanitizer.kt new file mode 100644 index 000000000..e21553c1b --- /dev/null +++ b/core/common/src/main/kotlin/tachiyomi/core/common/util/QuerySanitizer.kt @@ -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() +} diff --git a/data/src/main/java/tachiyomi/data/source/EHentaiPagingSource.kt b/data/src/main/java/tachiyomi/data/source/EHentaiPagingSource.kt index 104c4c83f..13d145f28 100644 --- a/data/src/main/java/tachiyomi/data/source/EHentaiPagingSource.kt +++ b/data/src/main/java/tachiyomi/data/source/EHentaiPagingSource.kt @@ -6,6 +6,7 @@ import eu.kanade.tachiyomi.source.model.MangasPage import eu.kanade.tachiyomi.source.model.MetadataMangasPage import exh.metadata.metadata.RaisedSearchMetadata import mihon.domain.manga.model.toDomainManga +import tachiyomi.core.common.util.QuerySanitizer.sanitize import tachiyomi.domain.manga.model.Manga abstract class EHentaiPagingSource( @@ -40,7 +41,7 @@ class EHentaiSearchPagingSource( val filters: FilterList, ) : EHentaiPagingSource(source) { override suspend fun requestNextPage(currentPage: Int): MangasPage { - return source.getSearchManga(currentPage, query, filters) + return source.getSearchManga(currentPage, query.sanitize(), filters) } } diff --git a/data/src/main/java/tachiyomi/data/source/SourcePagingSource.kt b/data/src/main/java/tachiyomi/data/source/SourcePagingSource.kt index e77d28d57..790c7a547 100644 --- a/data/src/main/java/tachiyomi/data/source/SourcePagingSource.kt +++ b/data/src/main/java/tachiyomi/data/source/SourcePagingSource.kt @@ -7,6 +7,7 @@ import eu.kanade.tachiyomi.source.model.MangasPage import eu.kanade.tachiyomi.source.model.MetadataMangasPage import exh.metadata.metadata.RaisedSearchMetadata import mihon.domain.manga.model.toDomainManga +import tachiyomi.core.common.util.QuerySanitizer.sanitize import tachiyomi.core.common.util.lang.withIOContext import tachiyomi.domain.manga.interactor.NetworkToLocalManga import tachiyomi.domain.manga.model.Manga @@ -20,7 +21,7 @@ class SourceSearchPagingSource( private val filters: FilterList, ) : BaseSourcePagingSource(source) { override suspend fun requestNextPage(currentPage: Int): MangasPage { - return source?.getSearchManga(currentPage, query, filters) + return source?.getSearchManga(currentPage, query.sanitize(), filters) // KMK --> ?: MangasPage(emptyList(), false) // KMK <-- diff --git a/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/CatalogueSource.kt b/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/CatalogueSource.kt index 83c82000d..d573c96a0 100644 --- a/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/CatalogueSource.kt +++ b/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/CatalogueSource.kt @@ -8,6 +8,7 @@ import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.launch import kotlinx.coroutines.supervisorScope import logcat.LogPriority +import tachiyomi.core.common.util.QuerySanitizer.sanitize import tachiyomi.core.common.util.system.logcat /** @@ -187,7 +188,7 @@ interface CatalogueSource : Source { words.map { keyword -> launch { runCatching { - getSearchManga(1, keyword, FilterList()).mangas + getSearchManga(1, keyword.sanitize(), FilterList()).mangas } .onSuccess { if (it.isNotEmpty()) pushResults(Pair(keyword, it), false) } .onFailure { e ->