From d0d9558eb2155a4b8664bf054ea15355604fa597 Mon Sep 17 00:00:00 2001 From: Chopper <156493704+choppeh@users.noreply.github.com> Date: Mon, 31 Mar 2025 11:39:23 -0300 Subject: [PATCH] GekkouScans: Migrate theme (#8275) Migrate theme --- src/pt/gekkouscans/build.gradle | 6 +- .../extension/pt/gekkouscans/GekkouScans.kt | 148 ++++++++++++++++-- .../pt/gekkouscans/GekkouScansDto.kt | 86 ++++++++++ 3 files changed, 223 insertions(+), 17 deletions(-) create mode 100644 src/pt/gekkouscans/src/eu/kanade/tachiyomi/extension/pt/gekkouscans/GekkouScansDto.kt diff --git a/src/pt/gekkouscans/build.gradle b/src/pt/gekkouscans/build.gradle index ec4223fee..4211b0e6d 100644 --- a/src/pt/gekkouscans/build.gradle +++ b/src/pt/gekkouscans/build.gradle @@ -1,10 +1,8 @@ ext { extName = 'Gekkou Scans' extClass = '.GekkouScans' - themePkg = 'madara' - baseUrl = 'https://gekkou.space' - overrideVersionCode = 0 - isNsfw = false + extVersionCode = 42 + isNsfw = true } apply from: "$rootDir/common.gradle" diff --git a/src/pt/gekkouscans/src/eu/kanade/tachiyomi/extension/pt/gekkouscans/GekkouScans.kt b/src/pt/gekkouscans/src/eu/kanade/tachiyomi/extension/pt/gekkouscans/GekkouScans.kt index 0c0fbde32..58d9c319f 100644 --- a/src/pt/gekkouscans/src/eu/kanade/tachiyomi/extension/pt/gekkouscans/GekkouScans.kt +++ b/src/pt/gekkouscans/src/eu/kanade/tachiyomi/extension/pt/gekkouscans/GekkouScans.kt @@ -1,21 +1,143 @@ package eu.kanade.tachiyomi.extension.pt.gekkouscans -import eu.kanade.tachiyomi.multisrc.madara.Madara -import eu.kanade.tachiyomi.network.interceptor.rateLimit -import java.text.SimpleDateFormat -import java.util.Locale +import eu.kanade.tachiyomi.network.GET +import eu.kanade.tachiyomi.network.interceptor.rateLimitHost +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 keiyoushi.utils.parseAs +import okhttp3.HttpUrl +import okhttp3.HttpUrl.Companion.toHttpUrl +import okhttp3.Interceptor +import okhttp3.Request +import okhttp3.Response +import okhttp3.internal.http.HTTP_FORBIDDEN +import java.io.IOException + +class GekkouScans : HttpSource() { + + override val name: String = "Gekkou Scans" + + override val baseUrl: String = "https://new.gekkou.space" + + private val apiUrl = "$baseUrl/api" + + override val lang: String = "pt-BR" + + override val supportsLatest: Boolean = true + + // Moved from Madara + override val versionId: Int = 2 -class GekkouScans : Madara( - "Gekkou Scans", - "https://gekkou.space", - "pt-BR", - SimpleDateFormat("dd 'de' MMM 'de' yyyy", Locale("pt", "BR")), -) { override val client = super.client.newBuilder() - .rateLimit(3) + .rateLimitHost(apiUrl.toHttpUrl(), 2, 1) + .addInterceptor(::verifyLogin) .build() - override val useNewChapterEndpoint = true + // ========================= Popular ==================================== - override val useLoadMoreRequest = LoadMoreStrategy.Never + override fun popularMangaRequest(page: Int): Request = GET("$apiUrl/manga/todos", headers) + + override fun popularMangaParse(response: Response): MangasPage { + val mangas = response.parseAs>() + .sortedByDescending(MangaDto::popular) + .map(MangaDto::toSManga) + return MangasPage(mangas, false) + } + + // ========================= Latest ===================================== + + override fun latestUpdatesRequest(page: Int): Request = + GET("$apiUrl/manga/recent-updates", headers) + + override fun latestUpdatesParse(response: Response): MangasPage { + val mangas = response.parseAs>().map { + SManga.create().apply { + title = it.name + url = "/projeto/${it.slug}" + thumbnail_url = it.thumbnailUrl + } + } + return MangasPage(mangas, false) + } + + // ========================= Search ===================================== + + override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { + val url = "$apiUrl/manga/search".toHttpUrl().newBuilder() + .setQueryParameter("query", query) + .setQueryParameter("limit", "10") + .build() + return GET(url, headers) + } + + override fun searchMangaParse(response: Response): MangasPage = popularMangaParse(response) + + // ========================= Details ==================================== + + override fun getMangaUrl(manga: SManga) = "$baseUrl/${manga.url}" + + override fun mangaDetailsRequest(manga: SManga): Request { + val slug = manga.url.substringAfterLast("/") + return GET("$apiUrl/manga/$slug", headers) + } + + override fun mangaDetailsParse(response: Response): SManga = + response.parseAs().let(MangaDto::toSManga) + + // ========================= Chapters =================================== + + override fun getChapterUrl(chapter: SChapter): String = "$baseUrl/${chapter.url}" + + override fun chapterListRequest(manga: SManga): Request = mangaDetailsRequest(manga) + + override fun chapterListParse(response: Response): List = + response.parseAs().toListSChapter().sortedByDescending(SChapter::chapter_number) + + // ========================= Pages ====================================== + + override fun pageListRequest(chapter: SChapter): Request { + val pathSegment = chapter.url.split("/").filter(String::isNotBlank) + .drop(1).joinToString("/") + return addRequestRequireSettings("$apiUrl/chapter/$pathSegment".toHttpUrl()) + } + + override fun pageListParse(response: Response): List { + return response.parseAs().pages.sortedBy(PagesDto.ImageUrl::index).map { + val imageUrl = "$apiUrl${it.url}" + Page(it.index, imageUrl = imageUrl) + } + } + + override fun imageRequest(page: Page): Request { + return addRequestRequireSettings(super.imageRequest(page).url) + } + + override fun imageUrlParse(response: Response): String = "" + + // ========================= Utilities ====================================== + + private fun addRequestRequireSettings(url: HttpUrl): Request { + val newUrl = url.newBuilder() + .addQueryParameter("cb", unixTime().toString()) + .build() + + // It's possible to add a real user here. + val newHeaders = headers.newBuilder() + .set("User-Id", (1..5000).random().toString()) + .build() + + return GET(newUrl, newHeaders) + } + + private fun verifyLogin(chain: Interceptor.Chain): Response = + chain.proceed(chain.request()).takeIf { it.code != HTTP_FORBIDDEN } ?: throw IOException("Faça o login na WebView") + + private fun unixTime(): Int { + val timestampMillis = System.currentTimeMillis() + return (timestampMillis / 1000).toInt() + } } diff --git a/src/pt/gekkouscans/src/eu/kanade/tachiyomi/extension/pt/gekkouscans/GekkouScansDto.kt b/src/pt/gekkouscans/src/eu/kanade/tachiyomi/extension/pt/gekkouscans/GekkouScansDto.kt new file mode 100644 index 000000000..d18d780d5 --- /dev/null +++ b/src/pt/gekkouscans/src/eu/kanade/tachiyomi/extension/pt/gekkouscans/GekkouScansDto.kt @@ -0,0 +1,86 @@ +package eu.kanade.tachiyomi.extension.pt.gekkouscans + +import eu.kanade.tachiyomi.source.model.SChapter +import eu.kanade.tachiyomi.source.model.SManga +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +private val CDN_URL = "https://usc1.contabostorage.com/783e4d097dbf4f83aefe59be94798c82:gekkou" + +@Serializable +class MangaDto( + val slug: String, + val name: String, + val artists: String, + val author: String, + val genres: List, + val status: String, + val popular: Boolean, + val summary: String, + val chapters: List, + private val urlCover: String, +) { + val thumbnailUrl: String get() = getAbsoluteThumbnailUrl(urlCover) + + fun toSManga() = SManga.create().apply { + title = name + url = "/projeto/$slug" + description = summary + genre = genres.joinToString() + artist = artists + author = this@MangaDto.author + initialized = true + status = when (this@MangaDto.status.lowercase()) { + "completo" -> SManga.COMPLETED + "ativo" -> SManga.ONGOING + "cancelado" -> SManga.CANCELLED + else -> SManga.UNKNOWN + } + thumbnail_url = thumbnailUrl + } + + fun toListSChapter(): List { + return chapters.map { + SChapter.create().apply { + name = it.chapterNumber + chapter_number = it.chapterNumber.toFloat() + url = "/leitor/$slug/${it.chapterNumber}" + } + } + } +} + +@Serializable +class ChapterDto( + @SerialName("chapterSlug") + val chapterNumber: String, +) + +@Serializable +class LatestMangaDto( + @SerialName("mangaSlug") + val slug: String, + val name: String, + private val urlCover: String, +) { + val thumbnailUrl: String get() = getAbsoluteThumbnailUrl(urlCover) +} + +@Serializable +class PagesDto( + val pages: List, +) { + @Serializable + class ImageUrl( + @SerialName("pageNumber") + val index: Int, + val url: String, + ) +} + +fun getAbsoluteThumbnailUrl(urlCover: String): String { + return when { + urlCover.startsWith("http", ignoreCase = true) -> urlCover + else -> "$CDN_URL/$urlCover" + } +}