SussyScan: Migration (#6855)

* Migrate

* Use HttpUrl

* Sort chapters

* Change popularRequest

Co-authored-by: Vetle Ledaal <vetle.ledaal@gmail.com>

* Update src/pt/sussyscan/src/eu/kanade/tachiyomi/extension/pt/sussyscan/SussyScan.kt

Change searchMangaRequest

Co-authored-by: Vetle Ledaal <vetle.ledaal@gmail.com>

* change latestUpdatesRequest

Co-authored-by: Vetle Ledaal <vetle.ledaal@gmail.com>

* Add comment

* Change chapters url

* changes

* Fix pages header

* Use setUrlWithoutDomain

* Use HttpUrl in SChapter::id and remove variable shadowing

---------

Co-authored-by: Vetle Ledaal <vetle.ledaal@gmail.com>
This commit is contained in:
Chopper 2024-12-30 12:41:31 -03:00 committed by Draff
parent a838bad72c
commit ae7fd918dd
No known key found for this signature in database
GPG Key ID: E8A89F3211677653
8 changed files with 297 additions and 24 deletions

View File

@ -1,9 +1,8 @@
ext {
extName = 'Sussy Scan'
extClass = '.SussyScan'
themePkg = 'madara'
baseUrl = 'https://oldi.sussytoons.com'
overrideVersionCode = 4
extVersionCode = 42
isNsfw = true
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.7 KiB

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 21 KiB

View File

@ -1,34 +1,228 @@
package eu.kanade.tachiyomi.extension.pt.sussyscan
import eu.kanade.tachiyomi.multisrc.madara.Madara
import android.annotation.SuppressLint
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.interceptor.rateLimit
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage
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 kotlinx.serialization.json.Json
import kotlinx.serialization.json.decodeFromStream
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Interceptor
import okhttp3.Request
import okhttp3.Response
import org.jsoup.Jsoup
import rx.Observable
import uy.kohesive.injekt.injectLazy
import java.text.SimpleDateFormat
import java.util.Locale
import java.util.concurrent.TimeUnit
class SussyScan : Madara(
"Sussy Scan",
"https://oldi.sussytoons.com",
"pt-BR",
SimpleDateFormat("MMMM dd, yyyy", Locale("pt", "BR")),
) {
override val client = super.client.newBuilder()
.rateLimit(2)
class SussyScan : HttpSource() {
override val name = "Sussy Scan"
override val baseUrl = "https://new.sussytoons.site"
private val apiUrl = "https://api-dev.sussytoons.site"
override val lang = "pt-BR"
override val supportsLatest = true
// Moved from Madara
override val versionId = 2
private val json: Json by injectLazy()
override val client = network.cloudflareClient.newBuilder()
.rateLimit(1, 2, TimeUnit.SECONDS)
.addInterceptor(::imageLocation)
.build()
override val useLoadMoreRequest = LoadMoreStrategy.Never
override val useNewChapterEndpoint = true
override fun headersBuilder() = super.headersBuilder()
.set("scan-id", "1") // Required header for requests
override val mangaDetailsSelectorTitle = "${super.mangaDetailsSelectorTitle}, span.rate-title, title"
override val mangaDetailsSelectorThumbnail = "head meta[property='og:image']"
// ============================= Popular ==================================
override fun mangaDetailsParse(document: Document) = super.mangaDetailsParse(document).apply {
title = title.substringBeforeLast("")
override fun popularMangaRequest(page: Int): Request {
return GET("$apiUrl/obras/top5", headers)
}
override fun imageFromElement(element: Element): String? {
return super.imageFromElement(element)?.takeIf { it.isNotEmpty() }
?: element.attr("content") // Thumbnail from <head>
override fun popularMangaParse(response: Response): MangasPage {
val dto = response.parseAs<WrapperDto<List<MangaDto>>>()
val mangas = dto.results.map { it.toSManga() }
return MangasPage(mangas, false) // There's a pagination bug
}
// ============================= Latest ===================================
override fun latestUpdatesRequest(page: Int): Request {
val url = "$apiUrl/obras/novos-capitulos".toHttpUrl().newBuilder()
.addQueryParameter("pagina", page.toString())
.addQueryParameter("limite", "24")
.build()
return GET(url, headers)
}
override fun latestUpdatesParse(response: Response): MangasPage {
val dto = response.parseAs<WrapperDto<List<MangaDto>>>()
val mangas = dto.results.map { it.toSManga() }
return MangasPage(mangas, dto.hasNextPage())
}
// ============================= Search ===================================
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val url = "$apiUrl/obras".toHttpUrl().newBuilder()
.addQueryParameter("pagina", page.toString())
.addQueryParameter("limite", "8")
.addQueryParameter("obr_nome", query)
.build()
return GET(url, headers)
}
override fun searchMangaParse(response: Response) = latestUpdatesParse(response)
// ============================= Details ==================================
override fun getMangaUrl(manga: SManga) = "$baseUrl${manga.url}"
override fun mangaDetailsRequest(manga: SManga): Request {
val url = "$apiUrl/obras".toHttpUrl().newBuilder()
.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 =================================
override fun getChapterUrl(chapter: SChapter): String {
return "$baseUrl/capitulo".toHttpUrl().newBuilder()
.addPathSegment(chapter.id)
.build()
.toString()
}
override fun chapterListRequest(manga: SManga) = mangaDetailsRequest(manga)
override fun chapterListParse(response: Response): List<SChapter> {
return response.parseAs<WrapperDto<WrapperChapterDto>>().results.chapters.map {
SChapter.create().apply {
name = it.name
it.chapterNumber?.let {
chapter_number = it
}
val chapterApiUrl = "$apiUrl/capitulos".toHttpUrl().newBuilder()
.addPathSegment(it.id.toString())
.build()
setUrlWithoutDomain(chapterApiUrl.toString())
date_upload = it.updateAt.toDate()
}
}
}
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
return super.fetchChapterList(manga)
.map { it.sortedBy(SChapter::chapter_number).reversed() }
}
private val SChapter.id: String get() {
val chapterApiUrl = apiUrl.toHttpUrl().newBuilder()
.addPathSegments(url)
.build()
return chapterApiUrl.pathSegments.last()
}
// ============================= Pages ====================================
override fun pageListRequest(chapter: SChapter) = GET("$apiUrl${chapter.url}", headers)
override fun pageListParse(response: Response): List<Page> {
val dto = response.parseAs<WrapperDto<ChapterPageDto>>().results
return dto.pages.mapIndexed { index, image ->
val imageUrl = CDN_URL.toHttpUrl().newBuilder()
.addPathSegments("wp-content/uploads/WP-manga/data")
.addPathSegments(image.src)
.build().toString()
Page(index, imageUrl = imageUrl)
}
}
override fun imageUrlParse(response: Response): String = ""
override fun imageUrlRequest(page: Page): Request {
val imageHeaders = headers.newBuilder()
.add("Referer", "$baseUrl/")
.build()
return GET(page.url, imageHeaders)
}
// ============================= Utilities ====================================
private fun MangaDto.toSManga(): SManga {
val sManga = SManga.create().apply {
title = name
thumbnail_url = thumbnail
initialized = true
val mangaUrl = "$baseUrl/obra".toHttpUrl().newBuilder()
.addPathSegment(this@toSManga.id.toString())
.addPathSegment(this@toSManga.slug)
.build()
setUrlWithoutDomain(mangaUrl.toString())
}
Jsoup.parseBodyFragment(description).let { sManga.description = it.text() }
sManga.status = status.toStatus()
return sManga
}
private fun imageLocation(chain: Interceptor.Chain): Response {
val request = chain.request()
val response = chain.proceed(request)
if (response.isSuccessful) {
return response
}
val url = request.url.toString()
if (url.contains(CDN_URL, ignoreCase = true)) {
response.close()
val newRequest = request.newBuilder()
.url(url.replace(CDN_URL, OLDI_URL, ignoreCase = true))
.build()
return chain.proceed(newRequest)
}
return response
}
private inline fun <reified T> Response.parseAs(): T {
return json.decodeFromStream(body.byteStream())
}
private fun String.toDate() =
try { dateFormat.parse(this)!!.time } catch (_: Exception) { 0L }
companion object {
const val CDN_URL = "https://usc1.contabostorage.com/23b45111d96c42c18a678c1d8cba7123:cdn"
const val OLDI_URL = "https://oldi.sussytoons.site"
@SuppressLint("SimpleDateFormat")
val dateFormat = SimpleDateFormat("yyyy-MM-dd")
}
}

View File

@ -0,0 +1,80 @@
package eu.kanade.tachiyomi.extension.pt.sussyscan
import eu.kanade.tachiyomi.source.model.SManga
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonNames
@Serializable
data class WrapperDto<T>(
@SerialName("pagina")
val currentPage: Int = 0,
@SerialName("totalPaginas")
val lastPage: Int = 0,
@JsonNames("resultado")
private val resultados: T,
) {
val results: T get() = resultados
fun hasNextPage() = currentPage < lastPage
}
@Serializable
class MangaDto(
@SerialName("obr_id")
val id: Int,
@SerialName("obr_descricao")
val description: String,
@SerialName("obr_imagem")
val thumbnail: String,
@SerialName("obr_nome")
val name: String,
@SerialName("obr_slug")
val slug: String,
@SerialName("status")
val status: MangaStatus,
) {
@Serializable
class MangaStatus(
@SerialName("stt_nome")
val value: String,
) {
fun toStatus(): Int {
return when (value.lowercase()) {
"em andamento" -> SManga.ONGOING
"completo" -> SManga.COMPLETED
"hiato" -> SManga.ON_HIATUS
else -> SManga.UNKNOWN
}
}
}
}
@Serializable
class ChapterDto(
@SerialName("cap_id")
val id: Int,
@SerialName("cap_nome")
val name: String,
@SerialName("cap_numero")
val chapterNumber: Float?,
@SerialName("cap_lancado_em")
val updateAt: String,
)
@Serializable
class WrapperChapterDto(
@SerialName("capitulos")
val chapters: List<ChapterDto>,
)
@Serializable
class ChapterPageDto(
@SerialName("cap_paginas")
val pages: List<PageDto>,
)
@Serializable
class PageDto(
val src: String,
)