diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/NHentaiParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/NHentaiParser.kt deleted file mode 100644 index fa549639..00000000 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/NHentaiParser.kt +++ /dev/null @@ -1,187 +0,0 @@ -package org.koitharu.kotatsu.parsers.site.all - -import androidx.collection.ArraySet -import kotlinx.coroutines.async -import kotlinx.coroutines.awaitAll -import kotlinx.coroutines.coroutineScope -import org.jsoup.nodes.Element -import org.koitharu.kotatsu.parsers.MangaLoaderContext -import org.koitharu.kotatsu.parsers.MangaSourceParser -import org.koitharu.kotatsu.parsers.PagedMangaParser -import org.koitharu.kotatsu.parsers.config.ConfigKey -import org.koitharu.kotatsu.parsers.model.* -import org.koitharu.kotatsu.parsers.util.* -import java.text.SimpleDateFormat -import java.util.* - -@MangaSourceParser("NHENTAI", "N-Hentai", type = ContentType.HENTAI) -class NHentaiParser(context: MangaLoaderContext) : PagedMangaParser(context, MangaSource.NHENTAI, pageSize = 25) { - - override val configKeyDomain: ConfigKey.Domain - get() = ConfigKey.Domain("nhentai.net") - - override val sortOrders: Set - get() = EnumSet.of(SortOrder.NEWEST, SortOrder.POPULARITY) - - override suspend fun getListPage( - page: Int, - query: String?, - tags: Set?, - sortOrder: SortOrder, - ): List { - if (query.isNullOrEmpty() && tags != null && tags.size > 1) { - return getListPage(page, buildQuery(tags), emptySet(), sortOrder) - } - val domain = domain - val url = buildString { - append("https://") - append(domain) - if (!query.isNullOrEmpty()) { - append("/search/?q=") - append(query.urlEncoded()) - append("&page=") - append(page) - if (sortOrder == SortOrder.POPULARITY) { - append("&sort=popular") - } - } else { - append('/') - if (!tags.isNullOrEmpty()) { - val tag = tags.single() - append("tag/") - append(tag.key) - append('/') - if (sortOrder == SortOrder.POPULARITY) { - append("popular") - } - append("?page=") - append(page) - } else { - if (sortOrder == SortOrder.POPULARITY) { - append("?sort=popular&page=") - } else { - append("?page=") - } - append(page) - } - } - } - val root = webClient.httpGet(url).parseHtml().body().requireElementById("content") - .selectLastOrThrow("div.index-container") - val regexBrackets = Regex("\\[[^]]+]|\\([^)]+\\)") - val regexSpaces = Regex("\\s+") - return root.select(".gallery").map { div -> - val a = div.selectFirstOrThrow("a.cover") - val href = a.attrAsRelativeUrl("href") - val img = div.selectFirstOrThrow("img") - val title = div.selectFirstOrThrow(".caption").text() - Manga( - id = generateUid(href), - title = title.replace(regexBrackets, "") - .replace(regexSpaces, " ") - .trim(), - altTitle = null, - url = href, - publicUrl = href.toAbsoluteUrl(domain), - rating = RATING_UNKNOWN, - isNsfw = true, - coverUrl = img.attrAsAbsoluteUrlOrNull("data-src") - ?: img.attrAsAbsoluteUrl("src"), - tags = setOf(), - state = null, - author = null, - largeCoverUrl = null, - description = null, - chapters = listOf(), - source = source, - ) - } - } - - override suspend fun getDetails(manga: Manga): Manga { - val root = webClient.httpGet( - url = manga.url.toAbsoluteUrl(domain), - ).parseHtml().body().requireElementById("bigcontainer") - val img = root.requireElementById("cover").selectFirstOrThrow("img") - val tagContainers = root.requireElementById("tags").select(".tag-container") - val dateFormat = SimpleDateFormat( - "yyyy-MM-dd'T'HH:mm:ss.SSSSSS'+00:00'", - Locale.ROOT, - ) - return manga.copy( - tags = tagContainers.find { x -> x.ownText() == "Tags:" }?.parseTags() ?: manga.tags, - author = tagContainers.find { x -> x.ownText() == "Artists:" } - ?.selectFirst("span.name")?.text()?.toCamelCase(), - largeCoverUrl = img.attrAsAbsoluteUrlOrNull("data-src") - ?: img.attrAsAbsoluteUrl("src"), - description = null, - chapters = listOf( - MangaChapter( - id = manga.id, - name = manga.title, - number = 1, - url = manga.url, - scanlator = null, - uploadDate = dateFormat.tryParse( - tagContainers.find { x -> x.ownText() == "Uploaded:" } - ?.selectFirst("time") - ?.attr("datetime"), - ), - branch = null, - source = source, - ), - ), - ) - } - - override suspend fun getPages(chapter: MangaChapter): List { - val url = chapter.url.toAbsoluteUrl(domain) - val root = webClient.httpGet(url).parseHtml().requireElementById("thumbnail-container") - return root.select(".thumb-container").map { div -> - val a = div.selectFirstOrThrow("a") - val img = div.selectFirstOrThrow("img") - val href = a.attrAsRelativeUrl("href") - MangaPage( - id = generateUid(href), - url = href, - preview = img.attrAsAbsoluteUrlOrNull("data-src") - ?: img.attrAsAbsoluteUrl("src"), - source = source, - ) - } - } - - override suspend fun getPageUrl(page: MangaPage): String { - val root = webClient.httpGet(page.url.toAbsoluteUrl(domain)).parseHtml().body() - .requireElementById("image-container") - return root.selectFirstOrThrow("img").attrAsAbsoluteUrl("src") - } - - override suspend fun getTags(): Set { - return coroutineScope { - // parse first 3 pages of tags - (1..3).map { page -> - async { getTags(page) } - } - }.awaitAll().flattenTo(ArraySet(360)) - } - - private suspend fun getTags(page: Int): Set { - val root = webClient.httpGet("https://${domain}/tags/popular?page=$page").parseHtml().body() - .getElementById("tag-container") - return root?.parseTags().orEmpty() - } - - private fun Element.parseTags() = select("a.tag").mapToSet { a -> - val href = a.attr("href").removeSuffix('/') - MangaTag( - title = a.selectFirstOrThrow(".name").text().toTitleCase(), - key = href.substringAfterLast('/'), - source = source, - ) - } - - private fun buildQuery(tags: Collection) = tags.joinToString(separator = " ") { tag -> - "tag:\"${tag.key}\"" - } -} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/HentaiFox.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/galleryadults/GalleryAdultsParser.kt similarity index 51% rename from src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/HentaiFox.kt rename to src/main/kotlin/org/koitharu/kotatsu/parsers/site/galleryadults/GalleryAdultsParser.kt index a6fb3059..b88db34a 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/HentaiFox.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/galleryadults/GalleryAdultsParser.kt @@ -1,23 +1,27 @@ -package org.koitharu.kotatsu.parsers.site.all +package org.koitharu.kotatsu.parsers.site.galleryadults import androidx.collection.ArraySet import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.coroutineScope +import org.jsoup.nodes.Document import org.jsoup.nodes.Element import org.koitharu.kotatsu.parsers.MangaLoaderContext -import org.koitharu.kotatsu.parsers.MangaSourceParser import org.koitharu.kotatsu.parsers.PagedMangaParser import org.koitharu.kotatsu.parsers.config.ConfigKey import org.koitharu.kotatsu.parsers.model.* import org.koitharu.kotatsu.parsers.util.* import java.util.* -@MangaSourceParser("HENTAIFOX", "HentaiFox", type = ContentType.HENTAI) -internal class HentaiFox(context: MangaLoaderContext) : PagedMangaParser(context, MangaSource.HENTAIFOX, 20) { +internal abstract class GalleryAdultsParser( + context: MangaLoaderContext, + source: MangaSource, + domain: String, + pageSize: Int = 20, +) : PagedMangaParser(context, source, pageSize) { override val sortOrders: Set = EnumSet.of(SortOrder.UPDATED) - override val configKeyDomain = ConfigKey.Domain("hentaifox.com") + override val configKeyDomain = ConfigKey.Domain(domain) override suspend fun getListPage( page: Int, @@ -26,49 +30,48 @@ internal class HentaiFox(context: MangaLoaderContext) : PagedMangaParser(context sortOrder: SortOrder, ): List { val tag = tags.oneOrThrowIfMany() - val url = buildString { append("https://") append(domain) if (!tags.isNullOrEmpty()) { append("/tag/") append(tag?.key.orEmpty()) - if (page > 1) { - append("/pag/") - append(page) - append("/") - } + append("/?") + } else if (!query.isNullOrEmpty()) { append("/search/?q=") append(query.urlEncoded()) - if (page > 1) { - append("&page=") - append(page) - } + append("&") } else { - if (page > 2) { - append("/pag/") - append(page) - append("/") - } else if (page > 1) { - append("/page/") - append(page) - append("/") - } + append("/?") } + append("page=") + append(page) } - val doc = webClient.httpGet(url).parseHtml() - return doc.select(".lc_galleries .thumb").map { div -> - val href = div.selectFirstOrThrow(".inner_thumb a").attrAsRelativeUrl("href") + return parseMangaList(webClient.httpGet(url).parseHtml()) + } + + protected open val selectGallery = ".preview_item" + protected open val selectGalleryLink = ".inner_thumb a" + protected open val selectGalleryImg = ".inner_thumb img" + protected open val selectGalleryTitle = "h2" + + protected open fun parseMangaList(doc: Document): List { + val regexBrackets = Regex("\\[[^]]+]|\\([^)]+\\)") + val regexSpaces = Regex("\\s+") + return doc.select(selectGallery).map { div -> + val href = div.selectFirstOrThrow(selectGalleryLink).attrAsRelativeUrl("href") Manga( id = generateUid(href), - title = div.select("h2.g_title").text(), + title = div.select(selectGalleryTitle).text().replace(regexBrackets, "") + .replace(regexSpaces, " ") + .trim(), altTitle = null, url = href, publicUrl = href.toAbsoluteUrl(domain), rating = RATING_UNKNOWN, isNsfw = isNsfwSource, - coverUrl = div.selectFirstOrThrow("img").src().orEmpty(), + coverUrl = div.selectFirstOrThrow(selectGalleryImg).src().orEmpty(), tags = emptySet(), state = null, author = null, @@ -87,37 +90,38 @@ internal class HentaiFox(context: MangaLoaderContext) : PagedMangaParser(context }.awaitAll().flattenTo(ArraySet(360)) } + protected open val pathTagUrl = "/tags/popular/pag/" + protected open val selectTags = ".tags_page ul.tags li" + private suspend fun getTags(page: Int): Set { - val url = "https://$domain/tags/popular/pag/$page/" - val root = webClient.httpGet(url).parseHtml() + val url = "https://$domain$pathTagUrl$page" + val root = webClient.httpGet(url).parseHtml().selectFirstOrThrow(selectTags) return root.parseTags() } - private fun Element.parseTags() = select(".list_tags a.tag_btn").mapToSet { + protected open fun Element.parseTags() = select("a.tag, .gallery_title a").mapToSet { val key = it.attr("href").removeSuffix('/').substringAfterLast('/') + val name = it.selectFirst(".item_name")?.text() ?: it.text() MangaTag( key = key, - title = it.selectFirstOrThrow("h3").text(), + title = name, source = source, ) } + protected open val selectTag = "div.tags:contains(Tags:) .tag_list" + protected open val selectAuthor = "ul.artists a.tag_btn" + protected open val urlReplaceBefore = "/g/" + protected open val urlReplaceAfter = "/gallery/" + protected open val selectLanguageChapter = "div.tags:contains(Languages:) .tag_list a span.tag" override suspend fun getDetails(manga: Manga): Manga { val doc = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml() - val urlChapters = manga.url.replace("/gallery/", "/g/") + "1/" + val urlChapters = manga.url.replace(urlReplaceBefore, urlReplaceAfter) + "1/" + val tag = doc.selectFirstOrThrow(selectTag) return manga.copy( - altTitle = null, - tags = doc.select("ul.tags a.tag_btn ").mapNotNullToSet { - val key = it.attr("href").removeSuffix('/').substringAfterLast('/') - MangaTag( - key = key, - title = it.html().substringBefore(" { - val doc = webClient.httpGet(seed.url.toAbsoluteUrl(domain)).parseHtml() - val root = doc.body().selectFirstOrThrow(".related_galleries") - return root.select("div.thumb").mapNotNull { div -> - val a = div.selectFirst(".inner_thumb a") ?: return@mapNotNull null - val href = a.attrAsRelativeUrl("href") - Manga( - id = generateUid(href), - url = href, - publicUrl = href.toAbsoluteUrl(a.host ?: domain), - altTitle = null, - title = div.selectFirstOrThrow("h2.g_title").text(), - author = null, - coverUrl = div.selectFirst("img")?.src().orEmpty(), - tags = emptySet(), - rating = RATING_UNKNOWN, - state = null, - isNsfw = isNsfwSource, - source = source, - ) - } + return parseMangaList(webClient.httpGet(seed.url.toAbsoluteUrl(domain)).parseHtml()) } + protected open val selectTotalPage = ".total_pages" + override suspend fun getPages(chapter: MangaChapter): List { val doc = webClient.httpGet(chapter.url.toAbsoluteUrl(domain)).parseHtml() - val totalPages = doc.selectFirstOrThrow(".total_pages").text().toInt() + val totalPages = doc.selectFirstOrThrow(selectTotalPage).text().toInt() val rawUrl = chapter.url.replace("/1/", "/") return (1..totalPages).map { val url = "$rawUrl$it/" @@ -171,9 +158,11 @@ internal class HentaiFox(context: MangaLoaderContext) : PagedMangaParser(context } } + protected open val idImg = "gimg" + override suspend fun getPageUrl(page: MangaPage): String { val doc = webClient.httpGet(page.url.toAbsoluteUrl(domain)).parseHtml() val root = doc.body() - return root.requireElementById("gimg").attrAsAbsoluteUrl("data-src") + return root.requireElementById(idImg).src() ?: root.parseFailed("Image src not found") } } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/galleryadults/all/AsmHentai.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/galleryadults/all/AsmHentai.kt new file mode 100644 index 00000000..2713dcc2 --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/galleryadults/all/AsmHentai.kt @@ -0,0 +1,30 @@ +package org.koitharu.kotatsu.parsers.site.galleryadults.all + +import org.jsoup.nodes.Element +import org.koitharu.kotatsu.parsers.MangaLoaderContext +import org.koitharu.kotatsu.parsers.MangaSourceParser +import org.koitharu.kotatsu.parsers.model.* +import org.koitharu.kotatsu.parsers.site.galleryadults.GalleryAdultsParser +import org.koitharu.kotatsu.parsers.util.mapToSet +import org.koitharu.kotatsu.parsers.util.removeSuffix + +@MangaSourceParser("ASMHENTAI", "AsmHentai", type = ContentType.HENTAI) +internal class AsmHentai(context: MangaLoaderContext) : + GalleryAdultsParser(context, MangaSource.ASMHENTAI, "asmhentai.com") { + override val selectGalleryLink = ".image a" + override val selectGalleryImg = ".image img" + override val pathTagUrl = "/tags/?page=" + override val selectAuthor = "div.tags:contains(Artists:) .tag_list a span.tag" + override val selectTotalPage = ".tp" + override val idImg = "fimg" + + override fun Element.parseTags() = select("a").mapToSet { + val key = it.attr("href").removeSuffix('/').substringAfterLast('/') + val name = it.selectFirst(".tag")?.html()?.substringBefore("<") ?: it.html().substringBefore("<") + MangaTag( + key = key, + title = name, + source = source, + ) + } +} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/galleryadults/all/HentaiEra.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/galleryadults/all/HentaiEra.kt new file mode 100644 index 00000000..2c58efa2 --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/galleryadults/all/HentaiEra.kt @@ -0,0 +1,75 @@ +package org.koitharu.kotatsu.parsers.site.galleryadults.all + +import org.koitharu.kotatsu.parsers.MangaLoaderContext +import org.koitharu.kotatsu.parsers.MangaSourceParser +import org.koitharu.kotatsu.parsers.model.* +import org.koitharu.kotatsu.parsers.site.galleryadults.GalleryAdultsParser +import org.koitharu.kotatsu.parsers.util.domain +import org.koitharu.kotatsu.parsers.util.oneOrThrowIfMany +import org.koitharu.kotatsu.parsers.util.parseHtml +import org.koitharu.kotatsu.parsers.util.selectFirstOrThrow +import org.koitharu.kotatsu.parsers.util.toAbsoluteUrl +import org.koitharu.kotatsu.parsers.util.urlEncoded + +@MangaSourceParser("HENTAIERA", "HentaiEra", type = ContentType.HENTAI) +internal class HentaiEra(context: MangaLoaderContext) : + GalleryAdultsParser(context, MangaSource.HENTAIERA, "hentaiera.com", 25) { + override val selectGallery = ".thumb" + override val pathTagUrl = "/tags/popular?page=" + override val selectTags = ".tags_section" + override val selectTag = ".galleries_info li:contains(Tags) div.info_tags" + override val selectAuthor = ".galleries_info li:contains(Artists) span.item_name" + override val urlReplaceBefore = "/gallery/" + override val urlReplaceAfter = "/view/" + override val selectLanguageChapter = ".galleries_info li:contains(Languages) div.info_tags .item_name" + + override suspend fun getListPage( + page: Int, + query: String?, + tags: Set?, + sortOrder: SortOrder, + ): List { + val tag = tags.oneOrThrowIfMany() + val url = buildString { + append("https://") + append(domain) + if (!tags.isNullOrEmpty()) { + append("/tag/") + append(tag?.key.orEmpty()) + append("/?") + + } else if (!query.isNullOrEmpty()) { + append("/search/?key=") + append(query.urlEncoded()) + append("&") + } else { + append("/?") + } + append("page=") + append(page) + } + return parseMangaList(webClient.httpGet(url).parseHtml()) + } + + override suspend fun getDetails(manga: Manga): Manga { + val doc = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml() + val urlChapters = manga.url.replace(urlReplaceBefore, urlReplaceAfter) + "1/" + val tag = doc.selectFirstOrThrow(selectTag) + return manga.copy( + tags = tag.parseTags(), + author = doc.selectFirst(selectAuthor)?.text(), + chapters = listOf( + MangaChapter( + id = manga.id, + name = manga.title, + number = 1, + url = urlChapters, + scanlator = null, + uploadDate = 0, + branch = doc.selectFirst(selectLanguageChapter)?.text(), + source = source, + ), + ), + ) + } +} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/galleryadults/all/HentaiFox.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/galleryadults/all/HentaiFox.kt new file mode 100644 index 00000000..3b72dd07 --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/galleryadults/all/HentaiFox.kt @@ -0,0 +1,71 @@ +package org.koitharu.kotatsu.parsers.site.galleryadults.all + +import org.jsoup.nodes.Element +import org.koitharu.kotatsu.parsers.MangaLoaderContext +import org.koitharu.kotatsu.parsers.MangaSourceParser +import org.koitharu.kotatsu.parsers.model.* +import org.koitharu.kotatsu.parsers.site.galleryadults.GalleryAdultsParser +import org.koitharu.kotatsu.parsers.util.* + +@MangaSourceParser("HENTAIFOX", "HentaiFox", type = ContentType.HENTAI) +internal class HentaiFox(context: MangaLoaderContext) : + GalleryAdultsParser(context, MangaSource.HENTAIFOX, "hentaifox.com") { + override val selectGallery = ".lc_galleries .thumb, .related_galleries .thumb" + override val selectTags = ".list_tags" + override val selectTag = "ul.tags" + override val urlReplaceBefore = "/gallery/" + override val urlReplaceAfter = "/g/" + override val selectLanguageChapter = "ul.languages a.tag_btn" + + override val selectTotalPage = ".total_pages" + + override suspend fun getListPage( + page: Int, + query: String?, + tags: Set?, + sortOrder: SortOrder, + ): List { + val tag = tags.oneOrThrowIfMany() + val url = buildString { + append("https://") + append(domain) + if (!tags.isNullOrEmpty()) { + append("/tag/") + append(tag?.key.orEmpty()) + if (page > 1) { + append("/pag/") + append(page) + append("/") + } + } else if (!query.isNullOrEmpty()) { + append("/search/?q=") + append(query.urlEncoded()) + if (page > 1) { + append("&page=") + append(page) + } + } else { + if (page > 2) { + append("/pag/") + append(page) + append("/") + } else if (page > 1) { + append("/page/") + append(page) + append("/") + } + } + } + return parseMangaList(webClient.httpGet(url).parseHtml()) + } + + override fun Element.parseTags() = select("a").mapToSet { + val key = it.attr("href").removeSuffix('/').substringAfterLast('/') + val name = it.html().substringBefore("<") + MangaTag( + key = key, + title = name, + source = source, + ) + } +} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/galleryadults/all/HentaiRox.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/galleryadults/all/HentaiRox.kt new file mode 100644 index 00000000..85dea867 --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/galleryadults/all/HentaiRox.kt @@ -0,0 +1,19 @@ +package org.koitharu.kotatsu.parsers.site.galleryadults.all + +import org.koitharu.kotatsu.parsers.MangaLoaderContext +import org.koitharu.kotatsu.parsers.MangaSourceParser +import org.koitharu.kotatsu.parsers.model.* +import org.koitharu.kotatsu.parsers.site.galleryadults.GalleryAdultsParser + +@MangaSourceParser("HENTAIROX", "HentaiRox", type = ContentType.HENTAI) +internal class HentaiRox(context: MangaLoaderContext) : + GalleryAdultsParser(context, MangaSource.HENTAIROX, "hentairox.com") { + override val selectGallery = ".thumb" + override val pathTagUrl = "/tags/popular?page=" + override val selectTags = ".gtags" + override val selectTag = "li:contains(Tags:)" + override val selectAuthor = "li:contains(Artists:) span.item_name" + override val urlReplaceBefore = "/gallery/" + override val urlReplaceAfter = "/view/" + override val selectLanguageChapter = "li:contains(Languages:) .item_name" +} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/galleryadults/all/NHentaiParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/galleryadults/all/NHentaiParser.kt new file mode 100644 index 00000000..c3254d0f --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/galleryadults/all/NHentaiParser.kt @@ -0,0 +1,79 @@ +package org.koitharu.kotatsu.parsers.site.galleryadults.all + +import org.jsoup.nodes.Element +import org.koitharu.kotatsu.parsers.MangaLoaderContext +import org.koitharu.kotatsu.parsers.MangaSourceParser +import org.koitharu.kotatsu.parsers.model.* +import org.koitharu.kotatsu.parsers.site.galleryadults.GalleryAdultsParser +import org.koitharu.kotatsu.parsers.util.* + +@MangaSourceParser("NHENTAI", "NHentai", type = ContentType.HENTAI) +internal class NHentaiParser(context: MangaLoaderContext) : + GalleryAdultsParser(context, MangaSource.NHENTAI, "nhentai.net", 25) { + override val selectGallery = "div.index-container:not(.index-popular) .gallery, #related-container .gallery" + override val selectGalleryLink = "a" + override val selectGalleryImg = "img" + override val selectGalleryTitle = ".caption" + override val pathTagUrl = "/tags/popular?page=" + override val selectTags = "#tag-container a" + override val selectTag = ".tag-container:contains(Tags:) span.tags" + override val selectAuthor = "#tags div.tag-container:contains(Artists:) span.name" + override val urlReplaceBefore = "/g/" + override val urlReplaceAfter = "/g/" + override val selectLanguageChapter = + ".tag-container:contains(Languages:) span.tags a:not(.tag-17249) span.name" // tag-17249 = translated + + override val selectTotalPage = ".num-pages" + override val idImg = "image-container" + + override suspend fun getListPage( + page: Int, + query: String?, + tags: Set?, + sortOrder: SortOrder, + ): List { + if (query.isNullOrEmpty() && tags != null && tags.size > 1) { + return getListPage(page, buildQuery(tags), emptySet(), sortOrder) + } + val url = buildString { + append("https://") + append(domain) + if (!tags.isNullOrEmpty()) { + val tag = tags.single() + append("/tag/") + append(tag.key) + append("/?") + + } else if (!query.isNullOrEmpty()) { + append("/search/?q=") + append(query.urlEncoded()) + append("&") + } else { + append("/?") + } + append("page=") + append(page) + } + return parseMangaList(webClient.httpGet(url).parseHtml()) + } + + override suspend fun getPageUrl(page: MangaPage): String { + val doc = webClient.httpGet(page.url.toAbsoluteUrl(domain)).parseHtml() + val root = doc.body() + return root.requireElementById(idImg).selectFirstOrThrow("img").src() ?: root.parseFailed("Image src not found") + } + + override fun Element.parseTags() = select("a").mapToSet { + val key = it.attr("href").removeSuffix('/').substringAfterLast('/') + val name = it.selectFirst(".name")?.text() ?: it.text() + MangaTag( + key = key, + title = name, + source = source, + ) + } + + private fun buildQuery(tags: Collection) = tags.joinToString(separator = " ") { tag -> + "tag:\"${tag.key}\"" + } +}