feat(sanitizer): Sanitize manga titles for special characters before searching (#956)
This commit is contained in:
parent
b1773eb08f
commit
aaa1d855ff
9 changed files with 75 additions and 10 deletions
|
|
@ -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),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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))
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 <--
|
||||||
|
|
|
||||||
|
|
@ -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 ->
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue