HeanCms: Add option to use ID instead slug (#17647)
* Use ID instead slug * Minor changes * Opps * ID * I cant explain this * Fix for search in old API * Unnecessary IF * Yugen domain * Change message * Ah xD
This commit is contained in:
parent
b1aa65cedc
commit
d1d9e03560
@ -21,7 +21,7 @@ class ReaperScans : HeanCms(
|
||||
// Site changed from Madara to HeanCms.
|
||||
override val versionId = 2
|
||||
|
||||
override val fetchAllTitles = true
|
||||
override val slugStrategy = SlugStrategy.FETCH_ALL
|
||||
override val useNewQueryEndpoint = true
|
||||
|
||||
override val coverPath: String = ""
|
||||
|
@ -11,7 +11,7 @@ import java.util.concurrent.TimeUnit
|
||||
class YugenMangas :
|
||||
HeanCms(
|
||||
"YugenMangas",
|
||||
"https://yugenmangas.lat",
|
||||
"https://yugenmangas.net",
|
||||
"es",
|
||||
"https://api.yugenmangas.net",
|
||||
) {
|
||||
@ -19,7 +19,7 @@ class YugenMangas :
|
||||
// Site changed from Madara to HeanCms.
|
||||
override val versionId = 2
|
||||
|
||||
override val fetchAllTitles = true
|
||||
override val slugStrategy = SlugStrategy.ID
|
||||
override val useNewQueryEndpoint = true
|
||||
|
||||
override val client = super.client.newBuilder()
|
||||
|
@ -1,5 +1,7 @@
|
||||
package eu.kanade.tachiyomi.multisrc.heancms
|
||||
|
||||
import android.app.Application
|
||||
import android.content.SharedPreferences
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.POST
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
@ -21,6 +23,8 @@ import okhttp3.Request
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import okhttp3.Response
|
||||
import rx.Observable
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
|
||||
@ -31,11 +35,15 @@ abstract class HeanCms(
|
||||
protected val apiUrl: String = baseUrl.replace("://", "://api."),
|
||||
) : HttpSource() {
|
||||
|
||||
private val preferences: SharedPreferences by lazy {
|
||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||
}
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
override val client: OkHttpClient = network.cloudflareClient
|
||||
|
||||
protected open val fetchAllTitles = false
|
||||
protected open val slugStrategy = SlugStrategy.NONE
|
||||
|
||||
protected open val useNewQueryEndpoint = false
|
||||
|
||||
@ -103,7 +111,13 @@ abstract class HeanCms(
|
||||
|
||||
if (json.startsWith("{")) {
|
||||
val result = json.parseAs<HeanCmsQuerySearchDto>()
|
||||
val mangaList = result.data.map { it.toSManga(apiUrl, coverPath, fetchAllTitles) }
|
||||
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, slugStrategy)
|
||||
}
|
||||
|
||||
fetchAllTitles()
|
||||
|
||||
@ -111,7 +125,13 @@ abstract class HeanCms(
|
||||
}
|
||||
|
||||
val mangaList = json.parseAs<List<HeanCmsSeriesDto>>()
|
||||
.map { it.toSManga(apiUrl, coverPath, fetchAllTitles) }
|
||||
.map {
|
||||
if (slugStrategy != SlugStrategy.NONE) {
|
||||
preferences.slugMap = preferences.slugMap.toMutableMap()
|
||||
.also { map -> map[it.slug.toPermSlugIfNeeded()] = it.slug }
|
||||
}
|
||||
it.toSManga(apiUrl, coverPath, slugStrategy)
|
||||
}
|
||||
|
||||
fetchAllTitles()
|
||||
|
||||
@ -163,11 +183,33 @@ abstract class HeanCms(
|
||||
}
|
||||
|
||||
val slug = query.substringAfter(SEARCH_PREFIX)
|
||||
val manga = SManga.create().apply { url = "/series/$slug" }
|
||||
val manga = SManga.create().apply {
|
||||
url = if (slugStrategy != SlugStrategy.NONE) {
|
||||
val mangaId = getIdBySlug(slug)
|
||||
"/series/${slug.toPermSlugIfNeeded()}#$mangaId"
|
||||
} else {
|
||||
"/series/$slug"
|
||||
}
|
||||
}
|
||||
|
||||
return fetchMangaDetails(manga).map { MangasPage(listOf(it), false) }
|
||||
}
|
||||
|
||||
private fun getIdBySlug(slug: String): Int {
|
||||
val result = runCatching {
|
||||
val response = client.newCall(GET("$apiUrl/series/$slug", headers)).execute()
|
||||
val json = response.body.string()
|
||||
|
||||
val seriesDetail = json.parseAs<HeanCmsSeriesDto>()
|
||||
|
||||
preferences.slugMap = preferences.slugMap.toMutableMap()
|
||||
.also { it[seriesDetail.slug.toPermSlugIfNeeded()] = seriesDetail.slug }
|
||||
|
||||
seriesDetail.id
|
||||
}
|
||||
return result.getOrNull() ?: throw Exception(intl.idNotFoundError + slug)
|
||||
}
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
if (useNewQueryEndpoint) {
|
||||
return newEndpointSearchMangaRequest(page, query, filters)
|
||||
@ -242,8 +284,8 @@ abstract class HeanCms(
|
||||
val mangaList = result
|
||||
.filter { it.type == "Comic" }
|
||||
.map {
|
||||
it.slug = it.slug.replace(TIMESTAMP_REGEX, "")
|
||||
it.toSManga(apiUrl, coverPath, seriesSlugMap.orEmpty(), fetchAllTitles)
|
||||
it.slug = it.slug.toPermSlugIfNeeded()
|
||||
it.toSManga(apiUrl, coverPath, seriesSlugMap.orEmpty(), slugStrategy)
|
||||
}
|
||||
|
||||
return MangasPage(mangaList, false)
|
||||
@ -251,7 +293,13 @@ abstract class HeanCms(
|
||||
|
||||
if (json.startsWith("{")) {
|
||||
val result = json.parseAs<HeanCmsQuerySearchDto>()
|
||||
val mangaList = result.data.map { it.toSManga(apiUrl, coverPath, fetchAllTitles) }
|
||||
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, slugStrategy)
|
||||
}
|
||||
|
||||
fetchAllTitles()
|
||||
|
||||
@ -259,7 +307,13 @@ abstract class HeanCms(
|
||||
}
|
||||
|
||||
val mangaList = json.parseAs<List<HeanCmsSeriesDto>>()
|
||||
.map { it.toSManga(apiUrl, coverPath, fetchAllTitles) }
|
||||
.map {
|
||||
if (slugStrategy != SlugStrategy.NONE) {
|
||||
preferences.slugMap = preferences.slugMap.toMutableMap()
|
||||
.also { map -> map[it.slug.toPermSlugIfNeeded()] = it.slug }
|
||||
}
|
||||
it.toSManga(apiUrl, coverPath, slugStrategy)
|
||||
}
|
||||
|
||||
fetchAllTitles()
|
||||
|
||||
@ -269,22 +323,34 @@ abstract class HeanCms(
|
||||
override fun getMangaUrl(manga: SManga): String {
|
||||
val seriesSlug = manga.url
|
||||
.substringAfterLast("/")
|
||||
.substringBefore("#")
|
||||
.toPermSlugIfNeeded()
|
||||
|
||||
val currentSlug = seriesSlugMap?.get(seriesSlug)?.slug ?: seriesSlug
|
||||
val currentSlug = if (slugStrategy != SlugStrategy.NONE) {
|
||||
preferences.slugMap[seriesSlug] ?: seriesSlug
|
||||
} else {
|
||||
seriesSlug
|
||||
}
|
||||
|
||||
return "$baseUrl/series/$currentSlug"
|
||||
}
|
||||
|
||||
override fun mangaDetailsRequest(manga: SManga): Request {
|
||||
if (fetchAllTitles && manga.url.contains(TIMESTAMP_REGEX)) {
|
||||
if (slugStrategy != SlugStrategy.NONE && (manga.url.contains(TIMESTAMP_REGEX))) {
|
||||
throw Exception(intl.urlChangedError(name))
|
||||
}
|
||||
|
||||
if (slugStrategy == SlugStrategy.ID && !manga.url.contains("#")) {
|
||||
throw Exception(intl.urlChangedError(name))
|
||||
}
|
||||
|
||||
val seriesSlug = manga.url
|
||||
.substringAfterLast("/")
|
||||
.substringBefore("#")
|
||||
.toPermSlugIfNeeded()
|
||||
|
||||
val seriesId = manga.url.substringAfterLast("#")
|
||||
|
||||
fetchAllTitles()
|
||||
|
||||
val seriesDetails = seriesSlugMap?.get(seriesSlug)
|
||||
@ -295,7 +361,11 @@ abstract class HeanCms(
|
||||
.add("Accept", ACCEPT_JSON)
|
||||
.build()
|
||||
|
||||
return GET("$apiUrl/series/$currentSlug#$currentStatus", apiHeaders)
|
||||
return if (slugStrategy == SlugStrategy.ID) {
|
||||
GET("$apiUrl/series/id/$seriesId", apiHeaders)
|
||||
} else {
|
||||
GET("$apiUrl/series/$currentSlug#$currentStatus", apiHeaders)
|
||||
}
|
||||
}
|
||||
|
||||
override fun mangaDetailsParse(response: Response): SManga {
|
||||
@ -303,8 +373,14 @@ abstract class HeanCms(
|
||||
|
||||
val result = runCatching { response.parseAs<HeanCmsSeriesDto>() }
|
||||
|
||||
val seriesDetails = result.getOrNull()?.toSManga(apiUrl, coverPath, fetchAllTitles)
|
||||
?: throw Exception(intl.urlChangedError(name))
|
||||
val seriesResult = result.getOrNull() ?: throw Exception(intl.urlChangedError(name))
|
||||
|
||||
if (slugStrategy != SlugStrategy.NONE) {
|
||||
preferences.slugMap = preferences.slugMap.toMutableMap()
|
||||
.also { it[seriesResult.slug.toPermSlugIfNeeded()] = seriesResult.slug }
|
||||
}
|
||||
|
||||
val seriesDetails = seriesResult.toSManga(apiUrl, coverPath, slugStrategy)
|
||||
|
||||
return seriesDetails.apply {
|
||||
status = status.takeUnless { it == SManga.UNKNOWN }
|
||||
@ -323,21 +399,39 @@ abstract class HeanCms(
|
||||
return result.seasons.orEmpty()
|
||||
.flatMap { it.chapters.orEmpty() }
|
||||
.filterNot { it.price == 1 }
|
||||
.map { it.toSChapter(result.slug, dateFormat) }
|
||||
.map { it.toSChapter(result.slug, dateFormat, slugStrategy) }
|
||||
.filter { it.date_upload <= currentTimestamp }
|
||||
}
|
||||
|
||||
return result.chapters.orEmpty()
|
||||
.filterNot { it.price == 1 }
|
||||
.map { it.toSChapter(result.slug, dateFormat) }
|
||||
.map { it.toSChapter(result.slug, dateFormat, slugStrategy) }
|
||||
.filter { it.date_upload <= currentTimestamp }
|
||||
.reversed()
|
||||
}
|
||||
|
||||
override fun getChapterUrl(chapter: SChapter): String = baseUrl + chapter.url
|
||||
override fun getChapterUrl(chapter: SChapter): String {
|
||||
if (slugStrategy == SlugStrategy.NONE) return baseUrl + chapter.url
|
||||
|
||||
val seriesSlug = chapter.url
|
||||
.substringAfter("/series/")
|
||||
.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("/series/").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)
|
||||
}
|
||||
|
||||
@ -389,7 +483,7 @@ abstract class HeanCms(
|
||||
}
|
||||
|
||||
protected open fun fetchAllTitles() {
|
||||
if (!seriesSlugMap.isNullOrEmpty() || !fetchAllTitles) {
|
||||
if (!seriesSlugMap.isNullOrEmpty() || slugStrategy != SlugStrategy.FETCH_ALL) {
|
||||
return
|
||||
}
|
||||
|
||||
@ -418,6 +512,8 @@ abstract class HeanCms(
|
||||
}
|
||||
|
||||
seriesSlugMap = result.getOrNull()
|
||||
preferences.slugMap = preferences.slugMap.toMutableMap()
|
||||
.also { it.putAll(seriesSlugMap.orEmpty().mapValues { (_, v) -> v.slug }) }
|
||||
}
|
||||
|
||||
protected open fun allTitlesRequest(page: Int): Request {
|
||||
@ -468,8 +564,21 @@ abstract class HeanCms(
|
||||
*/
|
||||
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 (fetchAllTitles) {
|
||||
return if (slugStrategy != SlugStrategy.NONE) {
|
||||
this.replace(TIMESTAMP_REGEX, "")
|
||||
} else {
|
||||
this
|
||||
@ -514,6 +623,18 @@ abstract class HeanCms(
|
||||
protected inline fun <reified R> List<*>.firstInstanceOrNull(): R? =
|
||||
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))
|
||||
.commit()
|
||||
}
|
||||
|
||||
companion object {
|
||||
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, */*"
|
||||
@ -525,5 +646,7 @@ abstract class HeanCms(
|
||||
private const val PER_PAGE_MANGA_TITLES = 10000
|
||||
|
||||
const val SEARCH_PREFIX = "slug:"
|
||||
|
||||
private const val PREF_URL_MAP_SLUG = "pref_url_map"
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
package eu.kanade.tachiyomi.multisrc.heancms
|
||||
|
||||
import eu.kanade.tachiyomi.multisrc.heancms.HeanCms.SlugStrategy
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import kotlinx.serialization.SerialName
|
||||
@ -36,9 +37,9 @@ data class HeanCmsSearchDto(
|
||||
apiUrl: String,
|
||||
coverPath: String,
|
||||
slugMap: Map<String, HeanCms.HeanCmsTitle>,
|
||||
fetchAllTiles: Boolean,
|
||||
slugStrategy: SlugStrategy,
|
||||
): SManga = SManga.create().apply {
|
||||
val slugOnly = slug.toPermSlugIfNeeded(fetchAllTiles)
|
||||
val slugOnly = slug.toPermSlugIfNeeded(slugStrategy)
|
||||
val thumbnailFileName = slugMap[slugOnly]?.thumbnailFileName
|
||||
title = this@HeanCmsSearchDto.title
|
||||
thumbnail_url = thumbnail?.toAbsoluteThumbnailUrl(apiUrl, coverPath)
|
||||
@ -66,10 +67,10 @@ data class HeanCmsSeriesDto(
|
||||
fun toSManga(
|
||||
apiUrl: String,
|
||||
coverPath: String,
|
||||
fetchAllTiles: Boolean,
|
||||
slugStrategy: SlugStrategy,
|
||||
): SManga = SManga.create().apply {
|
||||
val descriptionBody = this@HeanCmsSeriesDto.description?.let(Jsoup::parseBodyFragment)
|
||||
val slugOnly = slug.toPermSlugIfNeeded(fetchAllTiles)
|
||||
val slugOnly = slug.toPermSlugIfNeeded(slugStrategy)
|
||||
|
||||
title = this@HeanCmsSeriesDto.title
|
||||
author = this@HeanCmsSeriesDto.author?.trim()
|
||||
@ -83,7 +84,11 @@ data class HeanCmsSeriesDto(
|
||||
thumbnail_url = thumbnail.ifEmpty { null }
|
||||
?.toAbsoluteThumbnailUrl(apiUrl, coverPath)
|
||||
status = this@HeanCmsSeriesDto.status?.toStatus() ?: SManga.UNKNOWN
|
||||
url = "/series/$slugOnly"
|
||||
url = if (slugStrategy != SlugStrategy.NONE) {
|
||||
"/series/$slugOnly#$id"
|
||||
} else {
|
||||
"/series/$slug"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -105,11 +110,16 @@ data class HeanCmsChapterDto(
|
||||
@SerialName("created_at") val createdAt: String,
|
||||
val price: Int? = null,
|
||||
) {
|
||||
fun toSChapter(seriesSlug: String, dateFormat: SimpleDateFormat): SChapter = SChapter.create().apply {
|
||||
fun toSChapter(
|
||||
seriesSlug: String,
|
||||
dateFormat: SimpleDateFormat,
|
||||
slugStrategy: SlugStrategy,
|
||||
): SChapter = SChapter.create().apply {
|
||||
val seriesSlugOnly = seriesSlug.toPermSlugIfNeeded(slugStrategy)
|
||||
name = this@HeanCmsChapterDto.name.trim()
|
||||
date_upload = runCatching { dateFormat.parse(createdAt)?.time }
|
||||
.getOrNull() ?: 0L
|
||||
url = "/series/$seriesSlug/$slug#$id"
|
||||
url = "/series/$seriesSlugOnly/$slug#$id"
|
||||
}
|
||||
}
|
||||
|
||||
@ -140,8 +150,8 @@ private fun String.toAbsoluteThumbnailUrl(apiUrl: String, coverPath: String): St
|
||||
return if (startsWith("https://")) this else "$apiUrl/$coverPath$this"
|
||||
}
|
||||
|
||||
private fun String.toPermSlugIfNeeded(fetchAllTitles: Boolean): String {
|
||||
return if (fetchAllTitles) {
|
||||
private fun String.toPermSlugIfNeeded(slugStrategy: SlugStrategy): String {
|
||||
return if (slugStrategy != SlugStrategy.NONE) {
|
||||
this.replace(HeanCms.TIMESTAMP_REGEX, "")
|
||||
} else {
|
||||
this
|
||||
|
@ -9,13 +9,13 @@ class HeanCmsGenerator : ThemeSourceGenerator {
|
||||
|
||||
override val themeClass = "HeanCms"
|
||||
|
||||
override val baseVersionCode: Int = 17
|
||||
override val baseVersionCode: Int = 18
|
||||
|
||||
override val sources = listOf(
|
||||
SingleLang("Glorious Scan", "https://gloriousscan.com", "pt-BR", overrideVersionCode = 17),
|
||||
SingleLang("Omega Scans", "https://omegascans.org", "en", isNsfw = true, overrideVersionCode = 17),
|
||||
SingleLang("Reaper Scans", "https://reaperscans.net", "pt-BR", overrideVersionCode = 36),
|
||||
SingleLang("YugenMangas", "https://yugenmangas.lat", "es", isNsfw = true, overrideVersionCode = 7),
|
||||
SingleLang("YugenMangas", "https://yugenmangas.net", "es", isNsfw = true, overrideVersionCode = 7),
|
||||
)
|
||||
|
||||
companion object {
|
||||
|
@ -88,6 +88,12 @@ class HeanCmsIntl(lang: String) {
|
||||
"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"
|
||||
|
Loading…
x
Reference in New Issue
Block a user