[ExHentai] Improve tags loading

pull/421/head
Koitharu 2 years ago
parent 4e2d739840
commit 7c89f53988
Signed by: Koitharu
GPG Key ID: 676DEE768C17A9D7

@ -1,11 +1,11 @@
package org.koitharu.kotatsu.parsers.site.all package org.koitharu.kotatsu.parsers.site.all
import androidx.collection.ArrayMap import androidx.collection.ArrayMap
import androidx.collection.ArraySet
import androidx.collection.SparseArrayCompat import androidx.collection.SparseArrayCompat
import androidx.collection.set import androidx.collection.set
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import org.jsoup.internal.StringUtil
import org.jsoup.nodes.Element import org.jsoup.nodes.Element
import org.koitharu.kotatsu.parsers.MangaLoaderContext import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaParserAuthProvider import org.koitharu.kotatsu.parsers.MangaParserAuthProvider
@ -26,9 +26,8 @@ internal class ExHentaiParser(
context: MangaLoaderContext, context: MangaLoaderContext,
) : PagedMangaParser(context, MangaSource.EXHENTAI, pageSize = 25), MangaParserAuthProvider { ) : PagedMangaParser(context, MangaSource.EXHENTAI, pageSize = 25), MangaParserAuthProvider {
override val availableSortOrders: Set<SortOrder> = Collections.singleton( override val availableSortOrders: Set<SortOrder> = setOf(SortOrder.NEWEST)
SortOrder.NEWEST, override val isTagsExclusionSupported: Boolean = true
)
override val configKeyDomain: ConfigKey.Domain override val configKeyDomain: ConfigKey.Domain
get() = ConfigKey.Domain( get() = ConfigKey.Domain(
@ -44,6 +43,7 @@ internal class ExHentaiParser(
private var updateDm = false private var updateDm = false
private val nextPages = SparseArrayCompat<Long>() private val nextPages = SparseArrayCompat<Long>()
private val suspiciousContentKey = ConfigKey.ShowSuspiciousContent(false) private val suspiciousContentKey = ConfigKey.ShowSuspiciousContent(false)
private val tagsMap = SuspendLazy(::fetchTags)
override val isAuthorized: Boolean override val isAuthorized: Boolean
get() { get() {
@ -93,27 +93,18 @@ internal class ExHentaiParser(
is MangaListFilter.Advanced -> { is MangaListFilter.Advanced -> {
append("&f_search=") filter.toSearchQuery()?.let { sq ->
append("&f_search=")
append(sq.urlEncoded())
}
var fCats = 0 var fCats = 0
if (filter.tags.isNotEmpty()) { filter.tags.forEach { tag ->
filter.tags.forEach { tag.key.toIntOrNull()?.let {
if (it.title.startsWith("- ")) { fCats = fCats or it
it.key.toIntOrNull()?.let { fCats = fCats or it } ?: run {
search += it.key + " "
}
} else {
append(" tag:".urlEncoded())
append(it.key)
}
} }
} }
if (filter.locale != null) {
append(" language:".urlEncoded())
append(filter.locale.toLanguagePath())
}
if (fCats != 0) { if (fCats != 0) {
append("&f_cats=") append("&f_cats=")
append(1023 - fCats) append(1023 - fCats)
@ -182,15 +173,14 @@ internal class ExHentaiParser(
val title = root.getElementById("gd2") val title = root.getElementById("gd2")
val tagList = root.getElementById("taglist") val tagList = root.getElementById("taglist")
val tabs = doc.body().selectFirst("table.ptt")?.selectFirst("tr") val tabs = doc.body().selectFirst("table.ptt")?.selectFirst("tr")
val lang = val lang = root.getElementById("gd3")
root.getElementById("gd3")?.selectFirst("tr:contains(Language)")?.selectFirst(".gdt2")?.text() ?: "Unknown" ?.selectFirst("tr:contains(Language)")
?.selectFirst(".gdt2")?.text()
val tagMap = getOrCreateTagMap() val tagMap = tagsMap.get()
val tagF = val tags = ArraySet<MangaTag>()
tagList?.selectFirst("tr:contains(female:)")?.select("a")?.mapNotNullToSet { tagMap[it.text()] }.orEmpty() tagList?.selectFirst("tr:contains(female:)")?.select("a")?.mapNotNullTo(tags) { tagMap[it.text()] }
val tagM = tagList?.selectFirst("tr:contains(male:)")?.select("a")?.mapNotNullTo(tags) { tagMap[it.text()] }
tagList?.selectFirst("tr:contains(male:)")?.select("a")?.mapNotNullToSet { tagMap[it.text()] }.orEmpty()
val tags = tagF + tagM
return manga.copy( return manga.copy(
title = title?.getElementById("gn")?.text()?.cleanupTitle() ?: manga.title, 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," + "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" "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<String, MangaTag>? = null
private val mutex = Mutex()
override suspend fun getAvailableTags(): Set<MangaTag> { override suspend fun getAvailableTags(): Set<MangaTag> {
return getOrCreateTagMap().values.toSet() return tagsMap.get().values.toSet()
} }
private suspend fun getOrCreateTagMap(): Map<String, MangaTag> = mutex.withLock { private suspend fun fetchTags(): Map<String, MangaTag> {
tagCache?.let { return@withLock it }
val tagMap = ArrayMap<String, MangaTag>() val tagMap = ArrayMap<String, MangaTag>()
val tagElements = tags.split(",") val tagElements = tags.split(",")
for (el in tagElements) { for (el in tagElements) {
if (el.isEmpty()) continue if (el.isEmpty()) continue
tagMap[el] = MangaTag( tagMap[el] = MangaTag(
title = el, title = el.toTitleCase(Locale.ENGLISH),
key = el, key = el,
source = source, source = source,
) )
@ -289,16 +275,14 @@ internal class ExHentaiParser(
val root = doc.body().requireElementById("searchbox").selectFirstOrThrow("table") val root = doc.body().requireElementById("searchbox").selectFirstOrThrow("table")
root.select("div.cs").mapNotNullToSet { div -> root.select("div.cs").mapNotNullToSet { div ->
val id = div.id().substringAfterLast('_').toIntOrNull() ?: return@mapNotNullToSet null val id = div.id().substringAfterLast('_').toIntOrNull() ?: return@mapNotNullToSet null
val name = "- " + div.text().toTitleCase() val name = div.text().toTitleCase(Locale.ENGLISH)
tagMap[name] = MangaTag( tagMap[name] = MangaTag(
title = name, title = name,
key = id.toString(), key = id.toString(),
source = source, source = source,
) )
} }
return tagMap
tagCache = tagMap
return@withLock tagMap
} }
override suspend fun getAvailableLocales(): Set<Locale> = setOf( override suspend fun getAvailableLocales(): Set<Locale> = setOf(
@ -403,4 +387,32 @@ internal class ExHentaiParser(
?.queryParameter("next") ?.queryParameter("next")
?.toLongOrNull() ?: 1 ?.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() }
} }

@ -61,7 +61,11 @@ internal class MangaParserTest {
offset = 0, offset = 0,
filter = MangaListFilter.Advanced( filter = MangaListFilter.Advanced(
sortOrder = SortOrder.POPULARITY, sortOrder = SortOrder.POPULARITY,
tags = emptySet(), locale = null, states = emptySet(), tagsExclude = emptySet(), contentRating = emptySet() tags = emptySet(),
locale = null,
states = emptySet(),
tagsExclude = emptySet(),
contentRating = emptySet(),
), ),
).minByOrNull { ).minByOrNull {
it.title.length it.title.length
@ -92,7 +96,7 @@ internal class MangaParserTest {
assert(tags.all { it.source == source }) assert(tags.all { it.source == source })
val tag = tags.last() 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})") checkMangaList(list, "${tag.title} (${tag.key})")
assert(list.all { it.source == source }) assert(list.all { it.source == source })
} }
@ -101,17 +105,13 @@ internal class MangaParserTest {
@MangaSources @MangaSources
fun tagsMultiple(source: MangaSource) = runTest(timeout = timeout) { fun tagsMultiple(source: MangaSource) = runTest(timeout = timeout) {
val parser = context.newParserInstance(source) val parser = context.newParserInstance(source)
if (!parser.isMultipleTagsSupported) return@runTest
val tags = parser.getAvailableTags().shuffled().take(2).toSet() val tags = parser.getAvailableTags().shuffled().take(2).toSet()
val list = try { val filter = MangaListFilter.Advanced.Builder(parser.availableSortOrders.first())
parser.getList(offset = 0, tags = tags, tagsExclude = tags, sortOrder = null) .tags(tags)
} catch (e: IllegalArgumentException) { .build()
if (e.message == "Multiple genres are not supported by this source") { val list = parser.getList(0, filter)
return@runTest
} else {
throw e
}
}
checkMangaList(list, "${tags.joinToString { it.title }} (${tags.joinToString { it.key }})") checkMangaList(list, "${tags.joinToString { it.title }} (${tags.joinToString { it.key }})")
assert(list.all { it.source == source }) assert(list.all { it.source == source })
} }

Loading…
Cancel
Save