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, +)