From af89f422519b0e1eddade2bddaae340fbaf97817 Mon Sep 17 00:00:00 2001 From: Naga <94557604+NagaYZ@users.noreply.github.com> Date: Wed, 30 Jul 2025 21:59:05 +0200 Subject: [PATCH] [Komiku] Fixes (#2022) Close #1986 --- .../parsers/site/mangareader/id/KomikIndo.kt | 2 +- .../parsers/site/mangareader/id/Komiku.kt | 219 +++++++++++++++++- 2 files changed, 216 insertions(+), 5 deletions(-) diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/id/KomikIndo.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/id/KomikIndo.kt index de071c8f..9489fb23 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/id/KomikIndo.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/id/KomikIndo.kt @@ -13,7 +13,7 @@ import java.util.EnumSet @MangaSourceParser("KOMIKINDO_MOE", "KomikIndo.org", "id", ContentType.HENTAI) internal class KomikIndo(context: MangaLoaderContext) : - MangaReaderParser(context, MangaParserSource.KOMIKINDO_MOE, "komikindo.org", pageSize = 30, searchPageSize = 30) { + MangaReaderParser(context, MangaParserSource.KOMIKINDO_MOE, "komikindo.ch", pageSize = 30, searchPageSize = 30) { override val listUrl = "/daftar-manga" override val selectMangaList = "div.animepost" diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/id/Komiku.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/id/Komiku.kt index 6561f76b..696b6a19 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/id/Komiku.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/id/Komiku.kt @@ -1,17 +1,228 @@ package org.koitharu.kotatsu.parsers.site.mangareader.id +import org.jsoup.nodes.Document import org.koitharu.kotatsu.parsers.MangaLoaderContext import org.koitharu.kotatsu.parsers.MangaSourceParser -import org.koitharu.kotatsu.parsers.model.MangaListFilterCapabilities -import org.koitharu.kotatsu.parsers.model.MangaParserSource +import org.koitharu.kotatsu.parsers.model.* import org.koitharu.kotatsu.parsers.site.mangareader.MangaReaderParser +import org.koitharu.kotatsu.parsers.util.* +import java.text.SimpleDateFormat +import java.util.EnumSet @MangaSourceParser("KOMIKU", "Komiku", "id") internal class Komiku(context: MangaLoaderContext) : - MangaReaderParser(context, MangaParserSource.KOMIKU, "komiku.one", pageSize = 20, searchPageSize = 20) { - override val datePattern = "MMM d, yyyy" + MangaReaderParser(context, MangaParserSource.KOMIKU, "komiku.org", pageSize = 20, searchPageSize = 10) { + + private val apiDomain = "api.komiku.id" + override val datePattern = "dd/MM/yyyy" + override val selectPage = "#Baca_Komik img" + override val selectTestScript = "script:containsData(thisIsNeverFound)" + override val listUrl = "/manga/" + override val selectMangaList = "div.bge" + override val selectMangaListImg = "img" + override val selectMangaListTitle = "h3" + override val selectChapter = "#Daftar_Chapter tr:has(td.judulseries)" + override val detailsDescriptionSelector = "#Sinopsis > p" + override val filterCapabilities: MangaListFilterCapabilities get() = super.filterCapabilities.copy( isTagsExclusionSupported = false, ) + + override suspend fun getFilterOptions() = MangaListFilterOptions( + availableTags = fetchAvailableTags(), + availableStates = EnumSet.of(MangaState.ONGOING, MangaState.FINISHED), + availableContentTypes = EnumSet.of( + ContentType.MANGA, + ContentType.MANHWA, + ContentType.MANHUA, + ), + ) + + override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val url = buildString { + append("https://") + + when { + !filter.query.isNullOrEmpty() -> { + append(apiDomain) + append("/page/") + append(page.toString()) + append("/?post_type=manga&s=") + append(filter.query.urlEncoded()) + } + + else -> { + append(apiDomain) + append(listUrl) + if (page > 1) { + append("/page/") + append(page.toString()) + } + append("/?") + + append("orderby=") + append( + when (order) { + SortOrder.ALPHABETICAL, SortOrder.ALPHABETICAL_DESC -> "title" + SortOrder.NEWEST -> "date" + SortOrder.POPULARITY -> "meta_value_num" + SortOrder.UPDATED -> "modified" + else -> "modified" + }, + ) + + filter.tags.oneOrThrowIfMany()?.let { tag -> + append("&genre=") + append(tag.key) + } + + filter.types.oneOrThrowIfMany()?.let { + append("&tipe=") + append( + when (it) { + ContentType.MANGA -> "manga" + ContentType.MANHWA -> "manhwa" + ContentType.MANHUA -> "manhua" + else -> "" + }, + ) + } + + filter.states.oneOrThrowIfMany()?.let { + append("&status=") + when (it) { + MangaState.ONGOING -> append("ongoing") + MangaState.FINISHED -> append("end") + else -> append("") + } + } + } + } + } + + return parseMangaList(webClient.httpGet(url).parseHtml()) + } + + override fun parseMangaList(docs: Document): List { + return docs.select(selectMangaList).mapNotNull { element -> + val a = element.selectFirst("a:has(h3)") ?: return@mapNotNull null + val relativeUrl = a.attrAsRelativeUrl("href").toRelativeUrl(domain) + + val thumbnailUrl = element.selectFirst(selectMangaListImg)?.src()?.let { url -> + if (url.contains("/uploads/\\d{4}/\\d{2}/".toRegex())) { + url + } else { + url.substringBeforeLast("?") + .replace("/Manga-", "/Komik-") + .replace("/Manhua-", "/Komik-") + .replace("/Manhwa-", "/Komik-") + } + } + + Manga( + id = generateUid(relativeUrl), + url = relativeUrl, + title = element.selectFirst(selectMangaListTitle)?.text()?.trim() ?: return@mapNotNull null, + altTitles = emptySet(), + publicUrl = a.attrAsAbsoluteUrl("href"), + rating = RATING_UNKNOWN, + contentRating = if (isNsfwSource) ContentRating.ADULT else null, + coverUrl = thumbnailUrl, + tags = emptySet(), + state = null, + authors = emptySet(), + source = source, + ) + } + } + + override suspend fun getDetails(manga: Manga): Manga { + val docs = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml() + val dateFormat = SimpleDateFormat(datePattern, sourceLocale) + + val chapters = docs.select(selectChapter).mapChapters(reversed = true) { index, element -> + val a = element.selectFirst("td.judulseries a") ?: return@mapChapters null + val url = a.attrAsRelativeUrl("href") + val dateText = element.selectFirst("td.tanggalseries")?.text() + + MangaChapter( + id = generateUid(url), + title = a.selectFirst("span")?.text()?.trim() ?: a.text().trim(), + url = url, + number = index + 1f, + volume = 0, + scanlator = null, + uploadDate = dateFormat.parseSafe(dateText), + branch = null, + source = source, + ) + } + + return parseInfo(docs, manga, chapters) + } + + override suspend fun parseInfo(docs: Document, manga: Manga, chapters: List): Manga { + val tags = docs.select("ul.genre li.genre a").mapNotNullToSet { element -> + val href = element.attr("href") + val genreKey = href.substringAfter("/genre/").substringBefore("/") + val genreTitle = element.selectFirst("span[itemprop='genre']")?.text()?.trim() + ?: element.text().trim() + + MangaTag( + key = genreKey, + title = genreTitle.toTitleCase(sourceLocale), + source = source, + ) + } + val statusText = docs.selectFirst("table.inftable tr > td:contains(Status) + td")?.text() + val state = when { + statusText?.contains("Ongoing") == true -> MangaState.ONGOING + statusText?.contains("Completed") == true -> MangaState.FINISHED + statusText?.contains("Tamat", ignoreCase = true) == true -> MangaState.FINISHED + statusText?.contains("End", ignoreCase = true) == true -> MangaState.FINISHED + + else -> null + } + + val author = docs.selectFirst("table.inftable tr:has(td:contains(Pengarang)) td:last-child")?.text()?.trim() + + val altTitle = + docs.selectFirst("table.inftable tr:has(td:contains(Judul Indonesia)) td:last-child")?.text()?.trim() + val altTitles = if (!altTitle.isNullOrBlank()) setOf(altTitle) else emptySet() + + val thumbnail = docs.selectFirst("div.ims > img")?.attr("src")?.substringBeforeLast("?") + + return manga.copy( + altTitles = altTitles, + description = docs.selectFirst(detailsDescriptionSelector)?.text()?.trim(), + state = state, + authors = setOfNotNull(author), + contentRating = if (manga.contentRating == ContentRating.ADULT) ContentRating.ADULT else ContentRating.SAFE, + tags = tags, + chapters = chapters, + coverUrl = thumbnail ?: manga.coverUrl, + ) + } + + private suspend fun fetchAvailableTags(): Set { + val doc = webClient.httpGet("https://$apiDomain/").parseHtml() + val tags = mutableSetOf() + + doc.select("select[name='genre'] option").forEach { option -> + val value = option.attr("value") + val title = option.text().trim() + + if (value.isNotBlank() && !title.contains("Genre", ignoreCase = true)) { + tags.add( + MangaTag( + key = value, + title = title, + source = source, + ), + ) + } + } + return tags + } }