From 95e4d831069488a82b7bfb169ccffdd09f23f725 Mon Sep 17 00:00:00 2001 From: KenjieDec <65448230+KenjieDec@users.noreply.github.com> Date: Mon, 3 Jun 2024 18:20:00 +0700 Subject: [PATCH] Hitomi Add "Type" Filter (#3355) * Add "Type" Filter - Added "Type" Filter - Popular Section: Today -> Year - Fixed issue where manga is skipped if language is null * Change1 * * * Change2 - Use List instead of Set --- src/all/hitomi/build.gradle | 2 +- .../tachiyomi/extension/all/hitomi/Filters.kt | 113 +++++++---------- .../tachiyomi/extension/all/hitomi/Hitomi.kt | 114 ++++++++++-------- .../extension/all/hitomi/HitomiDto.kt | 4 +- 4 files changed, 109 insertions(+), 124 deletions(-) diff --git a/src/all/hitomi/build.gradle b/src/all/hitomi/build.gradle index cf5dd65a3..04020bcb5 100644 --- a/src/all/hitomi/build.gradle +++ b/src/all/hitomi/build.gradle @@ -1,7 +1,7 @@ ext { extName = 'Hitomi' extClass = '.HitomiFactory' - extVersionCode = 28 + extVersionCode = 29 isNsfw = true } diff --git a/src/all/hitomi/src/eu/kanade/tachiyomi/extension/all/hitomi/Filters.kt b/src/all/hitomi/src/eu/kanade/tachiyomi/extension/all/hitomi/Filters.kt index c9bbb8f8a..2a1756092 100644 --- a/src/all/hitomi/src/eu/kanade/tachiyomi/extension/all/hitomi/Filters.kt +++ b/src/all/hitomi/src/eu/kanade/tachiyomi/extension/all/hitomi/Filters.kt @@ -3,79 +3,50 @@ package eu.kanade.tachiyomi.extension.all.hitomi import eu.kanade.tachiyomi.source.model.Filter import eu.kanade.tachiyomi.source.model.FilterList -typealias OrderType = Pair -typealias ParsedFilter = Pair - -private fun parseFilter(query: StringBuilder, area: String, filterState: String) { - filterState - .trim() - .split(',') - .filter { it.isNotBlank() } - .forEach { - val trimmed = it.trim() - val negativePrefix = if (trimmed.startsWith("-")) "-" else "" - query.append(" $negativePrefix$area:${trimmed.removePrefix("-").replace(" ", "_")}") - } +fun getFilters(): FilterList { + return FilterList( + SelectFilter("Sort by", getSortsList), + TypeFilter("Types"), + Filter.Separator(), + Filter.Header("Separate tags with commas (,)"), + Filter.Header("Prepend with dash (-) to exclude"), + TextFilter("Groups", "group"), + TextFilter("Artists", "artist"), + TextFilter("Series", "series"), + TextFilter("Characters", "character"), + TextFilter("Male Tags", "male"), + TextFilter("Female Tags", "female"), + Filter.Header("Please don't put Female/Male tags here, they won't work!"), + TextFilter("Tags", "tag"), + ) } -fun parseFilters(filters: FilterList): ParsedFilter { - val query = StringBuilder() - var order: OrderType = Pair("date", "added") - filters.forEach { filter -> - when (filter) { - is SortFilter -> { - order = filter.getOrder - } - is AreaFilter -> { - parseFilter(query, filter.getAreaName, filter.state) - } - else -> { /* Do Nothing */ } - } - } - return Pair(query.toString(), order) +internal open class TextFilter(name: String, val type: String) : Filter.Text(name) +internal open class SelectFilter(name: String, val vals: List>, state: Int = 0) : + Filter.Select(name, vals.map { it.first }.toTypedArray(), state) { + fun getArea() = vals[state].second + fun getValue() = vals[state].third } +internal class TypeFilter(name: String) : + Filter.Group( + name, + listOf( + Pair("Anime", "anime"), + Pair("Artist CG", "artistcg"), + Pair("Doujinshi", "doujinshi"), + Pair("Game CG", "gamecg"), + Pair("Image Set", "imageset"), + Pair("Manga", "manga"), + ).map { CheckBoxFilter(it.first, it.second, true) }, + ) +internal open class CheckBoxFilter(name: String, val value: String, state: Boolean) : Filter.CheckBox(name, state) -private class OrderFilter(val name: String, val order: OrderType) { - val getFilterName: String - get() = name - val getOrder: OrderType - get() = order -} - -private class SortFilter : UriPartFilter( - "Sort By", - arrayOf( - OrderFilter("Date Added", Pair(null, "index")), - OrderFilter("Date Published", Pair("date", "published")), - OrderFilter("Popular: Today", Pair("popular", "today")), - OrderFilter("Popular: Week", Pair("popular", "week")), - OrderFilter("Popular: Month", Pair("popular", "month")), - OrderFilter("Popular: Year", Pair("popular", "year")), - ), -) - -private open class UriPartFilter(displayName: String, val vals: Array) : - Filter.Select(displayName, vals.map { it.getFilterName }.toTypedArray()) { - val getOrder: OrderType - get() = vals[state].getOrder -} - -private class AreaFilter(displayName: String, val areaName: String) : - Filter.Text(displayName) { - val getAreaName: String - get() = areaName -} - -fun getFilterListInternal(): FilterList = FilterList( - SortFilter(), - Filter.Header("Separate tags with commas (,)"), - Filter.Header("Prepend with dash (-) to exclude"), - AreaFilter("Artist(s)", "artist"), - AreaFilter("Character(s)", "character"), - AreaFilter("Group(s)", "group"), - AreaFilter("Series", "series"), - AreaFilter("Female Tag(s)", "female"), - AreaFilter("Male Tag(s)", "male"), - Filter.Header("Don't put Female/Male tags here, they won't work!"), - AreaFilter("Tag(s)", "tag"), +private val getSortsList: List> = listOf( + Triple("Date Added", null, "index"), + Triple("Date Published", "date", "published"), + Triple("Popular: Today", "popular", "today"), + Triple("Popular: Week", "popular", "week"), + Triple("Popular: Month", "popular", "month"), + Triple("Popular: Year", "popular", "year"), + Triple("Random", "popular", "year"), ) diff --git a/src/all/hitomi/src/eu/kanade/tachiyomi/extension/all/hitomi/Hitomi.kt b/src/all/hitomi/src/eu/kanade/tachiyomi/extension/all/hitomi/Hitomi.kt index 331df72a6..6aa09db12 100644 --- a/src/all/hitomi/src/eu/kanade/tachiyomi/extension/all/hitomi/Hitomi.kt +++ b/src/all/hitomi/src/eu/kanade/tachiyomi/extension/all/hitomi/Hitomi.kt @@ -69,7 +69,7 @@ class Hitomi( override fun fetchPopularManga(page: Int): Observable = Observable.fromCallable { runBlocking { - val entries = getGalleryIDsFromNozomi("popular", "today", nozomiLang, page.nextPageRange()) + val entries = getGalleryIDsFromNozomi("popular", "year", nozomiLang, page.nextPageRange()) .toMangaList() MangasPage(entries, entries.size >= 24) @@ -88,26 +88,23 @@ class Hitomi( private lateinit var searchResponse: List override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable = Observable.fromCallable { - val parsedFilter = parseFilters(filters) - runBlocking { if (page == 1) { searchResponse = hitomiSearch( - "$query${parsedFilter.first}".trim(), - parsedFilter.second, + query.trim(), + filters, nozomiLang, - ).toList() + ) } val end = min(page * 25, searchResponse.size) val entries = searchResponse.subList((page - 1) * 25, end) .toMangaList() - - MangasPage(entries, end != searchResponse.size) + MangasPage(entries, end < searchResponse.size) } } - override fun getFilterList(): FilterList = getFilterListInternal() + override fun getFilterList() = getFilters() private fun Int.nextPageRange(): LongRange { val byteOffset = ((this - 1) * 25) * 4L @@ -127,15 +124,49 @@ class Hitomi( private suspend fun hitomiSearch( query: String, - order: OrderType, + filters: FilterList, language: String = "all", - ): Set = + ): List = coroutineScope { + var sortBy: Pair = Pair(null, "index") + var random = false + val terms = query .trim() .replace(Regex("""^\?"""), "") .lowercase() .split(Regex("\\s+")) + .map { + it.replace('_', ' ') + }.toMutableList() + + filters.forEach { + when (it) { + is SelectFilter -> { + sortBy = Pair(it.getArea(), it.getValue()) + random = (it.vals[it.state].first == "Random") + } + + is TypeFilter -> { + val (activeFilter, inactiveFilters) = it.state.partition { stIt -> stIt.state } + terms += when { + inactiveFilters.size < 5 -> inactiveFilters.map { fil -> "-type:${fil.value}" } + inactiveFilters.size == 5 -> listOf("type:${activeFilter[0].value}") + else -> listOf("type: none") + } + } + + is TextFilter -> { + if (it.state.isNotEmpty()) { + terms += it.state.split(",").map { tag -> + val trimmed = tag.trim() + (if (trimmed.startsWith("-")) "-" else "") + it.type + ":" + trimmed.lowercase().removePrefix("-") + } + } + } + else -> {} + } + } val positiveTerms = LinkedList() val negativeTerms = LinkedList() @@ -151,32 +182,32 @@ class Hitomi( val positiveResults = positiveTerms.map { async { runCatching { - getGalleryIDsForQuery(it, language, order) - }.getOrDefault(emptySet()) + getGalleryIDsForQuery(it, language) + }.getOrDefault(ArrayList()) } } val negativeResults = negativeTerms.map { async { runCatching { - getGalleryIDsForQuery(it, language, order) - }.getOrDefault(emptySet()) + getGalleryIDsForQuery(it, language) + }.getOrDefault(ArrayList()) } } val results = when { - positiveTerms.isEmpty() -> getGalleryIDsFromNozomi(order.first, order.second, language) - else -> emptySet() - }.toMutableSet() + positiveTerms.isEmpty() -> getGalleryIDsFromNozomi(sortBy.first, sortBy.second, language) + else -> ArrayList() + } - fun filterPositive(newResults: Set) { + fun filterPositive(newResults: Collection) { when { results.isEmpty() -> results.addAll(newResults) else -> results.retainAll(newResults) } } - fun filterNegative(newResults: Set) { + fun filterNegative(newResults: Collection) { results.removeAll(newResults) } @@ -190,15 +221,14 @@ class Hitomi( filterNegative(it.await()) } - results + if (random) results.shuffled() else results } // search.js private suspend fun getGalleryIDsForQuery( query: String, language: String = "all", - order: OrderType, - ): Set { + ): MutableList { query.replace("_", " ").let { if (it.indexOf(':') > -1) { val sides = it.split(":") @@ -220,32 +250,18 @@ class Hitomi( } } - if (area != null) { - if (order.first != null) { - area = "$area/${order.first}" - if (tag.isBlank()) { - tag = order.second - } else { - area = "$area/${order.second}" - } - } - } else { - area = order.first - tag = order.second - } - return getGalleryIDsFromNozomi(area, tag, lang) } val key = hashTerm(it) val node = getGalleryNodeAtAddress(0) - val data = bSearch(key, node) ?: return emptySet() + val data = bSearch(key, node) ?: return ArrayList() return getGalleryIDsFromData(data) } } - private suspend fun getGalleryIDsFromData(data: Pair): Set { + private suspend fun getGalleryIDsFromData(data: Pair): MutableList { val url = "$ltnUrl/galleriesindex/galleries.$galleriesIndexVersion.data" val (offset, length) = data require(length in 1..100000000) { @@ -254,7 +270,7 @@ class Hitomi( val inbuf = getRangedResponse(url, offset.until(offset + length)) - val galleryIDs = mutableSetOf() + val galleryIDs = mutableListOf() val buffer = ByteBuffer @@ -343,14 +359,14 @@ class Hitomi( tag: String, language: String, range: LongRange? = null, - ): Set { + ): MutableList { val nozomiAddress = when (area) { null -> "$ltnUrl/$tag-$language.nozomi" else -> "$ltnUrl/$area/$tag-$language.nozomi" } val bytes = getRangedResponse(nozomiAddress, range) - val nozomi = mutableSetOf() + val nozomi = mutableListOf() val arrayBuffer = ByteBuffer .wrap(bytes) @@ -459,14 +475,15 @@ class Hitomi( "https://${subDomain}tn.$domain/webpbigtn/${thumbPathFromHash(hash)}/$hash.webp" } description = buildString { - characters?.joinToString { it.formatted }?.let { - append("Characters: ", it, "\n") - } parodys?.joinToString { it.formatted }?.let { - append("Parodies: ", it, "\n") + append("Series: ", it, "\n") } + characters?.joinToString { it.formatted }?.let { + append("Characters: ", it, "\n\n") + } + append("Type: ", type, "\n") append("Pages: ", files.size, "\n") - append("Language: ", language) + language?.let { append("Language: ", language) } } status = SManga.COMPLETED update_strategy = UpdateStrategy.ONLY_FETCH_ONCE @@ -620,7 +637,6 @@ class Hitomi( override fun popularMangaParse(response: Response) = throw UnsupportedOperationException() override fun popularMangaRequest(page: Int) = throw UnsupportedOperationException() - override fun setupPreferenceScreen(screen: PreferenceScreen) { SwitchPreferenceCompat(screen.context).apply { key = PREF_TAG_GENDER_ICON @@ -634,13 +650,11 @@ class Hitomi( } }.also(screen::addPreference) } - override fun latestUpdatesRequest(page: Int) = throw UnsupportedOperationException() override fun latestUpdatesParse(response: Response) = throw UnsupportedOperationException() override fun searchMangaRequest(page: Int, query: String, filters: FilterList) = throw UnsupportedOperationException() override fun searchMangaParse(response: Response) = throw UnsupportedOperationException() override fun imageUrlParse(response: Response) = throw UnsupportedOperationException() - companion object { private const val PREF_TAG_GENDER_ICON = "pref_tag_gender_icon" } diff --git a/src/all/hitomi/src/eu/kanade/tachiyomi/extension/all/hitomi/HitomiDto.kt b/src/all/hitomi/src/eu/kanade/tachiyomi/extension/all/hitomi/HitomiDto.kt index 9cc850f3a..d60ba4178 100644 --- a/src/all/hitomi/src/eu/kanade/tachiyomi/extension/all/hitomi/HitomiDto.kt +++ b/src/all/hitomi/src/eu/kanade/tachiyomi/extension/all/hitomi/HitomiDto.kt @@ -8,8 +8,8 @@ data class Gallery( val galleryurl: String, val title: String, val date: String, - val type: String, - val language: String, + val type: String?, + val language: String?, val tags: List?, val artists: List?, val groups: List?,