Webtoons.com: fixes (#9349)

* better compatibility with old urls

* use index for chapter number as fallback

* better chapter number logic

* escape chapter names

* fix deep link for canvas

* Update build.gradle

* bgm

* deeplink

* i + 1
This commit is contained in:
AwkwardPeak7 2025-06-21 15:54:58 +05:00 committed by Draff
parent 621dc6c121
commit dd47332ab9
Signed by: Draff
GPG Key ID: E8A89F3211677653
4 changed files with 82 additions and 24 deletions

View File

@ -1,7 +1,7 @@
ext {
extName = 'Webtoons.com'
extClass = '.WebtoonsFactory'
extVersionCode = 48
extVersionCode = 49
isNsfw = false
}

View File

@ -16,7 +16,6 @@ class EpisodeList(
@Serializable
class Episode(
val episodeNo: Float,
val episodeTitle: String,
val viewerLink: String,
val exposureDateMillis: Long,

View File

@ -23,6 +23,7 @@ import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import org.jsoup.parser.Parser
import rx.Observable
import java.net.SocketException
import java.util.Calendar
@ -122,18 +123,23 @@ open class Webtoons(
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
if (query.startsWith(ID_SEARCH_PREFIX)) {
val (_, titleLang, titleNo) = query.split(":", limit = 3)
val (_, type, lang, titleNo) = query.split(":", limit = 4)
val tmpManga = SManga.create().apply {
url = "/episodeList?titleNo=$titleNo"
url = buildString {
if (type == "canvas") {
append("/challenge")
}
append("/episodeList?titleNo=")
append(titleNo)
}
}
return if (titleLang == langCode) {
return if (lang == langCode) {
fetchMangaDetails(tmpManga).map {
MangasPage(listOf(it), false)
}
} else {
Observable.just(
MangasPage(emptyList(), false),
)
Observable.just(MangasPage(emptyList(), false))
}
}
@ -200,7 +206,7 @@ open class Webtoons(
else -> SManga.UNKNOWN
}
}
initialized = true
thumbnail_url = run {
val bannerFile = document.selectFirst(".detail_header .thmb img")
?.absUrl("src")
@ -232,18 +238,33 @@ open class Webtoons(
val webtoonUrl = getMangaUrl(manga).toHttpUrl()
val titleId = webtoonUrl.queryParameter("title_no")
?: webtoonUrl.queryParameter("titleNo")
?: throw Exception("id not found, Migrate from $name to $name")
?: throw Exception("Migrate from $name to $name")
val isCanvas = webtoonUrl.pathSegments.getOrNull(1)?.equals("canvas")
?: throw Exception("unknown type, Migrate from $name to $name")
val type = run {
val path = webtoonUrl.pathSegments.filter(String::isNotEmpty)
// older url pattern, people have in their library
if (webtoonUrl.encodedPath.contains("episodeList")) {
when (path[0]) {
// "/episodeList?titleNo=1049"
"episodeList" -> "webtoon"
// "/challenge/episodeList?titleNo=304446"
"challenge" -> "canvas"
else -> throw Exception("Migrate from $name to $name")
}
} else {
// "/en/canvas/meme-girls/list?title_no=304446"
if (path[1] == "canvas") {
"canvas"
} else {
"webtoon"
}
}
}
val url = mobileUrl.toHttpUrl().newBuilder().apply {
addPathSegments("api/v1")
if (isCanvas) {
addPathSegment("canvas")
} else {
addPathSegment("webtoon")
}
addPathSegment(type)
addPathSegment(titleId)
addPathSegment("episodes")
addQueryParameter("pageSize", "99999")
@ -255,19 +276,55 @@ open class Webtoons(
override fun chapterListParse(response: Response): List<SChapter> {
val result = response.parseAs<EpisodeListResponse>()
return result.result.episodeList.map { episode ->
val recognized: MutableList<Int> = mutableListOf()
val unrecognized: MutableList<Int> = mutableListOf()
val chapters = result.result.episodeList.mapIndexed { index, episode ->
SChapter.create().apply {
url = episode.viewerLink
name = episode.episodeTitle
name = Parser.unescapeEntities(episode.episodeTitle, false)
if (episode.hasBgm) {
name += ""
}
date_upload = episode.exposureDateMillis
chapter_number = episode.episodeNo
chapter_number = episodeNoRegex
.find(episode.episodeTitle)
?.groupValues
?.get(4)
?.toFloat()
?: -1f
if (chapter_number == -1f) {
unrecognized += index
} else {
recognized += index
}
}
}.asReversed()
}
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()
}
private val episodeNoRegex = Regex(
"""(ep(isode)?|ch(apter)?)\s*\.?\s*(\d+(\.\d+)?)""",
RegexOption.IGNORE_CASE,
)
override fun pageListParse(response: Response): List<Page> {
val document = response.asJsoup()
val useMaxQuality = useMaxQualityPref()

View File

@ -23,11 +23,13 @@ class WebtoonsUrlActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val titleNo = intent?.data?.getQueryParameter("title_no")
val lang = intent?.data?.pathSegments?.get(0)
if (titleNo != null) {
val path = intent?.data?.pathSegments
if (titleNo != null && path != null && path.size >= 3) {
val lang = path[0]
val type = path[1]
val mainIntent = Intent().apply {
action = "eu.kanade.tachiyomi.SEARCH"
putExtra("query", "$ID_SEARCH_PREFIX$lang:$titleNo")
putExtra("query", "$ID_SEARCH_PREFIX$type:$lang:$titleNo")
putExtra("filter", packageName)
}