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>
This commit is contained in:
Riztard Lanthorn 2022-02-28 19:22:35 +07:00 committed by GitHub
parent 7c566ae604
commit afc62b04a8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 169 additions and 94 deletions

View File

@ -2,10 +2,14 @@ apply plugin: 'com.android.application'
apply plugin: 'kotlin-android' apply plugin: 'kotlin-android'
ext { ext {
extName = 'Mangatoon (Limited)' extName = 'MangaToon (Limited)'
pkgNameSuffix = 'all.mangatoon' pkgNameSuffix = 'all.mangatoon'
extClass = '.MangaToonFactory' extClass = '.MangaToonFactory'
extVersionCode = 2 extVersionCode = 3
}
dependencies {
implementation project(':lib-ratelimit')
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"

View File

@ -1,118 +1,187 @@
package eu.kanade.tachiyomi.extension.all.mangatoon package eu.kanade.tachiyomi.extension.all.mangatoon
import eu.kanade.tachiyomi.lib.ratelimit.RateLimitInterceptor
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.ParsedHttpSource import eu.kanade.tachiyomi.source.online.ParsedHttpSource
import eu.kanade.tachiyomi.util.asJsoup import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import okhttp3.OkHttpClient
import okhttp3.Request 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 java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Locale import java.util.Locale
import java.util.concurrent.TimeUnit
open class MangaToon( open class MangaToon(
override val lang: String, final override val lang: String,
private val urllang: String private val urlLang: String = lang
) : ParsedHttpSource() { ) : ParsedHttpSource() {
override val name = "MangaToon (Limited)" override val name = "MangaToon (Limited)"
override val baseUrl = "https://mangatoon.mobi" override val baseUrl = "https://mangatoon.mobi"
override val id: Long = when (lang) {
"pt-BR" -> 2064722193112934135
else -> super.id
}
override val supportsLatest = true override val supportsLatest = true
override fun popularMangaSelector() = "div.genre-content div.items a" override val client: OkHttpClient = network.client.newBuilder()
override fun latestUpdatesSelector() = popularMangaSelector() .addInterceptor(RateLimitInterceptor(1, 1, TimeUnit.SECONDS))
override fun searchMangaSelector() = "div.recommend-item" .build()
override fun chapterListSelector() = "a.episode-item"
override fun popularMangaNextPageSelector() = "span.next" private val locale by lazy { Locale.forLanguageTag(lang) }
override fun latestUpdatesNextPageSelector() = popularMangaNextPageSelector()
override fun searchMangaNextPageSelector() = popularMangaNextPageSelector() 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 { override fun popularMangaRequest(page: Int): Request {
val page0 = page - 1 // Portuguese website doesn't seen to have popular titles.
return GET("$baseUrl/$urllang/genre/hot?page=$page0", headers) 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 { override fun latestUpdatesRequest(page: Int): Request {
val page0 = page - 1 return GET("$baseUrl/$urlLang/genre/new?type=1&page=${page - 1}", headers)
return GET("$baseUrl/$urllang/genre/new?page=$page0", 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 { override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val url = "$baseUrl/$urllang/search?word=$query".toHttpUrlOrNull()?.newBuilder() val searchUrl = "$baseUrl/$urlLang/search".toHttpUrl().newBuilder()
return GET(url.toString(), headers) .addQueryParameter("word", query)
.toString()
return GET(searchUrl, headers)
} }
// override fun mangaDetailsRequest(manga: SManga) = GET(baseUrl + manga.url, headers) override fun searchMangaSelector() = "div.comics-result div.recommend-item"
// override fun pageListRequest(chapter: SChapter) = GET(baseUrl + chapter.url, headers)
override fun chapterListRequest(manga: SManga) = GET(baseUrl + manga.url + "/episodes", headers)
override fun popularMangaFromElement(element: Element) = mangaFromElement(element) override fun searchMangaFromElement(element: Element): SManga = SManga.create().apply {
override fun latestUpdatesFromElement(element: Element) = mangaFromElement(element) title = element.select("div.recommend-comics-title").text().trim()
override fun searchMangaFromElement(element: Element): SManga { thumbnail_url = element.select("img").attr("abs:src").toNormalPosterUrl()
val manga = SManga.create() url = element.selectFirst("a").attr("href")
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
} }
private fun mangaFromElement(element: Element): SManga {
val manga = SManga.create() override fun searchMangaNextPageSelector() = popularMangaNextPageSelector()
manga.url = (element.select("a").first().attr("href"))
manga.title = element.select("div.content-title").text().trim() override fun mangaDetailsParse(document: Document): SManga = SManga.create().apply {
manga.thumbnail_url = element.select("img").attr("abs:src") author = document.select("div.detail-author-name span").text()
return manga .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> { override fun chapterListParse(response: Response): List<SChapter> {
return super.chapterListParse(response).reversed() val chapterList = super.chapterListParse(response)
// 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
} }
override fun chapterFromElement(element: Element): SChapter { val pageListRequest = pageListRequest(chapterList[breakpoint - 1])
val chapter = SChapter.create() val pageListResponse = client.newCall(pageListRequest).execute()
chapter.url = element.select("a").first().attr("href")
chapter.chapter_number = element.select("div.item-left").text().trim().toFloat() runCatching { pageListParse(pageListResponse) }
val date = element.select("div.episode-date").text() .getOrDefault(emptyList()).isEmpty()
chapter.date_upload = parseDate(date)
chapter.name = if (chapter.chapter_number> 20) { "\uD83D\uDD12 " } else { "" } + element.select("div.episode-title").text().trim()
return chapter
} }
private fun parseDate(date: String): Long { return chapterList
return SimpleDateFormat("yyyy-MM-dd", Locale.US).parse(date)?.time ?: 0L .let { if (firstPaid != null) it.take(firstPaid - 1) else it }
.reversed()
} }
override fun mangaDetailsParse(document: Document): SManga { override fun chapterListSelector() = "a.episode-item-new"
val manga = SManga.create()
manga.author = document.select("div.created-by").text().trim() override fun chapterFromElement(element: Element): SChapter = SChapter.create().apply {
manga.artist = manga.author name = element.select("div.episode-title-new:last-child").text().trim()
manga.description = document.select("div.description").text().trim() chapter_number = element.select("div.episode-number").text().trim()
manga.thumbnail_url = document.select("div.detail-top-right img").attr("abs:src") .toFloatOrNull() ?: -1f
val glist = document.select("div.description-tag div.tag").map { it.text() } date_upload = element.select("div.episode-date span.open-date").text().toDate()
manga.genre = glist.joinToString(", ") url = element.attr("href")
manga.status = when (document.select("span.update-date")?.first()?.text()) { }
"Update" -> SManga.ONGOING
"End", "完结" -> SManga.COMPLETED 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 else -> SManga.UNKNOWN
} }
return manga
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)
} }
override fun pageListParse(response: Response): List<Page> { private val POSTER_SUFFIX = "(jpg)-poster(.*)\\d+?$".toRegex()
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")))
}
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") private val PAID_CHECK_BREAKPOINTS = arrayOf(5, 10, 15, 20)
override fun imageUrlRequest(page: Page) = throw Exception("Not used") }
override fun imageUrlParse(document: Document) = throw Exception("Not used")
} }

View File

@ -4,24 +4,26 @@ import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceFactory import eu.kanade.tachiyomi.source.SourceFactory
class MangaToonFactory : SourceFactory { class MangaToonFactory : SourceFactory {
override fun createSources(): List<Source> = listOf(
ZH(),
EN(),
ID(),
VI(),
ES(),
PT(),
TH()
)
class ZH : MangaToon("zh", "cn") override fun createSources(): List<Source> = listOf(
class EN : MangaToon("en", "en") MangaToonZh(),
class ID : MangaToon("id", "id") MangaToonEn(),
class VI : MangaToon("vi", "vi") MangaToonId(),
class ES : MangaToon("es", "es") MangaToonVi(),
class PT : MangaToon("pt-BR", "pt") { MangaToonEs(),
// Hardcode the id because the language wasn't specific. MangaToonPt(),
override val id: Long = 2064722193112934135 MangaToonTh(),
} MangaToonFr(),
class TH : MangaToon("th", "th") 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")