mirror of
https://github.com/keiyoushi/extensions-source.git
synced 2024-11-25 11:42:47 +01:00
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:
parent
16011407c5
commit
8defa56f71
17
lib-multisrc/heancms/assets/i18n/messages_en.properties
Normal file
17
lib-multisrc/heancms/assets/i18n/messages_en.properties
Normal 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
|
17
lib-multisrc/heancms/assets/i18n/messages_es.properties
Normal file
17
lib-multisrc/heancms/assets/i18n/messages_es.properties
Normal 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
|
13
lib-multisrc/heancms/assets/i18n/messages_pt_br.properties
Normal file
13
lib-multisrc/heancms/assets/i18n/messages_pt_br.properties
Normal 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
|
@ -2,4 +2,8 @@ plugins {
|
|||||||
id("lib-multisrc")
|
id("lib-multisrc")
|
||||||
}
|
}
|
||||||
|
|
||||||
baseVersionCode = 20
|
baseVersionCode = 21
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
api(project(":lib:i18n"))
|
||||||
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
@ -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),
|
||||||
|
@ -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(
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user