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'
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"

View File

@ -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)
}
}

View File

@ -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")