HeanCMS: API update (#17345)

* API update

* Bump
This commit is contained in:
Rolando Lecca 2023-08-02 12:52:30 -05:00 committed by GitHub
parent bf6740e7e0
commit 2f1cae5bbf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 22 additions and 278 deletions

View File

@ -21,8 +21,6 @@ class ReaperScans : HeanCms(
// Site changed from Madara to HeanCms. // Site changed from Madara to HeanCms.
override val versionId = 2 override val versionId = 2
override val fetchAllTitlesStrategy = FetchAllStrategy.SEARCH_ALL
override val coverPath: String = "" override val coverPath: String = ""
override val dateFormat: SimpleDateFormat = super.dateFormat.apply { override val dateFormat: SimpleDateFormat = super.dateFormat.apply {

View File

@ -1,16 +1,9 @@
package eu.kanade.tachiyomi.extension.es.yugenmangas package eu.kanade.tachiyomi.extension.es.yugenmangas
import android.app.Application
import android.content.SharedPreferences
import android.widget.Toast
import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.multisrc.heancms.Genre import eu.kanade.tachiyomi.multisrc.heancms.Genre
import eu.kanade.tachiyomi.multisrc.heancms.HeanCms import eu.kanade.tachiyomi.multisrc.heancms.HeanCms
import eu.kanade.tachiyomi.network.interceptor.rateLimit import eu.kanade.tachiyomi.network.interceptor.rateLimitHost
import eu.kanade.tachiyomi.source.ConfigurableSource import okhttp3.HttpUrl.Companion.toHttpUrl
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.TimeZone import java.util.TimeZone
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
@ -21,26 +14,15 @@ class YugenMangas :
"https://yugenmangas.net", "https://yugenmangas.net",
"es", "es",
"https://api.yugenmangas.net", "https://api.yugenmangas.net",
), ) {
ConfigurableSource {
private val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
// Site changed from Madara to HeanCms. // Site changed from Madara to HeanCms.
override val versionId = 2 override val versionId = 2
override val fetchAllTitlesStrategy = when (getfetchAllStrategyPref()) {
"all" -> FetchAllStrategy.SEARCH_ALL
"each" -> FetchAllStrategy.SEARCH_EACH
else -> FetchAllStrategy.NONE
}
override val client = super.client.newBuilder() override val client = super.client.newBuilder()
.connectTimeout(60, TimeUnit.SECONDS) .connectTimeout(60, TimeUnit.SECONDS)
.readTimeout(90, TimeUnit.SECONDS) .readTimeout(90, TimeUnit.SECONDS)
.rateLimit(2, 3) .rateLimitHost(apiUrl.toHttpUrl(), 2, 3)
.build() .build()
override val coverPath: String = "" override val coverPath: String = ""
@ -99,42 +81,4 @@ class YugenMangas :
Genre("Yaoi", 43), Genre("Yaoi", 43),
Genre("Yuri", 44), Genre("Yuri", 44),
) )
private fun getfetchAllStrategyPref(): String? {
return preferences.getString(PREF_FETCH_ALL_STRATEGY_KEY, PREF_FETCH_ALL_STRATEGY_DEFAULT)
}
override fun setupPreferenceScreen(screen: PreferenceScreen) {
val fetchAllStrategyPreference = ListPreference(screen.context).apply {
key = PREF_FETCH_ALL_STRATEGY_KEY
title = PREF_FETCH_ALL_STRATEGY_TITLE
summary = PREF_FETCH_ALL_STRATEGY_SUMMARY
entries = PREF_FETCH_ALL_STRATEGY_ENTRIES
entryValues = PREF_FETCH_ALL_STRATEGY_VALUES
setDefaultValue(PREF_FETCH_ALL_STRATEGY_DEFAULT)
setOnPreferenceChangeListener { _, newValue ->
Toast.makeText(screen.context, RESTART_MESSAGE, Toast.LENGTH_LONG).show()
true
}
}
screen.addPreference(fetchAllStrategyPreference)
}
companion object {
const val PREF_FETCH_ALL_STRATEGY_KEY = "prefFetchAllStrategy"
const val PREF_FETCH_ALL_STRATEGY_TITLE = "Método de búsqueda"
const val PREF_FETCH_ALL_STRATEGY_SUMMARY = "Global: Busca las URLs de todas las series al iniciar la aplicación, lento pero más estable.\n" +
"Individual: Busca la URL de la serie al actualizar, rápido pero puede fallar.\n" +
"Ninguno: Usa la URL con la que fue agregado, tendrá que migrar si la URL cambia.\n" +
"Valor actual: %s"
val PREF_FETCH_ALL_STRATEGY_ENTRIES = arrayOf("Ninguno", "Individual", "Global")
val PREF_FETCH_ALL_STRATEGY_VALUES = arrayOf("off", "each", "all")
const val PREF_FETCH_ALL_STRATEGY_DEFAULT = "off"
const val RESTART_MESSAGE = "Reinicie la aplicación para que los cambios surtan efecto."
}
} }

