From 24aa61ed779a9e7bb61f405c6bdd3d1b37903350 Mon Sep 17 00:00:00 2001 From: Chopper <156493704+choppeh@users.noreply.github.com> Date: Thu, 23 Jan 2025 15:09:29 -0300 Subject: [PATCH] SussyToons: Fixes (#7294) Fixes --- src/pt/sussyscan/build.gradle | 2 +- .../extension/pt/sussyscan/SussyToons.kt | 401 ++---------------- 2 files changed, 30 insertions(+), 373 deletions(-) diff --git a/src/pt/sussyscan/build.gradle b/src/pt/sussyscan/build.gradle index 8573b40ad..b30fd3f20 100644 --- a/src/pt/sussyscan/build.gradle +++ b/src/pt/sussyscan/build.gradle @@ -1,7 +1,7 @@ ext { extName = 'Sussy Toons' extClass = '.SussyToons' - extVersionCode = 48 + extVersionCode = 49 isNsfw = true } diff --git a/src/pt/sussyscan/src/eu/kanade/tachiyomi/extension/pt/sussyscan/SussyToons.kt b/src/pt/sussyscan/src/eu/kanade/tachiyomi/extension/pt/sussyscan/SussyToons.kt index 2d48fe5ef..918d195c0 100644 --- a/src/pt/sussyscan/src/eu/kanade/tachiyomi/extension/pt/sussyscan/SussyToons.kt +++ b/src/pt/sussyscan/src/eu/kanade/tachiyomi/extension/pt/sussyscan/SussyToons.kt @@ -3,18 +3,11 @@ package eu.kanade.tachiyomi.extension.pt.sussyscan import android.annotation.SuppressLint import android.app.Application import android.content.SharedPreferences -import android.os.Handler -import android.os.Looper -import android.webkit.WebResourceRequest -import android.webkit.WebResourceResponse -import android.webkit.WebSettings -import android.webkit.WebView -import android.webkit.WebViewClient import android.widget.Toast import androidx.preference.EditTextPreference import androidx.preference.PreferenceScreen +import androidx.preference.SwitchPreferenceCompat import eu.kanade.tachiyomi.network.GET -import eu.kanade.tachiyomi.network.interceptor.rateLimit import eu.kanade.tachiyomi.source.ConfigurableSource import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.MangasPage @@ -22,26 +15,18 @@ 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.HttpSource -import eu.kanade.tachiyomi.util.asJsoup import kotlinx.serialization.json.Json import kotlinx.serialization.json.decodeFromStream -import okhttp3.Headers import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.Interceptor -import okhttp3.Protocol import okhttp3.Request import okhttp3.Response -import okhttp3.internal.http.HTTP_BAD_GATEWAY import org.jsoup.Jsoup -import org.jsoup.select.Elements import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get import uy.kohesive.injekt.injectLazy -import java.io.IOException import java.text.SimpleDateFormat import java.util.Locale -import java.util.concurrent.CountDownLatch -import java.util.concurrent.TimeUnit class SussyToons : HttpSource(), ConfigurableSource { @@ -67,13 +52,9 @@ class SussyToons : HttpSource(), ConfigurableSource { get() = preferences.getString(API_BASE_URL_PREF, defaultApiUrl)!! set(value) = preferences.edit().putString(API_BASE_URL_PREF, value).apply() - private var chapterScriptUrl: String - get() = preferences.getString(CHAPTER_SCRIPT_URL_PREF, "")!! - set(value) = preferences.edit().putString(CHAPTER_SCRIPT_URL_PREF, value).apply() - - private var pageScriptUrl: String - get() = preferences.getString(PAGE_SCRIPT_URL_PREF, "")!! - set(value) = preferences.edit().putString(PAGE_SCRIPT_URL_PREF, value).apply() + private var restoreDefaultEnable: Boolean + get() = preferences.getBoolean(DEFAULT_PREF, false) + set(value) = preferences.edit().putBoolean(DEFAULT_PREF, value).apply() override val baseUrl: String get() = when { isCi -> defaultBaseUrl @@ -84,14 +65,16 @@ class SussyToons : HttpSource(), ConfigurableSource { private val defaultApiUrl: String = "https://api-dev.sussytoons.site" override val client = network.cloudflareClient.newBuilder() - .rateLimit(2) - .addInterceptor(::findApiUrl) - .addInterceptor(::findChapterUrl) - .addInterceptor(::chapterPages) .addInterceptor(::imageLocation) .build() init { + if (restoreDefaultEnable) { + restoreDefaultEnable = false + preferences.edit().putString(DEFAULT_BASE_URL_PREF, null).apply() + preferences.edit().putString(API_DEFAULT_BASE_URL_PREF, null).apply() + } + preferences.getString(DEFAULT_BASE_URL_PREF, null).let { domain -> if (domain != defaultBaseUrl) { preferences.edit() @@ -187,11 +170,7 @@ class SussyToons : HttpSource(), ConfigurableSource { it.chapterNumber?.let { chapter_number = it } - val chapterApiUrl = apiUrl.toHttpUrl().newBuilder() - .addEncodedPathSegments(chapterUrl!!) - .addPathSegment(it.id.toString()) - .build() - setUrlWithoutDomain(chapterApiUrl.toString()) + setUrlWithoutDomain("$baseUrl/capitulo/${it.id}") date_upload = it.updateAt.toDate() } }.sortedBy(SChapter::chapter_number).reversed() @@ -200,38 +179,13 @@ class SussyToons : HttpSource(), ConfigurableSource { // ============================= Pages ==================================== override fun pageListRequest(chapter: SChapter): Request { - return super.pageListRequest(chapter).let { request -> - val url = request.url.newBuilder() - .fragment("$pageImagePrefix${chapter.url}") - .build() - - request.newBuilder() - .url(url) - .build() - } + val request = super.pageListRequest(chapter) + val chapterPageId = request.url.pathSegments.last() + return GET("$apiUrl/capitulos/$chapterPageId", headers) } - private var pageUrlSegment: String? = null - override fun pageListParse(response: Response): List { - pageUrlSegment = pageUrlSegment ?: findPageUrlSegment(response) - val chapterPageId = response.request.url.pathSegments.last() - - val chapterUrl = response.request.url.fragment - ?.substringAfter(pageImagePrefix) - ?: throw Exception("Não foi possivel carregar as páginas") - - val url = apiUrl.toHttpUrl().newBuilder() - .addEncodedPathSegments(pageUrlSegment!!) - .addPathSegment(chapterPageId) - .fragment( - "$chapterPagePrefix${"$baseUrl$chapterUrl"}", - ) - .build() - - val res = client.newCall(GET(url, headers)).execute() - val dto = res.parseAs>().results - + val dto = response.parseAs>().results return dto.pages.mapIndexed { index, image -> val imageUrl = when { image.isWordPressContent() -> { @@ -249,28 +203,6 @@ class SussyToons : HttpSource(), ConfigurableSource { } } - /** - * Get the “dynamic” path segment of the chapter page - */ - private fun findPageUrlSegment(response: Response): String { - val scriptUrls = when { - pageScriptUrl.isNotBlank() -> listOf(pageScriptUrl to headers) - else -> emptyList() - } - - val script = loadJsScript( - urls = scriptUrls, - doRequest = { client.newCall(it).execute() }, - pattern = pageUrlRegex, - fallback = { fetchAllNextJsScriptUrls(response.request) }, - ) - - pageScriptUrl = script.url - - return pageUrlRegex.find(script.body)?.groups?.get(2)?.value?.toPathSegment() - ?: throw IOException("Não foi encontrar o caminho das páginas") - } - override fun imageUrlParse(response: Response): String = "" override fun imageUrlRequest(page: Page): Request { @@ -282,135 +214,6 @@ class SussyToons : HttpSource(), ConfigurableSource { // ============================= Interceptors ================================= - private var chapterPageHeaders: Headers? = null - - private var chapterUrl: String? = null - - private fun findApiUrl(chain: Interceptor.Chain): Response { - val request = chain.request() - val response: Response = try { - chain.proceed(request) - } catch (ex: Exception) { - chain.createBadGatewayResponse(request) - } - - if (response.isSuccessful || request.url.toString().contains(apiUrl).not()) { - return response - } - - response.close() - - fetchApiUrl(chain).forEach { urlCandidate -> - val url = request.url.toString() - .replace(apiUrl, urlCandidate) - .toHttpUrl() - - val newRequest = request.newBuilder() - .url(url) - .build() - - val localResponse = chain.proceed(newRequest) - if (localResponse.isSuccessful.not()) { - localResponse.close() - return@forEach - } - apiUrl = urlCandidate - return localResponse - } - - throw IOException( - buildString { - append("Não foi possível encontrar a URL da API.") - append("Troque manualmente nas configurações da extensão") - }, - ) - } - - private fun Interceptor.Chain.createBadGatewayResponse(request: Request): Response { - return Response.Builder() - .request(request) - .protocol(Protocol.HTTP_1_1) - .message("") - .code(HTTP_BAD_GATEWAY) - .build() - } - - private fun fetchApiUrl(chain: Interceptor.Chain): List { - val scripts = chain.proceed(GET(baseUrl, headers)).asJsoup() - .select("script[src*=next]:not([nomodule]):not([src*=app])") - - val script = getScriptBodyWithUrls(scripts, chain) - ?: throw Exception("Não foi possivel localizar a URL da API") - - return apiUrlRegex.findAll(script) - .flatMap { stringsRegex.findAll(it.value).map { match -> match.groupValues[1] } } - .filter(urlRegex::containsMatchIn) - .toList() - } - - private fun getScriptBodyWithUrls(scripts: Elements, chain: Interceptor.Chain): String? { - val elements = scripts.toList().reversed() - for (element in elements) { - val scriptUrl = element.absUrl("src") - val script = chain.proceed(GET(scriptUrl, headers)).body.string() - if (apiUrlRegex.containsMatchIn(script)) { - return script - } - } - return null - } - - /** - * Get the “dynamic” path segment of the chapter list - */ - private fun findChapterUrl(chain: Interceptor.Chain): Response { - val request = chain.request() - - val mangaUrl = request.url.fragment - ?.takeIf { - it.contains(mangaPagePrefix, ignoreCase = true) && chapterUrl.isNullOrBlank() - }?.substringAfter(mangaPagePrefix) - ?: return chain.proceed(request) - - val scriptUrls = when { - chapterScriptUrl.isNotBlank() -> listOf(chapterScriptUrl to headers) - else -> emptyList() - } - - val script = loadJsScript( - urls = scriptUrls, - doRequest = chain::proceed, - pattern = chapterUrlRegex, - fallback = { fetchAllNextJsScriptUrls(GET(mangaUrl, headers)) }, - ) - - chapterScriptUrl = script.url - - chapterUrl = chapterUrlRegex.find(script.body)?.groups?.get(1)?.value?.toPathSegment() - ?: throw IOException("Não foi possivel extrair a URL do capitulo") - - return chain.proceed(request) - } - - private fun loadJsScript( - urls: List>, - doRequest: (request: Request) -> Response, - pattern: Regex, - fallback: (() -> List>)? = null, - ): Script { - val script = urls.map { pair -> - val request = GET(pair.first, pair.second) - Script( - url = request.url.toString(), - body = doRequest(request).use { response -> response.body.string() }, - ) - }.firstOrNull { pattern.containsMatchIn(it.body) } - - return script ?: fallback?.let { urlList -> - loadJsScript(urlList(), doRequest = doRequest, pattern = pattern) - } ?: throw IOException("Não foi possivel encontrar a URL do capitulo") - } - private fun imageLocation(chain: Interceptor.Chain): Response { val request = chain.request() val response = chain.proceed(request) @@ -431,146 +234,6 @@ class SussyToons : HttpSource(), ConfigurableSource { return response } - /** - * Resolve the “dynamic” headers of the chapter page - */ - private fun chapterPages(chain: Interceptor.Chain): Response { - val request = chain.request() - val chapterUrl = request.url.fragment - ?.takeIf { it.contains(chapterPagePrefix) } - ?.substringAfter(chapterPagePrefix)?.toHttpUrl()?.newBuilder()?.fragment(null) - ?.build() - ?: return chain.proceed(request) - - val originUrl = request.url.newBuilder() - .fragment(null) - .build() - - val newRequest = request.newBuilder() - .url(originUrl) - - chapterPageHeaders?.let { headers -> - newRequest.headers(headers) - val response = chain.proceed(newRequest.build()) - if (response.isSuccessful) { - return response - } - response.close() - } - - val chapterPageRequest = request.newBuilder() - .url(chapterUrl) - .build() - - return chain.proceed(fetchChapterPagesHeaders(chapterPageRequest, newRequest.build())) - } - - @SuppressLint("SetJavaScriptEnabled") - private fun fetchChapterPagesHeaders(baseRequest: Request, originRequest: Request): Request { - fun WebResourceRequest.isOriginRequest() = - originRequest.url.toString().equals(this.url.toString(), ignoreCase = true) - - chapterPageHeaders = handlingWithWebResourceRequest( - baseRequest, - initial = headersBuilder(), - stopCondition = { _, _, resource -> - resource.isOriginRequest() && resource.method.equals("GET", true) - }, - fold = { headers, _, resource -> - headers.apply { - if (resource.isOriginRequest().not() || resource.method.equals("GET", true).not()) { - return@apply - } - fill(resource.requestHeaders) - } - }, - ).build() - - return originRequest.newBuilder() - .headers(chapterPageHeaders!!) - .build() - } - - @SuppressLint("SetJavaScriptEnabled") - private fun fetchAllNextJsScriptUrls(baseRequest: Request): List> { - fun WebResourceRequest.isNextJSUrl() = this.url.toString().contains("_next", ignoreCase = true) && - this.url.toString().contains(".js", ignoreCase = true) - - return handlingWithWebResourceRequest( - baseRequest, - initial = mutableListOf(), - stopCondition = { urls, _, _ -> - val minUrlsAvailable = 24 - urls.size > minUrlsAvailable - }, - fold = { urls, base, resource -> - urls.apply { - if (resource.isNextJSUrl().not()) { - return@apply - } - val headers = base.headers.newBuilder().apply { - fill(resource.requestHeaders) - } - add(resource.url.toString() to headers.build()) - } - }, - ) - } - - @SuppressLint("SetJavaScriptEnabled") - private fun handlingWithWebResourceRequest( - baseRequest: Request, - initial: T, - stopCondition: (T, Request, WebResourceRequest) -> Boolean, - fold: (T, Request, WebResourceRequest) -> T, - ): T { - val latch = CountDownLatch(1) - var webView: WebView? = null - val looper = Handler(Looper.getMainLooper()) - var state = initial - looper.post { - webView = WebView(Injekt.get()) - webView?.let { - with(it.settings) { - javaScriptEnabled = true - domStorageEnabled = true - useWideViewPort = true - loadWithOverviewMode = true - cacheMode = WebSettings.LOAD_DEFAULT - } - } - webView?.webViewClient = object : WebViewClient() { - override fun shouldInterceptRequest( - view: WebView?, - request: WebResourceRequest, - ): WebResourceResponse? { - state = fold(state, baseRequest, request) - if (stopCondition(state, baseRequest, request)) { - latch.countDown() - } - return super.shouldInterceptRequest(view, request) - } - } - webView?.loadUrl(baseRequest.url.toString(), baseRequest.headers.toMap()) - } - - latch.await(client.readTimeoutMillis.toLong(), TimeUnit.MILLISECONDS) - - looper.post { - webView?.run { - stopLoading() - destroy() - } - } - return state - } - - private fun Headers.Builder.fill(from: Map): Headers.Builder { - return from.entries.fold(this) { builder, entry -> - builder.set(entry.key, entry.value) - } - } - // ============================= Settings ==================================== override fun setupPreferenceScreen(screen: PreferenceScreen) { @@ -584,10 +247,6 @@ class SussyToons : HttpSource(), ConfigurableSource { dialogMessage = "URL padrão:\n$defaultBaseUrl" setDefaultValue(defaultBaseUrl) - setOnPreferenceChangeListener { _, _ -> - Toast.makeText(screen.context, RESTART_APP_MESSAGE, Toast.LENGTH_LONG).show() - true - } }, EditTextPreference(screen.context).apply { key = API_BASE_URL_PREF @@ -603,6 +262,19 @@ class SussyToons : HttpSource(), ConfigurableSource { dialogMessage = "URL da API padrão:\n$defaultApiUrl" setDefaultValue(defaultApiUrl) + }, + + SwitchPreferenceCompat(screen.context).apply { + key = DEFAULT_PREF + title = "Redefinir configurações" + summary = buildString { + append("Habilite para redefinir as configurações padrões no próximo reinicialização da aplicação.") + appendLine("Você pode limpar os dados da extensão em Configurações > Avançado:") + appendLine("\t - Limpar os cookies") + appendLine("\t - Limpar os dados da WebView") + appendLine("\t - Limpar o banco de dados (Procure a '$name' e remova os dados)") + } + setDefaultValue(false) setOnPreferenceChangeListener { _, _ -> Toast.makeText(screen.context, RESTART_APP_MESSAGE, Toast.LENGTH_LONG).show() true @@ -615,11 +287,6 @@ class SussyToons : HttpSource(), ConfigurableSource { // ============================= Utilities ==================================== - class Script( - val url: String, - val body: String, - ) - private fun MangaDto.toSManga(): SManga { val sManga = SManga.create().apply { title = name @@ -658,8 +325,6 @@ class SussyToons : HttpSource(), ConfigurableSource { const val CDN_URL = "https://cdn.sussytoons.site" const val OLDI_URL = "https://oldi.sussytoons.site" const val mangaPagePrefix = "mangaPage:" - const val chapterPagePrefix = "chapterPage:" - const val pageImagePrefix = "pageImage:" private const val URL_PREF_SUMMARY = "Para uso temporário, se a extensão for atualizada, a alteração será perdida." @@ -672,15 +337,7 @@ class SussyToons : HttpSource(), ConfigurableSource { private const val API_BASE_URL_PREF_TITLE = "Editar URL da API da fonte" private const val API_DEFAULT_BASE_URL_PREF = "defaultApiUrl" - private const val CHAPTER_SCRIPT_URL_PREF = "chapterScriptUrl" - private const val PAGE_SCRIPT_URL_PREF = "pageScriptUrl" - - val chapterUrlRegex = """push\("([^"]*capitulo[^"]*)\/?"\.concat""".toRegex() - val pageUrlRegex = """\.(get|post)\("([^"]*capitulo[^"]*)\/?"\.concat""".toRegex() - - val apiUrlRegex = """(?<=production",)(.*?)(?=;function)""".toRegex() - val urlRegex = """https?://[\w\-]+(\.[\w\-]+)+[/#?]?.*$""".toRegex() - val stringsRegex = """"(.*?)"""".toRegex() + private const val DEFAULT_PREF = "defaultPref" @SuppressLint("SimpleDateFormat") val dateFormat = SimpleDateFormat("yyyy-MM-dd", Locale.ROOT)