OlympusScanlation: Use site bookmarks to update manga url ()

* use bookmarks

* bump

* use slugmap

* change message

* fixes
This commit is contained in:
bapeey 2025-04-14 09:42:45 -05:00 committed by Draff
parent 7586d7ff61
commit 2dee930bbf
No known key found for this signature in database
GPG Key ID: E8A89F3211677653
3 changed files with 199 additions and 92 deletions
src/es/olympusscanlation
build.gradle
src/eu/kanade/tachiyomi/extension/es/olympusscanlation

@ -1,7 +1,7 @@
ext { ext {
extName = 'Olympus Scanlation' extName = 'Olympus Scanlation'
extClass = '.OlympusScanlation' extClass = '.OlympusScanlation'
extVersionCode = 15 extVersionCode = 16
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"

@ -2,9 +2,9 @@ package eu.kanade.tachiyomi.extension.es.olympusscanlation
import android.content.SharedPreferences import android.content.SharedPreferences
import android.widget.Toast import android.widget.Toast
import androidx.preference.CheckBoxPreference
import androidx.preference.EditTextPreference import androidx.preference.EditTextPreference
import androidx.preference.PreferenceScreen import androidx.preference.PreferenceScreen
import androidx.preference.SwitchPreferenceCompat
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.interceptor.rateLimitHost import eu.kanade.tachiyomi.network.interceptor.rateLimitHost
import eu.kanade.tachiyomi.source.ConfigurableSource import eu.kanade.tachiyomi.source.ConfigurableSource
@ -17,12 +17,12 @@ 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 eu.kanade.tachiyomi.util.asJsoup
import keiyoushi.utils.getPreferences import keiyoushi.utils.getPreferences
import kotlinx.serialization.decodeFromString import keiyoushi.utils.parseAs
import kotlinx.serialization.json.Json import keiyoushi.utils.toJsonString
import kotlinx.serialization.SerializationException
import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request import okhttp3.Request
import okhttp3.Response import okhttp3.Response
import uy.kohesive.injekt.injectLazy
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Locale import java.util.Locale
import java.util.TimeZone import java.util.TimeZone
@ -31,7 +31,7 @@ import kotlin.math.min
class OlympusScanlation : HttpSource(), ConfigurableSource { class OlympusScanlation : HttpSource(), ConfigurableSource {
override val versionId = 2 override val versionId = 3
private val isCi = System.getenv("CI") == "true" private val isCi = System.getenv("CI") == "true"
override val baseUrl: String get() = when { override val baseUrl: String get() = when {
@ -67,16 +67,30 @@ class OlympusScanlation : HttpSource(), ConfigurableSource {
override val supportsLatest: Boolean = true override val supportsLatest: Boolean = true
private val preferences: SharedPreferences = getPreferences() private val preferences: SharedPreferences = getPreferences {
this.getString(DEFAULT_BASE_URL_PREF, null).let { domain ->
if (domain != defaultBaseUrl) {
this.edit()
.putString(BASE_URL_PREF, defaultBaseUrl)
.putString(DEFAULT_BASE_URL_PREF, defaultBaseUrl)
.apply()
}
}
}
override val client by lazy { override val client by lazy {
network.cloudflareClient.newBuilder() val client = network.cloudflareClient.newBuilder()
.rateLimitHost(fetchedDomainUrl.toHttpUrl(), 1, 2) .rateLimitHost(fetchedDomainUrl.toHttpUrl(), 1, 2)
.rateLimitHost(apiBaseUrl.toHttpUrl(), 2, 1) .rateLimitHost(apiBaseUrl.toHttpUrl(), 2, 1)
.build() .build()
fetchBookmarks()
return@lazy client
} }
private val json: Json by injectLazy() override fun headersBuilder() = super.headersBuilder()
.add("Referer", "$baseUrl/")
private val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSSSS'Z'", Locale.US).apply { private val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSSSS'Z'", Locale.US).apply {
timeZone = TimeZone.getTimeZone("UTC") timeZone = TimeZone.getTimeZone("UTC")
@ -89,8 +103,15 @@ class OlympusScanlation : HttpSource(), ConfigurableSource {
} }
override fun popularMangaParse(response: Response): MangasPage { override fun popularMangaParse(response: Response): MangasPage {
val result = json.decodeFromString<PayloadHomeDto>(response.body.string()) val result = response.parseAs<PayloadHomeDto>()
val mangaList = result.data.popularComics.filter { it.type == "comic" }.map { it.toSManga() } val slugMap = preferences.slugMap.toMutableMap()
val mangaList = result.data.popularComics
.filter { it.type == "comic" }
.map {
slugMap[it.id] = it.slug
it.toSManga()
}
preferences.slugMap = slugMap
return MangasPage(mangaList, hasNextPage = false) return MangasPage(mangaList, hasNextPage = false)
} }
@ -101,10 +122,15 @@ class OlympusScanlation : HttpSource(), ConfigurableSource {
} }
override fun latestUpdatesParse(response: Response): MangasPage { override fun latestUpdatesParse(response: Response): MangasPage {
val result = json.decodeFromString<NewChaptersDto>(response.body.string()) val result = response.parseAs<NewChaptersDto>()
val mangaList = result.data.filter { it.type == "comic" }.map { it.toSManga() } val slugMap = preferences.slugMap.toMutableMap()
val hasNextPage = result.current_page < result.last_page val mangaList = result.data.filter { it.type == "comic" }
return MangasPage(mangaList, hasNextPage) .map {
slugMap[it.id] = it.slug
it.toSManga()
}
preferences.slugMap = slugMap
return MangasPage(mangaList, result.hasNextPage())
} }
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
@ -148,80 +174,114 @@ class OlympusScanlation : HttpSource(), ConfigurableSource {
override fun searchMangaParse(response: Response): MangasPage { override fun searchMangaParse(response: Response): MangasPage {
if (response.request.url.toString().startsWith("$apiBaseUrl/api/search")) { if (response.request.url.toString().startsWith("$apiBaseUrl/api/search")) {
val result = json.decodeFromString<PayloadMangaDto>(response.body.string()) val result = response.parseAs<PayloadMangaDto>()
val mangaList = result.data.filter { it.type == "comic" }.map { it.toSManga() } val slugMap = preferences.slugMap.toMutableMap()
val mangaList = result.data.filter { it.type == "comic" }
.map {
slugMap[it.id] = it.slug
it.toSManga()
}
preferences.slugMap = slugMap
return MangasPage(mangaList, hasNextPage = false) return MangasPage(mangaList, hasNextPage = false)
} }
val result = json.decodeFromString<PayloadSeriesDto>(response.body.string()) val result = response.parseAs<PayloadSeriesDto>()
val mangaList = result.data.series.data.map { it.toSManga() } val mangaList = result.data.series.data.map { it.toSManga() }
val hasNextPage = result.data.series.current_page < result.data.series.last_page return MangasPage(mangaList, result.data.series.hasNextPage())
return MangasPage(mangaList, hasNextPage) }
private var bookmarksState = BookmarksState.NOT_FETCHED
private fun fetchBookmarks() {
if (!preferences.fetchBookmarksPref()) return
if (bookmarksState != BookmarksState.NOT_FETCHED) return
bookmarksState = BookmarksState.FETCHING
val slugMap = preferences.slugMap.toMutableMap()
var page = 1
try {
do {
val response = network.cloudflareClient.newCall(GET("$apiBaseUrl/api/user/bookmarks?page=$page", headers)).execute()
if (!response.isSuccessful) return
val result = response.parseAs<BookmarksWrapperDto>()
result.getBookmarks().forEach { bookmark ->
slugMap[bookmark.id!!] = bookmark.slug!!
}
page++
} while (result.meta.hasNextPage())
} catch (_: Exception) { } finally {
bookmarksState = BookmarksState.FETCHED
}
preferences.slugMap = slugMap
}
override fun getMangaUrl(manga: SManga): String {
val slug = preferences.slugMap[manga.url.toInt()]!!
return "$baseUrl/series/comic-$slug"
}
override fun mangaDetailsRequest(manga: SManga): Request {
val slug = preferences.slugMap[manga.url.toInt()]!!
val apiUrl = "$apiBaseUrl/api/series/$slug?type=comic"
return GET(url = apiUrl, headers = headers)
} }
override fun mangaDetailsParse(response: Response): SManga { override fun mangaDetailsParse(response: Response): SManga {
val slug = response.request.url val result = response.parseAs<MangaDetailDto>()
.toString()
.substringAfter("/series/comic-")
.substringBefore("/chapters")
val apiUrl = "$apiBaseUrl/api/series/$slug?type=comic"
val newRequest = GET(url = apiUrl, headers = headers)
val newResponse = client.newCall(newRequest).execute()
val result = json.decodeFromString<MangaDetailDto>(newResponse.body.string())
return result.data.toSMangaDetails() return result.data.toSMangaDetails()
} }
override fun getChapterUrl(chapter: SChapter): String = baseUrl + chapter.url override fun getChapterUrl(chapter: SChapter): String {
val mangaId = chapter.url.substringBefore("/")
override fun chapterListRequest(manga: SManga): Request { val chapterId = chapter.url.substringAfter("/")
return paginatedChapterListRequest( val mangaSlug = preferences.slugMap[mangaId.toInt()]!!
manga.url return "$baseUrl/capitulo/$chapterId/comic-$mangaSlug"
.substringAfter("/series/comic-")
.substringBefore("/chapters"),
1,
)
} }
private fun paginatedChapterListRequest(mangaUrl: String, page: Int): Request { override fun chapterListRequest(manga: SManga): Request {
val mangaId = manga.url
val mangaSlug = preferences.slugMap[mangaId.toInt()]!!
return paginatedChapterListRequest(mangaSlug, mangaId, 1)
}
private fun paginatedChapterListRequest(mangaSlug: String, mangaId: String, page: Int): Request {
return GET( return GET(
url = "$apiBaseUrl/api/series/$mangaUrl/chapters?page=$page&direction=desc&type=comic", url = "$apiBaseUrl/api/series/$mangaSlug/chapters?page=$page&direction=desc&type=comic#$mangaId",
headers = headers, headers = headers,
) )
} }
override fun chapterListParse(response: Response): List<SChapter> { override fun chapterListParse(response: Response): List<SChapter> {
val slug = response.request.url val mangaId = response.request.url.fragment ?: ""
.toString() val slug = response.request.url.toString()
.substringAfter("/series/") .substringAfter("/series/")
.substringBefore("/chapters") .substringBefore("/chapters")
val data = json.decodeFromString<PayloadChapterDto>(response.body.string())
val data = response.parseAs<PayloadChapterDto>()
var resultSize = data.data.size var resultSize = data.data.size
var page = 2 var page = 2
while (data.meta.total > resultSize) { while (data.meta.total > resultSize) {
val newRequest = paginatedChapterListRequest(slug, page) val newRequest = paginatedChapterListRequest(slug, mangaId, page)
val newResponse = client.newCall(newRequest).execute() val newResponse = client.newCall(newRequest).execute()
val newData = json.decodeFromString<PayloadChapterDto>(newResponse.body.string()) val newData = newResponse.parseAs<PayloadChapterDto>()
data.data += newData.data data.data += newData.data
resultSize += newData.data.size resultSize += newData.data.size
page += 1 page += 1
} }
return data.data.map { it.toSChapter(slug, dateFormat) } return data.data.map { it.toSChapter(mangaId, dateFormat) }
} }
override fun pageListRequest(chapter: SChapter): Request { override fun pageListRequest(chapter: SChapter): Request {
val id = chapter.url val mangaId = chapter.url.substringBefore("/")
.substringAfter("/capitulo/") val chapterId = chapter.url.substringAfter("/")
.substringBefore("/chapters") val mangaSlug = preferences.slugMap[mangaId.toInt()]!!
.substringBefore("/comic")
val slug = chapter.url return GET("$apiBaseUrl/api/series/$mangaSlug/chapters/$chapterId?type=comic")
.substringAfter("comic-")
.substringBefore("/chapters")
.substringBefore("/comic")
return GET("$apiBaseUrl/api/series/$slug/chapters/$id?type=comic")
} }
override fun pageListParse(response: Response): List<Page> { override fun pageListParse(response: Response): List<Page> {
return json.decodeFromString<PayloadPagesDto>(response.body.string()).chapter.pages.mapIndexed { i, img -> return response.parseAs<PayloadPagesDto>().chapter.pages.mapIndexed { i, img ->
Page(i, imageUrl = img) Page(i, imageUrl = img)
} }
} }
@ -292,7 +352,7 @@ class OlympusScanlation : HttpSource(), ConfigurableSource {
thread { thread {
try { try {
val response = client.newCall(GET("$apiBaseUrl/api/genres-statuses", headers)).execute() val response = client.newCall(GET("$apiBaseUrl/api/genres-statuses", headers)).execute()
val filters = json.decodeFromString<GenresStatusesDto>(response.body.string()) val filters = response.parseAs<GenresStatusesDto>()
genresList = filters.genres.map { it.name.trim() to it.id } genresList = filters.genres.map { it.name.trim() to it.id }
statusesList = filters.statuses.map { it.name.trim() to it.id } statusesList = filters.statuses.map { it.name.trim() to it.id }
@ -304,30 +364,42 @@ class OlympusScanlation : HttpSource(), ConfigurableSource {
} }
} }
open class UriPartFilter(displayName: String, val vals: Array<Pair<String, Int>>) : open class UriPartFilter(displayName: String, private val vals: Array<Pair<String, Int>>) :
Filter.Select<String>(displayName, vals.map { it.first }.toTypedArray()) { Filter.Select<String>(displayName, vals.map { it.first }.toTypedArray()) {
fun toUriPart() = vals[state].second fun toUriPart() = vals[state].second
} }
private enum class FiltersState { NOT_FETCHED, FETCHING, FETCHED } private enum class FiltersState { NOT_FETCHED, FETCHING, FETCHED }
private enum class BookmarksState { NOT_FETCHED, FETCHING, FETCHED }
override fun setupPreferenceScreen(screen: PreferenceScreen) { override fun setupPreferenceScreen(screen: PreferenceScreen) {
CheckBoxPreference(screen.context).apply { SwitchPreferenceCompat(screen.context).apply {
key = FETCH_BOOKMARKS_PREF
title = "Usar marcadores"
summary = "Usa los marcadores del sitio para obtener la url actual de la serie.\nRequiere iniciar sesión en WebView y seguir la serie."
setDefaultValue(FETCH_BOOKMARKS_PREF_DEFAULT)
setOnPreferenceChangeListener { _, _ ->
Toast.makeText(screen.context, "Reinicie la aplicación para aplicar los cambios", Toast.LENGTH_LONG).show()
true
}
}.also { screen.addPreference(it) }
SwitchPreferenceCompat(screen.context).apply {
key = FETCH_DOMAIN_PREF key = FETCH_DOMAIN_PREF
title = FETCH_DOMAIN_PREF_TITLE title = "Buscar dominio automáticamente"
summary = FETCH_DOMAIN_PREF_SUMMARY summary = "Intenta buscar el dominio automáticamente al abrir la fuente."
setDefaultValue(FETCH_DOMAIN_PREF_DEFAULT) setDefaultValue(FETCH_DOMAIN_PREF_DEFAULT)
}.also { screen.addPreference(it) } }.also { screen.addPreference(it) }
EditTextPreference(screen.context).apply { EditTextPreference(screen.context).apply {
key = BASE_URL_PREF key = BASE_URL_PREF
title = BASE_URL_PREF_TITLE title = "Editar URL de la fuente"
summary = BASE_URL_PREF_SUMMARY summary = "Para uso temporal, si la extensión se actualiza se perderá el cambio."
dialogTitle = BASE_URL_PREF_TITLE dialogTitle = "Editar URL de la fuente"
dialogMessage = "URL por defecto:\n$defaultBaseUrl" dialogMessage = "URL por defecto:\n$defaultBaseUrl"
setDefaultValue(defaultBaseUrl) setDefaultValue(defaultBaseUrl)
setOnPreferenceChangeListener { _, _ -> setOnPreferenceChangeListener { _, _ ->
Toast.makeText(screen.context, RESTART_APP_MESSAGE, Toast.LENGTH_LONG).show() Toast.makeText(screen.context, "Reinicie la aplicación para aplicar los cambios", Toast.LENGTH_LONG).show()
true true
} }
}.also { screen.addPreference(it) } }.also { screen.addPreference(it) }
@ -347,29 +419,35 @@ class OlympusScanlation : HttpSource(), ConfigurableSource {
} }
private fun SharedPreferences.fetchDomainPref() = getBoolean(FETCH_DOMAIN_PREF, FETCH_DOMAIN_PREF_DEFAULT) private fun SharedPreferences.fetchDomainPref() = getBoolean(FETCH_DOMAIN_PREF, FETCH_DOMAIN_PREF_DEFAULT)
private fun SharedPreferences.fetchBookmarksPref() = getBoolean(FETCH_BOOKMARKS_PREF, FETCH_BOOKMARKS_PREF_DEFAULT)
private var _slugMap: Map<Int, String>? = null
private var SharedPreferences.slugMap: Map<Int, String>
get() {
_slugMap?.let { return it }
val json = getString(SLUG_MAP, "{}")!!
_slugMap = try {
json.parseAs<Map<Int, String>>()
} catch (_: SerializationException) {
emptyMap()
}
return _slugMap!!
}
set(map) {
_slugMap = map
edit().putString(SLUG_MAP, map.toJsonString()).apply()
}
companion object { companion object {
private const val BASE_URL_PREF = "overrideBaseUrl" private const val BASE_URL_PREF = "overrideBaseUrl"
private const val BASE_URL_PREF_TITLE = "Editar URL de la fuente"
private const val BASE_URL_PREF_SUMMARY = "Para uso temporal, si la extensión se actualiza se perderá el cambio."
private const val DEFAULT_BASE_URL_PREF = "defaultBaseUrl" private const val DEFAULT_BASE_URL_PREF = "defaultBaseUrl"
private const val RESTART_APP_MESSAGE = "Reinicie la aplicación para aplicar los cambios"
private const val FETCH_DOMAIN_PREF = "fetchDomain" private const val FETCH_DOMAIN_PREF = "fetchDomain"
private const val FETCH_DOMAIN_PREF_TITLE = "Buscar dominio automáticamente"
private const val FETCH_DOMAIN_PREF_SUMMARY = "Intenta buscar el dominio automáticamente al abrir la fuente."
private const val FETCH_DOMAIN_PREF_DEFAULT = true private const val FETCH_DOMAIN_PREF_DEFAULT = true
}
init { private const val FETCH_BOOKMARKS_PREF = "fetchBookmarks"
preferences.getString(DEFAULT_BASE_URL_PREF, null).let { domain -> private const val FETCH_BOOKMARKS_PREF_DEFAULT = false
if (domain != defaultBaseUrl) {
preferences.edit() private const val SLUG_MAP = "slugMap"
.putString(BASE_URL_PREF, defaultBaseUrl)
.putString(DEFAULT_BASE_URL_PREF, defaultBaseUrl)
.apply()
}
}
} }
} }

@ -44,18 +44,21 @@ class PayloadSeriesDataDto(
@Serializable @Serializable
class SeriesDto( class SeriesDto(
val current_page: Int,
val data: List<MangaDto>, val data: List<MangaDto>,
val last_page: Int, @SerialName("current_page") private val currentPage: Int,
) @SerialName("last_page") private val lastPage: Int,
) {
fun hasNextPage() = currentPage < lastPage
}
@Serializable @Serializable
class PayloadMangaDto(val data: List<MangaDto>) class PayloadMangaDto(val data: List<MangaDto>)
@Serializable @Serializable
class MangaDto( class MangaDto(
val id: Int,
private val name: String, private val name: String,
private val slug: String, val slug: String,
private val cover: String? = null, private val cover: String? = null,
val type: String? = null, val type: String? = null,
private val summary: String? = null, private val summary: String? = null,
@ -64,7 +67,7 @@ class MangaDto(
) { ) {
fun toSManga() = SManga.create().apply { fun toSManga() = SManga.create().apply {
title = name title = name
url = "/series/comic-$slug" url = id.toString()
thumbnail_url = cover thumbnail_url = cover
} }
@ -89,20 +92,23 @@ class MangaDto(
@Serializable @Serializable
class NewChaptersDto( class NewChaptersDto(
val data: List<LatestMangaDto>, val data: List<LatestMangaDto>,
val current_page: Int, @SerialName("current_page") private val currentPage: Int,
val last_page: Int, @SerialName("last_page") private val lastPage: Int,
) ) {
fun hasNextPage() = currentPage < lastPage
}
@Serializable @Serializable
class LatestMangaDto( class LatestMangaDto(
val id: Int,
private val name: String, private val name: String,
private val slug: String, val slug: String,
private val cover: String? = null, private val cover: String? = null,
val type: String? = null, val type: String? = null,
) { ) {
fun toSManga() = SManga.create().apply { fun toSManga() = SManga.create().apply {
title = name title = name
url = "/series/comic-$slug" url = id.toString()
thumbnail_url = cover thumbnail_url = cover
} }
} }
@ -121,9 +127,9 @@ class ChapterDto(
private val name: String, private val name: String,
@SerialName("published_at") private val date: String, @SerialName("published_at") private val date: String,
) { ) {
fun toSChapter(mangaSlug: String, dateFormat: SimpleDateFormat) = SChapter.create().apply { fun toSChapter(mangaId: String, dateFormat: SimpleDateFormat) = SChapter.create().apply {
name = "Capitulo ${this@ChapterDto.name}" name = "Capitulo ${this@ChapterDto.name}"
url = "/capitulo/$id/comic-$mangaSlug" url = "$mangaId/$id"
date_upload = try { date_upload = try {
dateFormat.parse(date)!!.time dateFormat.parse(date)!!.time
} catch (e: ParseException) { } catch (e: ParseException) {
@ -157,3 +163,26 @@ class FilterDto(
val id: Int, val id: Int,
val name: String, val name: String,
) )
@Serializable
class BookmarksWrapperDto(
private val data: List<BookmarkDto> = emptyList(),
val meta: BookmarksMetaDto,
) {
fun getBookmarks() = data.filter { it.type == "comic" && it.id != null && it.slug != null }
}
@Serializable
class BookmarkDto(
val id: Int?,
val slug: String?,
val type: String?,
)
@Serializable
class BookmarksMetaDto(
@SerialName("current_page") private val currentPage: Int,
@SerialName("last_page") private val lastPage: Int,
) {
fun hasNextPage() = currentPage < lastPage
}