SussyToons: Fix source (#8155)

* Fix popular manga and latest manga

* Fix details and chapter

* Fix search

* Bump version
This commit is contained in:
Chopper 2025-03-23 12:04:28 -03:00 committed by Draff
parent 44a4f517d2
commit 3b85cfc5a2
No known key found for this signature in database
GPG Key ID: E8A89F3211677653
3 changed files with 118 additions and 83 deletions

View File

@ -1,7 +1,7 @@
ext { ext {
extName = 'Sussy Toons' extName = 'Sussy Toons'
extClass = '.SussyToons' extClass = '.SussyToons'
extVersionCode = 51 extVersionCode = 52
isNsfw = true isNsfw = true
} }

View File

@ -6,6 +6,7 @@ import android.widget.Toast
import androidx.preference.EditTextPreference import androidx.preference.EditTextPreference
import androidx.preference.PreferenceScreen import androidx.preference.PreferenceScreen
import androidx.preference.SwitchPreferenceCompat import androidx.preference.SwitchPreferenceCompat
import app.cash.quickjs.QuickJs
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.source.ConfigurableSource import eu.kanade.tachiyomi.source.ConfigurableSource
import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.FilterList
@ -16,17 +17,19 @@ import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.util.asJsoup import eu.kanade.tachiyomi.util.asJsoup
import keiyoushi.utils.getPreferences import keiyoushi.utils.getPreferences
import keiyoushi.utils.parseAs
import keiyoushi.utils.tryParse
import kotlinx.serialization.decodeFromString import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlinx.serialization.json.decodeFromStream import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Interceptor import okhttp3.Interceptor
import okhttp3.Request import okhttp3.Request
import okhttp3.Response import okhttp3.Response
import org.jsoup.Jsoup
import org.jsoup.nodes.Document import org.jsoup.nodes.Document
import org.jsoup.nodes.Element import org.jsoup.nodes.Element
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.io.IOException
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Locale import java.util.Locale
@ -63,7 +66,7 @@ class SussyToons : HttpSource(), ConfigurableSource {
} }
private val defaultBaseUrl: String = "https://www.sussytoons.wtf" private val defaultBaseUrl: String = "https://www.sussytoons.wtf"
private val defaultApiUrl: String = "https://api-dev.sussytoons.site" private val defaultApiUrl: String = "https://api.sussytoons.wtf"
override val client = network.cloudflareClient.newBuilder() override val client = network.cloudflareClient.newBuilder()
.addInterceptor(::imageLocation) .addInterceptor(::imageLocation)
@ -99,81 +102,72 @@ class SussyToons : HttpSource(), ConfigurableSource {
// ============================= Popular ================================== // ============================= Popular ==================================
override fun popularMangaRequest(page: Int): Request { override fun popularMangaRequest(page: Int) = GET(baseUrl, headers)
return GET("$apiUrl/obras/top5", headers)
}
override fun popularMangaParse(response: Response): MangasPage { override fun popularMangaParse(response: Response): MangasPage {
val dto = response.parseAs<WrapperDto<List<MangaDto>>>() val json = response.parseScriptToJson()
val mangas = dto.results.filterNot { it.slug.isNullOrBlank() }.map { it.toSManga() } ?: return MangasPage(emptyList(), false)
return MangasPage(mangas, false) // There's a pagination bug val mangas = json.parseAs<WrapperDto>().popular?.toSMangaList()
?: emptyList()
return MangasPage(mangas, false)
} }
// ============================= Latest =================================== // ============================= Latest ===================================
override fun latestUpdatesRequest(page: Int): Request { override fun latestUpdatesRequest(page: Int): Request {
val url = "$apiUrl/obras/novos-capitulos".toHttpUrl().newBuilder() val url = "$baseUrl/atualizacoes".toHttpUrl().newBuilder()
.addQueryParameter("pagina", page.toString()) .addQueryParameter("pagina", page.toString())
.addQueryParameter("limite", "24")
.build() .build()
return GET(url, headers) return GET(url, headers)
} }
override fun latestUpdatesParse(response: Response): MangasPage { override fun latestUpdatesParse(response: Response): MangasPage {
val dto = response.parseAs<WrapperDto<List<MangaDto>>>() val json = response.parseScriptToJson()
val mangas = dto.results.filterNot { it.slug.isNullOrBlank() }.map { it.toSManga() } ?: return MangasPage(emptyList(), false)
return MangasPage(mangas, dto.hasNextPage()) val dto = json.parseAs<WrapperDto>()
val mangas = dto.latest.toSMangaList()
return MangasPage(mangas, dto.latest.hasNextPage())
} }
// ============================= Search =================================== // ============================= Search ===================================
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val url = "$apiUrl/obras".toHttpUrl().newBuilder() val url = "$apiUrl/obras".toHttpUrl().newBuilder()
.addQueryParameter("pagina", page.toString())
.addQueryParameter("limite", "8")
.addQueryParameter("obr_nome", query) .addQueryParameter("obr_nome", query)
.addQueryParameter("limite", "8")
.addQueryParameter("pagina", page.toString())
.addQueryParameter("todos_generos", "true")
.build() .build()
return GET(url, headers) return GET(url, headers)
} }
override fun searchMangaParse(response: Response) = latestUpdatesParse(response) override fun searchMangaParse(response: Response): MangasPage {
val dto = response.parseAs<ResultDto<List<MangaDto>>>()
return MangasPage(dto.toSMangaList(), dto.hasNextPage())
}
// ============================= Details ================================== // ============================= Details ==================================
override fun getMangaUrl(manga: SManga) = "$baseUrl${manga.url}" override fun mangaDetailsParse(response: Response): SManga {
val json = response.parseScriptToJson()
override fun mangaDetailsRequest(manga: SManga): Request { ?: throw IOException("Details do mangá não foi encontrado")
val url = "$apiUrl/obras".toHttpUrl().newBuilder() return json.parseAs<ResultDto<MangaDto>>().results.toSManga()
.addPathSegment(manga.id)
.build()
return GET(url, headers)
}
override fun mangaDetailsParse(response: Response) =
response.parseAs<WrapperDto<MangaDto>>().results.toSManga()
private val SManga.id: String get() {
val mangaUrl = apiUrl.toHttpUrl().newBuilder()
.addPathSegments(url)
.build()
return mangaUrl.pathSegments[2]
} }
// ============================= Chapters ================================= // ============================= Chapters =================================
override fun chapterListRequest(manga: SManga) = mangaDetailsRequest(manga)
override fun chapterListParse(response: Response): List<SChapter> { override fun chapterListParse(response: Response): List<SChapter> {
return response.parseAs<WrapperDto<WrapperChapterDto>>().results.chapters.map { val json = response.parseScriptToJson() ?: return emptyList()
return json.parseAs<ResultDto<WrapperChapterDto>>().results.chapters.map {
SChapter.create().apply { SChapter.create().apply {
name = it.name name = it.name
it.chapterNumber?.let { it.chapterNumber?.let {
chapter_number = it chapter_number = it
} }
setUrlWithoutDomain("$baseUrl/capitulo/${it.id}") setUrlWithoutDomain("$baseUrl/capitulo/${it.id}")
date_upload = it.updateAt.toDate() date_upload = dateFormat.tryParse(it.updateAt)
} }
}.sortedBy(SChapter::chapter_number).reversed() }.sortedByDescending(SChapter::chapter_number)
} }
// ============================= Pages ==================================== // ============================= Pages ====================================
@ -225,7 +219,7 @@ class SussyToons : HttpSource(), ConfigurableSource {
private fun parseJsonToChapterPageDto(jsonContent: String): ChapterPageDto { private fun parseJsonToChapterPageDto(jsonContent: String): ChapterPageDto {
return try { return try {
jsonContent.parseAs<WrapperDto<ChapterPageDto>>().results jsonContent.parseAs<ResultDto<ChapterPageDto>>().results
} catch (e: Exception) { } catch (e: Exception) {
throw Exception("Failed to load pages: ${e.message}") throw Exception("Failed to load pages: ${e.message}")
} }
@ -249,18 +243,17 @@ class SussyToons : HttpSource(), ConfigurableSource {
return response return response
} }
val url = request.url.toString()
if (url.contains(CDN_URL, ignoreCase = true)) {
response.close() response.close()
val newRequest = request.newBuilder() val url = request.url.newBuilder()
.url(url.replace(CDN_URL, OLDI_URL, ignoreCase = true)) .dropPathSegment(4)
.build() .build()
val newRequest = request.newBuilder()
.url(url)
.build()
return chain.proceed(newRequest) return chain.proceed(newRequest)
} }
return response
}
// ============================= Settings ==================================== // ============================= Settings ====================================
@ -315,40 +308,32 @@ class SussyToons : HttpSource(), ConfigurableSource {
// ============================= Utilities ==================================== // ============================= Utilities ====================================
private fun MangaDto.toSManga(): SManga { private fun Response.parseScriptToJson(): String? {
val sManga = SManga.create().apply { val quickJs = QuickJs.create()
title = name val document = asJsoup()
thumbnail_url = thumbnail?.let { val script = document.select("script")
when { .map(Element::data)
it.startsWith("http") -> thumbnail .filter(String::isNotEmpty)
else -> "$OLDI_URL/scans/$scanId/obras/${this@toSManga.id}/$thumbnail" .joinToString("\n")
}
} val content = quickJs.evaluate(
initialized = true """
val mangaUrl = "$baseUrl/obra".toHttpUrl().newBuilder() globalThis.self = globalThis;
.addPathSegment(this@toSManga.id.toString()) $script
.addPathSegment(this@toSManga.slug!!) self.__next_f.map(it => it[it.length - 1]).join('')
.build() """.trimIndent(),
setUrlWithoutDomain(mangaUrl.toString()) ) as String
return PAGE_JSON_REGEX.find(content)?.groups?.get(0)?.value
} }
description?.let { Jsoup.parseBodyFragment(it).let { sManga.description = it.text() } } private fun HttpUrl.Builder.dropPathSegment(count: Int): HttpUrl.Builder {
sManga.status = status.toStatus() repeat(count) {
removePathSegment(0)
return sManga
} }
return this
private inline fun <reified T> Response.parseAs(): T = use {
return json.decodeFromStream(body.byteStream())
} }
private inline fun <reified T> String.parseAs(): T {
return json.decodeFromString(this)
}
private fun String.toDate() =
try { dateFormat.parse(this)!!.time } catch (_: Exception) { 0L }
/** /**
* Normalizes path segments: * Normalizes path segments:
* Ex: [ "/a/b/", "/a/b", "a/b/", "a/b" ] * Ex: [ "/a/b/", "/a/b", "a/b/", "a/b" ]
@ -360,9 +345,12 @@ class SussyToons : HttpSource(), ConfigurableSource {
companion object { companion object {
const val CDN_URL = "https://cdn.sussytoons.site" const val CDN_URL = "https://cdn.sussytoons.site"
const val OLDI_URL = "https://oldi.sussytoons.site"
val pageRegex = """capituloInicial.{3}(.*?)(\}\]\})""".toRegex() val pageRegex = """capituloInicial.{3}(.*?)(\}\]\})""".toRegex()
val POPULAR_JSON_REGEX = """\{\"dataFeatured.+totalPaginas":\d+\}{2}""".toRegex()
val LATEST_JSON_REGEX = """\{\"atualizacoesInicial.+\}\}""".toRegex()
val DETAILS_CHAPTER_REGEX = """\{\"resultado.+"\}{3}""".toRegex()
val PAGE_JSON_REGEX = """$POPULAR_JSON_REGEX|$LATEST_JSON_REGEX|$DETAILS_CHAPTER_REGEX""".toRegex()
private const val URL_PREF_SUMMARY = "Para uso temporário, se a extensão for atualizada, a alteração será perdida." private const val URL_PREF_SUMMARY = "Para uso temporário, se a extensão for atualizada, a alteração será perdida."

View File

@ -1,12 +1,14 @@
package eu.kanade.tachiyomi.extension.pt.sussyscan package eu.kanade.tachiyomi.extension.pt.sussyscan
import eu.kanade.tachiyomi.extension.pt.sussyscan.SussyToons.Companion.CDN_URL
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import kotlinx.serialization.SerialName import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonNames import kotlinx.serialization.json.JsonNames
import org.jsoup.Jsoup
@Serializable @Serializable
data class WrapperDto<T>( class ResultDto<T>(
@SerialName("pagina") @SerialName("pagina")
val currentPage: Int = 0, val currentPage: Int = 0,
@SerialName("totalPaginas") @SerialName("totalPaginas")
@ -17,6 +19,20 @@ data class WrapperDto<T>(
val results: T get() = resultados val results: T get() = resultados
fun hasNextPage() = currentPage < lastPage fun hasNextPage() = currentPage < lastPage
fun toSMangaList() = (results as List<MangaDto>)
.filterNot { it.slug.isNullOrBlank() }.map { it.toSManga() }
}
@Serializable
class WrapperDto(
@SerialName("dataTop")
val popular: ResultDto<List<MangaDto>>?,
@JsonNames("atualizacoesInicial")
private val dataLatest: ResultDto<List<MangaDto>>?,
) {
val latest: ResultDto<List<MangaDto>> get() = dataLatest!!
} }
@Serializable @Serializable
@ -35,7 +51,38 @@ class MangaDto(
val status: MangaStatus, val status: MangaStatus,
@SerialName("scan_id") @SerialName("scan_id")
val scanId: Int, val scanId: Int,
@SerialName("tags")
val genres: List<Genre>,
) { ) {
fun toSManga(): SManga {
val sManga = SManga.create().apply {
title = name
thumbnail_url = thumbnail?.let {
when {
it.startsWith("http") -> thumbnail
else -> "$CDN_URL/scans/$scanId/obras/${this@MangaDto.id}/$thumbnail"
}
}
initialized = true
url = "/obra/${this@MangaDto.id}/${this@MangaDto.slug}"
genre = genres.joinToString()
}
description?.let { Jsoup.parseBodyFragment(it).let { sManga.description = it.text() } }
sManga.status = status.toStatus()
return sManga
}
@Serializable
class Genre(
@SerialName("tag_nome")
val value: String,
) {
override fun toString(): String = value
}
@Serializable @Serializable
class MangaStatus( class MangaStatus(
@SerialName("stt_nome") @SerialName("stt_nome")