diff --git a/src/es/koinoboriscan/build.gradle b/src/es/koinoboriscan/build.gradle index 413bec684..1dffceaed 100644 --- a/src/es/koinoboriscan/build.gradle +++ b/src/es/koinoboriscan/build.gradle @@ -1,9 +1,7 @@ ext { extName = 'Koinobori Scan' extClass = '.KoinoboriScan' - themePkg = 'madara' - baseUrl = 'https://koinoboriscan.com' - overrideVersionCode = 0 + extVersionCode = 37 isNsfw = true } diff --git a/src/es/koinoboriscan/src/eu/kanade/tachiyomi/extension/es/koinoboriscan/KoinoboriScan.kt b/src/es/koinoboriscan/src/eu/kanade/tachiyomi/extension/es/koinoboriscan/KoinoboriScan.kt index 332da54fe..850d34830 100644 --- a/src/es/koinoboriscan/src/eu/kanade/tachiyomi/extension/es/koinoboriscan/KoinoboriScan.kt +++ b/src/es/koinoboriscan/src/eu/kanade/tachiyomi/extension/es/koinoboriscan/KoinoboriScan.kt @@ -1,17 +1,162 @@ package eu.kanade.tachiyomi.extension.es.koinoboriscan -import eu.kanade.tachiyomi.multisrc.madara.Madara +import eu.kanade.tachiyomi.network.GET +import eu.kanade.tachiyomi.network.asObservableSuccess import eu.kanade.tachiyomi.network.interceptor.rateLimit +import eu.kanade.tachiyomi.source.model.Filter +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.decodeFromString +import kotlinx.serialization.json.Json +import okhttp3.Request +import okhttp3.Response +import rx.Observable +import uy.kohesive.injekt.injectLazy import java.text.SimpleDateFormat import java.util.Locale +import kotlin.math.min -class KoinoboriScan : Madara( - "Koinobori Scan", - "https://koinoboriscan.com", - "es", - SimpleDateFormat("MMMM dd, yyyy", Locale("es")), -) { - override val client = super.client.newBuilder() +class KoinoboriScan : HttpSource() { + + // Site change theme from Madara to custom + override val versionId = 2 + + override val name = "Koinobori Scan" + + override val lang = "es" + + override val baseUrl = "https://visorkoi.com" + + private val apiBaseUrl = "https://api.visorkoi.com" + + override val supportsLatest = true + + private val json: Json by injectLazy() + + private val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ", Locale("es")) + + override val client = network.cloudflareClient.newBuilder() .rateLimit(2, 1) .build() + + override fun headersBuilder() = super.headersBuilder() + .set("Referer", "$baseUrl/") + + override fun popularMangaRequest(page: Int): Request = + GET("$apiBaseUrl/topSeries", headers) + + override fun popularMangaParse(response: Response): MangasPage { + val mangas = json.decodeFromString>(response.body.string()) + .map { it.toSManga(apiBaseUrl) } + + return MangasPage(mangas, false) + } + + override fun latestUpdatesRequest(page: Int): Request = + GET("$apiBaseUrl/lastupdates", headers) + + override fun latestUpdatesParse(response: Response): MangasPage { + val mangas = json.decodeFromString>(response.body.string()) + .map { it.toSManga(apiBaseUrl) } + + return MangasPage(mangas, false) + } + + private val seriesList = mutableListOf() + + override fun fetchSearchManga( + page: Int, + query: String, + filters: FilterList, + ): Observable { + return if (seriesList.isEmpty()) { + client.newCall(searchMangaRequest(page, query, filters)) + .asObservableSuccess() + .map { searchMangaParse(it, page, query) } + } else { + Observable.just(parseSeriesList(page, query)) + } + } + + override fun searchMangaRequest(page: Int, query: String, filters: FilterList) = + GET("$apiBaseUrl/all", headers) + + private fun searchMangaParse(response: Response, page: Int, query: String): MangasPage { + val result = json.decodeFromString>(response.body.string()) + seriesList.addAll(result) + return parseSeriesList(page, query) + } + + private fun parseSeriesList(page: Int, query: String): MangasPage { + val filteredSeries = seriesList.filter { + it.title.contains(query, ignoreCase = true) + } + + val mangas = filteredSeries.subList( + (page - 1) * SERIES_PER_PAGE, + min(page * SERIES_PER_PAGE, filteredSeries.size), + ).map { it.toSManga(apiBaseUrl) } + + val hasNextPage = filteredSeries.size > page * SERIES_PER_PAGE + + return MangasPage(mangas, hasNextPage) + } + + override fun getFilterList() = FilterList( + Filter.Header("Presione 'Filtrar' para mostrar toda la biblioteca"), + ) + + override fun getMangaUrl(manga: SManga) = "$baseUrl/?tipo=serie&identificador=${manga.url}" + + override fun mangaDetailsRequest(manga: SManga): Request = + GET("$apiBaseUrl/api/project/${manga.url}", headers) + + override fun mangaDetailsParse(response: Response): SManga { + return json.decodeFromString(response.body.string()).toSMangaDetails(apiBaseUrl) + } + + override fun getChapterUrl(chapter: SChapter) = "$baseUrl/?tipo=capitulo&identificador=${chapter.url}" + + override fun chapterListRequest(manga: SManga): Request = + GET("$apiBaseUrl/api/project/${manga.url}", headers) + + override fun chapterListParse(response: Response): List { + val result = json.decodeFromString(response.body.string()) + return result.seasons.flatMap { season -> + season.chapters.map { chapter -> + SChapter.create().apply { + url = chapter.id.toString() + name = "Capítulo ${chapter.name}: ${chapter.title}" + date_upload = try { + dateFormat.parse(chapter.date)?.time ?: 0 + } catch (e: Exception) { + 0 + } + } + } + }.reversed() + } + + override fun pageListRequest(chapter: SChapter): Request = + GET("$apiBaseUrl/api/chapter/${chapter.url}", headers) + + override fun pageListParse(response: Response): List { + val result = json.decodeFromString(response.body.string()) + val key = result.key + val chapterId = result.chapter.id + return result.chapter.images.mapIndexed { i, img -> + Page(i, imageUrl = "$apiBaseUrl/api/images/chapter/$chapterId/$img?token=$key") + } + } + + override fun searchMangaParse(response: Response) = throw UnsupportedOperationException() + override fun imageUrlParse(response: Response) = throw UnsupportedOperationException() + + companion object { + const val SERIES_PER_PAGE = 24 + } } diff --git a/src/es/koinoboriscan/src/eu/kanade/tachiyomi/extension/es/koinoboriscan/KoinoboriScanDto.kt b/src/es/koinoboriscan/src/eu/kanade/tachiyomi/extension/es/koinoboriscan/KoinoboriScanDto.kt new file mode 100644 index 000000000..739fc874b --- /dev/null +++ b/src/es/koinoboriscan/src/eu/kanade/tachiyomi/extension/es/koinoboriscan/KoinoboriScanDto.kt @@ -0,0 +1,74 @@ +package eu.kanade.tachiyomi.extension.es.koinoboriscan + +import eu.kanade.tachiyomi.source.model.SManga +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +class SeriesDto( + @SerialName("ID") private val id: Int, + val title: String, + private val description: String, + private val thumbnail: String, + private val status: String?, + private val author: String?, + private val tags: List? = emptyList(), +) { + fun toSManga(cdnUrl: String) = SManga.create().apply { + title = this@SeriesDto.title + thumbnail_url = cdnUrl + thumbnail + url = id.toString() + } + + fun toSMangaDetails(cdnUrl: String) = SManga.create().apply { + title = this@SeriesDto.title.trim() + author = this@SeriesDto.author?.trim() + status = parseStatus(this@SeriesDto.status) + thumbnail_url = cdnUrl + thumbnail + genre = tags?.joinToString { it.name.trim() } + description = this@SeriesDto.description.trim() + } + + private fun parseStatus(status: String?) = when (status?.trim()) { + "En emisión", "En curso" -> SManga.ONGOING + "Completado" -> SManga.COMPLETED + "Abandonado" -> SManga.CANCELLED + "Pausado" -> SManga.ON_HIATUS + else -> SManga.UNKNOWN + } +} + +@Serializable +class SeriesTagsDto( + val name: String, +) + +@Serializable +class ChaptersPayloadDto( + val seasons: List, +) + +@Serializable +class SeasonDto( + val chapters: List, +) + +@Serializable +class ChapterDto( + @SerialName("ID") val id: Int, + @SerialName("chapter_name") val name: String, + @SerialName("chapter_title") val title: String, + @SerialName("CreatedAt") val date: String, +) + +@Serializable +class PagesPayloadDto( + val chapter: ChapterImagesDto, + val key: String, +) + +@Serializable +class ChapterImagesDto( + @SerialName("ID") val id: Int, + val images: List, +)