Update HeanCMS theme (#1969)

* i hate this theme

* bump

* remove useless slug update

* lint

* Update series slug on chapter list update

This was made for sources that changed slugs constantly.

Currently no one uses it, but who knows if they enable that again

* what an unstable experience

* Remove empty lines

* Fix intl

* newline

Co-authored-by: AwkwardPeak7 <48650614+AwkwardPeak7@users.noreply.github.com>

* why my build took 5 minutes

* I hate iguanas

---------

Co-authored-by: AwkwardPeak7 <48650614+AwkwardPeak7@users.noreply.github.com>
This commit is contained in:
bapeey 2024-03-19 10:21:09 -05:00 committed by GitHub
parent 16011407c5
commit 8defa56f71
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 233 additions and 669 deletions

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -2,4 +2,8 @@ plugins {
id("lib-multisrc") id("lib-multisrc")
} }
baseVersionCode = 20 baseVersionCode = 21
dependencies {
api(project(":lib:i18n"))
}

View File

@ -4,31 +4,25 @@ import android.app.Application
import android.content.SharedPreferences import android.content.SharedPreferences
import androidx.preference.PreferenceScreen import androidx.preference.PreferenceScreen
import androidx.preference.SwitchPreferenceCompat import androidx.preference.SwitchPreferenceCompat
import eu.kanade.tachiyomi.lib.i18n.Intl
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.source.ConfigurableSource 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.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.util.asJsoup
import kotlinx.serialization.decodeFromString import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import okhttp3.Headers import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.Response import okhttp3.Response
import rx.Observable import rx.Observable
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import java.io.IOException
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Locale import java.util.Locale
@ -39,35 +33,15 @@ abstract class HeanCms(
protected val apiUrl: String = baseUrl.replace("://", "://api."), protected val apiUrl: String = baseUrl.replace("://", "://api."),
) : ConfigurableSource, HttpSource() { ) : ConfigurableSource, HttpSource() {
private val preferences: SharedPreferences by lazy { protected val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000) Injekt.get<Application>().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 supportsLatest = true
override val client: OkHttpClient = network.cloudflareClient override val client: OkHttpClient = network.cloudflareClient
protected open val slugStrategy = SlugStrategy.NONE protected open val useNewChapterEndpoint = false
protected open val useNewQueryEndpoint = false
private var seriesSlugMap: Map<String, HeanCmsTitle>? = null
/** /**
* Custom Json instance to make usage of `encodeDefaults`, * Custom Json instance to make usage of `encodeDefaults`,
@ -79,9 +53,14 @@ abstract class HeanCms(
encodeDefaults = true 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" protected open val mangaSubDirectory: String = "series"
@ -92,29 +71,6 @@ abstract class HeanCms(
.add("Referer", "$baseUrl/") .add("Referer", "$baseUrl/")
override fun popularMangaRequest(page: Int): Request { 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() val url = "$apiUrl/query".toHttpUrl().newBuilder()
.addQueryParameter("query_string", "") .addQueryParameter("query_string", "")
.addQueryParameter("series_status", "All") .addQueryParameter("series_status", "All")
@ -124,66 +80,14 @@ abstract class HeanCms(
.addQueryParameter("page", page.toString()) .addQueryParameter("page", page.toString())
.addQueryParameter("perPage", "12") .addQueryParameter("perPage", "12")
.addQueryParameter("tags_ids", "[]") .addQueryParameter("tags_ids", "[]")
.addQueryParameter("adult", "true")
return GET(url.build(), headers) return GET(url.build(), headers)
} }
override fun popularMangaParse(response: Response): MangasPage { override fun popularMangaParse(response: Response) = searchMangaParse(response)
val json = response.body.string()
if (json.startsWith("{")) {
val result = json.parseAs<HeanCmsQuerySearchDto>()
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<List<HeanCmsSeriesDto>>()
.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 latestUpdatesRequest(page: Int): Request { 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() val url = "$apiUrl/query".toHttpUrl().newBuilder()
.addQueryParameter("query_string", "") .addQueryParameter("query_string", "")
.addQueryParameter("series_status", "All") .addQueryParameter("series_status", "All")
@ -193,6 +97,7 @@ abstract class HeanCms(
.addQueryParameter("page", page.toString()) .addQueryParameter("page", page.toString())
.addQueryParameter("perPage", "12") .addQueryParameter("perPage", "12")
.addQueryParameter("tags_ids", "[]") .addQueryParameter("tags_ids", "[]")
.addQueryParameter("adult", "true")
return GET(url.build(), headers) return GET(url.build(), headers)
} }
@ -206,12 +111,8 @@ abstract class HeanCms(
val slug = query.substringAfter(SEARCH_PREFIX) val slug = query.substringAfter(SEARCH_PREFIX)
val manga = SManga.create().apply { val manga = SManga.create().apply {
url = if (slugStrategy != SlugStrategy.NONE) { val mangaId = getIdBySlug(slug)
val mangaId = getIdBySlug(slug) url = "/$mangaSubDirectory/$slug#$mangaId"
"/$mangaSubDirectory/${slug.toPermSlugIfNeeded()}#$mangaId"
} else {
"/$mangaSubDirectory/$slug"
}
} }
return fetchMangaDetails(manga).map { MangasPage(listOf(it), false) } return fetchMangaDetails(manga).map { MangasPage(listOf(it), false) }
@ -224,57 +125,12 @@ abstract class HeanCms(
val seriesDetail = json.parseAs<HeanCmsSeriesDto>() val seriesDetail = json.parseAs<HeanCmsSeriesDto>()
preferences.slugMap = preferences.slugMap.toMutableMap()
.also { it[seriesDetail.slug.toPermSlugIfNeeded()] = seriesDetail.slug }
seriesDetail.id 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 { 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<SortByFilter>()
val payloadObj = HeanCmsQuerySearchPayloadDto(
page = page,
order = if (sortByFilter?.state?.ascending == true) "asc" else "desc",
orderBy = sortByFilter?.selected ?: "total_views",
status = filters.firstInstanceOrNull<StatusFilter>()?.selected?.value ?: "Ongoing",
type = "Comic",
tagIds = filters.firstInstanceOrNull<GenreFilter>()?.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<SortByFilter>() val sortByFilter = filters.firstInstanceOrNull<SortByFilter>()
val statusFilter = filters.firstInstanceOrNull<StatusFilter>() val statusFilter = filters.firstInstanceOrNull<StatusFilter>()
@ -292,6 +148,7 @@ abstract class HeanCms(
.addQueryParameter("page", page.toString()) .addQueryParameter("page", page.toString())
.addQueryParameter("perPage", "12") .addQueryParameter("perPage", "12")
.addQueryParameter("tags_ids", tagIds) .addQueryParameter("tags_ids", tagIds)
.addQueryParameter("adult", "true")
return GET(url.build(), headers) return GET(url.build(), headers)
} }
@ -299,95 +156,34 @@ abstract class HeanCms(
override fun searchMangaParse(response: Response): MangasPage { override fun searchMangaParse(response: Response): MangasPage {
val json = response.body.string() val json = response.body.string()
if (response.request.url.pathSegments.last() == "search") { val result = json.parseAs<HeanCmsQuerySearchDto>()
fetchAllTitles() val mangaList = result.data.map {
it.toSManga(apiUrl, coverPath, mangaSubDirectory)
val result = json.parseAs<List<HeanCmsSearchDto>>()
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)
} }
if (json.startsWith("{")) { return MangasPage(mangaList, result.meta?.hasNextPage() ?: false)
val result = json.parseAs<HeanCmsQuerySearchDto>()
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<List<HeanCmsSeriesDto>>()
.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 getMangaUrl(manga: SManga): String { override fun getMangaUrl(manga: SManga): String {
val seriesSlug = manga.url val seriesSlug = manga.url
.substringAfterLast("/") .substringAfterLast("/")
.substringBefore("#") .substringBefore("#")
.toPermSlugIfNeeded()
val currentSlug = if (slugStrategy != SlugStrategy.NONE) { return "$baseUrl/$mangaSubDirectory/$seriesSlug"
preferences.slugMap[seriesSlug] ?: seriesSlug
} else {
seriesSlug
}
return "$baseUrl/$mangaSubDirectory/$currentSlug"
} }
override fun mangaDetailsRequest(manga: SManga): Request { override fun mangaDetailsRequest(manga: SManga): Request {
if (slugStrategy != SlugStrategy.NONE && (manga.url.contains(TIMESTAMP_REGEX))) { if (!manga.url.contains("#")) {
throw Exception(intl.urlChangedError(name)) 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("#") 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() val apiHeaders = headersBuilder()
.add("Accept", ACCEPT_JSON) .add("Accept", ACCEPT_JSON)
.build() .build()
return if (slugStrategy == SlugStrategy.ID) { return GET("$apiUrl/series/id/$seriesId", apiHeaders)
GET("$apiUrl/series/id/$seriesId", apiHeaders)
} else {
GET("$apiUrl/series/$currentSlug#$currentStatus", apiHeaders)
}
} }
override fun mangaDetailsParse(response: Response): SManga { override fun mangaDetailsParse(response: Response): SManga {
@ -395,14 +191,10 @@ abstract class HeanCms(
val result = runCatching { response.parseAs<HeanCmsSeriesDto>() } val result = runCatching { response.parseAs<HeanCmsSeriesDto>() }
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) { val seriesDetails = seriesResult.toSManga(apiUrl, coverPath, mangaSubDirectory)
preferences.slugMap = preferences.slugMap.toMutableMap()
.also { it[seriesResult.slug.toPermSlugIfNeeded()] = seriesResult.slug }
}
val seriesDetails = seriesResult.toSManga(apiUrl, coverPath, mangaSubDirectory, slugStrategy)
return seriesDetails.apply { return seriesDetails.apply {
status = status.takeUnless { it == SManga.UNKNOWN } 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<SChapter> { val seriesId = manga.url.substringAfterLast("#")
val result = response.parseAs<HeanCmsSeriesDto>() val seriesSlug = manga.url.substringAfterLast("/").substringBefore("#")
if (slugStrategy == SlugStrategy.ID) { val url = "$apiUrl/chapter/query".toHttpUrl().newBuilder()
preferences.slugMap = preferences.slugMap.toMutableMap() .addQueryParameter("page", "1")
.also { it[result.slug.toPermSlugIfNeeded()] = result.slug } .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<SChapter> {
val showPaidChapters = preferences.showPaidChapters val showPaidChapters = preferences.showPaidChapters
if (useNewQueryEndpoint) { if (useNewChapterEndpoint) {
return result.seasons.orEmpty() val apiHeaders = headersBuilder()
.flatMap { it.chapters.orEmpty() } .add("Accept", ACCEPT_JSON)
.build()
val seriesId = response.request.url.queryParameter("series_id")
val seriesSlug = response.request.url.fragment!!
var result = response.parseAs<HeanCmsChapterPayloadDto>()
val currentTimestamp = System.currentTimeMillis()
val chapterList = mutableListOf<HeanCmsChapterDto>()
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<HeanCmsChapterPayloadDto>()
chapterList.addAll(result.data)
page++
}
return chapterList
.filter { it.price == 0 || showPaidChapters } .filter { it.price == 0 || showPaidChapters }
.map { it.toSChapter(result.slug, mangaSubDirectory, dateFormat, slugStrategy) } .map { it.toSChapter(seriesSlug, mangaSubDirectory, dateFormat) }
.filter { it.date_upload <= currentTimestamp } .filter { it.date_upload <= currentTimestamp }
} }
return result.chapters.orEmpty() val result = response.parseAs<HeanCmsSeriesDto>()
val currentTimestamp = System.currentTimeMillis()
return result.seasons.orEmpty()
.flatMap { it.chapters.orEmpty() }
.filter { it.price == 0 || showPaidChapters } .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 } .filter { it.date_upload <= currentTimestamp }
.reversed()
} }
override fun getChapterUrl(chapter: SChapter): String { override fun getChapterUrl(chapter: SChapter) = baseUrl + chapter.url.substringBeforeLast("#")
if (slugStrategy == SlugStrategy.NONE) return baseUrl + chapter.url
val seriesSlug = chapter.url override fun pageListRequest(chapter: SChapter) =
.substringAfter("/$mangaSubDirectory/") GET(apiUrl + chapter.url.replace("/$mangaSubDirectory/", "/chapter/"), headers)
.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 pageListParse(response: Response): List<Page> { override fun pageListParse(response: Response): List<Page> {
if (useNewQueryEndpoint) { val result = response.parseAs<HeanCmsPagePayloadDto>()
val paidChapter = response.request.url.fragment?.contains("-paid")
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") return if (useNewChapterEndpoint) {
result.chapter.chapterData?.images.orEmpty().mapIndexed { i, img ->
if (images == null && paidChapter == true) { Page(i, imageUrl = img)
throw IOException(intl.paidChapterError)
} }
} else {
return images?.select("img").orEmpty().mapIndexed { i, img -> result.data.orEmpty().mapIndexed { i, img ->
val imageUrl = if (img.hasClass("lazy")) img.absUrl("data-src") else img.absUrl("src") Page(i, imageUrl = img)
Page(i, "", imageUrl)
} }
} }
val images = response.parseAs<HeanCmsReaderDto>().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<String> = Observable.just(page.imageUrl!!) override fun fetchImageUrl(page: Page): Observable<String> = Observable.just(page.imageUrl!!)
@ -523,121 +307,18 @@ abstract class HeanCms(
return GET(page.imageUrl!!, imageHeaders) 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<String, HeanCmsTitle>()
while (hasNextPage) {
val response = client.newCall(allTitlesRequest(page)).execute()
val json = response.body.string()
if (json.startsWith("{")) {
val result = json.parseAs<HeanCmsQuerySearchDto>()
tempMap.putAll(parseAllTitles(result.data))
hasNextPage = result.meta?.hasNextPage ?: false
page++
} else {
val result = json.parseAs<List<HeanCmsSeriesDto>>()
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<HeanCmsSeriesDto>): Map<String, HeanCmsTitle> {
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<Status> = listOf( protected open fun getStatusList(): List<Status> = listOf(
Status(intl.statusAll, "All"), Status(intl["status_all"], "All"),
Status(intl.statusOngoing, "Ongoing"), Status(intl["status_ongoing"], "Ongoing"),
Status(intl.statusOnHiatus, "Hiatus"), Status(intl["status_onhiatus"], "Hiatus"),
Status(intl.statusDropped, "Dropped"), Status(intl["status_dropped"], "Dropped"),
) )
protected open fun getSortProperties(): List<SortProperty> = listOf( protected open fun getSortProperties(): List<SortProperty> = listOf(
SortProperty(intl.sortByTitle, "title"), SortProperty(intl["sort_by_title"], "title"),
SortProperty(intl.sortByViews, "total_views"), SortProperty(intl["sort_by_views"], "total_views"),
SortProperty(intl.sortByLatest, "latest"), SortProperty(intl["sort_by_latest"], "latest"),
SortProperty(intl.sortByCreatedAt, "created_at"), SortProperty(intl["sort_by_created_at"], "created_at"),
) )
protected open fun getGenreList(): List<Genre> = emptyList() protected open fun getGenreList(): List<Genre> = emptyList()
@ -646,15 +327,24 @@ abstract class HeanCms(
val genres = getGenreList() val genres = getGenreList()
val filters = listOfNotNull( val filters = listOfNotNull(
Filter.Header(intl.filterWarning), StatusFilter(intl["status_filter_title"], getStatusList()),
StatusFilter(intl.statusFilterTitle, getStatusList()), SortByFilter(intl["sort_by_filter_title"], getSortProperties()),
SortByFilter(intl.sortByFilterTitle, getSortProperties()), GenreFilter(intl["genre_filter_title"], genres).takeIf { genres.isNotEmpty() },
GenreFilter(intl.genreFilterTitle, genres).takeIf { genres.isNotEmpty() },
) )
return FilterList(filters) 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 <reified T> Response.parseAs(): T = use { protected inline fun <reified T> Response.parseAs(): T = use {
it.body.string().parseAs() it.body.string().parseAs()
} }
@ -664,18 +354,6 @@ abstract class HeanCms(
protected inline fun <reified R> List<*>.firstInstanceOrNull(): R? = protected inline fun <reified R> List<*>.firstInstanceOrNull(): R? =
filterIsInstance<R>().firstOrNull() filterIsInstance<R>().firstOrNull()
protected var SharedPreferences.slugMap: MutableMap<String, String>
get() {
val jsonMap = getString(PREF_URL_MAP_SLUG, "{}")!!
val slugMap = runCatching { json.decodeFromString<Map<String, String>>(jsonMap) }
return slugMap.getOrNull()?.toMutableMap() ?: mutableMapOf()
}
set(newSlugMap) {
edit()
.putString(PREF_URL_MAP_SLUG, json.encodeToString(newSlugMap))
.apply()
}
private val SharedPreferences.showPaidChapters: Boolean private val SharedPreferences.showPaidChapters: Boolean
get() = getBoolean(SHOW_PAID_CHAPTERS_PREF, SHOW_PAID_CHAPTERS_DEFAULT) 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_IMAGE = "image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8"
private const val ACCEPT_JSON = "application/json, text/plain, */*" private const val ACCEPT_JSON = "application/json, text/plain, */*"
private val JSON_MEDIA_TYPE = "application/json".toMediaType() private const val PER_PAGE_CHAPTERS = 1000
val TIMESTAMP_REGEX = """-\d{13}$""".toRegex()
private const val PER_PAGE_MANGA_TITLES = 10000
const val SEARCH_PREFIX = "slug:" 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_PREF = "pref_show_paid_chap"
private const val SHOW_PAID_CHAPTERS_DEFAULT = false private const val SHOW_PAID_CHAPTERS_DEFAULT = false
} }

View File

@ -1,6 +1,5 @@
package eu.kanade.tachiyomi.multisrc.heancms 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.SChapter
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import kotlinx.serialization.SerialName import kotlinx.serialization.SerialName
@ -9,59 +8,30 @@ import org.jsoup.Jsoup
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
@Serializable @Serializable
data class HeanCmsQuerySearchDto( class HeanCmsQuerySearchDto(
val data: List<HeanCmsSeriesDto> = emptyList(), val data: List<HeanCmsSeriesDto> = emptyList(),
val meta: HeanCmsQuerySearchMetaDto? = null, val meta: HeanCmsQuerySearchMetaDto? = null,
) )
@Serializable @Serializable
data class HeanCmsQuerySearchMetaDto( class HeanCmsQuerySearchMetaDto(
@SerialName("current_page") val currentPage: Int, @SerialName("current_page") private val currentPage: Int,
@SerialName("last_page") val lastPage: Int, @SerialName("last_page") private val lastPage: Int,
) { ) {
fun hasNextPage() = currentPage < lastPage
val hasNextPage: Boolean
get() = currentPage < lastPage
} }
@Serializable @Serializable
data class HeanCmsSearchDto( class HeanCmsSeriesDto(
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<String, HeanCms.HeanCmsTitle>,
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(
val id: Int, val id: Int,
@SerialName("series_slug") val slug: String, @SerialName("series_slug") val slug: String,
@SerialName("series_type") val type: String = "Comic", private val author: String? = null,
val author: String? = null, private val description: String? = null,
val description: String? = null, private val studio: String? = null,
val studio: String? = null, private val status: String? = null,
val status: String? = null, private val thumbnail: String,
val thumbnail: String, private val title: String,
val title: String, private val tags: List<HeanCmsTagDto>? = emptyList(),
val tags: List<HeanCmsTagDto>? = emptyList(),
val chapters: List<HeanCmsChapterDto>? = emptyList(),
val seasons: List<HeanCmsSeasonsDto>? = emptyList(), val seasons: List<HeanCmsSeasonsDto>? = emptyList(),
) { ) {
@ -69,10 +39,8 @@ data class HeanCmsSeriesDto(
apiUrl: String, apiUrl: String,
coverPath: String, coverPath: String,
mangaSubDirectory: String, mangaSubDirectory: String,
slugStrategy: SlugStrategy,
): SManga = SManga.create().apply { ): SManga = SManga.create().apply {
val descriptionBody = this@HeanCmsSeriesDto.description?.let(Jsoup::parseBodyFragment) val descriptionBody = this@HeanCmsSeriesDto.description?.let(Jsoup::parseBodyFragment)
val slugOnly = slug.toPermSlugIfNeeded(slugStrategy)
title = this@HeanCmsSeriesDto.title title = this@HeanCmsSeriesDto.title
author = this@HeanCmsSeriesDto.author?.trim() author = this@HeanCmsSeriesDto.author?.trim()
@ -86,89 +54,84 @@ data class HeanCmsSeriesDto(
thumbnail_url = thumbnail.ifEmpty { null } thumbnail_url = thumbnail.ifEmpty { null }
?.toAbsoluteThumbnailUrl(apiUrl, coverPath) ?.toAbsoluteThumbnailUrl(apiUrl, coverPath)
status = this@HeanCmsSeriesDto.status?.toStatus() ?: SManga.UNKNOWN status = this@HeanCmsSeriesDto.status?.toStatus() ?: SManga.UNKNOWN
url = if (slugStrategy != SlugStrategy.NONE) { url = "/$mangaSubDirectory/$slug#$id"
"/$mangaSubDirectory/$slugOnly#$id"
} else {
"/$mangaSubDirectory/$slug"
}
} }
} }
@Serializable @Serializable
data class HeanCmsSeasonsDto( class HeanCmsSeasonsDto(
val index: Int,
val chapters: List<HeanCmsChapterDto>? = emptyList(), val chapters: List<HeanCmsChapterDto>? = emptyList(),
) )
@Serializable @Serializable
data class HeanCmsTagDto(val name: String) class HeanCmsTagDto(val name: String)
@Serializable @Serializable
data class HeanCmsChapterDto( class HeanCmsChapterPayloadDto(
val id: Int, val data: List<HeanCmsChapterDto>,
@SerialName("chapter_name") val name: String, val meta: HeanCmsChapterMetaDto,
@SerialName("chapter_slug") val slug: String, )
val index: String,
@SerialName("created_at") val createdAt: String, @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, val price: Int? = null,
) { ) {
fun toSChapter( fun toSChapter(
seriesSlug: String, seriesSlug: String,
mangaSubDirectory: String, mangaSubDirectory: String,
dateFormat: SimpleDateFormat, dateFormat: SimpleDateFormat,
slugStrategy: SlugStrategy,
): SChapter = SChapter.create().apply { ): SChapter = SChapter.create().apply {
val seriesSlugOnly = seriesSlug.toPermSlugIfNeeded(slugStrategy)
name = this@HeanCmsChapterDto.name.trim() name = this@HeanCmsChapterDto.name.trim()
if (price != 0) { if (price != 0) {
name += " \uD83D\uDD12" name += " \uD83D\uDD12"
} }
date_upload = runCatching { dateFormat.parse(createdAt)?.time } date_upload = try {
.getOrNull() ?: 0L dateFormat.parse(createdAt)?.time ?: 0L
} catch (_: Exception) {
0L
}
val paidStatus = if (price != 0 && price != null) "-paid" else "" url = "/$mangaSubDirectory/$seriesSlug/$slug#$id"
url = "/$mangaSubDirectory/$seriesSlugOnly/$slug#$id$paidStatus"
} }
} }
@Serializable @Serializable
data class HeanCmsReaderDto( class HeanCmsChapterMetaDto(
val content: HeanCmsReaderContentDto? = null, @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<String>? = emptyList(),
) {
fun isPaywalled() = paywall
}
@Serializable
class HeanCmsPageDto(
@SerialName("chapter_data") val chapterData: HeanCmsPageDataDto?,
) )
@Serializable @Serializable
data class HeanCmsReaderContentDto( class HeanCmsPageDataDto(
val images: List<String>? = emptyList(), val images: List<String>? = 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<Int> = emptyList(),
)
@Serializable
data class HeanCmsSearchPayloadDto(val term: String)
private fun String.toAbsoluteThumbnailUrl(apiUrl: String, coverPath: String): String { private fun String.toAbsoluteThumbnailUrl(apiUrl: String, coverPath: String): String {
return if (startsWith("https://")) this else "$apiUrl/$coverPath$this" 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) { fun String.toStatus(): Int = when (this) {
"Ongoing" -> SManga.ONGOING "Ongoing" -> SManga.ONGOING
"Hiatus" -> SManga.ON_HIATUS "Hiatus" -> SManga.ON_HIATUS

View File

@ -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)
}
}

View File

@ -11,12 +11,10 @@ class OmegaScans : HeanCms("Omega Scans", "https://omegascans.org", "en") {
.rateLimitHost(apiUrl.toHttpUrl(), 1) .rateLimitHost(apiUrl.toHttpUrl(), 1)
.build() .build()
override val useNewQueryEndpoint = true
// Site changed from MangaThemesia to HeanCms. // Site changed from MangaThemesia to HeanCms.
override val versionId = 2 override val versionId = 2
override val coverPath = "" override val useNewChapterEndpoint = true
override fun getGenreList() = listOf( override fun getGenreList() = listOf(
Genre("Romance", 1), Genre("Romance", 1),

View File

@ -17,8 +17,6 @@ class TempleScan : HeanCms(
.rateLimit(1) .rateLimit(1)
.build() .build()
override val useNewQueryEndpoint = true
override val coverPath = ""
override val mangaSubDirectory = "comic" override val mangaSubDirectory = "comic"
override fun getGenreList() = listOf( override fun getGenreList() = listOf(

View File

@ -6,12 +6,18 @@ import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
class PerfScan : HeanCms("Perf Scan", "https://perf-scan.fr", "fr") { class PerfScan : HeanCms("Perf Scan", "https://perf-scan.fr", "fr") {
override val client: OkHttpClient = super.client.newBuilder() override val client: OkHttpClient = super.client.newBuilder()
.rateLimitHost(apiUrl.toHttpUrl(), 1, 2) .rateLimitHost(apiUrl.toHttpUrl(), 1, 2)
.build() .build()
override val coverPath: String = "" init {
override val useNewQueryEndpoint = true preferences.run {
if (contains("pref_url_map")) {
edit().remove("pref_url_map").apply()
}
}
}
override val slugStrategy = SlugStrategy.ID override val useNewChapterEndpoint = true
} }