parent
7fcbbec4c7
commit
d0d9558eb2
@ -1,10 +1,8 @@
|
|||||||
ext {
|
ext {
|
||||||
extName = 'Gekkou Scans'
|
extName = 'Gekkou Scans'
|
||||||
extClass = '.GekkouScans'
|
extClass = '.GekkouScans'
|
||||||
themePkg = 'madara'
|
extVersionCode = 42
|
||||||
baseUrl = 'https://gekkou.space'
|
isNsfw = true
|
||||||
overrideVersionCode = 0
|
|
||||||
isNsfw = false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
@ -1,21 +1,143 @@
|
|||||||
package eu.kanade.tachiyomi.extension.pt.gekkouscans
|
package eu.kanade.tachiyomi.extension.pt.gekkouscans
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.multisrc.madara.Madara
|
import eu.kanade.tachiyomi.network.GET
|
||||||
import eu.kanade.tachiyomi.network.interceptor.rateLimit
|
import eu.kanade.tachiyomi.network.interceptor.rateLimitHost
|
||||||
import java.text.SimpleDateFormat
|
import eu.kanade.tachiyomi.source.model.FilterList
|
||||||
import java.util.Locale
|
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()
|
override val client = super.client.newBuilder()
|
||||||
.rateLimit(3)
|
.rateLimitHost(apiUrl.toHttpUrl(), 2, 1)
|
||||||
|
.addInterceptor(::verifyLogin)
|
||||||
.build()
|
.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<List<MangaDto>>()
|
||||||
|
.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<List<LatestMangaDto>>().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<MangaDto>().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<SChapter> =
|
||||||
|
response.parseAs<MangaDto>().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<Page> {
|
||||||
|
return response.parseAs<PagesDto>().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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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<String>,
|
||||||
|
val status: String,
|
||||||
|
val popular: Boolean,
|
||||||
|
val summary: String,
|
||||||
|
val chapters: List<ChapterDto>,
|
||||||
|
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<SChapter> {
|
||||||
|
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<ImageUrl>,
|
||||||
|
) {
|
||||||
|
@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"
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user