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 {
|
ext {
|
||||||
extName = 'Sussy Scan'
|
extName = 'Sussy Scan'
|
||||||
extClass = '.SussyScan'
|
extClass = '.SussyScan'
|
||||||
themePkg = 'madara'
|
extVersionCode = 42
|
||||||
baseUrl = 'https://oldi.sussytoons.com'
|
isNsfw = true
|
||||||
overrideVersionCode = 4
|
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
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
|
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 eu.kanade.tachiyomi.network.interceptor.rateLimit
|
||||||
import org.jsoup.nodes.Document
|
import eu.kanade.tachiyomi.source.model.FilterList
|
||||||
import org.jsoup.nodes.Element
|
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.text.SimpleDateFormat
|
||||||
import java.util.Locale
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
class SussyScan : Madara(
|
class SussyScan : HttpSource() {
|
||||||
"Sussy Scan",
|
|
||||||
"https://oldi.sussytoons.com",
|
override val name = "Sussy Scan"
|
||||||
"pt-BR",
|
|
||||||
SimpleDateFormat("MMMM dd, yyyy", Locale("pt", "BR")),
|
override val baseUrl = "https://new.sussytoons.site"
|
||||||
) {
|
|
||||||
override val client = super.client.newBuilder()
|
private val apiUrl = "https://api-dev.sussytoons.site"
|
||||||
.rateLimit(2)
|
|
||||||
|
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()
|
.build()
|
||||||
|
|
||||||
override val useLoadMoreRequest = LoadMoreStrategy.Never
|
override fun headersBuilder() = super.headersBuilder()
|
||||||
override val useNewChapterEndpoint = true
|
.set("scan-id", "1") // Required header for requests
|
||||||
|
|
||||||
override val mangaDetailsSelectorTitle = "${super.mangaDetailsSelectorTitle}, span.rate-title, title"
|
// ============================= Popular ==================================
|
||||||
override val mangaDetailsSelectorThumbnail = "head meta[property='og:image']"
|
|
||||||
|
|
||||||
override fun mangaDetailsParse(document: Document) = super.mangaDetailsParse(document).apply {
|
override fun popularMangaRequest(page: Int): Request {
|
||||||
title = title.substringBeforeLast("–")
|
return GET("$apiUrl/obras/top5", headers)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun imageFromElement(element: Element): String? {
|
override fun popularMangaParse(response: Response): MangasPage {
|
||||||
return super.imageFromElement(element)?.takeIf { it.isNotEmpty() }
|
val dto = response.parseAs<WrapperDto<List<MangaDto>>>()
|
||||||
?: element.attr("content") // Thumbnail from <head>
|
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,
|
||||||
|
)
|