From d773d2692b90172fbd30e336c36fab34f086d35c Mon Sep 17 00:00:00 2001 From: AwkwardPeak7 <48650614+AwkwardPeak7@users.noreply.github.com> Date: Wed, 9 Jul 2025 11:14:42 +0500 Subject: [PATCH] Webtoons.com: add rate limit & handle chapter count reset on new season (#9593) * Webtoons: ratelimit chapters fetch api closes https://github.com/keiyoushi/extensions-source/issues/9570 * handle season number reset closes https://github.com/keiyoushi/extensions-source/issues/9495 * edge case bug * bump --- src/all/webtoons/build.gradle | 2 +- .../tachiyomi/extension/all/webtoons/Dto.kt | 5 +- .../extension/all/webtoons/Webtoons.kt | 95 ++++++++++++------- 3 files changed, 66 insertions(+), 36 deletions(-) diff --git a/src/all/webtoons/build.gradle b/src/all/webtoons/build.gradle index 08722cd51..f4aca40ca 100644 --- a/src/all/webtoons/build.gradle +++ b/src/all/webtoons/build.gradle @@ -1,7 +1,7 @@ ext { extName = 'Webtoons.com' extClass = '.WebtoonsFactory' - extVersionCode = 49 + extVersionCode = 50 isNsfw = false } diff --git a/src/all/webtoons/src/eu/kanade/tachiyomi/extension/all/webtoons/Dto.kt b/src/all/webtoons/src/eu/kanade/tachiyomi/extension/all/webtoons/Dto.kt index a2e1ba21c..03f026afb 100644 --- a/src/all/webtoons/src/eu/kanade/tachiyomi/extension/all/webtoons/Dto.kt +++ b/src/all/webtoons/src/eu/kanade/tachiyomi/extension/all/webtoons/Dto.kt @@ -20,7 +20,10 @@ class Episode( val viewerLink: String, val exposureDateMillis: Long, val hasBgm: Boolean = false, -) +) { + var chapterNumber = -1f + var seasonNumber = 1 +} @Serializable class MotionToonResponse( diff --git a/src/all/webtoons/src/eu/kanade/tachiyomi/extension/all/webtoons/Webtoons.kt b/src/all/webtoons/src/eu/kanade/tachiyomi/extension/all/webtoons/Webtoons.kt index 9780e3cbe..8add88dce 100644 --- a/src/all/webtoons/src/eu/kanade/tachiyomi/extension/all/webtoons/Webtoons.kt +++ b/src/all/webtoons/src/eu/kanade/tachiyomi/extension/all/webtoons/Webtoons.kt @@ -7,6 +7,7 @@ import eu.kanade.tachiyomi.lib.textinterceptor.TextInterceptor import eu.kanade.tachiyomi.lib.textinterceptor.TextInterceptorHelper import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.asObservableSuccess +import eu.kanade.tachiyomi.network.interceptor.rateLimitHost import eu.kanade.tachiyomi.source.ConfigurableSource import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.MangasPage @@ -65,6 +66,7 @@ open class Webtoons( } } .addInterceptor(TextInterceptor()) + .rateLimitHost(mobileUrl.toHttpUrl(), 1) .build() private val preferences by getPreferencesLazy() @@ -276,10 +278,60 @@ open class Webtoons( override fun chapterListParse(response: Response): List { val result = response.parseAs() - val recognized: MutableList = mutableListOf() - val unrecognized: MutableList = mutableListOf() + var recognized = 0 + var unrecognized = 0 - val chapters = result.result.episodeList.mapIndexed { index, episode -> + val chapters = result.result.episodeList.onEach { episode -> + val match = episodeNoRegex + .find(episode.episodeTitle) + ?.groupValues + ?.takeIf { it[6].isEmpty() } // skip mini/bonus episodes + + episode.chapterNumber = match?.get(11)?.toFloat() ?: -1f + episode.seasonNumber = match?.get(4)?.takeIf(String::isNotBlank)?.toInt() ?: 1 + + if (episode.chapterNumber == -1f) { + unrecognized++ + } else { + recognized++ + } + } + + if (unrecognized > recognized) { + chapters.onEachIndexed { index, chapter -> + chapter.chapterNumber = (index + 1).toFloat() + } + } else { + var maxChapterNumber = 0f + var currentSeason = 1 + var seasonOffset = 0f + + chapters.forEachIndexed { idx, chapter -> + if (chapter.chapterNumber != -1f) { + val originalNumber = chapter.chapterNumber + + // Check if we've moved to a new season + if (chapter.seasonNumber > currentSeason) { + currentSeason = chapter.seasonNumber + if (originalNumber <= maxChapterNumber) { + seasonOffset = maxChapterNumber + } + } + + chapter.chapterNumber = seasonOffset + originalNumber + maxChapterNumber = maxOf(maxChapterNumber, chapter.chapterNumber) + } else { + val previous = chapters.getOrNull(idx - 1) + if (previous == null) { + chapter.chapterNumber = 0f + } else { + chapter.chapterNumber = previous.chapterNumber + 0.01f + } + } + } + } + + return chapters.map { episode -> SChapter.create().apply { url = episode.viewerLink name = Parser.unescapeEntities(episode.episodeTitle, false) @@ -287,41 +339,16 @@ open class Webtoons( name += " ♫" } date_upload = episode.exposureDateMillis - chapter_number = episodeNoRegex - .find(episode.episodeTitle) - ?.groupValues - ?.get(4) - ?.toFloat() - ?: -1f - if (chapter_number == -1f) { - unrecognized += index - } else { - recognized += index - } + chapter_number = episode.chapterNumber } - } - - if (unrecognized.size > recognized.size) { - chapters.onEachIndexed { index, chapter -> - chapter.chapter_number = (index + 1).toFloat() - } - } else { - unrecognized.forEach { uIdx -> - val chapter = chapters[uIdx] - val previous = chapters.getOrNull(uIdx - 1) - if (previous == null) { - chapter.chapter_number = 0f - } else { - chapter.chapter_number = previous.chapter_number + 0.01f - } - } - } - - return chapters.asReversed() + }.asReversed() } + // season number - 4 capture group + // possible bonus/mini/special episode - 6 capture group + // episode number - 11 capture group private val episodeNoRegex = Regex( - """(ep(isode)?|ch(apter)?)\s*\.?\s*(\d+(\.\d+)?)""", + """(?:(s(eason)?|part|vol(ume)?)\s*\.?\s*(\d+).*?)?(.*?(mini|bonus|special).*?)?(e(p(isode)?)?|ch(apter)?)\s*\.?\s*(\d+(\.\d+)?)""", RegexOption.IGNORE_CASE, )