LectorTmo: Remove LectorManga and fix crash on some devices (#7508)

* override fetch methods

* remove lectorManga
This commit is contained in:
bapeey 2025-02-06 08:24:46 -05:00 committed by Draff
parent 7dd76fe0c9
commit 2147b87816
No known key found for this signature in database
GPG Key ID: E8A89F3211677653
3 changed files with 74 additions and 180 deletions

View File

@ -1,7 +1,7 @@
ext { ext {
extName = 'TuMangaOnline / LectorManga' extName = 'TuMangaOnline'
extClass = '.LectorTmoFactory' extClass = '.LectorTmo'
extVersionCode = 4 extVersionCode = 6
isNsfw = true isNsfw = true
} }

View File

@ -4,13 +4,11 @@ import android.annotation.SuppressLint
import android.app.Application import android.app.Application
import android.content.SharedPreferences import android.content.SharedPreferences
import androidx.preference.CheckBoxPreference import androidx.preference.CheckBoxPreference
import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.network.asObservableSuccess import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.network.interceptor.rateLimit import eu.kanade.tachiyomi.network.interceptor.rateLimit
import eu.kanade.tachiyomi.network.interceptor.rateLimitHost
import eu.kanade.tachiyomi.source.ConfigurableSource import eu.kanade.tachiyomi.source.ConfigurableSource
import eu.kanade.tachiyomi.source.model.Filter import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.FilterList
@ -34,16 +32,20 @@ import java.security.SecureRandom
import java.security.cert.X509Certificate import java.security.cert.X509Certificate
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Locale import java.util.Locale
import java.util.concurrent.TimeUnit
import javax.net.ssl.SSLContext import javax.net.ssl.SSLContext
import javax.net.ssl.TrustManager import javax.net.ssl.TrustManager
import javax.net.ssl.X509TrustManager import javax.net.ssl.X509TrustManager
abstract class LectorTmo( class LectorTmo : ParsedHttpSource(), ConfigurableSource {
override val name: String,
override val baseUrl: String, override val id = 4146344224513899730
override val lang: String,
private val rateLimitClient: OkHttpClient, override val name = "TuMangaOnline"
) : ParsedHttpSource(), ConfigurableSource {
override val baseUrl = "https://zonatmo.com"
override val lang = "es"
private val preferences: SharedPreferences by lazy { private val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000) Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
@ -56,19 +58,6 @@ abstract class LectorTmo(
.set("Referer", "$baseUrl/") .set("Referer", "$baseUrl/")
.build() .build()
protected open val imageCDNUrls = arrayOf(
"https://img1.japanreader.com",
"https://japanreader.com",
"https://imgtmo.com",
)
private fun OkHttpClient.Builder.rateLimitImageCDNs(hosts: Array<String>, permits: Int, period: Long): OkHttpClient.Builder {
hosts.forEach { host ->
rateLimitHost(host.toHttpUrl(), permits, period)
}
return this
}
private fun OkHttpClient.Builder.ignoreAllSSLErrors(): OkHttpClient.Builder { private fun OkHttpClient.Builder.ignoreAllSSLErrors(): OkHttpClient.Builder {
val naiveTrustManager = @SuppressLint("CustomX509TrustManager") val naiveTrustManager = @SuppressLint("CustomX509TrustManager")
object : X509TrustManager { object : X509TrustManager {
@ -77,7 +66,7 @@ abstract class LectorTmo(
override fun checkServerTrusted(certs: Array<X509Certificate>, authType: String) = Unit override fun checkServerTrusted(certs: Array<X509Certificate>, authType: String) = Unit
} }
val insecureSocketFactory = SSLContext.getInstance("TLSv1.2").apply { val insecureSocketFactory = SSLContext.getInstance("SSL").apply {
val trustAllCerts = arrayOf<TrustManager>(naiveTrustManager) val trustAllCerts = arrayOf<TrustManager>(naiveTrustManager)
init(null, trustAllCerts, SecureRandom()) init(null, trustAllCerts, SecureRandom())
}.socketFactory }.socketFactory
@ -87,28 +76,18 @@ abstract class LectorTmo(
return this return this
} }
private val ignoreSslClient: OkHttpClient by lazy { override val client: OkHttpClient by lazy {
rateLimitClient.newBuilder() network.cloudflareClient.newBuilder()
.ignoreAllSSLErrors() .ignoreAllSSLErrors()
.followRedirects(false) .rateLimit(3, 1, TimeUnit.SECONDS)
.rateLimit(
preferences.getString(IMAGE_CDN_RATELIMIT_PREF, IMAGE_CDN_RATELIMIT_PREF_DEFAULT_VALUE)!!.toInt(),
60,
)
.build() .build()
} }
private var lastCFDomain: String = "" private var lastCFDomain: String = ""
override val client: OkHttpClient by lazy {
rateLimitClient.newBuilder() // Used on all request except on image requests
.addInterceptor { chain -> private val safeClient: OkHttpClient by lazy {
val request = chain.request() network.cloudflareClient.newBuilder()
val url = request.url
if (url.fragment == "imagereq") {
return@addInterceptor ignoreSslClient.newCall(request).execute()
}
chain.proceed(request)
}
.addInterceptor { chain -> .addInterceptor { chain ->
if (!getSaveLastCFUrlPref()) return@addInterceptor chain.proceed(chain.request()) if (!getSaveLastCFUrlPref()) return@addInterceptor chain.proceed(chain.request())
val request = chain.request() val request = chain.request()
@ -118,22 +97,21 @@ abstract class LectorTmo(
} }
response response
} }
.rateLimitHost( .rateLimit(1, 3, TimeUnit.SECONDS)
baseUrl.toHttpUrl(),
preferences.getString(WEB_RATELIMIT_PREF, WEB_RATELIMIT_PREF_DEFAULT_VALUE)!!.toInt(),
60,
)
.rateLimitImageCDNs(
imageCDNUrls,
preferences.getString(IMAGE_CDN_RATELIMIT_PREF, IMAGE_CDN_RATELIMIT_PREF_DEFAULT_VALUE)!!.toInt(),
60,
)
.build() .build()
} }
// Marks erotic content as false and excludes: Ecchi(6), GirlsLove(17), BoysLove(18), Harem(19), Trap(94) genders // Marks erotic content as false and excludes: Ecchi(6), GirlsLove(17), BoysLove(18), Harem(19), Trap(94) genders
private fun getSFWUrlPart(): String = if (getSFWModePref()) "&exclude_genders%5B%5D=6&exclude_genders%5B%5D=17&exclude_genders%5B%5D=18&exclude_genders%5B%5D=19&exclude_genders%5B%5D=94&erotic=false" else "" private fun getSFWUrlPart(): String = if (getSFWModePref()) "&exclude_genders%5B%5D=6&exclude_genders%5B%5D=17&exclude_genders%5B%5D=18&exclude_genders%5B%5D=19&exclude_genders%5B%5D=94&erotic=false" else ""
override fun fetchPopularManga(page: Int): Observable<MangasPage> {
return safeClient.newCall(popularMangaRequest(page))
.asObservableSuccess()
.map { response ->
popularMangaParse(response)
}
}
override fun popularMangaRequest(page: Int) = GET("$baseUrl/library?order_item=likes_count&order_dir=desc&filter_by=title${getSFWUrlPart()}&_pg=1&page=$page", tmoHeaders) override fun popularMangaRequest(page: Int) = GET("$baseUrl/library?order_item=likes_count&order_dir=desc&filter_by=title${getSFWUrlPart()}&_pg=1&page=$page", tmoHeaders)
override fun popularMangaNextPageSelector() = "a[rel='next']" override fun popularMangaNextPageSelector() = "a[rel='next']"
@ -148,6 +126,14 @@ abstract class LectorTmo(
} }
} }
override fun fetchLatestUpdates(page: Int): Observable<MangasPage> {
return safeClient.newCall(latestUpdatesRequest(page))
.asObservableSuccess()
.map { response ->
latestUpdatesParse(response)
}
}
override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/library?order_item=creation&order_dir=desc&filter_by=title${getSFWUrlPart()}&_pg=1&page=$page", tmoHeaders) override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/library?order_item=creation&order_dir=desc&filter_by=title${getSFWUrlPart()}&_pg=1&page=$page", tmoHeaders)
override fun latestUpdatesNextPageSelector() = popularMangaNextPageSelector() override fun latestUpdatesNextPageSelector() = popularMangaNextPageSelector()
@ -160,7 +146,7 @@ abstract class LectorTmo(
return if (query.startsWith(PREFIX_SLUG_SEARCH)) { return if (query.startsWith(PREFIX_SLUG_SEARCH)) {
val realQuery = query.removePrefix(PREFIX_SLUG_SEARCH) val realQuery = query.removePrefix(PREFIX_SLUG_SEARCH)
client.newCall(searchMangaBySlugRequest(realQuery)) safeClient.newCall(searchMangaBySlugRequest(realQuery))
.asObservableSuccess() .asObservableSuccess()
.map { response -> .map { response ->
val details = mangaDetailsParse(response) val details = mangaDetailsParse(response)
@ -168,7 +154,7 @@ abstract class LectorTmo(
MangasPage(listOf(details), false) MangasPage(listOf(details), false)
} }
} else { } else {
client.newCall(searchMangaRequest(page, query, filters)) safeClient.newCall(searchMangaRequest(page, query, filters))
.asObservableSuccess() .asObservableSuccess()
.map { response -> .map { response ->
searchMangaParse(response) searchMangaParse(response)
@ -241,6 +227,14 @@ abstract class LectorTmo(
return super.getMangaUrl(manga) return super.getMangaUrl(manga)
} }
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
return safeClient.newCall(mangaDetailsRequest(manga))
.asObservableSuccess()
.map { response ->
mangaDetailsParse(response).apply { initialized = true }
}
}
override fun mangaDetailsRequest(manga: SManga) = GET(baseUrl + manga.url, tmoHeaders) override fun mangaDetailsRequest(manga: SManga) = GET(baseUrl + manga.url, tmoHeaders)
override fun mangaDetailsParse(document: Document) = SManga.create().apply { override fun mangaDetailsParse(document: Document) = SManga.create().apply {
@ -257,19 +251,27 @@ abstract class LectorTmo(
thumbnail_url = document.select(".book-thumbnail").attr("src") thumbnail_url = document.select(".book-thumbnail").attr("src")
} }
protected fun parseStatus(status: String) = when { private fun parseStatus(status: String) = when {
status.contains("Publicándose") -> SManga.ONGOING status.contains("Publicándose") -> SManga.ONGOING
status.contains("Finalizado") -> SManga.COMPLETED status.contains("Finalizado") -> SManga.COMPLETED
else -> SManga.UNKNOWN else -> SManga.UNKNOWN
} }
protected open val oneShotChapterName = "One Shot" private val oneShotChapterName = "One Shot"
override fun getChapterUrl(chapter: SChapter): String { override fun getChapterUrl(chapter: SChapter): String {
if (lastCFDomain.isNotEmpty()) return lastCFDomain.also { lastCFDomain = "" } if (lastCFDomain.isNotEmpty()) return lastCFDomain.also { lastCFDomain = "" }
return super.getChapterUrl(chapter) return super.getChapterUrl(chapter)
} }
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
return safeClient.newCall(chapterListRequest(manga))
.asObservableSuccess()
.map { response ->
chapterListParse(response)
}
}
override fun chapterListRequest(manga: SManga) = mangaDetailsRequest(manga) override fun chapterListRequest(manga: SManga) = mangaDetailsRequest(manga)
override fun chapterListParse(response: Response): List<SChapter> { override fun chapterListParse(response: Response): List<SChapter> {
@ -294,15 +296,15 @@ abstract class LectorTmo(
return chapters return chapters
} }
protected open val oneShotChapterListSelector = "div.chapter-list-element > ul.list-group li.list-group-item" private val oneShotChapterListSelector = "div.chapter-list-element > ul.list-group li.list-group-item"
protected open val regularChapterListSelector = "div.chapters > ul.list-group li.p-0.list-group-item" private val regularChapterListSelector = "div.chapters > ul.list-group li.p-0.list-group-item"
override fun chapterListSelector() = throw UnsupportedOperationException() override fun chapterListSelector() = throw UnsupportedOperationException()
override fun chapterFromElement(element: Element) = throw UnsupportedOperationException() override fun chapterFromElement(element: Element) = throw UnsupportedOperationException()
protected open fun chapterFromElement(element: Element, chName: String) = SChapter.create().apply { private fun chapterFromElement(element: Element, chName: String) = SChapter.create().apply {
url = element.select("div.row > .text-right > a").attr("href") url = element.select("div.row > .text-right > a").attr("href")
name = chName name = chName
scanlator = element.select("div.col-md-6.text-truncate").text() scanlator = element.select("div.col-md-6.text-truncate").text()
@ -311,11 +313,19 @@ abstract class LectorTmo(
} ?: 0 } ?: 0
} }
protected open fun parseChapterDate(date: String): Long { private fun parseChapterDate(date: String): Long {
return SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()) return SimpleDateFormat("yyyy-MM-dd", Locale.getDefault())
.parse(date)?.time ?: 0 .parse(date)?.time ?: 0
} }
override fun fetchPageList(chapter: SChapter): Observable<List<Page>> {
return safeClient.newCall(pageListRequest(chapter))
.asObservableSuccess()
.map { response ->
pageListParse(response)
}
}
override fun pageListRequest(chapter: SChapter): Request { override fun pageListRequest(chapter: SChapter): Request {
return GET(chapter.url, tmoHeaders) return GET(chapter.url, tmoHeaders)
} }
@ -440,7 +450,7 @@ abstract class LectorTmo(
} }
override fun imageRequest(page: Page) = GET( override fun imageRequest(page: Page) = GET(
url = page.imageUrl!! + "#imagereq", url = page.imageUrl!!,
headers = headers.newBuilder() headers = headers.newBuilder()
.set("Referer", page.url.substringBefore("news/")) .set("Referer", page.url.substringBefore("news/"))
.build(), .build(),
@ -564,11 +574,11 @@ abstract class LectorTmo(
Genre("Trap", "94"), Genre("Trap", "94"),
) )
protected fun getScanlatorPref(): Boolean = preferences.getBoolean(SCANLATOR_PREF, SCANLATOR_PREF_DEFAULT_VALUE) private fun getScanlatorPref(): Boolean = preferences.getBoolean(SCANLATOR_PREF, SCANLATOR_PREF_DEFAULT_VALUE)
protected fun getSFWModePref(): Boolean = preferences.getBoolean(SFW_MODE_PREF, SFW_MODE_PREF_DEFAULT_VALUE) private fun getSFWModePref(): Boolean = preferences.getBoolean(SFW_MODE_PREF, SFW_MODE_PREF_DEFAULT_VALUE)
protected fun getSaveLastCFUrlPref(): Boolean = preferences.getBoolean(SAVE_LAST_CF_URL_PREF, SAVE_LAST_CF_URL_PREF_DEFAULT_VALUE) private fun getSaveLastCFUrlPref(): Boolean = preferences.getBoolean(SAVE_LAST_CF_URL_PREF, SAVE_LAST_CF_URL_PREF_DEFAULT_VALUE)
override fun setupPreferenceScreen(screen: PreferenceScreen) { override fun setupPreferenceScreen(screen: PreferenceScreen) {
val sfwModePref = CheckBoxPreference(screen.context).apply { val sfwModePref = CheckBoxPreference(screen.context).apply {
@ -585,25 +595,6 @@ abstract class LectorTmo(
setDefaultValue(SCANLATOR_PREF_DEFAULT_VALUE) setDefaultValue(SCANLATOR_PREF_DEFAULT_VALUE)
} }
// Rate limit
val apiRateLimitPreference = ListPreference(screen.context).apply {
key = WEB_RATELIMIT_PREF
title = WEB_RATELIMIT_PREF_TITLE
summary = WEB_RATELIMIT_PREF_SUMMARY
entries = ENTRIES_ARRAY
entryValues = ENTRIES_ARRAY
setDefaultValue(WEB_RATELIMIT_PREF_DEFAULT_VALUE)
}
val imgCDNRateLimitPreference = ListPreference(screen.context).apply {
key = IMAGE_CDN_RATELIMIT_PREF
title = IMAGE_CDN_RATELIMIT_PREF_TITLE
summary = IMAGE_CDN_RATELIMIT_PREF_SUMMARY
entries = ENTRIES_ARRAY
entryValues = ENTRIES_ARRAY
setDefaultValue(IMAGE_CDN_RATELIMIT_PREF_DEFAULT_VALUE)
}
val saveLastCFUrlPreference = CheckBoxPreference(screen.context).apply { val saveLastCFUrlPreference = CheckBoxPreference(screen.context).apply {
key = SAVE_LAST_CF_URL_PREF key = SAVE_LAST_CF_URL_PREF
title = SAVE_LAST_CF_URL_PREF_TITLE title = SAVE_LAST_CF_URL_PREF_TITLE
@ -613,8 +604,6 @@ abstract class LectorTmo(
screen.addPreference(sfwModePref) screen.addPreference(sfwModePref)
screen.addPreference(scanlatorPref) screen.addPreference(scanlatorPref)
screen.addPreference(apiRateLimitPreference)
screen.addPreference(imgCDNRateLimitPreference)
screen.addPreference(saveLastCFUrlPreference) screen.addPreference(saveLastCFUrlPreference)
} }
@ -633,23 +622,11 @@ abstract class LectorTmo(
private const val SFW_MODE_PREF_DEFAULT_VALUE = false private const val SFW_MODE_PREF_DEFAULT_VALUE = false
private val SFW_MODE_PREF_EXCLUDE_GENDERS = listOf("6", "17", "18", "19") private val SFW_MODE_PREF_EXCLUDE_GENDERS = listOf("6", "17", "18", "19")
private const val WEB_RATELIMIT_PREF = "webRatelimitPreference"
private const val WEB_RATELIMIT_PREF_TITLE = "Ratelimit por minuto para el sitio web"
private const val WEB_RATELIMIT_PREF_SUMMARY = "Este valor afecta la cantidad de solicitudes de red a la URL de TMO. Reducir este valor puede disminuir la posibilidad de obtener un error HTTP 429, pero la velocidad de descarga será más lenta. Se requiere reiniciar la app. \nValor actual: %s"
private const val WEB_RATELIMIT_PREF_DEFAULT_VALUE = "8"
private const val IMAGE_CDN_RATELIMIT_PREF = "imgCDNRatelimitPreference"
private const val IMAGE_CDN_RATELIMIT_PREF_TITLE = "Ratelimit por minuto para descarga de imágenes"
private const val IMAGE_CDN_RATELIMIT_PREF_SUMMARY = "Este valor afecta la cantidad de solicitudes de red para descargar imágenes. Reducir este valor puede disminuir errores al cargar imagenes, pero la velocidad de descarga será más lenta. Se requiere reiniciar la app. \nValor actual: %s"
private const val IMAGE_CDN_RATELIMIT_PREF_DEFAULT_VALUE = "50"
private const val SAVE_LAST_CF_URL_PREF = "saveLastCFUrlPreference" private const val SAVE_LAST_CF_URL_PREF = "saveLastCFUrlPreference"
private const val SAVE_LAST_CF_URL_PREF_TITLE = "Guardar la última URL con error de Cloudflare" private const val SAVE_LAST_CF_URL_PREF_TITLE = "Guardar la última URL con error de Cloudflare"
private const val SAVE_LAST_CF_URL_PREF_SUMMARY = "Guarda la última URL con error de Cloudflare para que se pueda acceder a ella al abrir la serie en WebView." private const val SAVE_LAST_CF_URL_PREF_SUMMARY = "Guarda la última URL con error de Cloudflare para que se pueda acceder a ella al abrir la serie en WebView."
private const val SAVE_LAST_CF_URL_PREF_DEFAULT_VALUE = true private const val SAVE_LAST_CF_URL_PREF_DEFAULT_VALUE = true
private val ENTRIES_ARRAY = listOf(1, 2, 3, 5, 6, 7, 8, 9, 10, 15, 20, 30, 40, 50, 100).map { i -> i.toString() }.toTypedArray()
const val PREFIX_LIBRARY = "library" const val PREFIX_LIBRARY = "library"
const val PREFIX_SLUG_SEARCH = "slug:" const val PREFIX_SLUG_SEARCH = "slug:"

View File

@ -1,83 +0,0 @@
package eu.kanade.tachiyomi.extension.es.lectortmo
import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.network.interceptor.rateLimit
import eu.kanade.tachiyomi.source.SourceFactory
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.Response
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.util.concurrent.TimeUnit
class LectorTmoFactory : SourceFactory {
override fun createSources() = listOf(
LectorManga(),
TuMangaOnline(),
)
}
val rateLimitClient = Injekt.get<NetworkHelper>().cloudflareClient.newBuilder()
.rateLimit(1, 1500, TimeUnit.MILLISECONDS)
.build()
class TuMangaOnline : LectorTmo("TuMangaOnline", "https://zonatmo.com", "es", rateLimitClient) {
override val id = 4146344224513899730
}
class LectorManga : LectorTmo("LectorManga", "https://lectormanga.com", "es", rateLimitClient) {
override val id = 7925520943983324764
override fun popularMangaSelector() = ".col-6 .card"
override fun popularMangaFromElement(element: Element) = SManga.create().apply {
setUrlWithoutDomain(element.select("a").attr("href"))
title = element.select("a").text()
thumbnail_url = element.select("img").attr("src")
}
override fun mangaDetailsParse(document: Document) = SManga.create().apply {
document.selectFirst("h1:has(small)")?.let { title = it.ownText() }
genre = document.select("a.py-2").joinToString(", ") {
it.text()
}
description = document.select(".col-12.mt-2").text()
status = parseStatus(document.select(".status-publishing").text())
thumbnail_url = document.select(".text-center img.img-fluid").attr("src")
}
override fun chapterListParse(response: Response): List<SChapter> = mutableListOf<SChapter>().apply {
val document = response.asJsoup()
// One-shot
if (document.select("#chapters").isEmpty()) {
return document.select(oneShotChapterListSelector).map { chapterFromElement(it, oneShotChapterName) }
}
// Regular list of chapters
val chapterNames = document.select("#chapters h4.text-truncate")
val chapterInfos = document.select("#chapters .chapter-list")
chapterNames.forEachIndexed { index, _ ->
val scanlator = chapterInfos[index].select("li")
if (getScanlatorPref()) {
scanlator.forEach { add(chapterFromElement(it, chapterNames[index].text())) }
} else {
scanlator.last { add(chapterFromElement(it, chapterNames[index].text())) }
}
}
}
override fun chapterFromElement(element: Element, chName: String) = SChapter.create().apply {
url = element.select("div.row > .text-right > a").attr("href")
name = chName
scanlator = element.select("div.col-12.text-truncate span").text()
date_upload = element.select("span.badge.badge-primary.p-2").first()?.text()?.let {
parseChapterDate(it)
} ?: 0
}
}