From e73d636979bfb55eed18bd24560dfe9dd9fecf99 Mon Sep 17 00:00:00 2001 From: Naga <94557604+NagaYZ@users.noreply.github.com> Date: Tue, 29 Jul 2025 07:23:01 +0200 Subject: [PATCH] [KomikCast] Fixes (Close #1995) --- .../parsers/site/mangareader/id/Komikcast.kt | 229 ++++++++++++------ 1 file changed, 160 insertions(+), 69 deletions(-) diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/id/Komikcast.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/id/Komikcast.kt index 6546aca2..341f2507 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/id/Komikcast.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/id/Komikcast.kt @@ -1,9 +1,12 @@ package org.koitharu.kotatsu.parsers.site.mangareader.id -import org.json.JSONObject +import kotlinx.coroutines.delay +import okhttp3.Interceptor +import okhttp3.Response 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.model.* import org.koitharu.kotatsu.parsers.site.mangareader.MangaReaderParser import org.koitharu.kotatsu.parsers.util.* @@ -13,21 +16,59 @@ import java.util.* @MangaSourceParser("KOMIKCAST", "KomikCast", "id") internal class Komikcast(context: MangaLoaderContext) : - MangaReaderParser(context, MangaParserSource.KOMIKCAST, "komikcast02.com", pageSize = 60, searchPageSize = 28) { + MangaReaderParser(context, MangaParserSource.KOMIKCAST, "komikcast.li", pageSize = 60, searchPageSize = 28) { + + override val userAgentKey = ConfigKey.UserAgent( + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36" + ) + + override fun getRequestHeaders() = super.getRequestHeaders().newBuilder() + .add("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8") + .add("Accept-Language", "en-US,en;q=0.5") + .add("Cache-Control", "no-cache") + .add("Pragma", "no-cache") + .build() + + override fun intercept(chain: Interceptor.Chain): Response { + val request = chain.request() + val newRequest = when { + request.url.pathSegments.contains("chapter") -> { + // Add referer for chapter pages + val mangaSlug = request.url.toString() + .substringAfter("/chapter/") + .substringBefore("-chapter-") + .substringBefore("-ch-") + request.newBuilder() + .header("Referer", "https://$domain/komik/$mangaSlug/") + .build() + } + else -> { + request.newBuilder() + .header("Referer", "https://$domain/") + .build() + } + } + return chain.proceed(newRequest) + } override val listUrl = "/daftar-komik" override val datePattern = "MMM d, yyyy" override val sourceLocale: Locale = Locale.ENGLISH override val availableSortOrders: Set = - EnumSet.of(SortOrder.UPDATED, SortOrder.POPULARITY, SortOrder.ALPHABETICAL) + EnumSet.of(SortOrder.UPDATED, SortOrder.POPULARITY, SortOrder.ALPHABETICAL, SortOrder.ALPHABETICAL_DESC) override val filterCapabilities: MangaListFilterCapabilities get() = super.filterCapabilities.copy( - isTagsExclusionSupported = false, + isTagsExclusionSupported = false ) override suspend fun getFilterOptions() = super.getFilterOptions().copy( 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 { @@ -48,16 +89,28 @@ internal class Komikcast(context: MangaLoaderContext) : append(listUrl) append("/page/") append(page.toString()) - append("/?type=") + append("/?") + + filter.types.oneOrThrowIfMany()?.let { contentType -> + append("type=") + append(when (contentType) { + ContentType.MANGA -> "manga" + ContentType.MANHWA -> "manhwa" + ContentType.MANHUA -> "manhua" + else -> "" + }) + append("&") + } + append( when (order) { - SortOrder.ALPHABETICAL -> "&orderby=titleasc" - SortOrder.ALPHABETICAL_DESC -> "&orderby=titledesc" - SortOrder.POPULARITY -> "&orderby=popular" - SortOrder.UPDATED -> "" // To get the Updated list, you don't need "orderby" in the url. - else -> "" - }, + SortOrder.ALPHABETICAL -> "orderby=titleasc" + SortOrder.ALPHABETICAL_DESC -> "orderby=titledesc" + SortOrder.POPULARITY -> "orderby=popular" + else -> "orderby=update" + } ) + val tagKey = "genre[]".urlEncoded() val tagQuery = if (filter.tags.isEmpty()) "" @@ -67,11 +120,13 @@ internal class Komikcast(context: MangaLoaderContext) : if (filter.states.isNotEmpty()) { filter.states.oneOrThrowIfMany()?.let { append("&status=") - when (it) { - MangaState.ONGOING -> append("Ongoing") - MangaState.FINISHED -> append("Completed") - else -> append("") - } + append( + when (it) { + MangaState.ONGOING -> "Ongoing" + MangaState.FINISHED -> "Completed" + else -> "" + } + ) } } } @@ -82,53 +137,68 @@ internal class Komikcast(context: MangaLoaderContext) : } 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("#chapter-wrapper > li").mapChapters(reversed = true) { index, element -> - val url = element.selectFirst("a.chapter-link-item")?.attrAsRelativeUrl("href") ?: return@mapChapters null - MangaChapter( - id = generateUid(url), - title = element.selectFirst("a.chapter-link-item")?.ownTextOrNull(), - url = url, - number = index + 1f, - volume = 0, - scanlator = null, - uploadDate = parseChapterDate( - dateFormat, - element.selectFirst("div.chapter-link-time")?.text(), - ), - branch = null, - source = source, - ) + val mangaUrl = manga.url.toAbsoluteUrl(domain) + + repeat(3) { attempt -> + if (attempt > 0) { + delay(1500) + } + + val docs = webClient.httpGet(mangaUrl).parseHtml() + val chapterElements = docs.select("#chapter-wrapper > li") + val dateFormat = SimpleDateFormat(datePattern, sourceLocale) + + val chapters = chapterElements.mapChapters(reversed = true) { index, element -> + val url = element.selectFirst("a.chapter-link-item")?.attrAsRelativeUrl("href") + ?: return@mapChapters null + MangaChapter( + id = generateUid(url), + title = element.selectFirst("a.chapter-link-item")?.ownTextOrNull(), + url = url, + number = index + 1f, + volume = 0, + scanlator = null, + uploadDate = parseChapterDate( + dateFormat, + element.selectFirst("div.chapter-link-time")?.text(), + ), + branch = null, + source = source, + ) + } + + if (chapters.isNotEmpty()) { + return parseInfo(docs, manga, chapters) + } } - return parseInfo(docs, manga, chapters) + + throw Exception("Failed to get manga details after 3 attempts for: $mangaUrl") } override suspend fun parseInfo(docs: Document, manga: Manga, chapters: List): Manga { val tagMap = getOrCreateTagMap() val tags = docs.select(".komik_info-content-genre > a").mapNotNullToSet { tagMap[it.text()] } val state = docs.selectFirst(".komik_info-content-meta span:contains(Status)")?.html() - val mangaState = if (state!!.contains("Ongoing")) { + val mangaState = if (state?.contains("Ongoing") == true) { MangaState.ONGOING } else { MangaState.FINISHED } val author = docs.selectFirst(".komik_info-content-meta span:contains(Author)") ?.lastElementChild()?.textOrNull() - val nsfw = - docs.selectFirst(".restrictcontainer") != null || docs.selectFirst(".info-right .alr") != null || docs.selectFirst( - ".postbody .alr", - ) != null + val nsfw = docs.select("div") + .any { it.text().contains("Peringatan", ignoreCase = true) && it.text().contains("konten", ignoreCase = true) } + + val title = docs.selectFirst("h1.komik_info-content-body-title")?.text()!! + .replace(" Bahasa Indonesia", "").trim() + val description = docs.selectFirst("div.komik_info-description-sinopsis")?.text() return manga.copy( - description = docs.selectFirst("div.komik_info-description-sinopsis")?.text(), + title = title, + description = description, state = mangaState, authors = setOfNotNull(author), - contentRating = if (manga.isNsfw || nsfw) { - ContentRating.ADULT - } else { - ContentRating.SAFE - }, + contentRating = if (isNsfwSource || nsfw || manga.contentRating == ContentRating.ADULT) ContentRating.ADULT else ContentRating.SAFE, tags = tags, chapters = chapters, ) @@ -157,37 +227,58 @@ internal class Komikcast(context: MangaLoaderContext) : } } + override suspend fun getPages(chapter: MangaChapter): List { val chapterUrl = chapter.url.toAbsoluteUrl(domain) - val docs = webClient.httpGet(chapterUrl).parseHtml() - val test = docs.select("script:containsData(ts_reader)") - if (test.isNullOrEmpty()) { - return docs.select("div#chapter_body img").map { img -> - val url = img.requireSrc().toRelativeUrl(domain) - MangaPage( - id = generateUid(url), - url = url, - preview = null, - source = source, - ) + + repeat(3) { attempt -> + if (attempt > 0) { + delay(500) } - } else { - val script = docs.selectFirstOrThrow("script:containsData(ts_reader)") - val images = JSONObject(script.data().substringAfter('(').substringBeforeLast(')')).getJSONArray("sources") - .getJSONObject(0).getJSONArray("images") - val pages = ArrayList(images.length()) - for (i in 0 until images.length()) { - pages.add( + + val docs = webClient.httpGet(chapterUrl).parseHtml() + val pages = extractPages(docs) + + if (pages.isNotEmpty()) { + return pages + } + } + return emptyList() + } + + private fun extractPages(docs: Document): List { + val imageSelectors = listOf( + "div#chapter_body img", + "img[src*='.jpg'], img[src*='.png'], img[src*='.jpeg'], img[src*='.webp']", + ) + + for (selector in imageSelectors) { + val chapterImages = docs.select(selector) + if (chapterImages.isNotEmpty()) { + val pages = chapterImages.mapNotNull { img -> + val src = img.attr("src").takeIf { it.isNotEmpty() } + ?: img.attr("data-src").takeIf { it.isNotEmpty() } + ?: img.attr("data-lazy-src").takeIf { it.isNotEmpty() } + ?: img.attr("data-original").takeIf { it.isNotEmpty() } + ?: return@mapNotNull null + + if (src.contains("loading") || src.contains("spinner") || src.contains("placeholder") || src.contains("logo")) { + return@mapNotNull null + } + MangaPage( - id = generateUid(images.getString(i)), - url = images.getString(i), + id = generateUid(src), + url = if (src.startsWith("http")) src else src.toAbsoluteUrl(domain), preview = null, source = source, - ), - ) + ) + } + if (pages.isNotEmpty()) { + return pages + } } - return pages } + return emptyList() } private fun parseChapterDate(dateFormat: DateFormat, date: String?): Long {