diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/ExHentaiParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/ExHentaiParser.kt index 91fae31b..5d75933a 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/ExHentaiParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/ExHentaiParser.kt @@ -1,11 +1,11 @@ package org.koitharu.kotatsu.parsers.site.all import androidx.collection.ArrayMap +import androidx.collection.ArraySet import androidx.collection.SparseArrayCompat import androidx.collection.set -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock import okhttp3.HttpUrl.Companion.toHttpUrlOrNull +import org.jsoup.internal.StringUtil import org.jsoup.nodes.Element import org.koitharu.kotatsu.parsers.MangaLoaderContext import org.koitharu.kotatsu.parsers.MangaParserAuthProvider @@ -26,9 +26,8 @@ internal class ExHentaiParser( context: MangaLoaderContext, ) : PagedMangaParser(context, MangaSource.EXHENTAI, pageSize = 25), MangaParserAuthProvider { - override val availableSortOrders: Set = Collections.singleton( - SortOrder.NEWEST, - ) + override val availableSortOrders: Set = setOf(SortOrder.NEWEST) + override val isTagsExclusionSupported: Boolean = true override val configKeyDomain: ConfigKey.Domain get() = ConfigKey.Domain( @@ -44,6 +43,7 @@ internal class ExHentaiParser( private var updateDm = false private val nextPages = SparseArrayCompat() private val suspiciousContentKey = ConfigKey.ShowSuspiciousContent(false) + private val tagsMap = SuspendLazy(::fetchTags) override val isAuthorized: Boolean get() { @@ -93,27 +93,18 @@ internal class ExHentaiParser( is MangaListFilter.Advanced -> { - append("&f_search=") + filter.toSearchQuery()?.let { sq -> + append("&f_search=") + append(sq.urlEncoded()) + } var fCats = 0 - if (filter.tags.isNotEmpty()) { - filter.tags.forEach { - if (it.title.startsWith("- ")) { - it.key.toIntOrNull()?.let { fCats = fCats or it } ?: run { - search += it.key + " " - } - } else { - append(" tag:".urlEncoded()) - append(it.key) - } + filter.tags.forEach { tag -> + tag.key.toIntOrNull()?.let { + fCats = fCats or it } } - if (filter.locale != null) { - append(" language:".urlEncoded()) - append(filter.locale.toLanguagePath()) - } - if (fCats != 0) { append("&f_cats=") append(1023 - fCats) @@ -182,15 +173,14 @@ internal class ExHentaiParser( val title = root.getElementById("gd2") val tagList = root.getElementById("taglist") val tabs = doc.body().selectFirst("table.ptt")?.selectFirst("tr") - val lang = - root.getElementById("gd3")?.selectFirst("tr:contains(Language)")?.selectFirst(".gdt2")?.text() ?: "Unknown" + val lang = root.getElementById("gd3") + ?.selectFirst("tr:contains(Language)") + ?.selectFirst(".gdt2")?.text() - val tagMap = getOrCreateTagMap() - val tagF = - tagList?.selectFirst("tr:contains(female:)")?.select("a")?.mapNotNullToSet { tagMap[it.text()] }.orEmpty() - val tagM = - tagList?.selectFirst("tr:contains(male:)")?.select("a")?.mapNotNullToSet { tagMap[it.text()] }.orEmpty() - val tags = tagF + tagM + val tagMap = tagsMap.get() + val tags = ArraySet() + tagList?.selectFirst("tr:contains(female:)")?.select("a")?.mapNotNullTo(tags) { tagMap[it.text()] } + tagList?.selectFirst("tr:contains(male:)")?.select("a")?.mapNotNullTo(tags) { tagMap[it.text()] } return manga.copy( title = title?.getElementById("gn")?.text()?.cleanupTitle() ?: manga.title, @@ -265,21 +255,17 @@ internal class ExHentaiParser( "unusual pupils,urination,vore,vtuber,widow,wings,witch,wolf girl,x-ray,yuri,zombie,sole male,males only,yaoi," + "tomgirl,tall man,oni,shotacon,prostate massage,policeman,males only,huge penis,fox boy,feminization,dog boy,dickgirl on male,big penis" - private var tagCache: ArrayMap? = null - private val mutex = Mutex() - override suspend fun getAvailableTags(): Set { - return getOrCreateTagMap().values.toSet() + return tagsMap.get().values.toSet() } - private suspend fun getOrCreateTagMap(): Map = mutex.withLock { - tagCache?.let { return@withLock it } + private suspend fun fetchTags(): Map { val tagMap = ArrayMap() val tagElements = tags.split(",") for (el in tagElements) { if (el.isEmpty()) continue tagMap[el] = MangaTag( - title = el, + title = el.toTitleCase(Locale.ENGLISH), key = el, source = source, ) @@ -289,16 +275,14 @@ internal class ExHentaiParser( val root = doc.body().requireElementById("searchbox").selectFirstOrThrow("table") root.select("div.cs").mapNotNullToSet { div -> val id = div.id().substringAfterLast('_').toIntOrNull() ?: return@mapNotNullToSet null - val name = "- " + div.text().toTitleCase() + val name = div.text().toTitleCase(Locale.ENGLISH) tagMap[name] = MangaTag( title = name, key = id.toString(), source = source, ) } - - tagCache = tagMap - return@withLock tagMap + return tagMap } override suspend fun getAvailableLocales(): Set = setOf( @@ -403,4 +387,32 @@ internal class ExHentaiParser( ?.queryParameter("next") ?.toLongOrNull() ?: 1 } + + private fun MangaListFilter.Advanced.toSearchQuery(): String? { + val joiner = StringUtil.StringJoiner(" ") + for (tag in tags) { + if (tag.key.isNumeric()) { + continue + } + joiner.add("tag:\"") + joiner.append(tag.key) + joiner.append("\"$") + } + for (tag in tagsExclude) { + if (tag.key.isNumeric()) { + continue + } + joiner.add("-tag:\"") + joiner.append(tag.key) + joiner.append("\"$") + } + locale?.let { lc -> + joiner.add("language:\"") + joiner.append(lc.toLanguagePath()) + joiner.append("\"$") + } + return joiner.complete().takeUnless { it.isEmpty() } + } + + private fun String.isNumeric() = all { c -> c.isDigit() } } diff --git a/src/test/kotlin/org/koitharu/kotatsu/parsers/MangaParserTest.kt b/src/test/kotlin/org/koitharu/kotatsu/parsers/MangaParserTest.kt index 5f76cdc3..813d53ae 100644 --- a/src/test/kotlin/org/koitharu/kotatsu/parsers/MangaParserTest.kt +++ b/src/test/kotlin/org/koitharu/kotatsu/parsers/MangaParserTest.kt @@ -61,7 +61,11 @@ internal class MangaParserTest { offset = 0, filter = MangaListFilter.Advanced( sortOrder = SortOrder.POPULARITY, - tags = emptySet(), locale = null, states = emptySet(), tagsExclude = emptySet(), contentRating = emptySet() + tags = emptySet(), + locale = null, + states = emptySet(), + tagsExclude = emptySet(), + contentRating = emptySet(), ), ).minByOrNull { it.title.length @@ -92,7 +96,7 @@ internal class MangaParserTest { assert(tags.all { it.source == source }) val tag = tags.last() - val list = parser.getList(offset = 0, tags = setOf(tag), tagsExclude = setOf(tag), sortOrder = null) + val list = parser.getList(offset = 0, tags = setOf(tag), null, sortOrder = null) checkMangaList(list, "${tag.title} (${tag.key})") assert(list.all { it.source == source }) } @@ -101,17 +105,13 @@ internal class MangaParserTest { @MangaSources fun tagsMultiple(source: MangaSource) = runTest(timeout = timeout) { val parser = context.newParserInstance(source) + if (!parser.isMultipleTagsSupported) return@runTest val tags = parser.getAvailableTags().shuffled().take(2).toSet() - val list = try { - parser.getList(offset = 0, tags = tags, tagsExclude = tags, sortOrder = null) - } catch (e: IllegalArgumentException) { - if (e.message == "Multiple genres are not supported by this source") { - return@runTest - } else { - throw e - } - } + val filter = MangaListFilter.Advanced.Builder(parser.availableSortOrders.first()) + .tags(tags) + .build() + val list = parser.getList(0, filter) checkMangaList(list, "${tags.joinToString { it.title }} (${tags.joinToString { it.key }})") assert(list.all { it.source == source }) }