diff --git a/src/es/lectortmo/build.gradle b/src/es/lectortmo/build.gradle index 7ddb2ba98..275278c88 100644 --- a/src/es/lectortmo/build.gradle +++ b/src/es/lectortmo/build.gradle @@ -1,7 +1,7 @@ ext { - extName = 'TuMangaOnline / LectorManga' - extClass = '.LectorTmoFactory' - extVersionCode = 4 + extName = 'TuMangaOnline' + extClass = '.LectorTmo' + extVersionCode = 6 isNsfw = true } diff --git a/src/es/lectortmo/src/eu/kanade/tachiyomi/extension/es/lectortmo/LectorTmo.kt b/src/es/lectortmo/src/eu/kanade/tachiyomi/extension/es/lectortmo/LectorTmo.kt index 993aaa6cc..08a92ad62 100644 --- a/src/es/lectortmo/src/eu/kanade/tachiyomi/extension/es/lectortmo/LectorTmo.kt +++ b/src/es/lectortmo/src/eu/kanade/tachiyomi/extension/es/lectortmo/LectorTmo.kt @@ -4,13 +4,11 @@ import android.annotation.SuppressLint import android.app.Application import android.content.SharedPreferences import androidx.preference.CheckBoxPreference -import androidx.preference.ListPreference import androidx.preference.PreferenceScreen import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.POST import eu.kanade.tachiyomi.network.asObservableSuccess 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.model.Filter import eu.kanade.tachiyomi.source.model.FilterList @@ -34,16 +32,20 @@ import java.security.SecureRandom import java.security.cert.X509Certificate import java.text.SimpleDateFormat import java.util.Locale +import java.util.concurrent.TimeUnit import javax.net.ssl.SSLContext import javax.net.ssl.TrustManager import javax.net.ssl.X509TrustManager -abstract class LectorTmo( - override val name: String, - override val baseUrl: String, - override val lang: String, - private val rateLimitClient: OkHttpClient, -) : ParsedHttpSource(), ConfigurableSource { +class LectorTmo : ParsedHttpSource(), ConfigurableSource { + + override val id = 4146344224513899730 + + override val name = "TuMangaOnline" + + override val baseUrl = "https://zonatmo.com" + + override val lang = "es" private val preferences: SharedPreferences by lazy { Injekt.get().getSharedPreferences("source_$id", 0x0000) @@ -56,19 +58,6 @@ abstract class LectorTmo( .set("Referer", "$baseUrl/") .build() - protected open val imageCDNUrls = arrayOf( - "https://img1.japanreader.com", - "https://japanreader.com", - "https://imgtmo.com", - ) - - private fun OkHttpClient.Builder.rateLimitImageCDNs(hosts: Array, permits: Int, period: Long): OkHttpClient.Builder { - hosts.forEach { host -> - rateLimitHost(host.toHttpUrl(), permits, period) - } - return this - } - private fun OkHttpClient.Builder.ignoreAllSSLErrors(): OkHttpClient.Builder { val naiveTrustManager = @SuppressLint("CustomX509TrustManager") object : X509TrustManager { @@ -77,7 +66,7 @@ abstract class LectorTmo( override fun checkServerTrusted(certs: Array, authType: String) = Unit } - val insecureSocketFactory = SSLContext.getInstance("TLSv1.2").apply { + val insecureSocketFactory = SSLContext.getInstance("SSL").apply { val trustAllCerts = arrayOf(naiveTrustManager) init(null, trustAllCerts, SecureRandom()) }.socketFactory @@ -87,28 +76,18 @@ abstract class LectorTmo( return this } - private val ignoreSslClient: OkHttpClient by lazy { - rateLimitClient.newBuilder() + override val client: OkHttpClient by lazy { + network.cloudflareClient.newBuilder() .ignoreAllSSLErrors() - .followRedirects(false) - .rateLimit( - preferences.getString(IMAGE_CDN_RATELIMIT_PREF, IMAGE_CDN_RATELIMIT_PREF_DEFAULT_VALUE)!!.toInt(), - 60, - ) + .rateLimit(3, 1, TimeUnit.SECONDS) .build() } private var lastCFDomain: String = "" - override val client: OkHttpClient by lazy { - rateLimitClient.newBuilder() - .addInterceptor { chain -> - val request = chain.request() - val url = request.url - if (url.fragment == "imagereq") { - return@addInterceptor ignoreSslClient.newCall(request).execute() - } - chain.proceed(request) - } + + // Used on all request except on image requests + private val safeClient: OkHttpClient by lazy { + network.cloudflareClient.newBuilder() .addInterceptor { chain -> if (!getSaveLastCFUrlPref()) return@addInterceptor chain.proceed(chain.request()) val request = chain.request() @@ -118,22 +97,21 @@ abstract class LectorTmo( } response } - .rateLimitHost( - 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, - ) + .rateLimit(1, 3, TimeUnit.SECONDS) .build() } // 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 "" + override fun fetchPopularManga(page: Int): Observable { + 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 popularMangaNextPageSelector() = "a[rel='next']" @@ -148,6 +126,14 @@ abstract class LectorTmo( } } + override fun fetchLatestUpdates(page: Int): Observable { + 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 latestUpdatesNextPageSelector() = popularMangaNextPageSelector() @@ -160,7 +146,7 @@ abstract class LectorTmo( return if (query.startsWith(PREFIX_SLUG_SEARCH)) { val realQuery = query.removePrefix(PREFIX_SLUG_SEARCH) - client.newCall(searchMangaBySlugRequest(realQuery)) + safeClient.newCall(searchMangaBySlugRequest(realQuery)) .asObservableSuccess() .map { response -> val details = mangaDetailsParse(response) @@ -168,7 +154,7 @@ abstract class LectorTmo( MangasPage(listOf(details), false) } } else { - client.newCall(searchMangaRequest(page, query, filters)) + safeClient.newCall(searchMangaRequest(page, query, filters)) .asObservableSuccess() .map { response -> searchMangaParse(response) @@ -241,6 +227,14 @@ abstract class LectorTmo( return super.getMangaUrl(manga) } + override fun fetchMangaDetails(manga: SManga): Observable { + 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 mangaDetailsParse(document: Document) = SManga.create().apply { @@ -257,19 +251,27 @@ abstract class LectorTmo( 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("Finalizado") -> SManga.COMPLETED else -> SManga.UNKNOWN } - protected open val oneShotChapterName = "One Shot" + private val oneShotChapterName = "One Shot" override fun getChapterUrl(chapter: SChapter): String { if (lastCFDomain.isNotEmpty()) return lastCFDomain.also { lastCFDomain = "" } return super.getChapterUrl(chapter) } + override fun fetchChapterList(manga: SManga): Observable> { + return safeClient.newCall(chapterListRequest(manga)) + .asObservableSuccess() + .map { response -> + chapterListParse(response) + } + } + override fun chapterListRequest(manga: SManga) = mangaDetailsRequest(manga) override fun chapterListParse(response: Response): List { @@ -294,15 +296,15 @@ abstract class LectorTmo( 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 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") name = chName scanlator = element.select("div.col-md-6.text-truncate").text() @@ -311,11 +313,19 @@ abstract class LectorTmo( } ?: 0 } - protected open fun parseChapterDate(date: String): Long { + private fun parseChapterDate(date: String): Long { return SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()) .parse(date)?.time ?: 0 } + override fun fetchPageList(chapter: SChapter): Observable> { + return safeClient.newCall(pageListRequest(chapter)) + .asObservableSuccess() + .map { response -> + pageListParse(response) + } + } + override fun pageListRequest(chapter: SChapter): Request { return GET(chapter.url, tmoHeaders) } @@ -440,7 +450,7 @@ abstract class LectorTmo( } override fun imageRequest(page: Page) = GET( - url = page.imageUrl!! + "#imagereq", + url = page.imageUrl!!, headers = headers.newBuilder() .set("Referer", page.url.substringBefore("news/")) .build(), @@ -564,11 +574,11 @@ abstract class LectorTmo( 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) { val sfwModePref = CheckBoxPreference(screen.context).apply { @@ -585,25 +595,6 @@ abstract class LectorTmo( 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 { key = SAVE_LAST_CF_URL_PREF title = SAVE_LAST_CF_URL_PREF_TITLE @@ -613,8 +604,6 @@ abstract class LectorTmo( screen.addPreference(sfwModePref) screen.addPreference(scanlatorPref) - screen.addPreference(apiRateLimitPreference) - screen.addPreference(imgCDNRateLimitPreference) screen.addPreference(saveLastCFUrlPreference) } @@ -633,23 +622,11 @@ abstract class LectorTmo( private const val SFW_MODE_PREF_DEFAULT_VALUE = false 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_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_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_SLUG_SEARCH = "slug:" diff --git a/src/es/lectortmo/src/eu/kanade/tachiyomi/extension/es/lectortmo/LectorTmoFactory.kt b/src/es/lectortmo/src/eu/kanade/tachiyomi/extension/es/lectortmo/LectorTmoFactory.kt deleted file mode 100644 index 6c87a4da4..000000000 --- a/src/es/lectortmo/src/eu/kanade/tachiyomi/extension/es/lectortmo/LectorTmoFactory.kt +++ /dev/null @@ -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().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 = mutableListOf().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 - } -}