diff --git a/src/pt/sussyscan/build.gradle b/src/pt/sussyscan/build.gradle
index 9007f70d3..ae1210e13 100644
--- a/src/pt/sussyscan/build.gradle
+++ b/src/pt/sussyscan/build.gradle
@@ -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"
diff --git a/src/pt/sussyscan/res/mipmap-hdpi/ic_launcher.png b/src/pt/sussyscan/res/mipmap-hdpi/ic_launcher.png
index 5dd832796..c4156782a 100644
Binary files a/src/pt/sussyscan/res/mipmap-hdpi/ic_launcher.png and b/src/pt/sussyscan/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/src/pt/sussyscan/res/mipmap-mdpi/ic_launcher.png b/src/pt/sussyscan/res/mipmap-mdpi/ic_launcher.png
index fac7c9d14..2b889aa84 100644
Binary files a/src/pt/sussyscan/res/mipmap-mdpi/ic_launcher.png and b/src/pt/sussyscan/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/src/pt/sussyscan/res/mipmap-xhdpi/ic_launcher.png b/src/pt/sussyscan/res/mipmap-xhdpi/ic_launcher.png
index 628cfb919..a8f4a3bd0 100644
Binary files a/src/pt/sussyscan/res/mipmap-xhdpi/ic_launcher.png and b/src/pt/sussyscan/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/src/pt/sussyscan/res/mipmap-xxhdpi/ic_launcher.png b/src/pt/sussyscan/res/mipmap-xxhdpi/ic_launcher.png
index 869a10de0..311d33bcc 100644
Binary files a/src/pt/sussyscan/res/mipmap-xxhdpi/ic_launcher.png and b/src/pt/sussyscan/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/src/pt/sussyscan/res/mipmap-xxxhdpi/ic_launcher.png b/src/pt/sussyscan/res/mipmap-xxxhdpi/ic_launcher.png
index 24bfd94d9..12f4963ae 100644
Binary files a/src/pt/sussyscan/res/mipmap-xxxhdpi/ic_launcher.png and b/src/pt/sussyscan/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/src/pt/sussyscan/src/eu/kanade/tachiyomi/extension/pt/sussyscan/SussyScan.kt b/src/pt/sussyscan/src/eu/kanade/tachiyomi/extension/pt/sussyscan/SussyScan.kt
index bb600fb75..edf692cbe 100644
--- a/src/pt/sussyscan/src/eu/kanade/tachiyomi/extension/pt/sussyscan/SussyScan.kt
+++ b/src/pt/sussyscan/src/eu/kanade/tachiyomi/extension/pt/sussyscan/SussyScan.kt
@@ -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
+ override fun popularMangaParse(response: Response): MangasPage {
+ val dto = response.parseAs>>()
+ 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>>()
+ 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>().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 {
+ return response.parseAs>().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> {
+ 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 {
+ val dto = response.parseAs>().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 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")
}
}
diff --git a/src/pt/sussyscan/src/eu/kanade/tachiyomi/extension/pt/sussyscan/SussyScanDto.kt b/src/pt/sussyscan/src/eu/kanade/tachiyomi/extension/pt/sussyscan/SussyScanDto.kt
new file mode 100644
index 000000000..54fbb5d91
--- /dev/null
+++ b/src/pt/sussyscan/src/eu/kanade/tachiyomi/extension/pt/sussyscan/SussyScanDto.kt
@@ -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(
+ @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,
+)
+
+@Serializable
+class ChapterPageDto(
+ @SerialName("cap_paginas")
+ val pages: List,
+)
+
+@Serializable
+class PageDto(
+ val src: String,
+)