SussyToons: Fix source (#8155)
* Fix popular manga and latest manga * Fix details and chapter * Fix search * Bump version
This commit is contained in:
parent
44a4f517d2
commit
3b85cfc5a2
@ -1,7 +1,7 @@
|
|||||||
ext {
|
ext {
|
||||||
extName = 'Sussy Toons'
|
extName = 'Sussy Toons'
|
||||||
extClass = '.SussyToons'
|
extClass = '.SussyToons'
|
||||||
extVersionCode = 51
|
extVersionCode = 52
|
||||||
isNsfw = true
|
isNsfw = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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."
|
||||||
|
|
||||||
|
@ -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")
|
||||||
|
Loading…
x
Reference in New Issue
Block a user