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.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),
)
}

View file

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

View file

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

View file

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

View file

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

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

View file

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

View file

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