From 4d3adaa1435cde1d3fff5c9c6aec2d59e7f2b22b Mon Sep 17 00:00:00 2001 From: Draken Date: Mon, 10 Mar 2025 13:51:22 +0000 Subject: [PATCH 1/5] [Kumapage] Add source --- .github/summary.yaml | 2 +- .../kotatsu/parsers/site/id/Kumapage.kt | 168 ++++++++++++++++++ 2 files changed, 169 insertions(+), 1 deletion(-) create mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/site/id/Kumapage.kt diff --git a/.github/summary.yaml b/.github/summary.yaml index 9274529e..9f0e625c 100644 --- a/.github/summary.yaml +++ b/.github/summary.yaml @@ -1 +1 @@ -total: 1194 \ No newline at end of file +total: 1195 \ No newline at end of file diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/id/Kumapage.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/id/Kumapage.kt new file mode 100644 index 00000000..dea3a885 --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/id/Kumapage.kt @@ -0,0 +1,168 @@ +package org.koitharu.kotatsu.parsers.site.id + +import okhttp3.Headers +import okhttp3.HttpUrl.Companion.toHttpUrl +import org.jsoup.nodes.Document +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.LegacyPagedMangaParser +import org.koitharu.kotatsu.parsers.model.* +import org.koitharu.kotatsu.parsers.util.* +import java.text.SimpleDateFormat +import java.util.* + +@MangaSourceParser("KUMAPAGE", "Kumapage", "id") +internal class Kumapage(context: MangaLoaderContext) : + LegacyPagedMangaParser(context, MangaParserSource.KUMAPAGE, 14) { + + override val configKeyDomain: ConfigKey.Domain + get() = ConfigKey.Domain("www.kumapage.com") + + override fun onCreateConfig(keys: MutableCollection>) { + super.onCreateConfig(keys) + keys.add(userAgentKey) + } + + override val availableSortOrders: Set + get() = EnumSet.of(SortOrder.UPDATED) + + override val filterCapabilities: MangaListFilterCapabilities + get() = MangaListFilterCapabilities( + isMultipleTagsSupported = true, + isSearchSupported = true, + isSearchWithFiltersSupported = true, + ) + + override suspend fun getFilterOptions() = MangaListFilterOptions( + availableTags = fetchTags() + ) + + override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + return when { + !filter.query.isNullOrEmpty() -> { + val genre = if (filter.tags.isNotEmpty()) filter.tags.first().key else "all" + val url = "https://$domain/search-process/" + val headers = Headers.Builder() + .add("Content-Type", "application/x-www-form-urlencoded") + .build() + val response = webClient.httpPost(url.toHttpUrl(), payload = "view=1&keyword=${filter.query}&genre=$genre", headers).parseHtml() + parseSearchList(response) + } + + filter.tags.isNotEmpty() -> { + val tags = filter.tags.joinToString("&genre[]=") { it.key } + val url = "https://$domain/daftar-komik?page=$page&genre[]=$tags" + val response = webClient.httpGet(url.toHttpUrl()).parseHtml() + parseMangaList(response) + } + + else -> { + val url = "https://$domain/daftar-komik?page=$page" + val response = webClient.httpGet(url.toHttpUrl()).parseHtml() + parseMangaList(response) + } + } + } + + private fun parseSearchList(doc: Document): List { + return doc.select("div.item").map { item -> + val a = item.selectFirst("a.header") + val img = item.selectFirst("div.image img") + val url = a?.attr("href") ?: "" + val title = a?.text() ?: "" + Manga( + id = generateUid(url), + url = url, + publicUrl = url, + title = title, + altTitles = emptySet(), + authors = emptySet(), + description = item.selectFirst("div.description p")?.text(), + tags = emptySet(), + rating = RATING_UNKNOWN, + state = null, + coverUrl = img?.attr("src"), + contentRating = if (isNsfwSource) ContentRating.ADULT else null, + source = source, + ) + } + } + + private fun parseMangaList(doc: Document): List { + return doc.select("div#daftar-komik a.ui.link").map { item -> + val title = item.selectFirst("p.nama-full")?.text() ?: "" + val img = item.selectFirst("img")?.attr("src") ?: "" + val url = item.attr("href") ?: "" + + Manga( + id = generateUid(url), + url = url, + publicUrl = url, + title = title, + altTitles = emptySet(), + authors = emptySet(), + description = null, + tags = emptySet(), + rating = RATING_UNKNOWN, + state = null, + coverUrl = img, + contentRating = if (isNsfwSource) ContentRating.ADULT else null, + source = source, + ) + } + } + + override suspend fun getDetails(manga: Manga): Manga { + val doc = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml() + val state = if (doc.selectFirst("div.detail-singular:has(p:contains(Status)) + p") + ?.text() == "Active") { MangaState.ONGOING } else { MangaState.FINISHED } + val description = doc.selectFirst("td:contains(Synopsis) + td")?.text() + val tags = doc.select("div.categories p.category").map { tag -> + MangaTag(title = tag.text(), key = "", source = source) + }.toSet() + + val chapters = doc.select("tbody#result-comic tr").mapIndexed { i, row -> + MangaChapter( + id = generateUid(row.select("td:nth-child(4) a").attr("href")), + title = row.select("td:nth-child(2)").text(), + number = i + 1f, + volume = 0, + url = row.select("td:nth-child(4) a").attr("href"), + scanlator = null, + uploadDate = SimpleDateFormat("yyyy-MM-dd HH:mm:ss").tryParse(row.select("td:nth-child(3)").text()), + branch = null, + source = source, + ) + } + + return manga.copy( + description = description, + state = state, + tags = tags, + chapters = chapters, + ) + } + + override suspend fun getPages(chapter: MangaChapter): List { + val doc = webClient.httpGet(chapter.url.toAbsoluteUrl(domain)).parseHtml() + return doc.select("div.content_place img").mapNotNull { img -> + val url = img.attr("src") + MangaPage( + id = generateUid(url), + url = url, + preview = null, + source = source, + ) + } + } + + private suspend fun fetchTags(): Set { + val doc = webClient.httpGet("https://$domain/search").parseHtml() + return doc.select("select#genre option").map { option -> + val key = option.attr("value") + val title = option.text() + MangaTag(title = title, key = key, source = source) + }.toSet() + } +} \ No newline at end of file From e94d434f922f6586292596d17c4828368f887180 Mon Sep 17 00:00:00 2001 From: Draken Date: Mon, 10 Mar 2025 14:19:31 +0000 Subject: [PATCH 2/5] [Kumapage] Fixes --- .../kotlin/org/koitharu/kotatsu/parsers/site/id/Kumapage.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/id/Kumapage.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/id/Kumapage.kt index dea3a885..25afcc6b 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/id/Kumapage.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/id/Kumapage.kt @@ -51,7 +51,7 @@ internal class Kumapage(context: MangaLoaderContext) : } filter.tags.isNotEmpty() -> { - val tags = filter.tags.joinToString("&genre[]=") { it.key } + val tags = filter.tags.joinToString("&genre[]=") { it.title.replace(" ", "+") } val url = "https://$domain/daftar-komik?page=$page&genre[]=$tags" val response = webClient.httpGet(url.toHttpUrl()).parseHtml() parseMangaList(response) @@ -126,7 +126,7 @@ internal class Kumapage(context: MangaLoaderContext) : MangaChapter( id = generateUid(row.select("td:nth-child(4) a").attr("href")), title = row.select("td:nth-child(2)").text(), - number = i + 1f, + number = (doc.select("tbody#result-comic tr").size - i).toFloat(), volume = 0, url = row.select("td:nth-child(4) a").attr("href"), scanlator = null, @@ -134,7 +134,7 @@ internal class Kumapage(context: MangaLoaderContext) : branch = null, source = source, ) - } + }.reversed() return manga.copy( description = description, From cc3701250a577a2bc03a93f2dd24a0916594c6a9 Mon Sep 17 00:00:00 2001 From: Draken Date: Mon, 10 Mar 2025 14:27:07 +0000 Subject: [PATCH 3/5] [Kumapage] Fixes --- .../org/koitharu/kotatsu/parsers/site/id/Kumapage.kt | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/id/Kumapage.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/id/Kumapage.kt index 25afcc6b..740e70a2 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/id/Kumapage.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/id/Kumapage.kt @@ -118,6 +118,13 @@ internal class Kumapage(context: MangaLoaderContext) : val state = if (doc.selectFirst("div.detail-singular:has(p:contains(Status)) + p") ?.text() == "Active") { MangaState.ONGOING } else { MangaState.FINISHED } val description = doc.selectFirst("td:contains(Synopsis) + td")?.text() + val altTitles = doc.select("div.comic-details p").let { paragraphs -> + if (paragraphs.size > 1) { + setOf(paragraphs[1].text()) + } else { + emptySet() + } + } val tags = doc.select("div.categories p.category").map { tag -> MangaTag(title = tag.text(), key = "", source = source) }.toSet() @@ -137,6 +144,7 @@ internal class Kumapage(context: MangaLoaderContext) : }.reversed() return manga.copy( + altTitles = altTitles, description = description, state = state, tags = tags, From 5edbdf801aeb7b50a52d62cc31d301240b1d140f Mon Sep 17 00:00:00 2001 From: Draken Date: Mon, 10 Mar 2025 14:41:21 +0000 Subject: [PATCH 4/5] [Kumapage] Fixes --- .../koitharu/kotatsu/parsers/site/id/Kumapage.kt | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/id/Kumapage.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/id/Kumapage.kt index 740e70a2..d27d6861 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/id/Kumapage.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/id/Kumapage.kt @@ -51,8 +51,17 @@ internal class Kumapage(context: MangaLoaderContext) : } filter.tags.isNotEmpty() -> { - val tags = filter.tags.joinToString("&genre[]=") { it.title.replace(" ", "+") } - val url = "https://$domain/daftar-komik?page=$page&genre[]=$tags" + val url = buildString { + append("https://") + append(domain) + append("/daftar-komik") + append("?page=") + append(page) + filter.tags.forEach { + append("&genre[]=") + append(it.title.replace(" ", "+")) + } + } val response = webClient.httpGet(url.toHttpUrl()).parseHtml() parseMangaList(response) } From a02b00176868d8a2d6737ebba20d1d1c3ac6ec48 Mon Sep 17 00:00:00 2001 From: Draken Date: Mon, 10 Mar 2025 14:43:03 +0000 Subject: [PATCH 5/5] [Kumapage] Add source + TODO --- src/main/kotlin/org/koitharu/kotatsu/parsers/site/id/Kumapage.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/id/Kumapage.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/id/Kumapage.kt index d27d6861..3acffa8d 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/id/Kumapage.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/id/Kumapage.kt @@ -116,6 +116,7 @@ internal class Kumapage(context: MangaLoaderContext) : rating = RATING_UNKNOWN, state = null, coverUrl = img, + // TODO: Check if manga is NSFW by checking the genre contentRating = if (isNsfwSource) ContentRating.ADULT else null, source = source, )