From afc62b04a8089340683ad7c08d84a366b4e9e051 Mon Sep 17 00:00:00 2001 From: Riztard Lanthorn <riyanluqman@gmail.com> Date: Mon, 28 Feb 2022 19:22:35 +0700 Subject: [PATCH] MangaToon: Fix empty chapter list and covers (#10949) Also adds some missing languages. Closes #9472. Co-Authored-By: Alessandro Jean <14254807+alessandrojean@users.noreply.github.com> Co-authored-by: Alessandro Jean <14254807+alessandrojean@users.noreply.github.com> --- src/all/mangatoon/build.gradle | 8 +- .../extension/all/mangatoon/MangaToon.kt | 215 ++++++++++++------ .../all/mangatoon/MangaToonFactory.kt | 40 ++-- 3 files changed, 169 insertions(+), 94 deletions(-) diff --git a/src/all/mangatoon/build.gradle b/src/all/mangatoon/build.gradle index 8f76d8a80..72ddc8ff6 100644 --- a/src/all/mangatoon/build.gradle +++ b/src/all/mangatoon/build.gradle @@ -2,10 +2,14 @@ apply plugin: 'com.android.application' apply plugin: 'kotlin-android' ext { - extName = 'Mangatoon (Limited)' + extName = 'MangaToon (Limited)' pkgNameSuffix = 'all.mangatoon' extClass = '.MangaToonFactory' - extVersionCode = 2 + extVersionCode = 3 +} + +dependencies { + implementation project(':lib-ratelimit') } apply from: "$rootDir/common.gradle" diff --git a/src/all/mangatoon/src/eu/kanade/tachiyomi/extension/all/mangatoon/MangaToon.kt b/src/all/mangatoon/src/eu/kanade/tachiyomi/extension/all/mangatoon/MangaToon.kt index 4aba053a7..15a24972d 100644 --- a/src/all/mangatoon/src/eu/kanade/tachiyomi/extension/all/mangatoon/MangaToon.kt +++ b/src/all/mangatoon/src/eu/kanade/tachiyomi/extension/all/mangatoon/MangaToon.kt @@ -1,118 +1,187 @@ package eu.kanade.tachiyomi.extension.all.mangatoon +import eu.kanade.tachiyomi.lib.ratelimit.RateLimitInterceptor import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.online.ParsedHttpSource -import eu.kanade.tachiyomi.util.asJsoup -import okhttp3.HttpUrl.Companion.toHttpUrlOrNull +import okhttp3.HttpUrl.Companion.toHttpUrl +import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.Response import org.jsoup.nodes.Document import org.jsoup.nodes.Element import java.text.SimpleDateFormat import java.util.Locale +import java.util.concurrent.TimeUnit open class MangaToon( - override val lang: String, - private val urllang: String + final override val lang: String, + private val urlLang: String = lang ) : ParsedHttpSource() { override val name = "MangaToon (Limited)" + override val baseUrl = "https://mangatoon.mobi" + + override val id: Long = when (lang) { + "pt-BR" -> 2064722193112934135 + else -> super.id + } + override val supportsLatest = true - override fun popularMangaSelector() = "div.genre-content div.items a" - override fun latestUpdatesSelector() = popularMangaSelector() - override fun searchMangaSelector() = "div.recommend-item" - override fun chapterListSelector() = "a.episode-item" + override val client: OkHttpClient = network.client.newBuilder() + .addInterceptor(RateLimitInterceptor(1, 1, TimeUnit.SECONDS)) + .build() - override fun popularMangaNextPageSelector() = "span.next" - override fun latestUpdatesNextPageSelector() = popularMangaNextPageSelector() - override fun searchMangaNextPageSelector() = popularMangaNextPageSelector() + private val locale by lazy { Locale.forLanguageTag(lang) } + + private val lockedError = when (lang) { + "pt-BR" -> + "Este capítulo é pago e não pode ser lido. " + + "Use o app oficial do MangaToon para comprar e ler." + else -> + "This chapter is paid and can't be read. " + + "Use the MangaToon official app to purchase and read it." + } override fun popularMangaRequest(page: Int): Request { - val page0 = page - 1 - return GET("$baseUrl/$urllang/genre/hot?page=$page0", headers) + // Portuguese website doesn't seen to have popular titles. + val path = if (lang == "pt-BR") "comic" else "hot" + + return GET("$baseUrl/$urlLang/genre/$path?type=1&page=${page - 1}", headers) } + + override fun popularMangaSelector() = "div.genre-content div.items a" + + override fun popularMangaFromElement(element: Element): SManga = SManga.create().apply { + title = element.select("div.content-title").text().trim() + thumbnail_url = element.select("img").attr("abs:src").toNormalPosterUrl() + url = element.selectFirst("a").attr("href") + } + + override fun popularMangaNextPageSelector() = "span.next" + override fun latestUpdatesRequest(page: Int): Request { - val page0 = page - 1 - return GET("$baseUrl/$urllang/genre/new?page=$page0", headers) + return GET("$baseUrl/$urlLang/genre/new?type=1&page=${page - 1}", headers) } + + override fun latestUpdatesSelector() = popularMangaSelector() + + override fun latestUpdatesFromElement(element: Element) = popularMangaFromElement(element) + + override fun latestUpdatesNextPageSelector() = popularMangaNextPageSelector() + override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { - val url = "$baseUrl/$urllang/search?word=$query".toHttpUrlOrNull()?.newBuilder() - return GET(url.toString(), headers) + val searchUrl = "$baseUrl/$urlLang/search".toHttpUrl().newBuilder() + .addQueryParameter("word", query) + .toString() + + return GET(searchUrl, headers) } - // override fun mangaDetailsRequest(manga: SManga) = GET(baseUrl + manga.url, headers) - // override fun pageListRequest(chapter: SChapter) = GET(baseUrl + chapter.url, headers) - override fun chapterListRequest(manga: SManga) = GET(baseUrl + manga.url + "/episodes", headers) + override fun searchMangaSelector() = "div.comics-result div.recommend-item" - override fun popularMangaFromElement(element: Element) = mangaFromElement(element) - override fun latestUpdatesFromElement(element: Element) = mangaFromElement(element) - override fun searchMangaFromElement(element: Element): SManga { - val manga = SManga.create() - manga.url = (element.select("a").first().attr("href")) - manga.title = element.select("div.recommend-comics-title").text().trim() - manga.thumbnail_url = element.select("img").attr("abs:src") - return manga + override fun searchMangaFromElement(element: Element): SManga = SManga.create().apply { + title = element.select("div.recommend-comics-title").text().trim() + thumbnail_url = element.select("img").attr("abs:src").toNormalPosterUrl() + url = element.selectFirst("a").attr("href") } - private fun mangaFromElement(element: Element): SManga { - val manga = SManga.create() - manga.url = (element.select("a").first().attr("href")) - manga.title = element.select("div.content-title").text().trim() - manga.thumbnail_url = element.select("img").attr("abs:src") - return manga + + override fun searchMangaNextPageSelector() = popularMangaNextPageSelector() + + override fun mangaDetailsParse(document: Document): SManga = SManga.create().apply { + author = document.select("div.detail-author-name span").text() + .substringAfter(": ") + description = document.select("div.detail-description-short p") + .joinToString("\n\n") { it.text() } + genre = document.select("div.detail-tags-info span").text() + .split("/") + .map { it.capitalize(locale) } + .sorted() + .joinToString { it.trim() } + status = document.select("div.detail-status").text().trim().toStatus() + thumbnail_url = document.select("div.detail-img img.ori-image").attr("abs:src") + .toNormalPosterUrl() + } + + override fun chapterListRequest(manga: SManga): Request { + return GET(baseUrl + manga.url + "/episodes", headers) } override fun chapterListParse(response: Response): List<SChapter> { - return super.chapterListParse(response).reversed() - } + val chapterList = super.chapterListParse(response) - override fun chapterFromElement(element: Element): SChapter { - val chapter = SChapter.create() - chapter.url = element.select("a").first().attr("href") - chapter.chapter_number = element.select("div.item-left").text().trim().toFloat() - val date = element.select("div.episode-date").text() - chapter.date_upload = parseDate(date) - chapter.name = if (chapter.chapter_number> 20) { "\uD83D\uDD12 " } else { "" } + element.select("div.episode-title").text().trim() - return chapter - } + // Finds the last free chapter to filter the paid ones from the list. + // The desktop website doesn't indicate which chapters are paid in + // the title page, and the mobile API is heavily encrypted. + val firstPaid = PAID_CHECK_BREAKPOINTS.find { breakpoint -> + if (breakpoint > chapterList.size) { + return@find false + } - private fun parseDate(date: String): Long { - return SimpleDateFormat("yyyy-MM-dd", Locale.US).parse(date)?.time ?: 0L - } + val pageListRequest = pageListRequest(chapterList[breakpoint - 1]) + val pageListResponse = client.newCall(pageListRequest).execute() - override fun mangaDetailsParse(document: Document): SManga { - val manga = SManga.create() - manga.author = document.select("div.created-by").text().trim() - manga.artist = manga.author - manga.description = document.select("div.description").text().trim() - manga.thumbnail_url = document.select("div.detail-top-right img").attr("abs:src") - val glist = document.select("div.description-tag div.tag").map { it.text() } - manga.genre = glist.joinToString(", ") - manga.status = when (document.select("span.update-date")?.first()?.text()) { - "Update" -> SManga.ONGOING - "End", "完结" -> SManga.COMPLETED - else -> SManga.UNKNOWN + runCatching { pageListParse(pageListResponse) } + .getOrDefault(emptyList()).isEmpty() } - return manga + + return chapterList + .let { if (firstPaid != null) it.take(firstPaid - 1) else it } + .reversed() } - override fun pageListParse(response: Response): List<Page> { - val body = response.asJsoup() - val pages = mutableListOf<Page>() - val elements = body.select("div.pictures img") - for (i in 0 until elements.size) { - pages.add(Page(i, "", elements[i].attr("abs:src"))) + override fun chapterListSelector() = "a.episode-item-new" + + override fun chapterFromElement(element: Element): SChapter = SChapter.create().apply { + name = element.select("div.episode-title-new:last-child").text().trim() + chapter_number = element.select("div.episode-number").text().trim() + .toFloatOrNull() ?: -1f + date_upload = element.select("div.episode-date span.open-date").text().toDate() + url = element.attr("href") + } + + override fun pageListParse(document: Document): List<Page> { + return document.select("div.pictures div img:first-child") + .mapIndexed { i, element -> Page(i, "", element.attr("abs:src")) } + .takeIf { it.isNotEmpty() } ?: throw Exception(lockedError) + } + + override fun imageUrlParse(document: Document) = "" + + private fun String.toDate(): Long { + return runCatching { DATE_FORMAT.parse(this)?.time } + .getOrNull() ?: 0L + } + + private fun String.toNormalPosterUrl(): String = replace(POSTER_SUFFIX, "$1") + + private fun String.toStatus(): Int = when (toLowerCase(locale)) { + in ONGOING_STATUS -> SManga.ONGOING + in COMPLETED_STATUS -> SManga.COMPLETED + else -> SManga.UNKNOWN + } + + companion object { + private val ONGOING_STATUS = listOf( + "连载", "on going", "sedang berlangsung", "tiếp tục cập nhật", + "en proceso", "atualizando", "เซเรียล", "en cours", "連載中" + ) + + private val COMPLETED_STATUS = listOf( + "完结", "completed", "tamat", "đã full", "terminada", "concluído", "จบ", "fin" + ) + + private val DATE_FORMAT by lazy { + SimpleDateFormat("yyyy-MM-dd", Locale.US) } - if (pages.size == 1) throw Exception("Locked episode, download MangaToon APP and read for free!") - return pages - } - override fun pageListParse(document: Document) = throw Exception("Not used") - override fun imageUrlRequest(page: Page) = throw Exception("Not used") - override fun imageUrlParse(document: Document) = throw Exception("Not used") + private val POSTER_SUFFIX = "(jpg)-poster(.*)\\d+?$".toRegex() + + private val PAID_CHECK_BREAKPOINTS = arrayOf(5, 10, 15, 20) + } } diff --git a/src/all/mangatoon/src/eu/kanade/tachiyomi/extension/all/mangatoon/MangaToonFactory.kt b/src/all/mangatoon/src/eu/kanade/tachiyomi/extension/all/mangatoon/MangaToonFactory.kt index c29a2935c..a5a32c56a 100644 --- a/src/all/mangatoon/src/eu/kanade/tachiyomi/extension/all/mangatoon/MangaToonFactory.kt +++ b/src/all/mangatoon/src/eu/kanade/tachiyomi/extension/all/mangatoon/MangaToonFactory.kt @@ -4,24 +4,26 @@ import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.SourceFactory class MangaToonFactory : SourceFactory { - override fun createSources(): List<Source> = listOf( - ZH(), - EN(), - ID(), - VI(), - ES(), - PT(), - TH() - ) - class ZH : MangaToon("zh", "cn") - class EN : MangaToon("en", "en") - class ID : MangaToon("id", "id") - class VI : MangaToon("vi", "vi") - class ES : MangaToon("es", "es") - class PT : MangaToon("pt-BR", "pt") { - // Hardcode the id because the language wasn't specific. - override val id: Long = 2064722193112934135 - } - class TH : MangaToon("th", "th") + override fun createSources(): List<Source> = listOf( + MangaToonZh(), + MangaToonEn(), + MangaToonId(), + MangaToonVi(), + MangaToonEs(), + MangaToonPt(), + MangaToonTh(), + MangaToonFr(), + MangaToonJa() + ) } + +class MangaToonZh : MangaToon("zh", "cn") +class MangaToonEn : MangaToon("en") +class MangaToonId : MangaToon("id") +class MangaToonVi : MangaToon("vi") +class MangaToonEs : MangaToon("es") +class MangaToonPt : MangaToon("pt-BR", "pt") +class MangaToonTh : MangaToon("th") +class MangaToonFr : MangaToon("fr") +class MangaToonJa : MangaToon("ja")