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 { ext {
extName = 'Webtoons.com' extName = 'Webtoons.com'
extClass = '.WebtoonsFactory' extClass = '.WebtoonsFactory'
extVersionCode = 48 extVersionCode = 49
isNsfw = false isNsfw = false
} }

View File

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

View File

@ -23,6 +23,7 @@ import okhttp3.Request
import okhttp3.Response import okhttp3.Response
import org.jsoup.nodes.Document import org.jsoup.nodes.Document
import org.jsoup.nodes.Element import org.jsoup.nodes.Element
import org.jsoup.parser.Parser
import rx.Observable import rx.Observable
import java.net.SocketException import java.net.SocketException
import java.util.Calendar import java.util.Calendar
@ -122,18 +123,23 @@ open class Webtoons(
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> { override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
if (query.startsWith(ID_SEARCH_PREFIX)) { 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 { val tmpManga = SManga.create().apply {
url = "/episodeList?titleNo=$titleNo" url = buildString {
if (type == "canvas") {
append("/challenge")
} }
return if (titleLang == langCode) { append("/episodeList?titleNo=")
append(titleNo)
}
}
return if (lang == langCode) {
fetchMangaDetails(tmpManga).map { fetchMangaDetails(tmpManga).map {
MangasPage(listOf(it), false) MangasPage(listOf(it), false)
} }
} else { } else {
Observable.just( Observable.just(MangasPage(emptyList(), false))
MangasPage(emptyList(), false),
)
} }
} }
@ -200,7 +206,7 @@ open class Webtoons(
else -> SManga.UNKNOWN else -> SManga.UNKNOWN
} }
} }
initialized = true
thumbnail_url = run { thumbnail_url = run {
val bannerFile = document.selectFirst(".detail_header .thmb img") val bannerFile = document.selectFirst(".detail_header .thmb img")
?.absUrl("src") ?.absUrl("src")
@ -232,18 +238,33 @@ open class Webtoons(
val webtoonUrl = getMangaUrl(manga).toHttpUrl() val webtoonUrl = getMangaUrl(manga).toHttpUrl()
val titleId = webtoonUrl.queryParameter("title_no") val titleId = webtoonUrl.queryParameter("title_no")
?: webtoonUrl.queryParameter("titleNo") ?: 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") val type = run {
?: throw Exception("unknown type, Migrate from $name to $name") 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 { val url = mobileUrl.toHttpUrl().newBuilder().apply {
addPathSegments("api/v1") addPathSegments("api/v1")
if (isCanvas) { addPathSegment(type)
addPathSegment("canvas")
} else {
addPathSegment("webtoon")
}
addPathSegment(titleId) addPathSegment(titleId)
addPathSegment("episodes") addPathSegment("episodes")
addQueryParameter("pageSize", "99999") addQueryParameter("pageSize", "99999")
@ -255,18 +276,54 @@ open class Webtoons(
override fun chapterListParse(response: Response): List<SChapter> { override fun chapterListParse(response: Response): List<SChapter> {
val result = response.parseAs<EpisodeListResponse>() 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 { SChapter.create().apply {
url = episode.viewerLink url = episode.viewerLink
name = episode.episodeTitle name = Parser.unescapeEntities(episode.episodeTitle, false)
if (episode.hasBgm) { if (episode.hasBgm) {
name += "" name += ""
} }
date_upload = episode.exposureDateMillis 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> { override fun pageListParse(response: Response): List<Page> {
val document = response.asJsoup() val document = response.asJsoup()

View File

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