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>
|
@ -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"
|
||||
|
|
Before Width: | Height: | Size: 5.7 KiB After Width: | Height: | Size: 6.2 KiB |
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 3.3 KiB |
Before Width: | Height: | Size: 8.7 KiB After Width: | Height: | Size: 8.5 KiB |
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 21 KiB |
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
)
|