From a5219ceb6c8e228db99748e97a8634b1ba55e6d1 Mon Sep 17 00:00:00 2001 From: devi Date: Sat, 23 Dec 2023 18:59:48 +0100 Subject: [PATCH] New Template GattsuParser Add Baozimh, MuitoHentai, TopReadManhwa, HentaiSeason, HentaiTokyo, MundoHentaiOficial, UniversoHentai --- .../parsers/site/gattsu/GattsuParser.kt | 150 ++++++++++++++ .../parsers/site/gattsu/pt/HentaiSeason.kt | 11 + .../parsers/site/gattsu/pt/HentaiTokyo.kt | 13 ++ .../site/gattsu/pt/MundoHentaiOficial.kt | 48 +++++ .../parsers/site/gattsu/pt/UniversoHentai.kt | 50 +++++ .../parsers/site/madara/en/TopReadManhwa.kt | 12 ++ .../kotatsu/parsers/site/pt/MuitoHentai.kt | 126 +++++++++++ .../kotatsu/parsers/site/zh/Baozimh.kt | 195 ++++++++++++++++++ 8 files changed, 605 insertions(+) create mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/site/gattsu/GattsuParser.kt create mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/site/gattsu/pt/HentaiSeason.kt create mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/site/gattsu/pt/HentaiTokyo.kt create mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/site/gattsu/pt/MundoHentaiOficial.kt create mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/site/gattsu/pt/UniversoHentai.kt create mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/en/TopReadManhwa.kt create mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/site/pt/MuitoHentai.kt create mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/site/zh/Baozimh.kt diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/gattsu/GattsuParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/gattsu/GattsuParser.kt new file mode 100644 index 00000000..80bb6a6b --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/gattsu/GattsuParser.kt @@ -0,0 +1,150 @@ +package org.koitharu.kotatsu.parsers.site.gattsu + +import org.jsoup.nodes.Document +import org.jsoup.nodes.Element +import org.koitharu.kotatsu.parsers.MangaLoaderContext +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.* + +internal abstract class GattsuParser( + context: MangaLoaderContext, + source: MangaSource, + domain: String, + pageSize: Int = 20, +) : PagedMangaParser(context, source, pageSize) { + + override val availableSortOrders: Set = EnumSet.of(SortOrder.UPDATED) + + override val configKeyDomain = ConfigKey.Domain(domain) + + override val isMultipleTagsSupported = false + + protected open val tagPrefix = "tag" + + override suspend fun getListPage(page: Int, filter: MangaListFilter?): List { + + val url = buildString { + append("https://") + append(domain) + when (filter) { + + is MangaListFilter.Search -> { + append("/page/") + append(page.toString()) + append("/?s=") + append(filter.query.urlEncoded()) + } + + is MangaListFilter.Advanced -> { + + filter.tags.oneOrThrowIfMany()?.let { + append("/$tagPrefix/") + append(it.key) + } + + append("/page/") + append(page.toString()) + + } + + null -> { + append("/page/") + append(page.toString()) + } + } + } + return parseMangaList(webClient.httpGet(url).parseHtml()) + } + + protected open fun parseMangaList(doc: Document): List { + return doc.select("div.lista ul li, div.videos div.video").mapNotNull { li -> + val a = li.selectFirstOrThrow("a") + val href = a.attrAsAbsoluteUrl("href") + if (!href.contains(domain)) { + //Some sources include ads in manga lists + return@mapNotNull null + } + Manga( + id = generateUid(href), + url = href, + publicUrl = href, + title = li.selectLastOrThrow(".thumb-titulo, .video-titulo").text(), + coverUrl = li.selectFirst("img")?.src().orEmpty(), + altTitle = null, + rating = RATING_UNKNOWN, + tags = emptySet(), + description = null, + state = null, + author = null, + isNsfw = isNsfwSource, + source = source, + ) + } + } + + protected open val tagUrl = "generos" + + override suspend fun getAvailableTags(): Set { + val doc = webClient.httpGet("https://$domain/$tagUrl/").parseHtml() + return doc.selectLastOrThrow(".meio-conteudo p, div.lista-tags ul").parseTags() + } + + protected open fun Element.parseTags() = select("a").mapToSet { + val key = it.attr("href").removeSuffix("/").substringAfterLast("/") + val name = it.selectFirst(".tag-titulo")?.text() ?: key + MangaTag( + key = key, + title = name, + source = source, + ) + } + + override suspend fun getDetails(manga: Manga): Manga { + val doc = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml() + val urlChapter = doc.selectFirstOrThrow("ul.post-fotos li a, ul.paginaPostBotoes a").attrAsAbsoluteUrl("href") + return manga.copy( + description = doc.selectFirst("div.post-texto")?.html(), + tags = doc.selectFirst(".post-itens li:contains(Tags), .paginaPostInfo li:contains(Categorias)") + ?.parseTags().orEmpty(), + author = doc.selectFirst(".post-itens li:contains(Autor) a, .paginaPostInfo li:contains(Artista) a") + ?.text(), + chapters = listOf( + MangaChapter( + id = manga.id, + name = manga.title, + number = 1, + url = urlChapter, + scanlator = null, + uploadDate = 0, + branch = null, + source = source, + ), + ), + ) + } + + override suspend fun getPages(chapter: MangaChapter): List { + val doc = webClient.httpGet(chapter.url.toAbsoluteUrl(domain)).parseHtml() + val totalPages = + doc.selectLastOrThrow("div.galeria-paginacao span").text().substringAfterLast("- ").substringBeforeLast(')') + .toInt() + val rawUrl = chapter.url.substringBeforeLast("=") + return (1..totalPages).map { + val url = "$rawUrl=$it" + MangaPage( + id = generateUid(url), + url = url, + preview = null, + source = source, + ) + } + } + + override suspend fun getPageUrl(page: MangaPage): String { + val doc = webClient.httpGet(page.url.toAbsoluteUrl(domain)).parseHtml() + return doc.selectFirstOrThrow("div.galeria-foto img").src() ?: doc.parseFailed("Image src not found") + } +} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/gattsu/pt/HentaiSeason.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/gattsu/pt/HentaiSeason.kt new file mode 100644 index 00000000..c58f23e5 --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/gattsu/pt/HentaiSeason.kt @@ -0,0 +1,11 @@ +package org.koitharu.kotatsu.parsers.site.gattsu.pt + +import org.koitharu.kotatsu.parsers.MangaLoaderContext +import org.koitharu.kotatsu.parsers.MangaSourceParser +import org.koitharu.kotatsu.parsers.model.ContentType +import org.koitharu.kotatsu.parsers.model.MangaSource +import org.koitharu.kotatsu.parsers.site.gattsu.GattsuParser + +@MangaSourceParser("HENTAISEASON", "HentaiSeason", type = ContentType.HENTAI) +internal class HentaiSeason(context: MangaLoaderContext) : + GattsuParser(context, MangaSource.HENTAISEASON, "hentaiseason.com") diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/gattsu/pt/HentaiTokyo.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/gattsu/pt/HentaiTokyo.kt new file mode 100644 index 00000000..614f272e --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/gattsu/pt/HentaiTokyo.kt @@ -0,0 +1,13 @@ +package org.koitharu.kotatsu.parsers.site.gattsu.pt + +import org.koitharu.kotatsu.parsers.MangaLoaderContext +import org.koitharu.kotatsu.parsers.MangaSourceParser +import org.koitharu.kotatsu.parsers.model.ContentType +import org.koitharu.kotatsu.parsers.model.MangaSource +import org.koitharu.kotatsu.parsers.site.gattsu.GattsuParser + +@MangaSourceParser("HENTAITOKYO", "HentaiTokyo", type = ContentType.HENTAI) +internal class HentaiTokyo(context: MangaLoaderContext) : + GattsuParser(context, MangaSource.HENTAITOKYO, "hentaitokyo.net") { + override val tagUrl = "tags" +} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/gattsu/pt/MundoHentaiOficial.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/gattsu/pt/MundoHentaiOficial.kt new file mode 100644 index 00000000..bdb95dce --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/gattsu/pt/MundoHentaiOficial.kt @@ -0,0 +1,48 @@ +package org.koitharu.kotatsu.parsers.site.gattsu.pt + +import org.jsoup.nodes.Document +import org.koitharu.kotatsu.parsers.MangaLoaderContext +import org.koitharu.kotatsu.parsers.MangaSourceParser +import org.koitharu.kotatsu.parsers.model.ContentType +import org.koitharu.kotatsu.parsers.model.Manga +import org.koitharu.kotatsu.parsers.model.MangaSource +import org.koitharu.kotatsu.parsers.model.RATING_UNKNOWN +import org.koitharu.kotatsu.parsers.site.gattsu.GattsuParser +import org.koitharu.kotatsu.parsers.util.attrAsAbsoluteUrl +import org.koitharu.kotatsu.parsers.util.domain +import org.koitharu.kotatsu.parsers.util.generateUid +import org.koitharu.kotatsu.parsers.util.selectLastOrThrow +import org.koitharu.kotatsu.parsers.util.src + +@MangaSourceParser("MUNDOHENTAIOFICIAL", "MundoHentaiOficial", type = ContentType.HENTAI) +internal class MundoHentaiOficial(context: MangaLoaderContext) : + GattsuParser(context, MangaSource.MUNDOHENTAIOFICIAL, "mundohentaioficial.com") { + + override val tagUrl = "tags" + + override fun parseMangaList(doc: Document): List { + return doc.select("div.lista ul li, div.videos div.video").mapNotNull { li -> + val a = li.selectLastOrThrow("a") + val href = a.attrAsAbsoluteUrl("href") + if (!href.contains(domain)) { + //Some sources include ads in manga lists + return@mapNotNull null + } + Manga( + id = generateUid(href), + url = href, + publicUrl = href, + title = li.selectLastOrThrow(".thumb-titulo, .video-titulo").text(), + coverUrl = li.selectFirst("img")?.src().orEmpty(), + altTitle = null, + rating = RATING_UNKNOWN, + tags = emptySet(), + description = null, + state = null, + author = null, + isNsfw = isNsfwSource, + source = source, + ) + } + } +} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/gattsu/pt/UniversoHentai.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/gattsu/pt/UniversoHentai.kt new file mode 100644 index 00000000..1133b0d8 --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/gattsu/pt/UniversoHentai.kt @@ -0,0 +1,50 @@ +package org.koitharu.kotatsu.parsers.site.gattsu.pt + +import org.jsoup.nodes.Element +import org.koitharu.kotatsu.parsers.MangaLoaderContext +import org.koitharu.kotatsu.parsers.MangaSourceParser +import org.koitharu.kotatsu.parsers.model.ContentType +import org.koitharu.kotatsu.parsers.model.MangaChapter +import org.koitharu.kotatsu.parsers.model.MangaPage +import org.koitharu.kotatsu.parsers.model.MangaSource +import org.koitharu.kotatsu.parsers.model.MangaTag +import org.koitharu.kotatsu.parsers.site.gattsu.GattsuParser +import org.koitharu.kotatsu.parsers.util.* + +@MangaSourceParser("UNIVERSOHENTAI", "UniversoHentai", type = ContentType.HENTAI) +internal class UniversoHentai(context: MangaLoaderContext) : + GattsuParser(context, MangaSource.UNIVERSOHENTAI, "universohentai.com") { + + override val tagPrefix = "category" + + override suspend fun getAvailableTags(): Set { + val doc = webClient.httpGet("https://$domain/tags/").parseHtml() + return doc.requireElementById("menu-topo").parseTags() + } + + override fun Element.parseTags() = select("a").mapNotNullToSet { + if (!it.attr("href").contains("/category/")) return@mapNotNullToSet null + val key = it.attr("href").removeSuffix("/").substringAfterLast("/") + MangaTag( + key = key, + title = it.text(), + source = source, + ) + } + + override suspend fun getPages(chapter: MangaChapter): List { + val images = webClient.httpGet(chapter.url.toAbsoluteUrl(domain)).parseHtml().requireElementById("galeria") + .select(".galeria-foto img") + return images.map { img -> + val urlImages = img.src() ?: img.parseFailed("Image src not found") + MangaPage( + id = generateUid(urlImages), + url = urlImages, + preview = null, + source = source, + ) + } + } + + override suspend fun getPageUrl(page: MangaPage): String = page.url.toAbsoluteUrl(domain) +} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/en/TopReadManhwa.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/en/TopReadManhwa.kt new file mode 100644 index 00000000..febb9201 --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/en/TopReadManhwa.kt @@ -0,0 +1,12 @@ +package org.koitharu.kotatsu.parsers.site.madara.en + +import org.koitharu.kotatsu.parsers.MangaLoaderContext +import org.koitharu.kotatsu.parsers.MangaSourceParser +import org.koitharu.kotatsu.parsers.model.MangaSource +import org.koitharu.kotatsu.parsers.site.madara.MadaraParser + +@MangaSourceParser("TOPREADMANHWA", "TopReadManhwa", "en") +internal class TopReadManhwa(context: MangaLoaderContext) : + MadaraParser(context, MangaSource.TOPREADMANHWA, "topreadmanhwa.com") { + override val datePattern = "MM/dd/yyyy" +} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pt/MuitoHentai.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pt/MuitoHentai.kt new file mode 100644 index 00000000..4fde9118 --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pt/MuitoHentai.kt @@ -0,0 +1,126 @@ +package org.koitharu.kotatsu.parsers.site.pt + +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("MUITOHENTAI", "MuitoHentai", "pt", ContentType.HENTAI) +class MuitoHentai(context: MangaLoaderContext) : PagedMangaParser(context, MangaSource.MUITOHENTAI, 24) { + + override val availableSortOrders: Set = EnumSet.of(SortOrder.POPULARITY) + + override val configKeyDomain = ConfigKey.Domain("www.muitohentai.com") + + override val isMultipleTagsSupported = false + + override suspend fun getListPage(page: Int, filter: MangaListFilter?): List { + + val url = buildString { + append("https://") + append(domain) + when (filter) { + + is MangaListFilter.Search -> { + if (page > 1) return emptyList() + append("/buscar-manga/?q=") + append(filter.query.urlEncoded()) + } + + is MangaListFilter.Advanced -> { + append("/mangas") + + filter.tags.oneOrThrowIfMany()?.let { + append("/genero/") + append(it.key) + } + + append('/') + append(page.toString()) + append('/') + } + + null -> { + append("/mangas/") + append(page.toString()) + append('/') + } + } + } + val doc = webClient.httpGet(url).parseHtml() + return doc.requireElementById("archive-content").select("article").map { div -> + val a = div.selectFirstOrThrow("a") + val href = a.attrAsAbsoluteUrl("href") + Manga( + id = generateUid(href), + url = href, + publicUrl = href, + title = div.selectLastOrThrow("h3").text(), + coverUrl = div.selectFirst("img")?.src().orEmpty(), + altTitle = null, + rating = RATING_UNKNOWN, + tags = emptySet(), + description = null, + state = null, + author = null, + isNsfw = isNsfwSource, + source = source, + ) + } + } + + override suspend fun getAvailableTags(): Set { + val doc = webClient.httpGet("https://$domain/generos-dos-mangas/").parseHtml() + return doc.select("div.content a.profileSideBar").mapNotNullToSet { a -> + MangaTag( + key = a.attr("href").removeSuffix("/").substringAfterLast("/"), + title = a.text(), + source = source, + ) + } + } + + override suspend fun getDetails(manga: Manga): Manga { + val doc = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml() + return manga.copy( + description = doc.selectFirstOrThrow(".backgroundpost:contains(Sinopse)").html(), + tags = doc.select("a.genero_btn").mapNotNullToSet { a -> + MangaTag( + key = a.attr("href").removeSuffix("/").substringAfterLast("/"), + title = a.text(), + source = source, + ) + }, + chapters = doc.select(".backgroundpost h3 a").mapChapters(reversed = true) { i, a -> + val href = a.attrAsAbsoluteUrl("href") + MangaChapter( + id = generateUid(href), + name = a.text(), + number = i + 1, + url = href, + scanlator = null, + uploadDate = 0, + branch = null, + source = source, + ) + }, + ) + } + + override suspend fun getPages(chapter: MangaChapter): List { + val doc = webClient.httpGet(chapter.url.toAbsoluteUrl(domain)).parseHtml() + val data = doc.selectFirstOrThrow("script:containsData(var arr = [)").data() + val images = data.substringAfter("[").substringBefore("];").replace("\"", "").split(",") + return images.map { img -> + MangaPage( + id = generateUid(img), + url = img, + preview = null, + source = source, + ) + } + } +} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/zh/Baozimh.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/zh/Baozimh.kt new file mode 100644 index 00000000..52b46ed7 --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/zh/Baozimh.kt @@ -0,0 +1,195 @@ +package org.koitharu.kotatsu.parsers.site.zh + +import androidx.collection.ArrayMap +import org.json.JSONArray +import org.jsoup.nodes.Document +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 org.koitharu.kotatsu.parsers.util.json.mapJSON +import java.util.* + +@MangaSourceParser("BAOZIMH", "Baozimh", "zh") +internal class Baozimh(context: MangaLoaderContext) : + PagedMangaParser(context, MangaSource.BAOZIMH, pageSize = 36) { + + override val availableSortOrders: Set = EnumSet.of(SortOrder.POPULARITY) + + override val availableStates: Set = EnumSet.of(MangaState.ONGOING, MangaState.FINISHED) + + override val configKeyDomain = ConfigKey.Domain("www.baozimh.com") + + override val isMultipleTagsSupported = false + + private val tagsMap = SuspendLazy(::parseTags) + + override suspend fun getListPage(page: Int, filter: MangaListFilter?): List { + + when (filter) { + is MangaListFilter.Search -> { + if (page > 1) return emptyList() + val url = buildString { + append("https://") + append(domain) + append("/search?q=") + append(filter.query.urlEncoded()) + } + return parseMangaListSearch(webClient.httpGet(url).parseHtml()) + } + + is MangaListFilter.Advanced -> { + val url = buildString { + append("https://") + append(domain) + append("/api/bzmhq/amp_comic_list?filter=*®ion=all") + + if (filter.tags.isNotEmpty()) { + filter.tags.oneOrThrowIfMany()?.let { + append("&type=") + append(it.key) + } + } else { + append("&type=all") + } + + if (filter.states.isNotEmpty()) { + filter.states.oneOrThrowIfMany()?.let { + append("&state=") + append( + when (it) { + MangaState.ONGOING -> "serial" + MangaState.FINISHED -> "pub" + else -> "all" + }, + ) + } + } else { + append("&state=all") + } + + append("&limit=36&page=") + append(page.toString()) + } + + return parseMangaList(webClient.httpGet(url).parseJson().getJSONArray("items")) + } + + null -> { + val url = buildString { + append("https://") + append(domain) + append("/api/bzmhq/amp_comic_list?filter=*®ion=all&type=all&state=all&limit=36&page=") + append(page.toString()) + } + return parseMangaList(webClient.httpGet(url).parseJson().getJSONArray("items")) + } + } + } + + private fun parseMangaList(json: JSONArray): List { + return json.mapJSON { j -> + val href = "https://$domain/comic/" + j.getString("comic_id") + Manga( + id = generateUid(href), + url = href, + publicUrl = href, + coverUrl = "https://static-tw${domain.removePrefix("www")}/cover/" + j.getString("topic_img"), + title = j.getString("name"), + altTitle = null, + rating = RATING_UNKNOWN, + tags = emptySet(), + author = j.getString("author"), + state = null, + source = source, + isNsfw = isNsfwSource, + ) + } + } + + private fun parseMangaListSearch(doc: Document): List { + return doc.select("div.comics-card").map { div -> + val href = div.selectFirstOrThrow("a").attrAsRelativeUrl("href") + Manga( + id = generateUid(href), + url = href, + publicUrl = href, + coverUrl = div.selectFirst("amp-img")?.src().orEmpty(), + title = div.selectFirstOrThrow(".comics-card__title h3").text(), + altTitle = null, + rating = RATING_UNKNOWN, + tags = emptySet(), + author = null, + state = null, + source = source, + isNsfw = isNsfwSource, + ) + } + } + + override suspend fun getAvailableTags(): Set { + return tagsMap.get().values.toSet() + } + + private suspend fun parseTags(): Map { + val tagElements = webClient.httpGet("https://$domain/classify").parseHtml() + .select("div.nav")[3].select("a.item:not(.active)") + val tagMap = ArrayMap(tagElements.size) + for (el in tagElements) { + val name = el.text() + if (name.isEmpty()) continue + tagMap[name] = MangaTag( + key = el.attr("href").substringAfter("type=").substringBefore("&"), + title = name, + source = source, + ) + } + return tagMap + } + + override suspend fun getDetails(manga: Manga): Manga { + val doc = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml() + val state = doc.selectFirstOrThrow(".tag-list span.tag").text() + val tagMap = tagsMap.get() + val selectTag = doc.select(".tag-list span.tag").drop(1) + val tags = selectTag.mapNotNullToSet { tagMap[it.text()] } + return manga.copy( + description = doc.selectFirst(".comics-detail__desc")?.text().orEmpty(), + state = when (state) { + "連載中" -> MangaState.ONGOING + "已完結" -> MangaState.FINISHED + else -> null + }, + tags = tags, + chapters = doc.requireElementById("chapter-items").select("div.comics-chapters a") + .mapChapters(reversed = true) { i, a -> + val url = a.attrAsRelativeUrl("href").toAbsoluteUrl(domain) + MangaChapter( + id = generateUid(url), + name = a.selectFirstOrThrow("span").text(), + number = i + 1, + url = url, + scanlator = null, + uploadDate = 0, + branch = null, + source = source, + ) + }, + ) + } + + override suspend fun getPages(chapter: MangaChapter): List { + val doc = webClient.httpGet(chapter.url.toAbsoluteUrl(domain)).parseHtml().requireElementById("__nuxt") + return doc.select("button.pure-button").map { btn -> + val urlPage = btn.attr("on").substringAfter(": '").substringBefore("?t=") + MangaPage( + id = generateUid(urlPage), + url = urlPage, + preview = null, + source = source, + ) + } + } +}