diff --git a/lib-multisrc/heancms/assets/i18n/messages_en.properties b/lib-multisrc/heancms/assets/i18n/messages_en.properties new file mode 100644 index 000000000..27bebd940 --- /dev/null +++ b/lib-multisrc/heancms/assets/i18n/messages_en.properties @@ -0,0 +1,17 @@ +genre_filter_title=Genres +status_filter_title=Status +status_all=All +status_ongoing=Ongoing +status_onhiatus=On hiatus +status_dropped=Dropped +sort_by_filter_title=Sort By +sort_by_title=Title +sort_by_views=Views +sort_by_latest=Latest +sort_by_created_at=Created at +pref_show_paid_chapter_title=Display paid chapters +pref_show_paid_chapter_summary_on=Paid chapters will appear. +pref_show_paid_chapter_summary_off=Only free chapters will be displayed. +url_changed_error=The URL of the series has changed. Migrate from %s to %s to update the URL +paid_chapter_error=Paid chapter unavailable. +id_not_found_error=Failed to get the ID for slug: %s diff --git a/lib-multisrc/heancms/assets/i18n/messages_es.properties b/lib-multisrc/heancms/assets/i18n/messages_es.properties new file mode 100644 index 000000000..5ec44c7af --- /dev/null +++ b/lib-multisrc/heancms/assets/i18n/messages_es.properties @@ -0,0 +1,17 @@ +genre_filter_title=Géneros +status_filter_title=Estado +status_all=Todos +status_ongoing=En curso +status_onhiatus=En hiatus +status_dropped=Abandonada +sort_by_filter_title=Ordenar por +sort_by_title=Título +sort_by_views=Número de vistas +sort_by_latest=Recientes +sort_by_created_at=Fecha de creación +pref_show_paid_chapter_title=Mostrar capítulos de pago +pref_show_paid_chapter_summary_on=Se mostrarán capítulos de pago. +pref_show_paid_chapter_summary_off=Solo se mostrarán los capítulos gratuitos. +url_changed_error= La URL de la serie ha cambiado. Migre de %s a %s para actualizar la URL +paid_chapter_error=Capítulo no disponible. +id_not_found_error=No se pudo encontrar el ID para: %s diff --git a/lib-multisrc/heancms/assets/i18n/messages_pt_br.properties b/lib-multisrc/heancms/assets/i18n/messages_pt_br.properties new file mode 100644 index 000000000..1521a4cec --- /dev/null +++ b/lib-multisrc/heancms/assets/i18n/messages_pt_br.properties @@ -0,0 +1,13 @@ +genre_filter_title=Gêneros +status_filter_title=Estado +status_all=Todos +status_ongoing=Em andamento +status_onhiatus=Em hiato +status_dropped=Cancelada +sort_by_filter_title=Ordenar por +sort_by_title=Título +sort_by_views=Visualizações +sort_by_latest=Recentes +sort_by_created_at=Data de criação +url_changed_error=A URL da série mudou. Migre de %s para %s para atualizar a URL +id_not_found_error=Falha ao obter o ID do slug: %s diff --git a/lib-multisrc/heancms/build.gradle.kts b/lib-multisrc/heancms/build.gradle.kts index b105a49df..32ff56488 100644 --- a/lib-multisrc/heancms/build.gradle.kts +++ b/lib-multisrc/heancms/build.gradle.kts @@ -2,4 +2,8 @@ plugins { id("lib-multisrc") } -baseVersionCode = 20 +baseVersionCode = 21 + +dependencies { + api(project(":lib:i18n")) +} diff --git a/lib-multisrc/heancms/src/eu/kanade/tachiyomi/multisrc/heancms/HeanCms.kt b/lib-multisrc/heancms/src/eu/kanade/tachiyomi/multisrc/heancms/HeanCms.kt index 9cf9660d3..cdb8a4e8e 100644 --- a/lib-multisrc/heancms/src/eu/kanade/tachiyomi/multisrc/heancms/HeanCms.kt +++ b/lib-multisrc/heancms/src/eu/kanade/tachiyomi/multisrc/heancms/HeanCms.kt @@ -4,31 +4,25 @@ import android.app.Application import android.content.SharedPreferences import androidx.preference.PreferenceScreen import androidx.preference.SwitchPreferenceCompat +import eu.kanade.tachiyomi.lib.i18n.Intl import eu.kanade.tachiyomi.network.GET -import eu.kanade.tachiyomi.network.POST import eu.kanade.tachiyomi.source.ConfigurableSource -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 eu.kanade.tachiyomi.util.asJsoup import kotlinx.serialization.decodeFromString -import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import okhttp3.Headers import okhttp3.HttpUrl.Companion.toHttpUrl -import okhttp3.MediaType.Companion.toMediaType import okhttp3.OkHttpClient import okhttp3.Request -import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.Response import rx.Observable import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get -import java.io.IOException import java.text.SimpleDateFormat import java.util.Locale @@ -39,35 +33,15 @@ abstract class HeanCms( protected val apiUrl: String = baseUrl.replace("://", "://api."), ) : ConfigurableSource, HttpSource() { - private val preferences: SharedPreferences by lazy { + protected val preferences: SharedPreferences by lazy { Injekt.get().getSharedPreferences("source_$id", 0x0000) } - override fun setupPreferenceScreen(screen: PreferenceScreen) { - SwitchPreferenceCompat(screen.context).apply { - key = SHOW_PAID_CHAPTERS_PREF - title = intl.prefShowPaidChapterTitle - summaryOn = intl.prefShowPaidChapterSummaryOn - summaryOff = intl.prefShowPaidChapterSummaryOff - setDefaultValue(SHOW_PAID_CHAPTERS_DEFAULT) - - setOnPreferenceChangeListener { _, newValue -> - preferences.edit() - .putBoolean(SHOW_PAID_CHAPTERS_PREF, newValue as Boolean) - .commit() - } - }.also(screen::addPreference) - } - override val supportsLatest = true override val client: OkHttpClient = network.cloudflareClient - protected open val slugStrategy = SlugStrategy.NONE - - protected open val useNewQueryEndpoint = false - - private var seriesSlugMap: Map? = null + protected open val useNewChapterEndpoint = false /** * Custom Json instance to make usage of `encodeDefaults`, @@ -79,9 +53,14 @@ abstract class HeanCms( encodeDefaults = true } - protected val intl by lazy { HeanCmsIntl(lang) } + protected val intl = Intl( + language = lang, + baseLanguage = "en", + availableLanguages = setOf("en", "pt-BR", "es"), + classLoader = this::class.java.classLoader!!, + ) - protected open val coverPath: String = "cover/" + protected open val coverPath: String = "" protected open val mangaSubDirectory: String = "series" @@ -92,29 +71,6 @@ abstract class HeanCms( .add("Referer", "$baseUrl/") override fun popularMangaRequest(page: Int): Request { - if (useNewQueryEndpoint) { - return newEndpointPopularMangaRequest(page) - } - - val payloadObj = HeanCmsQuerySearchPayloadDto( - page = page, - order = "desc", - orderBy = "total_views", - status = "All", - type = "Comic", - ) - - val payload = json.encodeToString(payloadObj).toRequestBody(JSON_MEDIA_TYPE) - - val apiHeaders = headersBuilder() - .add("Accept", ACCEPT_JSON) - .add("Content-Type", payload.contentType().toString()) - .build() - - return POST("$apiUrl/series/querysearch", apiHeaders, payload) - } - - protected fun newEndpointPopularMangaRequest(page: Int): Request { val url = "$apiUrl/query".toHttpUrl().newBuilder() .addQueryParameter("query_string", "") .addQueryParameter("series_status", "All") @@ -124,66 +80,14 @@ abstract class HeanCms( .addQueryParameter("page", page.toString()) .addQueryParameter("perPage", "12") .addQueryParameter("tags_ids", "[]") + .addQueryParameter("adult", "true") return GET(url.build(), headers) } - override fun popularMangaParse(response: Response): MangasPage { - val json = response.body.string() - - if (json.startsWith("{")) { - val result = json.parseAs() - val mangaList = result.data.map { - if (slugStrategy != SlugStrategy.NONE) { - preferences.slugMap = preferences.slugMap.toMutableMap() - .also { map -> map[it.slug.toPermSlugIfNeeded()] = it.slug } - } - it.toSManga(apiUrl, coverPath, mangaSubDirectory, slugStrategy) - } - - fetchAllTitles() - - return MangasPage(mangaList, result.meta?.hasNextPage ?: false) - } - - val mangaList = json.parseAs>() - .map { - if (slugStrategy != SlugStrategy.NONE) { - preferences.slugMap = preferences.slugMap.toMutableMap() - .also { map -> map[it.slug.toPermSlugIfNeeded()] = it.slug } - } - it.toSManga(apiUrl, coverPath, mangaSubDirectory, slugStrategy) - } - - fetchAllTitles() - - return MangasPage(mangaList, hasNextPage = false) - } + override fun popularMangaParse(response: Response) = searchMangaParse(response) override fun latestUpdatesRequest(page: Int): Request { - if (useNewQueryEndpoint) { - return newEndpointLatestUpdatesRequest(page) - } - - val payloadObj = HeanCmsQuerySearchPayloadDto( - page = page, - order = "desc", - orderBy = "latest", - status = "All", - type = "Comic", - ) - - val payload = json.encodeToString(payloadObj).toRequestBody(JSON_MEDIA_TYPE) - - val apiHeaders = headersBuilder() - .add("Accept", ACCEPT_JSON) - .add("Content-Type", payload.contentType().toString()) - .build() - - return POST("$apiUrl/series/querysearch", apiHeaders, payload) - } - - protected fun newEndpointLatestUpdatesRequest(page: Int): Request { val url = "$apiUrl/query".toHttpUrl().newBuilder() .addQueryParameter("query_string", "") .addQueryParameter("series_status", "All") @@ -193,6 +97,7 @@ abstract class HeanCms( .addQueryParameter("page", page.toString()) .addQueryParameter("perPage", "12") .addQueryParameter("tags_ids", "[]") + .addQueryParameter("adult", "true") return GET(url.build(), headers) } @@ -206,12 +111,8 @@ abstract class HeanCms( val slug = query.substringAfter(SEARCH_PREFIX) val manga = SManga.create().apply { - url = if (slugStrategy != SlugStrategy.NONE) { - val mangaId = getIdBySlug(slug) - "/$mangaSubDirectory/${slug.toPermSlugIfNeeded()}#$mangaId" - } else { - "/$mangaSubDirectory/$slug" - } + val mangaId = getIdBySlug(slug) + url = "/$mangaSubDirectory/$slug#$mangaId" } return fetchMangaDetails(manga).map { MangasPage(listOf(it), false) } @@ -224,57 +125,12 @@ abstract class HeanCms( val seriesDetail = json.parseAs() - preferences.slugMap = preferences.slugMap.toMutableMap() - .also { it[seriesDetail.slug.toPermSlugIfNeeded()] = seriesDetail.slug } - seriesDetail.id } - return result.getOrNull() ?: throw Exception(intl.idNotFoundError + slug) + return result.getOrNull() ?: throw Exception(intl.format("id_not_found_error", slug)) } override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { - if (useNewQueryEndpoint) { - return newEndpointSearchMangaRequest(page, query, filters) - } - - if (query.isNotBlank()) { - val searchPayloadObj = HeanCmsSearchPayloadDto(query) - val searchPayload = json.encodeToString(searchPayloadObj) - .toRequestBody(JSON_MEDIA_TYPE) - - val apiHeaders = headersBuilder() - .add("Accept", ACCEPT_JSON) - .add("Content-Type", searchPayload.contentType().toString()) - .build() - - return POST("$apiUrl/series/search", apiHeaders, searchPayload) - } - - val sortByFilter = filters.firstInstanceOrNull() - - val payloadObj = HeanCmsQuerySearchPayloadDto( - page = page, - order = if (sortByFilter?.state?.ascending == true) "asc" else "desc", - orderBy = sortByFilter?.selected ?: "total_views", - status = filters.firstInstanceOrNull()?.selected?.value ?: "Ongoing", - type = "Comic", - tagIds = filters.firstInstanceOrNull()?.state - ?.filter(Genre::state) - ?.map(Genre::id) - .orEmpty(), - ) - - val payload = json.encodeToString(payloadObj).toRequestBody(JSON_MEDIA_TYPE) - - val apiHeaders = headersBuilder() - .add("Accept", ACCEPT_JSON) - .add("Content-Type", payload.contentType().toString()) - .build() - - return POST("$apiUrl/series/querysearch", apiHeaders, payload) - } - - protected fun newEndpointSearchMangaRequest(page: Int, query: String, filters: FilterList): Request { val sortByFilter = filters.firstInstanceOrNull() val statusFilter = filters.firstInstanceOrNull() @@ -292,6 +148,7 @@ abstract class HeanCms( .addQueryParameter("page", page.toString()) .addQueryParameter("perPage", "12") .addQueryParameter("tags_ids", tagIds) + .addQueryParameter("adult", "true") return GET(url.build(), headers) } @@ -299,95 +156,34 @@ abstract class HeanCms( override fun searchMangaParse(response: Response): MangasPage { val json = response.body.string() - if (response.request.url.pathSegments.last() == "search") { - fetchAllTitles() - - val result = json.parseAs>() - val mangaList = result - .filter { it.type == "Comic" } - .map { - it.slug = it.slug.toPermSlugIfNeeded() - it.toSManga(apiUrl, coverPath, mangaSubDirectory, seriesSlugMap.orEmpty(), slugStrategy) - } - - return MangasPage(mangaList, false) + val result = json.parseAs() + val mangaList = result.data.map { + it.toSManga(apiUrl, coverPath, mangaSubDirectory) } - if (json.startsWith("{")) { - val result = json.parseAs() - val mangaList = result.data.map { - if (slugStrategy != SlugStrategy.NONE) { - preferences.slugMap = preferences.slugMap.toMutableMap() - .also { map -> map[it.slug.toPermSlugIfNeeded()] = it.slug } - } - it.toSManga(apiUrl, coverPath, mangaSubDirectory, slugStrategy) - } - - fetchAllTitles() - - return MangasPage(mangaList, result.meta?.hasNextPage ?: false) - } - - val mangaList = json.parseAs>() - .map { - if (slugStrategy != SlugStrategy.NONE) { - preferences.slugMap = preferences.slugMap.toMutableMap() - .also { map -> map[it.slug.toPermSlugIfNeeded()] = it.slug } - } - it.toSManga(apiUrl, coverPath, mangaSubDirectory, slugStrategy) - } - - fetchAllTitles() - - return MangasPage(mangaList, hasNextPage = false) + return MangasPage(mangaList, result.meta?.hasNextPage() ?: false) } override fun getMangaUrl(manga: SManga): String { val seriesSlug = manga.url .substringAfterLast("/") .substringBefore("#") - .toPermSlugIfNeeded() - val currentSlug = if (slugStrategy != SlugStrategy.NONE) { - preferences.slugMap[seriesSlug] ?: seriesSlug - } else { - seriesSlug - } - - return "$baseUrl/$mangaSubDirectory/$currentSlug" + return "$baseUrl/$mangaSubDirectory/$seriesSlug" } override fun mangaDetailsRequest(manga: SManga): Request { - if (slugStrategy != SlugStrategy.NONE && (manga.url.contains(TIMESTAMP_REGEX))) { - throw Exception(intl.urlChangedError(name)) + if (!manga.url.contains("#")) { + throw Exception(intl.format("url_changed_error", name, name)) } - if (slugStrategy == SlugStrategy.ID && !manga.url.contains("#")) { - throw Exception(intl.urlChangedError(name)) - } - - val seriesSlug = manga.url - .substringAfterLast("/") - .substringBefore("#") - .toPermSlugIfNeeded() - val seriesId = manga.url.substringAfterLast("#") - fetchAllTitles() - - val seriesDetails = seriesSlugMap?.get(seriesSlug) - val currentSlug = seriesDetails?.slug ?: seriesSlug - val currentStatus = seriesDetails?.status ?: manga.status - val apiHeaders = headersBuilder() .add("Accept", ACCEPT_JSON) .build() - return if (slugStrategy == SlugStrategy.ID) { - GET("$apiUrl/series/id/$seriesId", apiHeaders) - } else { - GET("$apiUrl/series/$currentSlug#$currentStatus", apiHeaders) - } + return GET("$apiUrl/series/id/$seriesId", apiHeaders) } override fun mangaDetailsParse(response: Response): SManga { @@ -395,14 +191,10 @@ abstract class HeanCms( val result = runCatching { response.parseAs() } - val seriesResult = result.getOrNull() ?: throw Exception(intl.urlChangedError(name)) + val seriesResult = result.getOrNull() + ?: throw Exception(intl.format("url_changed_error", name, name)) - if (slugStrategy != SlugStrategy.NONE) { - preferences.slugMap = preferences.slugMap.toMutableMap() - .also { it[seriesResult.slug.toPermSlugIfNeeded()] = seriesResult.slug } - } - - val seriesDetails = seriesResult.toSManga(apiUrl, coverPath, mangaSubDirectory, slugStrategy) + val seriesDetails = seriesResult.toSManga(apiUrl, coverPath, mangaSubDirectory) return seriesDetails.apply { status = status.takeUnless { it == SManga.UNKNOWN } @@ -410,105 +202,97 @@ abstract class HeanCms( } } - override fun chapterListRequest(manga: SManga): Request = mangaDetailsRequest(manga) + override fun chapterListRequest(manga: SManga): Request { + if (useNewChapterEndpoint) { + if (!manga.url.contains("#")) { + throw Exception(intl.format("url_changed_error", name, name)) + } - override fun chapterListParse(response: Response): List { - val result = response.parseAs() + val seriesId = manga.url.substringAfterLast("#") + val seriesSlug = manga.url.substringAfterLast("/").substringBefore("#") - if (slugStrategy == SlugStrategy.ID) { - preferences.slugMap = preferences.slugMap.toMutableMap() - .also { it[result.slug.toPermSlugIfNeeded()] = result.slug } + val url = "$apiUrl/chapter/query".toHttpUrl().newBuilder() + .addQueryParameter("page", "1") + .addQueryParameter("perPage", PER_PAGE_CHAPTERS.toString()) + .addQueryParameter("series_id", seriesId) + .fragment(seriesSlug) + + return GET(url.build(), headers) } - val currentTimestamp = System.currentTimeMillis() + return mangaDetailsRequest(manga) + } + override fun chapterListParse(response: Response): List { val showPaidChapters = preferences.showPaidChapters - if (useNewQueryEndpoint) { - return result.seasons.orEmpty() - .flatMap { it.chapters.orEmpty() } + if (useNewChapterEndpoint) { + val apiHeaders = headersBuilder() + .add("Accept", ACCEPT_JSON) + .build() + + val seriesId = response.request.url.queryParameter("series_id") + + val seriesSlug = response.request.url.fragment!! + + var result = response.parseAs() + + val currentTimestamp = System.currentTimeMillis() + + val chapterList = mutableListOf() + + chapterList.addAll(result.data) + + var page = 2 + while (result.meta.hasNextPage()) { + val url = "$apiUrl/chapter/query".toHttpUrl().newBuilder() + .addQueryParameter("page", page.toString()) + .addQueryParameter("perPage", PER_PAGE_CHAPTERS.toString()) + .addQueryParameter("series_id", seriesId) + .build() + + val nextResponse = client.newCall(GET(url, apiHeaders)).execute() + result = nextResponse.parseAs() + chapterList.addAll(result.data) + page++ + } + + return chapterList .filter { it.price == 0 || showPaidChapters } - .map { it.toSChapter(result.slug, mangaSubDirectory, dateFormat, slugStrategy) } + .map { it.toSChapter(seriesSlug, mangaSubDirectory, dateFormat) } .filter { it.date_upload <= currentTimestamp } } - return result.chapters.orEmpty() + val result = response.parseAs() + + val currentTimestamp = System.currentTimeMillis() + + return result.seasons.orEmpty() + .flatMap { it.chapters.orEmpty() } .filter { it.price == 0 || showPaidChapters } - .map { it.toSChapter(result.slug, mangaSubDirectory, dateFormat, slugStrategy) } + .map { it.toSChapter(result.slug, mangaSubDirectory, dateFormat) } .filter { it.date_upload <= currentTimestamp } - .reversed() } - override fun getChapterUrl(chapter: SChapter): String { - if (slugStrategy == SlugStrategy.NONE) return baseUrl + chapter.url + override fun getChapterUrl(chapter: SChapter) = baseUrl + chapter.url.substringBeforeLast("#") - val seriesSlug = chapter.url - .substringAfter("/$mangaSubDirectory/") - .substringBefore("/") - .toPermSlugIfNeeded() - - val currentSlug = preferences.slugMap[seriesSlug] ?: seriesSlug - val chapterUrl = chapter.url.replaceFirst(seriesSlug, currentSlug) - - return baseUrl + chapterUrl - } - - override fun pageListRequest(chapter: SChapter): Request { - if (useNewQueryEndpoint) { - if (slugStrategy != SlugStrategy.NONE) { - val seriesPermSlug = chapter.url.substringAfter("/$mangaSubDirectory/").substringBefore("/") - val seriesSlug = preferences.slugMap[seriesPermSlug] ?: seriesPermSlug - val chapterUrl = chapter.url.replaceFirst(seriesPermSlug, seriesSlug) - return GET(baseUrl + chapterUrl, headers) - } - return GET(baseUrl + chapter.url, headers) - } - - val chapterId = chapter.url.substringAfterLast("#").substringBefore("-paid") - - val apiHeaders = headersBuilder() - .add("Accept", ACCEPT_JSON) - .build() - - return GET("$apiUrl/series/chapter/$chapterId", apiHeaders) - } + override fun pageListRequest(chapter: SChapter) = + GET(apiUrl + chapter.url.replace("/$mangaSubDirectory/", "/chapter/"), headers) override fun pageListParse(response: Response): List { - if (useNewQueryEndpoint) { - val paidChapter = response.request.url.fragment?.contains("-paid") + val result = response.parseAs() - val document = response.asJsoup() + if (result.isPaywalled()) throw Exception(intl["paid_chapter_error"]) - val images = document.selectFirst("div.min-h-screen > div.container > p.items-center") - - if (images == null && paidChapter == true) { - throw IOException(intl.paidChapterError) + return if (useNewChapterEndpoint) { + result.chapter.chapterData?.images.orEmpty().mapIndexed { i, img -> + Page(i, imageUrl = img) } - - return images?.select("img").orEmpty().mapIndexed { i, img -> - val imageUrl = if (img.hasClass("lazy")) img.absUrl("data-src") else img.absUrl("src") - Page(i, "", imageUrl) + } else { + result.data.orEmpty().mapIndexed { i, img -> + Page(i, imageUrl = img) } } - - val images = response.parseAs().content?.images.orEmpty() - val paidChapter = response.request.url.fragment?.contains("-paid") - - if (images.isEmpty() && paidChapter == true) { - throw IOException(intl.paidChapterError) - } - - return images.filterNot { imageUrl -> - // Their image server returns HTTP 403 for hidden files that starts - // with a dot in the file name. To avoid download errors, these are removed. - imageUrl - .removeSuffix("/") - .substringAfterLast("/") - .startsWith(".") - } - .mapIndexed { i, url -> - Page(i, imageUrl = if (url.startsWith("http")) url else "$apiUrl/$url") - } } override fun fetchImageUrl(page: Page): Observable = Observable.just(page.imageUrl!!) @@ -523,121 +307,18 @@ abstract class HeanCms( return GET(page.imageUrl!!, imageHeaders) } - protected open fun fetchAllTitles() { - if (!seriesSlugMap.isNullOrEmpty() || slugStrategy != SlugStrategy.FETCH_ALL) { - return - } - - val result = runCatching { - var hasNextPage = true - var page = 1 - val tempMap = mutableMapOf() - - while (hasNextPage) { - val response = client.newCall(allTitlesRequest(page)).execute() - val json = response.body.string() - - if (json.startsWith("{")) { - val result = json.parseAs() - tempMap.putAll(parseAllTitles(result.data)) - hasNextPage = result.meta?.hasNextPage ?: false - page++ - } else { - val result = json.parseAs>() - tempMap.putAll(parseAllTitles(result)) - hasNextPage = false - } - } - - tempMap.toMap() - } - - seriesSlugMap = result.getOrNull() - preferences.slugMap = preferences.slugMap.toMutableMap() - .also { it.putAll(seriesSlugMap.orEmpty().mapValues { (_, v) -> v.slug }) } - } - - protected open fun allTitlesRequest(page: Int): Request { - if (useNewQueryEndpoint) { - val url = "$apiUrl/query".toHttpUrl().newBuilder() - .addQueryParameter("series_type", "Comic") - .addQueryParameter("page", page.toString()) - .addQueryParameter("perPage", PER_PAGE_MANGA_TITLES.toString()) - - return GET(url.build(), headers) - } - - val payloadObj = HeanCmsQuerySearchPayloadDto( - page = page, - order = "desc", - orderBy = "total_views", - type = "Comic", - ) - - val payload = json.encodeToString(payloadObj).toRequestBody(JSON_MEDIA_TYPE) - - val apiHeaders = headersBuilder() - .add("Accept", ACCEPT_JSON) - .add("Content-Type", payload.contentType().toString()) - .build() - - return POST("$apiUrl/series/querysearch", apiHeaders, payload) - } - - protected open fun parseAllTitles(result: List): Map { - return result - .filter { it.type == "Comic" } - .associateBy( - keySelector = { it.slug.replace(TIMESTAMP_REGEX, "") }, - valueTransform = { - HeanCmsTitle( - slug = it.slug, - thumbnailFileName = it.thumbnail, - status = it.status?.toStatus() ?: SManga.UNKNOWN, - ) - }, - ) - } - - /** - * Used to store the current slugs for sources that change it periodically and for the - * search that doesn't return the thumbnail URLs. - */ - data class HeanCmsTitle(val slug: String, val thumbnailFileName: String, val status: Int) - - /** - * Used to specify the strategy to use when fetching the slug for a manga. - * This is needed because some sources change the slug periodically. - * [NONE]: Use series_slug without changes. - * [ID]: Use series_id to fetch the slug from the API. - * IMPORTANT: [ID] is only available in the new query endpoint. - * [FETCH_ALL]: Convert the slug to a permanent slug by removing the timestamp. - * At extension start, all the slugs are fetched and stored in a map. - */ - enum class SlugStrategy { - NONE, ID, FETCH_ALL - } - - private fun String.toPermSlugIfNeeded(): String { - return if (slugStrategy != SlugStrategy.NONE) { - this.replace(TIMESTAMP_REGEX, "") - } else { - this - } - } - protected open fun getStatusList(): List = listOf( - Status(intl.statusAll, "All"), - Status(intl.statusOngoing, "Ongoing"), - Status(intl.statusOnHiatus, "Hiatus"), - Status(intl.statusDropped, "Dropped"), + Status(intl["status_all"], "All"), + Status(intl["status_ongoing"], "Ongoing"), + Status(intl["status_onhiatus"], "Hiatus"), + Status(intl["status_dropped"], "Dropped"), ) protected open fun getSortProperties(): List = listOf( - SortProperty(intl.sortByTitle, "title"), - SortProperty(intl.sortByViews, "total_views"), - SortProperty(intl.sortByLatest, "latest"), - SortProperty(intl.sortByCreatedAt, "created_at"), + SortProperty(intl["sort_by_title"], "title"), + SortProperty(intl["sort_by_views"], "total_views"), + SortProperty(intl["sort_by_latest"], "latest"), + SortProperty(intl["sort_by_created_at"], "created_at"), ) protected open fun getGenreList(): List = emptyList() @@ -646,15 +327,24 @@ abstract class HeanCms( val genres = getGenreList() val filters = listOfNotNull( - Filter.Header(intl.filterWarning), - StatusFilter(intl.statusFilterTitle, getStatusList()), - SortByFilter(intl.sortByFilterTitle, getSortProperties()), - GenreFilter(intl.genreFilterTitle, genres).takeIf { genres.isNotEmpty() }, + StatusFilter(intl["status_filter_title"], getStatusList()), + SortByFilter(intl["sort_by_filter_title"], getSortProperties()), + GenreFilter(intl["genre_filter_title"], genres).takeIf { genres.isNotEmpty() }, ) return FilterList(filters) } + override fun setupPreferenceScreen(screen: PreferenceScreen) { + SwitchPreferenceCompat(screen.context).apply { + key = SHOW_PAID_CHAPTERS_PREF + title = intl["pref_show_paid_chapter_title"] + summaryOn = intl["pref_show_paid_chapter_summary_on"] + summaryOff = intl["pref_show_paid_chapter_summary_off"] + setDefaultValue(SHOW_PAID_CHAPTERS_DEFAULT) + }.also(screen::addPreference) + } + protected inline fun Response.parseAs(): T = use { it.body.string().parseAs() } @@ -664,18 +354,6 @@ abstract class HeanCms( protected inline fun List<*>.firstInstanceOrNull(): R? = filterIsInstance().firstOrNull() - protected var SharedPreferences.slugMap: MutableMap - get() { - val jsonMap = getString(PREF_URL_MAP_SLUG, "{}")!! - val slugMap = runCatching { json.decodeFromString>(jsonMap) } - return slugMap.getOrNull()?.toMutableMap() ?: mutableMapOf() - } - set(newSlugMap) { - edit() - .putString(PREF_URL_MAP_SLUG, json.encodeToString(newSlugMap)) - .apply() - } - private val SharedPreferences.showPaidChapters: Boolean get() = getBoolean(SHOW_PAID_CHAPTERS_PREF, SHOW_PAID_CHAPTERS_DEFAULT) @@ -683,16 +361,10 @@ abstract class HeanCms( private const val ACCEPT_IMAGE = "image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8" private const val ACCEPT_JSON = "application/json, text/plain, */*" - private val JSON_MEDIA_TYPE = "application/json".toMediaType() - - val TIMESTAMP_REGEX = """-\d{13}$""".toRegex() - - private const val PER_PAGE_MANGA_TITLES = 10000 + private const val PER_PAGE_CHAPTERS = 1000 const val SEARCH_PREFIX = "slug:" - private const val PREF_URL_MAP_SLUG = "pref_url_map" - private const val SHOW_PAID_CHAPTERS_PREF = "pref_show_paid_chap" private const val SHOW_PAID_CHAPTERS_DEFAULT = false } diff --git a/lib-multisrc/heancms/src/eu/kanade/tachiyomi/multisrc/heancms/HeanCmsDto.kt b/lib-multisrc/heancms/src/eu/kanade/tachiyomi/multisrc/heancms/HeanCmsDto.kt index 9a400cc6f..73870233e 100644 --- a/lib-multisrc/heancms/src/eu/kanade/tachiyomi/multisrc/heancms/HeanCmsDto.kt +++ b/lib-multisrc/heancms/src/eu/kanade/tachiyomi/multisrc/heancms/HeanCmsDto.kt @@ -1,6 +1,5 @@ package eu.kanade.tachiyomi.multisrc.heancms -import eu.kanade.tachiyomi.multisrc.heancms.HeanCms.SlugStrategy import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SManga import kotlinx.serialization.SerialName @@ -9,59 +8,30 @@ import org.jsoup.Jsoup import java.text.SimpleDateFormat @Serializable -data class HeanCmsQuerySearchDto( +class HeanCmsQuerySearchDto( val data: List = emptyList(), val meta: HeanCmsQuerySearchMetaDto? = null, ) @Serializable -data class HeanCmsQuerySearchMetaDto( - @SerialName("current_page") val currentPage: Int, - @SerialName("last_page") val lastPage: Int, +class HeanCmsQuerySearchMetaDto( + @SerialName("current_page") private val currentPage: Int, + @SerialName("last_page") private val lastPage: Int, ) { - - val hasNextPage: Boolean - get() = currentPage < lastPage + fun hasNextPage() = currentPage < lastPage } @Serializable -data class HeanCmsSearchDto( - val description: String? = null, - @SerialName("series_slug") var slug: String, - @SerialName("series_type") val type: String, - val title: String, - val thumbnail: String? = null, -) { - - fun toSManga( - apiUrl: String, - coverPath: String, - mangaSubDirectory: String, - slugMap: Map, - slugStrategy: SlugStrategy, - ): SManga = SManga.create().apply { - val slugOnly = slug.toPermSlugIfNeeded(slugStrategy) - val thumbnailFileName = slugMap[slugOnly]?.thumbnailFileName - title = this@HeanCmsSearchDto.title - thumbnail_url = thumbnail?.toAbsoluteThumbnailUrl(apiUrl, coverPath) - ?: thumbnailFileName?.toAbsoluteThumbnailUrl(apiUrl, coverPath) - url = "/$mangaSubDirectory/$slugOnly" - } -} - -@Serializable -data class HeanCmsSeriesDto( +class HeanCmsSeriesDto( val id: Int, @SerialName("series_slug") val slug: String, - @SerialName("series_type") val type: String = "Comic", - val author: String? = null, - val description: String? = null, - val studio: String? = null, - val status: String? = null, - val thumbnail: String, - val title: String, - val tags: List? = emptyList(), - val chapters: List? = emptyList(), + private val author: String? = null, + private val description: String? = null, + private val studio: String? = null, + private val status: String? = null, + private val thumbnail: String, + private val title: String, + private val tags: List? = emptyList(), val seasons: List? = emptyList(), ) { @@ -69,10 +39,8 @@ data class HeanCmsSeriesDto( apiUrl: String, coverPath: String, mangaSubDirectory: String, - slugStrategy: SlugStrategy, ): SManga = SManga.create().apply { val descriptionBody = this@HeanCmsSeriesDto.description?.let(Jsoup::parseBodyFragment) - val slugOnly = slug.toPermSlugIfNeeded(slugStrategy) title = this@HeanCmsSeriesDto.title author = this@HeanCmsSeriesDto.author?.trim() @@ -86,89 +54,84 @@ data class HeanCmsSeriesDto( thumbnail_url = thumbnail.ifEmpty { null } ?.toAbsoluteThumbnailUrl(apiUrl, coverPath) status = this@HeanCmsSeriesDto.status?.toStatus() ?: SManga.UNKNOWN - url = if (slugStrategy != SlugStrategy.NONE) { - "/$mangaSubDirectory/$slugOnly#$id" - } else { - "/$mangaSubDirectory/$slug" - } + url = "/$mangaSubDirectory/$slug#$id" } } @Serializable -data class HeanCmsSeasonsDto( - val index: Int, +class HeanCmsSeasonsDto( val chapters: List? = emptyList(), ) @Serializable -data class HeanCmsTagDto(val name: String) +class HeanCmsTagDto(val name: String) @Serializable -data class HeanCmsChapterDto( - val id: Int, - @SerialName("chapter_name") val name: String, - @SerialName("chapter_slug") val slug: String, - val index: String, - @SerialName("created_at") val createdAt: String, +class HeanCmsChapterPayloadDto( + val data: List, + val meta: HeanCmsChapterMetaDto, +) + +@Serializable +class HeanCmsChapterDto( + private val id: Int, + @SerialName("chapter_name") private val name: String, + @SerialName("chapter_slug") private val slug: String, + @SerialName("created_at") private val createdAt: String, val price: Int? = null, ) { fun toSChapter( seriesSlug: String, mangaSubDirectory: String, dateFormat: SimpleDateFormat, - slugStrategy: SlugStrategy, ): SChapter = SChapter.create().apply { - val seriesSlugOnly = seriesSlug.toPermSlugIfNeeded(slugStrategy) name = this@HeanCmsChapterDto.name.trim() if (price != 0) { name += " \uD83D\uDD12" } - date_upload = runCatching { dateFormat.parse(createdAt)?.time } - .getOrNull() ?: 0L + date_upload = try { + dateFormat.parse(createdAt)?.time ?: 0L + } catch (_: Exception) { + 0L + } - val paidStatus = if (price != 0 && price != null) "-paid" else "" - - url = "/$mangaSubDirectory/$seriesSlugOnly/$slug#$id$paidStatus" + url = "/$mangaSubDirectory/$seriesSlug/$slug#$id" } } @Serializable -data class HeanCmsReaderDto( - val content: HeanCmsReaderContentDto? = null, +class HeanCmsChapterMetaDto( + @SerialName("current_page") private val currentPage: Int, + @SerialName("last_page") private val lastPage: Int, +) { + fun hasNextPage() = currentPage < lastPage +} + +@Serializable +class HeanCmsPagePayloadDto( + val chapter: HeanCmsPageDto, + private val paywall: Boolean = false, + val data: List? = emptyList(), +) { + fun isPaywalled() = paywall +} + +@Serializable +class HeanCmsPageDto( + @SerialName("chapter_data") val chapterData: HeanCmsPageDataDto?, ) @Serializable -data class HeanCmsReaderContentDto( +class HeanCmsPageDataDto( val images: List? = emptyList(), ) -@Serializable -data class HeanCmsQuerySearchPayloadDto( - val order: String, - val page: Int, - @SerialName("order_by") val orderBy: String, - @SerialName("series_status") val status: String? = null, - @SerialName("series_type") val type: String, - @SerialName("tags_ids") val tagIds: List = emptyList(), -) - -@Serializable -data class HeanCmsSearchPayloadDto(val term: String) - private fun String.toAbsoluteThumbnailUrl(apiUrl: String, coverPath: String): String { return if (startsWith("https://")) this else "$apiUrl/$coverPath$this" } -private fun String.toPermSlugIfNeeded(slugStrategy: SlugStrategy): String { - return if (slugStrategy != SlugStrategy.NONE) { - this.replace(HeanCms.TIMESTAMP_REGEX, "") - } else { - this - } -} - fun String.toStatus(): Int = when (this) { "Ongoing" -> SManga.ONGOING "Hiatus" -> SManga.ON_HIATUS diff --git a/lib-multisrc/heancms/src/eu/kanade/tachiyomi/multisrc/heancms/HeanCmsIntl.kt b/lib-multisrc/heancms/src/eu/kanade/tachiyomi/multisrc/heancms/HeanCmsIntl.kt deleted file mode 100644 index 520eec381..000000000 --- a/lib-multisrc/heancms/src/eu/kanade/tachiyomi/multisrc/heancms/HeanCmsIntl.kt +++ /dev/null @@ -1,124 +0,0 @@ -package eu.kanade.tachiyomi.multisrc.heancms - -class HeanCmsIntl(lang: String) { - - val availableLang: String = if (lang in AVAILABLE_LANGS) lang else ENGLISH - - val genreFilterTitle: String = when (availableLang) { - BRAZILIAN_PORTUGUESE -> "Gêneros" - SPANISH -> "Géneros" - else -> "Genres" - } - - val statusFilterTitle: String = when (availableLang) { - BRAZILIAN_PORTUGUESE -> "Estado" - SPANISH -> "Estado" - else -> "Status" - } - - val statusAll: String = when (availableLang) { - BRAZILIAN_PORTUGUESE -> "Todos" - SPANISH -> "Todos" - else -> "All" - } - - val statusOngoing: String = when (availableLang) { - BRAZILIAN_PORTUGUESE -> "Em andamento" - SPANISH -> "En curso" - else -> "Ongoing" - } - - val statusOnHiatus: String = when (availableLang) { - BRAZILIAN_PORTUGUESE -> "Em hiato" - SPANISH -> "En hiatus" - else -> "On Hiatus" - } - - val statusDropped: String = when (availableLang) { - BRAZILIAN_PORTUGUESE -> "Cancelada" - SPANISH -> "Abandonada" - else -> "Dropped" - } - - val sortByFilterTitle: String = when (availableLang) { - BRAZILIAN_PORTUGUESE -> "Ordenar por" - SPANISH -> "Ordenar por" - else -> "Sort by" - } - - val sortByTitle: String = when (availableLang) { - BRAZILIAN_PORTUGUESE -> "Título" - SPANISH -> "Titulo" - else -> "Title" - } - - val sortByViews: String = when (availableLang) { - BRAZILIAN_PORTUGUESE -> "Visualizações" - SPANISH -> "Número de vistas" - else -> "Views" - } - - val sortByLatest: String = when (availableLang) { - BRAZILIAN_PORTUGUESE -> "Recentes" - SPANISH -> "Recientes" - else -> "Latest" - } - - val sortByCreatedAt: String = when (availableLang) { - BRAZILIAN_PORTUGUESE -> "Data de criação" - SPANISH -> "Fecha de creación" - else -> "Created at" - } - - val filterWarning: String = when (availableLang) { - BRAZILIAN_PORTUGUESE -> "Os filtros serão ignorados se a busca não estiver vazia." - SPANISH -> "Los filtros serán ignorados si la búsqueda no está vacía." - else -> "Filters will be ignored if the search is not empty." - } - - val prefShowPaidChapterTitle: String = when (availableLang) { - SPANISH -> "Mostrar capítulos de pago" - else -> "Display paid chapters" - } - - val prefShowPaidChapterSummaryOn: String = when (availableLang) { - SPANISH -> "Se mostrarán capítulos de pago. Deberá iniciar sesión" - else -> "Paid chapters will appear. A login might be needed!" - } - - val prefShowPaidChapterSummaryOff: String = when (availableLang) { - SPANISH -> "Solo se mostrarán los capítulos gratuitos" - else -> "Only free chapters will be displayed." - } - - val paidChapterError: String = when (availableLang) { - SPANISH -> "Capítulo no disponible. Debe iniciar sesión en Webview y tener el capítulo comprado." - else -> "Paid chapter unavailable.\nA login/purchase might be needed (using webview)." - } - - fun urlChangedError(sourceName: String): String = when (availableLang) { - BRAZILIAN_PORTUGUESE -> - "A URL da série mudou. Migre de $sourceName " + - "para $sourceName para atualizar a URL." - SPANISH -> - "La URL de la serie ha cambiado. Migre de $sourceName a " + - "$sourceName para actualizar la URL." - else -> - "The URL of the series has changed. Migrate from $sourceName " + - "to $sourceName to update the URL." - } - - val idNotFoundError: String = when (availableLang) { - BRAZILIAN_PORTUGUESE -> "Falha ao obter o ID do slug: " - SPANISH -> "No se pudo encontrar el ID para: " - else -> "Failed to get the ID for slug: " - } - - companion object { - const val BRAZILIAN_PORTUGUESE = "pt-BR" - const val ENGLISH = "en" - const val SPANISH = "es" - - val AVAILABLE_LANGS = arrayOf(BRAZILIAN_PORTUGUESE, ENGLISH, SPANISH) - } -} diff --git a/src/en/omegascans/src/eu/kanade/tachiyomi/extension/en/omegascans/OmegaScans.kt b/src/en/omegascans/src/eu/kanade/tachiyomi/extension/en/omegascans/OmegaScans.kt index 6daaec326..05c807587 100644 --- a/src/en/omegascans/src/eu/kanade/tachiyomi/extension/en/omegascans/OmegaScans.kt +++ b/src/en/omegascans/src/eu/kanade/tachiyomi/extension/en/omegascans/OmegaScans.kt @@ -11,12 +11,10 @@ class OmegaScans : HeanCms("Omega Scans", "https://omegascans.org", "en") { .rateLimitHost(apiUrl.toHttpUrl(), 1) .build() - override val useNewQueryEndpoint = true - // Site changed from MangaThemesia to HeanCms. override val versionId = 2 - override val coverPath = "" + override val useNewChapterEndpoint = true override fun getGenreList() = listOf( Genre("Romance", 1), diff --git a/src/en/templescan/src/eu/kanade/tachiyomi/extension/en/templescan/TempleScan.kt b/src/en/templescan/src/eu/kanade/tachiyomi/extension/en/templescan/TempleScan.kt index e3362f2c2..11666a020 100644 --- a/src/en/templescan/src/eu/kanade/tachiyomi/extension/en/templescan/TempleScan.kt +++ b/src/en/templescan/src/eu/kanade/tachiyomi/extension/en/templescan/TempleScan.kt @@ -17,8 +17,6 @@ class TempleScan : HeanCms( .rateLimit(1) .build() - override val useNewQueryEndpoint = true - override val coverPath = "" override val mangaSubDirectory = "comic" override fun getGenreList() = listOf( diff --git a/src/fr/perfscan/src/eu/kanade/tachiyomi/extension/fr/perfscan/PerfScan.kt b/src/fr/perfscan/src/eu/kanade/tachiyomi/extension/fr/perfscan/PerfScan.kt index 00e8b879a..02e06a16f 100644 --- a/src/fr/perfscan/src/eu/kanade/tachiyomi/extension/fr/perfscan/PerfScan.kt +++ b/src/fr/perfscan/src/eu/kanade/tachiyomi/extension/fr/perfscan/PerfScan.kt @@ -6,12 +6,18 @@ import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.OkHttpClient class PerfScan : HeanCms("Perf Scan", "https://perf-scan.fr", "fr") { + override val client: OkHttpClient = super.client.newBuilder() .rateLimitHost(apiUrl.toHttpUrl(), 1, 2) .build() - override val coverPath: String = "" - override val useNewQueryEndpoint = true + init { + preferences.run { + if (contains("pref_url_map")) { + edit().remove("pref_url_map").apply() + } + } + } - override val slugStrategy = SlugStrategy.ID + override val useNewChapterEndpoint = true }