From 522b3d53ccebc503ae134e856d95a0f31bd1358f Mon Sep 17 00:00:00 2001 From: Koitharu Date: Tue, 18 Feb 2025 17:48:20 +0200 Subject: [PATCH] Artist filter support --- .../kotatsu/parsers/model/MangaListFilter.kt | 4 ++- .../model/MangaListFilterCapabilities.kt | 6 +++++ .../parsers/site/all/ExHentaiParser.kt | 26 +++++++++++++++---- .../parsers/site/ru/grouple/GroupleParser.kt | 9 ++++--- 4 files changed, 35 insertions(+), 10 deletions(-) diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/model/MangaListFilter.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/model/MangaListFilter.kt index b357edd9..e996a588 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/model/MangaListFilter.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/model/MangaListFilter.kt @@ -15,6 +15,7 @@ public data class MangaListFilter( @JvmField val year: Int = YEAR_UNKNOWN, @JvmField val yearFrom: Int = YEAR_UNKNOWN, @JvmField val yearTo: Int = YEAR_UNKNOWN, + @JvmField val author: String? = null, ) { private fun isNonSearchOptionsEmpty(): Boolean = tags.isEmpty() && @@ -27,7 +28,8 @@ public data class MangaListFilter( yearFrom == YEAR_UNKNOWN && yearTo == YEAR_UNKNOWN && types.isEmpty() && - demographics.isEmpty() + demographics.isEmpty() && + author.isNullOrEmpty() public fun isEmpty(): Boolean = isNonSearchOptionsEmpty() && query.isNullOrEmpty() diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/model/MangaListFilterCapabilities.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/model/MangaListFilterCapabilities.kt index 33427f2a..d0daa06d 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/model/MangaListFilterCapabilities.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/model/MangaListFilterCapabilities.kt @@ -47,4 +47,10 @@ public data class MangaListFilterCapabilities @InternalParsersApi constructor( * @see [MangaListFilterOptions.availableLocales] */ val isOriginalLocaleSupported: Boolean = false, + + /** + * Whether parser supports searching by author name + * @see [MangaListFilter.author] + */ + val isAuthorSearchSupported: Boolean = false, ) 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 f611d691..3cdfe301 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 @@ -19,6 +19,7 @@ import org.koitharu.kotatsu.parsers.exception.AuthRequiredException import org.koitharu.kotatsu.parsers.exception.TooManyRequestExceptions import org.koitharu.kotatsu.parsers.model.* import org.koitharu.kotatsu.parsers.util.* +import java.text.SimpleDateFormat import java.util.* import java.util.Collections.emptyList import java.util.concurrent.TimeUnit @@ -54,6 +55,7 @@ internal class ExHentaiParser( isTagsExclusionSupported = true, isSearchSupported = true, isSearchWithFiltersSupported = true, + isAuthorSearchSupported = true, ) override val isAuthorized: Boolean @@ -172,8 +174,8 @@ internal class ExHentaiParser( url = href, publicUrl = a.absUrl("href"), rating = td2.selectFirst("div.ir")?.parseRating() ?: RATING_UNKNOWN, - isNsfw = true, - coverUrl = td1.selectFirst("img")?.absUrl("src").orEmpty(), + contentRating = ContentRating.ADULT, + coverUrl = td1.selectFirst("img")?.attrAsAbsoluteUrlOrNull("src"), tags = tagsDiv.parseTags(), state = null, author = tagsDiv.getElementsContainingOwnText("artist:").first() @@ -190,9 +192,18 @@ 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") + val gd3 = root.getElementById("gd3") + val lang = gd3 ?.selectFirst("tr:contains(Language)") ?.selectFirst(".gdt2")?.ownTextOrNull() + val uploadDate = gd3 + ?.selectFirst("tr:contains(Posted)") + ?.selectFirst(".gdt2")?.ownTextOrNull() + .let { SimpleDateFormat("yyyy-MM-dd HH:mm", sourceLocale).tryParse(it) } + val uploader = gd3 + ?.getElementsByAttributeValueContaining("href", "/uploader/") + ?.firstOrNull() + ?.ownTextOrNull() val tags = tagList?.parseTags().orEmpty() return manga.copy( @@ -223,9 +234,9 @@ internal class ExHentaiParser( number = i.toFloat(), volume = 0, url = url, - uploadDate = 0L, + uploadDate = uploadDate, source = source, - scanlator = null, + scanlator = uploader, branch = lang, ) } @@ -415,6 +426,11 @@ internal class ExHentaiParser( joiner.append(lc.toLanguagePath()) joiner.append("\"$") } + if (!author.isNullOrEmpty()) { + joiner.add("artist:\"") + joiner.append(author) + joiner.append("\"$") + } return joiner.complete().nullIfEmpty() } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/grouple/GroupleParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/grouple/GroupleParser.kt index a98a31b0..274ccf3e 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/grouple/GroupleParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/grouple/GroupleParser.kt @@ -103,12 +103,13 @@ internal abstract class GroupleParser( } else { advancedSearch(offset, order, filter).parseHtml() } - val tiles = - root.selectFirst("div.tiles.row") ?: if (root.select(".alert").any { it.ownText() == NOTHING_FOUND }) { + val tiles = root.selectFirst("div.tiles.row") + if (tiles == null) { + if (!root.getElementsContainingOwnText(NOTHING_FOUND).isNullOrEmpty()) { return emptyList() - } else { - root.parseFailed("No tiles found") } + root.parseFailed("No tiles found") + } return tiles.select("div.tile").mapNotNull(::parseManga) }