diff --git a/src/es/olympusscanlation/build.gradle b/src/es/olympusscanlation/build.gradle index 5fe7a45f0..228febd4e 100644 --- a/src/es/olympusscanlation/build.gradle +++ b/src/es/olympusscanlation/build.gradle @@ -7,7 +7,7 @@ ext { extName = 'Olympus Scanlation' pkgNameSuffix = 'es.olympusscanlation' extClass = '.OlympusScanlation' - extVersionCode = 4 + extVersionCode = 5 } apply from: "$rootDir/common.gradle" diff --git a/src/es/olympusscanlation/src/eu/kanade/tachiyomi/extension/es/olympusscanlation/OlympusScanlation.kt b/src/es/olympusscanlation/src/eu/kanade/tachiyomi/extension/es/olympusscanlation/OlympusScanlation.kt index 518017fd4..30101be23 100644 --- a/src/es/olympusscanlation/src/eu/kanade/tachiyomi/extension/es/olympusscanlation/OlympusScanlation.kt +++ b/src/es/olympusscanlation/src/eu/kanade/tachiyomi/extension/es/olympusscanlation/OlympusScanlation.kt @@ -1,6 +1,8 @@ package eu.kanade.tachiyomi.extension.es.olympusscanlation import eu.kanade.tachiyomi.network.GET +import eu.kanade.tachiyomi.network.interceptor.rateLimitHost +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 @@ -15,29 +17,41 @@ import okhttp3.Response import uy.kohesive.injekt.injectLazy import java.text.SimpleDateFormat import java.util.Locale +import java.util.TimeZone class OlympusScanlation : HttpSource() { + override val versionId = 2 + override val baseUrl: String = "https://olympusv2.gg" private val apiBaseUrl: String = "https://dashboard.olympusv2.gg" + override val lang: String = "es" override val name: String = "Olympus Scanlation" - override val versionId = 2 - override val supportsLatest: Boolean = true - private val json: Json by injectLazy() - private val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSSSS'Z'", Locale.US) - // Search - override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { - val apiUrl = "$apiBaseUrl/api/search".toHttpUrl().newBuilder() - .addQueryParameter("name", query) + override val supportsLatest: Boolean = true + + override val client = super.client.newBuilder() + .rateLimitHost(baseUrl.toHttpUrl(), 1, 2) + .rateLimitHost(apiBaseUrl.toHttpUrl(), 2, 1) + .build() + + private val json: Json by injectLazy() + private val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSSSS'Z'", Locale.US).apply { + timeZone = TimeZone.getTimeZone("UTC") + } + + override fun popularMangaRequest(page: Int): Request { + val apiUrl = "$apiBaseUrl/api/series?page=1&direction=asc&type=comic".toHttpUrl().newBuilder() .build() return GET(apiUrl, headers) } - override fun searchMangaParse(response: Response): MangasPage { - val result = json.decodeFromString(response.body.string()) - val mangaList = result.data.map { + override fun popularMangaParse(response: Response): MangasPage { + runCatching { fetchFilters() } + val result = json.decodeFromString(response.body.string()) + val resultMangaList = json.decodeFromString>(result.data.recommended_series) + val mangaList = resultMangaList.filter { it.type == "comic" }.map { SManga.create().apply { url = "/series/comic-${it.slug}" title = it.name @@ -47,28 +61,95 @@ class OlympusScanlation : HttpSource() { return MangasPage(mangaList, hasNextPage = false) } - // Latest - override fun latestUpdatesRequest(page: Int): Request = popularMangaRequest(1) + override fun latestUpdatesRequest(page: Int): Request { + val apiUrl = "$apiBaseUrl/api/sf/new-chapters?page=$page".toHttpUrl().newBuilder() + .build() + return GET(apiUrl, headers) + } + override fun latestUpdatesParse(response: Response): MangasPage { - val result = json.decodeFromString(response.body.string()) - val mangaList = result.data.new_chapters.map { + runCatching { fetchFilters() } + val result = json.decodeFromString(response.body.string()) + val mangaList = result.data.filter { it.type == "comic" }.map { SManga.create().apply { url = "/series/comic-${it.slug}" title = it.name thumbnail_url = it.cover } } - return MangasPage(mangaList, hasNextPage = false) + val hasNextPage = result.current_page < result.last_page + return MangasPage(mangaList, hasNextPage) + } + + override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { + if (query.isNotEmpty()) { + val apiUrl = "$apiBaseUrl/api/search".toHttpUrl().newBuilder() + .addQueryParameter("name", query) + .build() + return GET(apiUrl, headers) + } + + val url = "$apiBaseUrl/api/series".toHttpUrl().newBuilder() + filters.forEach { filter -> + when (filter) { + is SortFilter -> { + if (filter.state?.ascending == true) { + url.addQueryParameter("direction", "desc") + } else { + url.addQueryParameter("direction", "asc") + } + } + is GenreFilter -> { + if (filter.toUriPart() != 9999) { + url.addQueryParameter("genres", filter.toUriPart().toString()) + } + } + is StatusFilter -> { + if (filter.toUriPart() != 9999) { + url.addQueryParameter("status", filter.toUriPart().toString()) + } + } + else -> {} + } + } + url.addQueryParameter("type", "comic") + url.addQueryParameter("page", page.toString()) + return GET(url.build().toString(), headers) + } + + override fun searchMangaParse(response: Response): MangasPage { + runCatching { fetchFilters() } + if (response.request.url.toString().startsWith("$apiBaseUrl/api/search")) { + val result = json.decodeFromString(response.body.string()) + val mangaList = result.data.filter { it.type == "comic" }.map { + SManga.create().apply { + url = "/series/comic-${it.slug}" + title = it.name + thumbnail_url = it.cover + } + } + return MangasPage(mangaList, hasNextPage = false) + } + + val result = json.decodeFromString(response.body.string()) + val mangaList = result.data.series.data.map { + SManga.create().apply { + url = "/series/comic-${it.slug}" + title = it.name + thumbnail_url = it.cover + } + } + val hasNextPage = result.data.series.current_page < result.data.series.last_page + return MangasPage(mangaList, hasNextPage) } - // Details override fun mangaDetailsParse(response: Response): SManga { val slug = response.request.url .toString() .substringAfter("/series/comic-") .substringBefore("/chapters") - val urla = "$apiBaseUrl/api/series/$slug?type=comic" - val newRequest = GET(url = urla, headers = headers) + val apiUrl = "$apiBaseUrl/api/series/$slug?type=comic" + val newRequest = GET(url = apiUrl, headers = headers) val newResponse = client.newCall(newRequest).execute() val result = json.decodeFromString(newResponse.body.string()) return SManga.create().apply { @@ -76,12 +157,13 @@ class OlympusScanlation : HttpSource() { title = result.data.name thumbnail_url = result.data.cover description = result.data.summary + status = parseStatus(result.data.status?.id) + genre = result.data.genres?.joinToString { it.name.trim() } } } - override fun imageUrlParse(response: Response): String = throw Exception("Not used") + override fun getChapterUrl(chapter: SChapter): String = baseUrl + chapter.url - // Chapters override fun chapterListRequest(manga: SManga): Request { return paginatedChapterListRequest( manga.url @@ -124,9 +206,6 @@ class OlympusScanlation : HttpSource() { .getOrNull() ?: 0L } - override fun getChapterUrl(chapter: SChapter): String = baseUrl + chapter.url - - // Pages override fun pageListRequest(chapter: SChapter): Request { val id = chapter.url .substringAfter("/capitulo/") @@ -139,28 +218,94 @@ class OlympusScanlation : HttpSource() { return GET("$apiBaseUrl/api/series/$slug/chapters/$id?type=comic") } - override fun pageListParse(response: Response): List = - json.decodeFromString(response.body.string()).chapter.pages.mapIndexed { i, img -> + override fun pageListParse(response: Response): List { + return json.decodeFromString(response.body.string()).chapter.pages.mapIndexed { i, img -> Page(i, "", img) } - - // Popular - override fun popularMangaParse(response: Response): MangasPage { - val result = json.decodeFromString(response.body.string()) - val resultMangaList = json.decodeFromString>(result.data.popular_comics) - val mangaList = resultMangaList.map { - SManga.create().apply { - url = "/series/comic-${it.slug}" - title = it.name - thumbnail_url = it.cover - } - } - return MangasPage(mangaList, hasNextPage = false) } - override fun popularMangaRequest(page: Int): Request { - val apiUrl = "$apiBaseUrl/api/home".toHttpUrl().newBuilder() - .build() - return GET(apiUrl, headers) + override fun imageUrlParse(response: Response): String = throw Exception("Not used") + + private fun parseStatus(statusId: Int?) = when (statusId) { + 1 -> SManga.ONGOING + 3 -> SManga.ON_HIATUS + 4 -> SManga.COMPLETED + 5 -> SManga.CANCELLED + else -> SManga.UNKNOWN + } + + private class SortFilter : Filter.Sort( + "Ordenar", + arrayOf("Alfabético"), + Selection(0, false), + ) + + private class GenreFilter(genres: List>) : UriPartFilter( + "Género", + arrayOf( + Pair("Todos", 9999), + *genres.toTypedArray(), + ), + ) + + private class StatusFilter(statuses: List>) : UriPartFilter( + "Estado", + arrayOf( + Pair("Todos", 9999), + *statuses.toTypedArray(), + ), + ) + + override fun getFilterList(): FilterList { + val filters = mutableListOf>( + Filter.Header("Los filtros no funcionan en la búsqueda por texto"), + Filter.Separator(), + SortFilter(), + ) + + if (genresList.isNotEmpty() || statusesList.isNotEmpty()) { + filters += listOf( + Filter.Separator(), + Filter.Header("Filtrar por género"), + GenreFilter(genresList), + ) + + filters += listOf( + Filter.Separator(), + Filter.Header("Filtrar por estado"), + StatusFilter(statusesList), + ) + } else { + filters += listOf( + Filter.Separator(), + Filter.Header("Presione 'Reiniciar' para intentar cargar los filtros"), + ) + } + + return FilterList(filters) + } + + private var genresList: List> = emptyList() + private var statusesList: List> = emptyList() + private var fetchFiltersAttemps = 0 + private var fetchFiltersFailed = false + + private fun fetchFilters() { + if (fetchFiltersAttemps <= 3 && ((genresList.isEmpty() && statusesList.isEmpty()) || fetchFiltersFailed)) { + val filters = runCatching { + val response = client.newCall(GET("$apiBaseUrl/api/genres-statuses", headers)).execute() + json.decodeFromString(response.body.string()) + } + + fetchFiltersFailed = filters.isFailure + genresList = filters.getOrNull()?.genres?.map { it.name.trim() to it.id } ?: emptyList() + statusesList = filters.getOrNull()?.statuses?.map { it.name.trim() to it.id } ?: emptyList() + fetchFiltersAttemps++ + } + } + + open class UriPartFilter(displayName: String, val vals: Array>) : + Filter.Select(displayName, vals.map { it.first }.toTypedArray()) { + fun toUriPart() = vals[state].second } } diff --git a/src/es/olympusscanlation/src/eu/kanade/tachiyomi/extension/es/olympusscanlation/OlympusScanlationDto.kt b/src/es/olympusscanlation/src/eu/kanade/tachiyomi/extension/es/olympusscanlation/OlympusScanlationDto.kt index 7d3ffdba2..afb609bd9 100644 --- a/src/es/olympusscanlation/src/eu/kanade/tachiyomi/extension/es/olympusscanlation/OlympusScanlationDto.kt +++ b/src/es/olympusscanlation/src/eu/kanade/tachiyomi/extension/es/olympusscanlation/OlympusScanlationDto.kt @@ -4,12 +4,24 @@ import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @Serializable -data class ChapterDto( - val id: Int, - val name: String, - @SerialName("published_at") val date: String, +data class PayloadSeriesDto(val data: PayloadSeriesDataDto) + +@Serializable +data class PayloadSeriesDataDto( + val series: SeriesDto, + val recommended_series: String, ) +@Serializable +data class SeriesDto( + val current_page: Int, + val data: List, + val last_page: Int, +) + +@Serializable +data class PayloadMangaDto(val data: List) + @Serializable data class MangaDto( val id: Int, @@ -18,6 +30,24 @@ data class MangaDto( val cover: String? = null, val type: String? = null, val summary: String? = null, + val status: MangaStatusDto? = null, + val genres: List? = null, +) + +@Serializable +data class NewChaptersDto( + val data: List, + val current_page: Int, + val last_page: Int, +) + +@Serializable +data class LatestMangaDto( + val id: Int, + val name: String, + val slug: String, + val cover: String? = null, + val type: String? = null, ) @Serializable @@ -25,28 +55,39 @@ data class MangaDetailDto( var data: MangaDto, ) -@Serializable -data class MetaDto(val total: Int) - -@Serializable -data class PageDto(val pages: List) - @Serializable data class PayloadChapterDto(var data: List, val meta: MetaDto) @Serializable -data class PayloadMangaDto(val data: List) +data class ChapterDto( + val id: Int, + val name: String, + @SerialName("published_at") val date: String, +) + +@Serializable +data class MetaDto(val total: Int) @Serializable data class PayloadPagesDto(val chapter: PageDto) @Serializable -data class HomeDto( - val popular_comics: String, - val new_chapters: List, +data class PageDto(val pages: List) + +@Serializable +data class MangaStatusDto( + val id: Int, + val name: String, ) @Serializable -data class PayloadHomeDto( - val data: HomeDto, +data class GenresStatusesDto( + val genres: List, + val statuses: List, +) + +@Serializable +data class FilterDto( + val id: Int, + val name: String, )