View File

@ -1,7 +1,5 @@
package eu.kanade.tachiyomi.multisrc.heancms 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.GET
import eu.kanade.tachiyomi.network.POST import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.source.model.Filter import eu.kanade.tachiyomi.source.model.Filter
@ -21,8 +19,6 @@ import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody 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.api.get
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Locale import java.util.Locale
@ -33,10 +29,6 @@ abstract class HeanCms(
protected val apiUrl: String = baseUrl.replace("://", "://api."), protected val apiUrl: String = baseUrl.replace("://", "://api."),
) : HttpSource() { ) : HttpSource() {
private val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
override val supportsLatest = true override val supportsLatest = true
override val client: OkHttpClient = network.cloudflareClient override val client: OkHttpClient = network.cloudflareClient
@ -53,14 +45,10 @@ abstract class HeanCms(
protected val intl by lazy { HeanCmsIntl(lang) } protected val intl by lazy { HeanCmsIntl(lang) }
protected open val fetchAllTitlesStrategy = FetchAllStrategy.NONE
protected open val coverPath: String = "cover/" protected open val coverPath: String = "cover/"
protected open val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZZZZZ", Locale.US) protected open val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZZZZZ", Locale.US)
private var seriesSlugMap: Map<String, HeanCmsTitle>? = null
override fun headersBuilder(): Headers.Builder = Headers.Builder() override fun headersBuilder(): Headers.Builder = Headers.Builder()
.add("Origin", baseUrl) .add("Origin", baseUrl)
.add("Referer", "$baseUrl/") .add("Referer", "$baseUrl/")
@ -70,7 +58,7 @@ abstract class HeanCms(
page = page, page = page,
order = "desc", order = "desc",
orderBy = "total_views", orderBy = "total_views",
status = "Ongoing", status = "All",
type = "Comic", type = "Comic",
) )
@ -89,17 +77,13 @@ abstract class HeanCms(
if (json.startsWith("{")) { if (json.startsWith("{")) {
val result = json.parseAs<HeanCmsQuerySearchDto>() val result = json.parseAs<HeanCmsQuerySearchDto>()
val mangaList = result.data.map { it.toSManga(apiUrl, coverPath, fetchAllTitlesStrategy) } val mangaList = result.data.map { it.toSManga(apiUrl, coverPath) }
fetchAllTitles()
return MangasPage(mangaList, result.meta?.hasNextPage ?: false) return MangasPage(mangaList, result.meta?.hasNextPage ?: false)
} }
val mangaList = json.parseAs<List<HeanCmsSeriesDto>>() val mangaList = json.parseAs<List<HeanCmsSeriesDto>>()
.map { it.toSManga(apiUrl, coverPath, fetchAllTitlesStrategy) } .map { it.toSManga(apiUrl, coverPath) }
fetchAllTitles()
return MangasPage(mangaList, hasNextPage = false) return MangasPage(mangaList, hasNextPage = false)
} }
@ -109,7 +93,7 @@ abstract class HeanCms(
page = page, page = page,
order = "desc", order = "desc",
orderBy = "latest", orderBy = "latest",
status = "Ongoing", status = "All",
type = "Comic", type = "Comic",
) )
@ -137,10 +121,6 @@ abstract class HeanCms(
} }
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
/**
* Their query search endpoint doesn't return the thumbnails, so we need to do
* later an special parsing to get the thumbnails as well from the slug map.
*/
if (query.isNotBlank()) { if (query.isNotBlank()) {
val searchPayloadObj = HeanCmsSearchPayloadDto(query) val searchPayloadObj = HeanCmsSearchPayloadDto(query)
val searchPayload = json.encodeToString(searchPayloadObj) val searchPayload = json.encodeToString(searchPayloadObj)
@ -182,96 +162,49 @@ abstract class HeanCms(
val json = response.body.string() val json = response.body.string()
if (response.request.url.pathSegments.last() == "search") { if (response.request.url.pathSegments.last() == "search") {
fetchAllTitles()
val result = json.parseAs<List<HeanCmsSearchDto>>() val result = json.parseAs<List<HeanCmsSearchDto>>()
val mangaList = result val mangaList = result
.filter { it.type == "Comic" } .filter { it.type == "Comic" }
.map { it.toSManga(apiUrl, coverPath, seriesSlugMap.orEmpty(), fetchAllTitlesStrategy) } .map {
it.slug = it.slug.replace(TIMESTAMP_REGEX, "")
it.toSManga(apiUrl, coverPath)
}
return MangasPage(mangaList, false) return MangasPage(mangaList, false)
} }
if (json.startsWith("{")) { if (json.startsWith("{")) {
val result = json.parseAs<HeanCmsQuerySearchDto>() val result = json.parseAs<HeanCmsQuerySearchDto>()
val mangaList = result.data.map { it.toSManga(apiUrl, coverPath, fetchAllTitlesStrategy) } val mangaList = result.data.map { it.toSManga(apiUrl, coverPath) }
fetchAllTitles()
return MangasPage(mangaList, result.meta?.hasNextPage ?: false) return MangasPage(mangaList, result.meta?.hasNextPage ?: false)
} }
val mangaList = json.parseAs<List<HeanCmsSeriesDto>>() val mangaList = json.parseAs<List<HeanCmsSeriesDto>>()
.map { it.toSManga(apiUrl, coverPath, fetchAllTitlesStrategy) } .map { it.toSManga(apiUrl, coverPath) }
fetchAllTitles()
return MangasPage(mangaList, hasNextPage = false) return MangasPage(mangaList, hasNextPage = false)
} }
override fun getMangaUrl(manga: SManga): String { override fun getMangaUrl(manga: SManga) = baseUrl + manga.url
val seriesSlug = manga.url
.substringAfterLast("/")
.replace(TIMESTAMP_REGEX, "")
val currentSlug = if (fetchAllTitlesStrategy == FetchAllStrategy.SEARCH_EACH) {
preferences.slugMap[seriesSlug] ?: manga.url.substringAfterLast("/")
} else {
seriesSlugMap?.get(seriesSlug)?.slug ?: manga.url.substringAfterLast("/")
}
return "$baseUrl/series/$currentSlug"
}
override fun mangaDetailsRequest(manga: SManga): Request { override fun mangaDetailsRequest(manga: SManga): Request {
if (fetchAllTitlesStrategy == FetchAllStrategy.SEARCH_EACH) {
val searchQuery = manga.title
val searchPayloadObj = HeanCmsSearchPayloadDto(searchQuery)
val searchPayload = json.encodeToString(searchPayloadObj)
.toRequestBody(JSON_MEDIA_TYPE)
val apiHeaders = headersBuilder()
.add("Accept", ACCEPT_JSON)
.add("Content-Type", searchPayload.contentType().toString())
.build()
val mangaSlug = manga.url
.substringAfterLast("/")
return POST("$apiUrl/series/search#$mangaSlug", apiHeaders, searchPayload)
}
val seriesSlug = manga.url val seriesSlug = manga.url
.substringAfterLast("/") .substringAfterLast("/")
.replace(TIMESTAMP_REGEX, "")
fetchAllTitles()
val seriesDetails = seriesSlugMap?.get(seriesSlug)
val currentSlug = seriesDetails?.slug ?: manga.url.substringAfterLast("/")
val currentStatus = seriesDetails?.status ?: manga.status
val apiHeaders = headersBuilder() val apiHeaders = headersBuilder()
.add("Accept", ACCEPT_JSON) .add("Accept", ACCEPT_JSON)
.build() .build()
return GET("$apiUrl/series/$currentSlug#$currentStatus", apiHeaders) return GET("$apiUrl/series/$seriesSlug#${manga.status}", apiHeaders)
} }
override fun mangaDetailsParse(response: Response): SManga { override fun mangaDetailsParse(response: Response): SManga {
val mangaStatus = response.request.url.fragment?.toIntOrNull() ?: SManga.UNKNOWN val mangaStatus = response.request.url.fragment?.toIntOrNull() ?: SManga.UNKNOWN
val result = runCatching { val result = runCatching { response.parseAs<HeanCmsSeriesDto>() }
if (fetchAllTitlesStrategy == FetchAllStrategy.SEARCH_EACH) {
val originalSlug = response.request.url.fragment!!
val searchResult = response.parseAs<List<HeanCmsSearchDto>>()
searchResultToSeries(originalSlug, searchResult)
} else {
response.parseAs<HeanCmsSeriesDto>()
}
}
val seriesDetails = result.getOrNull()?.toSManga(apiUrl, coverPath, fetchAllTitlesStrategy) val seriesDetails = result.getOrNull()?.toSManga(apiUrl, coverPath)
?: throw Exception(intl.urlChangedError(name)) ?: throw Exception(intl.urlChangedError(name))
return seriesDetails.apply { return seriesDetails.apply {
@ -283,13 +216,7 @@ abstract class HeanCms(
override fun chapterListRequest(manga: SManga): Request = mangaDetailsRequest(manga) override fun chapterListRequest(manga: SManga): Request = mangaDetailsRequest(manga)
override fun chapterListParse(response: Response): List<SChapter> { override fun chapterListParse(response: Response): List<SChapter> {
val result = if (fetchAllTitlesStrategy == FetchAllStrategy.SEARCH_EACH) { val result = response.parseAs<HeanCmsSeriesDto>()
val originalSlug = response.request.url.fragment!!
val searchResult = response.parseAs<List<HeanCmsSearchDto>>()
searchResultToSeries(originalSlug, searchResult)
} else {
response.parseAs<HeanCmsSeriesDto>()
}
val currentTimestamp = System.currentTimeMillis() val currentTimestamp = System.currentTimeMillis()
@ -300,31 +227,6 @@ abstract class HeanCms(
.reversed() .reversed()
} }
private fun searchResultToSeries(originalSlug: String, searchResult: List<HeanCmsSearchDto>): HeanCmsSeriesDto {
val mangaSlug = searchResult
.filter { it.type == "Comic" }
.map { it.slug }
.find { it.startsWith(originalSlug) || originalSlug.startsWith(it) }
?: originalSlug
val apiHeaders = headersBuilder()
.add("Accept", ACCEPT_JSON)
.build()
val detailsRequest = GET("$apiUrl/series/$mangaSlug", apiHeaders)
val result = client.newCall(detailsRequest).execute()
.parseAs<HeanCmsSeriesDto>()
val permSlug = result.slug
.substringAfterLast("/")
.replace(TIMESTAMP_REGEX, "")
preferences.slugMap = preferences.slugMap.toMutableMap()
.also { it[permSlug] = result.slug.substringAfterLast("/") }
return result
}
override fun getChapterUrl(chapter: SChapter): String = baseUrl + chapter.url override fun getChapterUrl(chapter: SChapter): String = baseUrl + chapter.url
override fun pageListRequest(chapter: SChapter): Request { override fun pageListRequest(chapter: SChapter): Request {
@ -379,83 +281,6 @@ abstract class HeanCms(
protected open fun getGenreList(): List<Genre> = emptyList() protected open fun getGenreList(): List<Genre> = emptyList()
protected open fun fetchAllTitles() {
if (!seriesSlugMap.isNullOrEmpty() || fetchAllTitlesStrategy != FetchAllStrategy.SEARCH_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()
}
protected open fun allTitlesRequest(page: Int): Request {
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 var SharedPreferences.slugMap: MutableMap<String, String>
get() {
val jsonMap = getString(PREF_URL_MAP, "{}")!!
val slugMap = runCatching { json.decodeFromString<Map<String, String>>(jsonMap) }
return slugMap.getOrNull()?.toMutableMap() ?: mutableMapOf()
}
set(newSlugMap) {
edit()
.putString(PREF_URL_MAP, json.encodeToString(newSlugMap))
.commit()
}
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,
)
},
)
}
override fun getFilterList(): FilterList { override fun getFilterList(): FilterList {
val genres = getGenreList() val genres = getGenreList()
@ -478,28 +303,14 @@ abstract class HeanCms(
private inline fun <reified R> List<*>.firstInstanceOrNull(): R? = private inline fun <reified R> List<*>.firstInstanceOrNull(): R? =
filterIsInstance<R>().firstOrNull() filterIsInstance<R>().firstOrNull()
/**
* 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)
enum class FetchAllStrategy {
NONE,
SEARCH_EACH,
SEARCH_ALL,
}
companion object { companion object {
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 val JSON_MEDIA_TYPE = "application/json".toMediaType()
val TIMESTAMP_REGEX = "-\\d+$".toRegex() val TIMESTAMP_REGEX = """-\d{13}$""".toRegex()
const val SEARCH_PREFIX = "slug:" const val SEARCH_PREFIX = "slug:"
private const val PREF_URL_MAP = "pref_url_map"
} }
} }

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.FetchAllStrategy
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
@ -27,7 +26,7 @@ data class HeanCmsQuerySearchMetaDto(
@Serializable @Serializable
data class HeanCmsSearchDto( data class HeanCmsSearchDto(
val description: String? = null, val description: String? = null,
@SerialName("series_slug") val slug: String, @SerialName("series_slug") var slug: String,
@SerialName("series_type") val type: String, @SerialName("series_type") val type: String,
val title: String, val title: String,
val thumbnail: String? = null, val thumbnail: String? = null,
@ -36,15 +35,9 @@ data class HeanCmsSearchDto(
fun toSManga( fun toSManga(
apiUrl: String, apiUrl: String,
coverPath: String, coverPath: String,
slugMap: Map<String, HeanCms.HeanCmsTitle>,
fetchStrategy: FetchAllStrategy = FetchAllStrategy.NONE,
): SManga = SManga.create().apply { ): SManga = SManga.create().apply {
val slug = if (fetchStrategy == FetchAllStrategy.NONE) slug else slug.replace(HeanCms.TIMESTAMP_REGEX, "")
val thumbnailFileName = slugMap[slug]?.thumbnailFileName
title = this@HeanCmsSearchDto.title title = this@HeanCmsSearchDto.title
thumbnail_url = thumbnail?.toAbsoluteThumbnailUrl(apiUrl, coverPath) thumbnail_url = thumbnail?.toAbsoluteThumbnailUrl(apiUrl, coverPath)
?: thumbnailFileName?.toAbsoluteThumbnailUrl(apiUrl, coverPath)
url = "/series/$slug" url = "/series/$slug"
} }
} }
@ -67,10 +60,8 @@ data class HeanCmsSeriesDto(
fun toSManga( fun toSManga(
apiUrl: String, apiUrl: String,
coverPath: String, coverPath: String,
fetchStrategy: FetchAllStrategy = FetchAllStrategy.NONE,
): 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 slug = if (fetchStrategy == FetchAllStrategy.NONE) slug else slug.replace(HeanCms.TIMESTAMP_REGEX, "")
title = this@HeanCmsSeriesDto.title title = this@HeanCmsSeriesDto.title
author = this@HeanCmsSeriesDto.author?.trim() author = this@HeanCmsSeriesDto.author?.trim()

View File

@ -9,7 +9,7 @@ class HeanCmsGenerator : ThemeSourceGenerator {
override val themeClass = "HeanCms" override val themeClass = "HeanCms"
override val baseVersionCode: Int = 15 override val baseVersionCode: Int = 16
override val sources = listOf( override val sources = listOf(
SingleLang("Glorious Scan", "https://gloriousscan.com", "pt-BR", overrideVersionCode = 17), SingleLang("Glorious Scan", "https://gloriousscan.com", "pt-BR", overrideVersionCode = 17),