diff --git a/multisrc/overrides/mangathemesia/luminousscans/src/LuminousScans.kt b/multisrc/overrides/mangathemesia/luminousscans/src/LuminousScans.kt index ac8ab637..1fe2356e 100644 --- a/multisrc/overrides/mangathemesia/luminousscans/src/LuminousScans.kt +++ b/multisrc/overrides/mangathemesia/luminousscans/src/LuminousScans.kt @@ -1,10 +1,235 @@ package eu.kanade.tachiyomi.extension.en.luminousscans +import android.app.Application +import android.content.SharedPreferences +import androidx.preference.PreferenceScreen +import androidx.preference.SwitchPreferenceCompat import eu.kanade.tachiyomi.multisrc.mangathemesia.MangaThemesia import eu.kanade.tachiyomi.network.interceptor.rateLimit +import eu.kanade.tachiyomi.source.model.FilterList +import eu.kanade.tachiyomi.source.model.MangasPage +import eu.kanade.tachiyomi.source.model.SChapter +import eu.kanade.tachiyomi.source.model.SManga +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.encodeToString +import okhttp3.Interceptor +import okhttp3.Request +import okhttp3.Response +import rx.Observable +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get +import java.io.IOException class LuminousScans : MangaThemesia("Luminous Scans", "https://luminousscans.net", "en", mangaUrlDirectory = "/series") { override val client = super.client.newBuilder() + .addInterceptor(::urlChangeInterceptor) .rateLimit(2) .build() + + private val preferences by lazy { + Injekt.get().getSharedPreferences("source_$id", 0x0000) + } + + // Permanent Url for Manga/Chapter End + override fun fetchPopularManga(page: Int): Observable { + return super.fetchPopularManga(page).tempUrlToPermIfNeeded() + } + + override fun fetchLatestUpdates(page: Int): Observable { + return super.fetchLatestUpdates(page).tempUrlToPermIfNeeded() + } + + override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable { + return super.fetchSearchManga(page, query, filters).tempUrlToPermIfNeeded() + } + + override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { + val request = super.searchMangaRequest(page, query, filters) + if (query.isBlank()) return request + + val url = request.url.newBuilder() + .addPathSegment("page/$page/") + .removeAllQueryParameters("page") + .removeAllQueryParameters("title") + .addQueryParameter("s", query) + .build() + + return request.newBuilder() + .url(url) + .build() + } + + // Temp Url for manga/chapter + override fun fetchChapterList(manga: SManga): Observable> { + val newManga = manga.titleToUrlFrag() + + return super.fetchChapterList(newManga) + } + + override fun fetchMangaDetails(manga: SManga): Observable { + val newManga = manga.titleToUrlFrag() + + return super.fetchMangaDetails(newManga) + } + + override fun getMangaUrl(manga: SManga): String { + val dbSlug = manga.url + .substringBefore("#") + .removeSuffix("/") + .substringAfterLast("/") + + val storedSlug = preferences.slugMap[dbSlug] ?: dbSlug + + return "$baseUrl$mangaUrlDirectory/$storedSlug/" + } + + private fun Observable.tempUrlToPermIfNeeded(): Observable { + return this.map { mangasPage -> + MangasPage( + mangasPage.mangas.map { it.tempUrlToPermIfNeeded() }, + mangasPage.hasNextPage, + ) + } + } + + private fun SManga.tempUrlToPermIfNeeded(): SManga { + if (!preferences.permaUrlPref) return this + + val slugMap = preferences.slugMap + + val sMangaTitleFirstWord = this.title.split(" ")[0] + if (!this.url.contains("/$sMangaTitleFirstWord", ignoreCase = true)) { + val currentSlug = this.url + .removeSuffix("/") + .substringAfterLast("/") + + val permaSlug = currentSlug.replaceFirst(TEMP_TO_PERM_REGEX, "") + + slugMap[permaSlug] = currentSlug + + this.url = "$mangaUrlDirectory/$permaSlug/" + } + preferences.slugMap = slugMap + return this + } + + private fun SManga.titleToUrlFrag(): SManga { + return try { + this.apply { + url = "$url#${title.toSearchQuery()}" + } + } catch (e: UninitializedPropertyAccessException) { + // when called from deep link, title is not present + this + } + } + + private fun urlChangeInterceptor(chain: Interceptor.Chain): Response { + val request = chain.request() + + val frag = request.url.fragment + + if (frag.isNullOrEmpty()) { + return chain.proceed(request) + } + + val dbSlug = request.url.toString() + .substringBefore("#") + .removeSuffix("/") + .substringAfterLast("/") + + val slugMap = preferences.slugMap + + val storedSlug = slugMap[dbSlug] ?: dbSlug + + val response = chain.proceed( + request.newBuilder() + .url("$baseUrl$mangaUrlDirectory/$storedSlug/") + .build(), + ) + + if (!response.isSuccessful && response.code == 404) { + response.close() + + val newSlug = getNewSlug(storedSlug, frag) + ?: throw IOException("Migrate from Luminous to Luminous") + + slugMap[dbSlug] = newSlug + preferences.slugMap = slugMap + + return chain.proceed( + request.newBuilder() + .url("$baseUrl$mangaUrlDirectory/$newSlug/") + .build(), + ) + } + + return response + } + + private fun getNewSlug(existingSlug: String, frag: String): String? { + val permaSlug = existingSlug + .replaceFirst(TEMP_TO_PERM_REGEX, "") + + val search = frag.substringBefore("#") + + val mangas = client.newCall(searchMangaRequest(1, search, FilterList())) + .execute() + .use { + searchMangaParse(it) + } + + return mangas.mangas.firstOrNull { newManga -> + newManga.url.contains(permaSlug, true) + } + ?.url + ?.removeSuffix("/") + ?.substringAfterLast("/") + } + + private fun String.toSearchQuery(): String { + return this.trim() + .lowercase() + .replace(titleSpecialCharactersRegex, "+") + .replace(trailingPlusRegex, "") + } + + override fun setupPreferenceScreen(screen: PreferenceScreen) { + SwitchPreferenceCompat(screen.context).apply { + key = PREF_PERM_MANGA_URL_KEY_PREFIX + lang + title = PREF_PERM_MANGA_URL_TITLE + summary = PREF_PERM_MANGA_URL_SUMMARY + setDefaultValue(true) + }.also(screen::addPreference) + + super.setupPreferenceScreen(screen) + } + + private val SharedPreferences.permaUrlPref + get() = getBoolean(PREF_PERM_MANGA_URL_KEY_PREFIX + lang, true) + + private var SharedPreferences.slugMap: MutableMap + get() { + val serialized = getString(PREF_URL_MAP, null) ?: return mutableMapOf() + + return try { + json.decodeFromString(serialized) + } catch (e: Exception) { + mutableMapOf() + } + } + set(slugMap) { + val serialized = json.encodeToString(slugMap) + edit().putString(PREF_URL_MAP, serialized).commit() + } + + companion object { + private const val PREF_PERM_MANGA_URL_KEY_PREFIX = "pref_permanent_manga_url_2_" + private const val PREF_PERM_MANGA_URL_TITLE = "Permanent Manga URL" + private const val PREF_PERM_MANGA_URL_SUMMARY = "Turns all manga urls into permanent ones." + private const val PREF_URL_MAP = "pref_url_map" + private val TEMP_TO_PERM_REGEX = Regex("""^\d+-""") + private val titleSpecialCharactersRegex = Regex("""[^a-z0-9]+""") + private val trailingPlusRegex = Regex("""\++$""") + } } diff --git a/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/mangathemesia/MangaThemesiaGenerator.kt b/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/mangathemesia/MangaThemesiaGenerator.kt index fb7ef3b5..f0c0294c 100644 --- a/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/mangathemesia/MangaThemesiaGenerator.kt +++ b/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/mangathemesia/MangaThemesiaGenerator.kt @@ -75,7 +75,7 @@ class MangaThemesiaGenerator : ThemeSourceGenerator { SingleLang("Legacy Scans", "https://legacy-scans.com", "fr", pkgName = "flamescansfr"), SingleLang("Lelmanga", "https://www.lelmanga.com", "fr"), SingleLang("LianScans", "https://www.lianscans.my.id", "id", isNsfw = true), - SingleLang("Luminous Scans", "https://luminousscans.net", "en"), + SingleLang("Luminous Scans", "https://luminousscans.net", "en", overrideVersionCode = 1), SingleLang("Lunar Scans", "https://lunarscan.org", "en", isNsfw = true, overrideVersionCode = 1), SingleLang("LynxScans", "https://lynxscans.com", "en"), SingleLang("Lyra Scans", "https://lyrascans.com", "en"),