From cc98931147b59decce218c08d91f915f275e28a3 Mon Sep 17 00:00:00 2001 From: Draken <131387159+dragonx943@users.noreply.github.com> Date: Mon, 5 May 2025 13:11:12 +0700 Subject: [PATCH] [Kiutaku + Xiutaku] Add sources (#1755) Co-authored-by: Draken --- .github/summary.yaml | 2 +- .../parsers/site/gallery/GalleryParser.kt | 130 ++++++++++++++++++ .../parsers/site/gallery/all/Kiutaku.kt | 15 ++ .../parsers/site/gallery/vi/BuonDua.kt | 18 +++ .../parsers/site/gallery/zh/Xiutaku.kt | 18 +++ .../kotatsu/parsers/site/vi/BuonDuaParser.kt | 114 --------------- 6 files changed, 182 insertions(+), 115 deletions(-) create mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/site/gallery/GalleryParser.kt create mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/site/gallery/all/Kiutaku.kt create mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/site/gallery/vi/BuonDua.kt create mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/site/gallery/zh/Xiutaku.kt delete mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/BuonDuaParser.kt diff --git a/.github/summary.yaml b/.github/summary.yaml index ec64a145..9aaea001 100644 --- a/.github/summary.yaml +++ b/.github/summary.yaml @@ -1 +1 @@ -total: 1218 +total: 1220 diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/gallery/GalleryParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/gallery/GalleryParser.kt new file mode 100644 index 00000000..bfe3ca0b --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/gallery/GalleryParser.kt @@ -0,0 +1,130 @@ +package org.koitharu.kotatsu.parsers.site.gallery + +import org.koitharu.kotatsu.parsers.MangaLoaderContext +import org.koitharu.kotatsu.parsers.config.ConfigKey +import org.koitharu.kotatsu.parsers.core.LegacyMangaParser +import org.koitharu.kotatsu.parsers.model.* +import org.koitharu.kotatsu.parsers.util.* +import java.text.SimpleDateFormat +import java.util.* + +internal abstract class GalleryParser( + context: MangaLoaderContext, + source: MangaParserSource, + domain: String +) : LegacyMangaParser(context, source) { + + override val configKeyDomain = ConfigKey.Domain(domain) + + override fun onCreateConfig(keys: MutableCollection>) { + super.onCreateConfig(keys) + keys.add(userAgentKey) + } + + override val availableSortOrders: Set + get() = EnumSet.of(SortOrder.NEWEST, SortOrder.POPULARITY) + + override val filterCapabilities: MangaListFilterCapabilities + get() = MangaListFilterCapabilities( + isSearchSupported = true, + ) + + override suspend fun getFilterOptions(): MangaListFilterOptions = MangaListFilterOptions() + + override suspend fun getList(offset: Int, order: SortOrder, filter: MangaListFilter): List { + val url = urlBuilder().apply { + when { + !filter.query.isNullOrEmpty() -> addQueryParameter("search", filter.query) + filter.tags.isNotEmpty() -> addPathSegments(filter.tags.first().key) + order == SortOrder.POPULARITY -> addPathSegment("hot") + } + + addQueryParameter("start", offset.toString()) + }.build() + + val content = webClient.httpGet(url).parseHtml() + val currentPage = content.selectFirst("a.pagination-link.is-current")?.text()?.toIntOrNull() + val titlePage = content.selectFirst("head > title")?.text() + ?.substringAfter("page ", "") + ?.substringBefore(" ", "") + ?.toIntOrNull() + + if (titlePage != null && currentPage != titlePage) return emptyList() + + return content.select("div.items-row").map { el -> + val titleEl = el.selectFirstOrThrow("div.page-header a.item-link") + val relUrl = titleEl.attrOrThrow("href") + Manga( + id = generateUid(relUrl), + url = relUrl, + title = titleEl.text(), + altTitles = emptySet(), + publicUrl = relUrl.toAbsoluteUrl(domain), + rating = RATING_UNKNOWN, + contentRating = ContentRating.ADULT, + coverUrl = el.selectFirst("div.item-thumb img")?.attr("src"), + tags = el.select("div.item-tags > a.tag").mapNotNullToSet { tagEl -> + MangaTag( + title = tagEl.text(), + key = tagEl.attrAsRelativeUrlOrNull("href") + ?.removePrefix("/") ?: return@mapNotNullToSet null, + source = source, + ) + }, + state = MangaState.FINISHED, + authors = emptySet(), + largeCoverUrl = null, + description = null, + chapters = null, + source = source, + ) + } + } + + override suspend fun getDetails(manga: Manga): Manga { + val content = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml() + val description = content.selectFirst("div.article-info")?.text()?.trim() ?: "" + val df = SimpleDateFormat("HH:mm dd-MM-yyyy") + val time = content.selectFirst("div.article-info > small")?.text()?.trim() + val chapters = content.selectFirst("nav.pagination")?.select("a.pagination-link") + ?.mapChapters { index, element -> + val relUrl = element.attrAsRelativeUrl("href") + MangaChapter( + id = generateUid(relUrl), + title = null, + number = index + 1f, + volume = 0, + url = relUrl, + scanlator = null, + uploadDate = df.tryParse(time), + branch = null, + source = source, + ) + }.orEmpty() + return manga.copy(chapters = chapters, description = description) + } + + override suspend fun getPages(chapter: MangaChapter): List { + val content = webClient.httpGet(chapter.url.toAbsoluteUrl(domain)).parseHtml() + return content.selectFirstOrThrow("div.article-fulltext").select("p > img").mapNotNull { el -> + val url = el.attrOrNull("src") ?: return@mapNotNull null + MangaPage( + id = generateUid(url), + url = url, + preview = null, + source = source, + ) + } + } + + protected suspend fun fetchTags(): Set { + val root = webClient.httpGet("https://$domain").parseHtml() + return root.select("div#navbar-main a.navbar-item").map { a -> + MangaTag( + title = a.text(), + key = a.attr("href").removePrefix("/"), + source = source, + ) + }.toSet() + } +} \ No newline at end of file diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/gallery/all/Kiutaku.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/gallery/all/Kiutaku.kt new file mode 100644 index 00000000..c1d5e32e --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/gallery/all/Kiutaku.kt @@ -0,0 +1,15 @@ +package org.koitharu.kotatsu.parsers.site.gallery.all + +import org.koitharu.kotatsu.parsers.MangaLoaderContext +import org.koitharu.kotatsu.parsers.MangaSourceParser +import org.koitharu.kotatsu.parsers.model.MangaParserSource +import org.koitharu.kotatsu.parsers.config.ConfigKey +import org.koitharu.kotatsu.parsers.model.ContentType +import org.koitharu.kotatsu.parsers.model.MangaListFilterOptions +import org.koitharu.kotatsu.parsers.site.gallery.GalleryParser +import org.koitharu.kotatsu.parsers.Broken + +@Broken("Blocked by Cloudflare") +@MangaSourceParser("KIUTAKU", "Kiutaku", type = ContentType.OTHER) +internal class Kiutaku(context: MangaLoaderContext) : + GalleryParser(context, MangaParserSource.KIUTAKU, "kiutaku.com") diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/gallery/vi/BuonDua.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/gallery/vi/BuonDua.kt new file mode 100644 index 00000000..cdc9a42f --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/gallery/vi/BuonDua.kt @@ -0,0 +1,18 @@ +package org.koitharu.kotatsu.parsers.site.gallery.vi + +import org.koitharu.kotatsu.parsers.MangaLoaderContext +import org.koitharu.kotatsu.parsers.MangaSourceParser +import org.koitharu.kotatsu.parsers.model.MangaParserSource +import org.koitharu.kotatsu.parsers.config.ConfigKey +import org.koitharu.kotatsu.parsers.model.ContentType +import org.koitharu.kotatsu.parsers.site.gallery.GalleryParser + +@MangaSourceParser("BUONDUA", "Buon Dua", "vi", type = ContentType.OTHER) +internal class BuonDua(context: MangaLoaderContext) : + GalleryParser(context, MangaParserSource.BUONDUA, "buondua.com") { + + override val configKeyDomain: ConfigKey.Domain = ConfigKey.Domain( + "buondua.com", + "buondua.us", + ) +} \ No newline at end of file diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/gallery/zh/Xiutaku.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/gallery/zh/Xiutaku.kt new file mode 100644 index 00000000..9e79cc05 --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/gallery/zh/Xiutaku.kt @@ -0,0 +1,18 @@ +package org.koitharu.kotatsu.parsers.site.gallery.zh + +import org.koitharu.kotatsu.parsers.MangaLoaderContext +import org.koitharu.kotatsu.parsers.MangaSourceParser +import org.koitharu.kotatsu.parsers.model.MangaParserSource +import org.koitharu.kotatsu.parsers.config.ConfigKey +import org.koitharu.kotatsu.parsers.model.ContentType +import org.koitharu.kotatsu.parsers.model.MangaListFilterOptions +import org.koitharu.kotatsu.parsers.site.gallery.GalleryParser + +@MangaSourceParser("XIUTAKU", "Xiutaku", "zh", type = ContentType.OTHER) +internal class Xiutaku(context: MangaLoaderContext) : + GalleryParser(context, MangaParserSource.XIUTAKU, "xiutaku.com") { + + override suspend fun getFilterOptions(): + MangaListFilterOptions = MangaListFilterOptions(availableTags = fetchTags()) + +} \ No newline at end of file diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/BuonDuaParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/BuonDuaParser.kt deleted file mode 100644 index 6a2ac288..00000000 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/BuonDuaParser.kt +++ /dev/null @@ -1,114 +0,0 @@ -package org.koitharu.kotatsu.parsers.site.vi - -import org.koitharu.kotatsu.parsers.MangaLoaderContext -import org.koitharu.kotatsu.parsers.MangaSourceParser -import org.koitharu.kotatsu.parsers.config.ConfigKey -import org.koitharu.kotatsu.parsers.core.LegacyMangaParser -import org.koitharu.kotatsu.parsers.model.* -import org.koitharu.kotatsu.parsers.util.* -import java.text.SimpleDateFormat -import java.util.* - -@MangaSourceParser("BUONDUA", "Buon Dua", type = ContentType.OTHER) -internal class BuonDuaParser(context: MangaLoaderContext) : LegacyMangaParser(context, MangaParserSource.BUONDUA) { - - override val configKeyDomain: ConfigKey.Domain = ConfigKey.Domain( - "buondua.com", - "buondua.us", - ) - - override val availableSortOrders: Set - get() = EnumSet.of(SortOrder.NEWEST, SortOrder.POPULARITY) - - override val filterCapabilities: MangaListFilterCapabilities - get() = MangaListFilterCapabilities( - isSearchSupported = true, - ) - - override suspend fun getFilterOptions(): MangaListFilterOptions = MangaListFilterOptions() - - override suspend fun getDetails(manga: Manga): Manga { - val content = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml() - val df = SimpleDateFormat("HH:mm dd-MM-yyyy") - val time = content.selectFirst("div.article-info > small")?.text()?.trim() - val chapters = content.selectFirst("nav.pagination")?.select("a.pagination-link") - ?.mapChapters { index, element -> - val relUrl = element.attrAsRelativeUrl("href") - MangaChapter( - id = generateUid(relUrl), - title = null, - number = index + 1f, - volume = 0, - url = relUrl, - scanlator = null, - uploadDate = df.tryParse(time), - branch = null, - source = source, - ) - }.orEmpty() - return manga.copy(chapters = chapters) - } - - override suspend fun getList(offset: Int, order: SortOrder, filter: MangaListFilter): List { - val url = urlBuilder().apply { - when { - !filter.query.isNullOrEmpty() -> addQueryParameter("search", filter.query) - filter.tags.isNotEmpty() -> addPathSegments(filter.tags.first().key) - order == SortOrder.POPULARITY -> addPathSegment("hot") - } - - addQueryParameter("start", offset.toString()) - }.build() - - val content = webClient.httpGet(url).parseHtml() - val currentPage = content.selectFirst("a.pagination-link.is-current")?.text()?.toIntOrNull() - val titlePage = content.selectFirst("head > title")?.text() - ?.substringAfter("page ", "") - ?.substringBefore(" ", "") - ?.toIntOrNull() - - if (titlePage != null && currentPage != titlePage) return emptyList() - - return content.select("div.items-row").map { el -> - val titleEl = el.selectFirstOrThrow("div.page-header a.item-link") - val relUrl = titleEl.attrOrThrow("href") - Manga( - id = generateUid(relUrl), - url = relUrl, - title = titleEl.text(), - altTitles = emptySet(), - publicUrl = relUrl.toAbsoluteUrl(domain), - rating = RATING_UNKNOWN, - contentRating = ContentRating.ADULT, - coverUrl = el.selectFirst("div.item-thumb img")?.attr("src"), - tags = el.select("div.item-tags > a.tag").mapNotNullToSet { tagEl -> - MangaTag( - title = tagEl.text(), - key = tagEl.attrAsRelativeUrlOrNull("href") - ?.removePrefix("/") ?: return@mapNotNullToSet null, - source = source, - ) - }, - state = MangaState.FINISHED, - authors = emptySet(), - largeCoverUrl = null, - description = null, - chapters = null, - source = source, - ) - } - } - - override suspend fun getPages(chapter: MangaChapter): List { - val content = webClient.httpGet(chapter.url.toAbsoluteUrl(domain)).parseHtml() - return content.selectFirstOrThrow("div.article-fulltext").select("p > img").mapNotNull { el -> - val url = el.attrOrNull("src") ?: return@mapNotNull null - MangaPage( - id = generateUid(url), - url = url, - preview = null, - source = source, - ) - } - } -}