diff --git a/lib-multisrc/mangathemesia/src/eu/kanade/tachiyomi/multisrc/mangathemesia/MangaThemesiaAlt.kt b/lib-multisrc/mangathemesia/src/eu/kanade/tachiyomi/multisrc/mangathemesia/MangaThemesiaAlt.kt new file mode 100644 index 000000000..0d3a1879a --- /dev/null +++ b/lib-multisrc/mangathemesia/src/eu/kanade/tachiyomi/multisrc/mangathemesia/MangaThemesiaAlt.kt @@ -0,0 +1,162 @@ +package eu.kanade.tachiyomi.multisrc.mangathemesia + +import android.app.Application +import android.content.SharedPreferences +import androidx.preference.PreferenceScreen +import androidx.preference.SwitchPreferenceCompat +import eu.kanade.tachiyomi.network.GET +import eu.kanade.tachiyomi.network.await +import eu.kanade.tachiyomi.source.ConfigurableSource +import eu.kanade.tachiyomi.source.model.MangasPage +import eu.kanade.tachiyomi.source.model.SManga +import eu.kanade.tachiyomi.util.asJsoup +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import okhttp3.Request +import okhttp3.Response +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get +import java.lang.ref.SoftReference +import java.text.SimpleDateFormat +import java.util.Locale + +abstract class MangaThemesiaAlt( + name: String, + baseUrl: String, + lang: String, + mangaUrlDirectory: String = "/manga", + dateFormat: SimpleDateFormat = SimpleDateFormat("MMMM dd, yyyy", Locale.US), + private val randomUrlPrefKey: String = "pref_auto_random_url", +) : MangaThemesia(name, baseUrl, lang, mangaUrlDirectory, dateFormat), ConfigurableSource { + + protected val preferences: SharedPreferences by lazy { + Injekt.get().getSharedPreferences("source_$id", 0x0000) + } + + override fun setupPreferenceScreen(screen: PreferenceScreen) { + SwitchPreferenceCompat(screen.context).apply { + key = randomUrlPrefKey + title = "Automatically update dynamic URLs" + summary = "Automatically update random numbers in manga URLs.\n" + + "Helps mitigating HTTP 404 errors during update and \"in library\" marks when browsing.\n\n" + + "example: https://example.com/manga/12345-cool-manga -> https://example.com/manga/4567-cool-manga\n\n" + + "Note: This setting may require clearing database in advanced settings\n" + + "and migrating all manga to the same source" + setDefaultValue(true) + }.also(screen::addPreference) + } + + private fun getRandomUrlPref() = preferences.getBoolean(randomUrlPrefKey, true) + + private var randomPartCache = SuspendLazy(::updateRandomPart) + + protected open fun getRandomPart(response: Response): String { + return response.asJsoup() + .selectFirst(searchMangaSelector())!! + .select("a").attr("href") + .removeSuffix("/") + .substringAfterLast("/") + .substringBefore("-") + } + + protected suspend fun updateRandomPart() = + client.newCall(GET("$baseUrl$mangaUrlDirectory/", headers)) + .await() + .use(::getRandomPart) + + override fun searchMangaParse(response: Response): MangasPage { + val mp = super.searchMangaParse(response) + + if (!getRandomUrlPref()) return mp + + val mangas = mp.mangas.toPermanentMangaUrls() + + return MangasPage(mangas, mp.hasNextPage) + } + + protected fun List.toPermanentMangaUrls(): List { + for (i in indices) { + val permaSlug = this[i].url + .removeSuffix("/") + .substringAfterLast("/") + .replaceFirst(slugRegex, "") + + this[i].url = "$mangaUrlDirectory/$permaSlug/" + } + + return this + } + + protected open val slugRegex = Regex("""^\d+-""") + + override fun mangaDetailsRequest(manga: SManga): Request { + if (!getRandomUrlPref()) return super.mangaDetailsRequest(manga) + + val slug = manga.url + .substringBefore("#") + .removeSuffix("/") + .substringAfterLast("/") + .replaceFirst(slugRegex, "") + + val randomPart = randomPartCache.blockingGet() + + return GET("$baseUrl$mangaUrlDirectory/$randomPart-$slug/", headers) + } + + override fun getMangaUrl(manga: SManga): String { + if (!getRandomUrlPref()) return super.getMangaUrl(manga) + + val slug = manga.url + .substringBefore("#") + .removeSuffix("/") + .substringAfterLast("/") + .replaceFirst(slugRegex, "") + + // we don't want to make network calls when user simply opens the entry + val randomPart = randomPartCache.peek()?.let { "$it-" } ?: "" + + return "$baseUrl$mangaUrlDirectory/$randomPart$slug/" + } + + override fun chapterListRequest(manga: SManga) = mangaDetailsRequest(manga) +} + +internal class SuspendLazy( + private val initializer: suspend () -> T, +) { + + private val mutex = Mutex() + private var cachedValue: SoftReference? = null + private var fetchTime = 0L + + suspend fun get(): T { + if (fetchTime + 3600000 < System.currentTimeMillis()) { + // reset cache + cachedValue = null + } + + // fast way + cachedValue?.get()?.let { + return it + } + return mutex.withLock { + cachedValue?.get()?.let { + return it + } + val result = initializer() + cachedValue = SoftReference(result) + fetchTime = System.currentTimeMillis() + + result + } + } + + fun peek(): T? { + return cachedValue?.get() + } + + fun blockingGet(): T { + return runBlocking { get() } + } +} diff --git a/src/en/asurascans/build.gradle b/src/en/asurascans/build.gradle index f283968d2..f9d11a5ec 100644 --- a/src/en/asurascans/build.gradle +++ b/src/en/asurascans/build.gradle @@ -3,7 +3,7 @@ ext { extClass = '.AsuraScans' themePkg = 'mangathemesia' baseUrl = 'https://asuratoon.com' - overrideVersionCode = 1 + overrideVersionCode = 2 } apply from: "$rootDir/common.gradle" diff --git a/src/en/asurascans/src/eu/kanade/tachiyomi/extension/en/asurascans/AsuraScans.kt b/src/en/asurascans/src/eu/kanade/tachiyomi/extension/en/asurascans/AsuraScans.kt index 0b8180239..31c66bc72 100644 --- a/src/en/asurascans/src/eu/kanade/tachiyomi/extension/en/asurascans/AsuraScans.kt +++ b/src/en/asurascans/src/eu/kanade/tachiyomi/extension/en/asurascans/AsuraScans.kt @@ -1,53 +1,35 @@ package eu.kanade.tachiyomi.extension.en.asurascans -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.multisrc.mangathemesia.MangaThemesiaAlt import eu.kanade.tachiyomi.network.interceptor.rateLimit -import eu.kanade.tachiyomi.source.ConfigurableSource import eu.kanade.tachiyomi.source.model.FilterList -import eu.kanade.tachiyomi.source.model.MangasPage import eu.kanade.tachiyomi.source.model.Page -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.OkHttpClient import okhttp3.Request -import okhttp3.Response import org.jsoup.nodes.Document -import rx.Observable -import uy.kohesive.injekt.Injekt -import uy.kohesive.injekt.api.get -import java.io.IOException import java.text.SimpleDateFormat import java.util.Locale -import java.util.concurrent.TimeUnit -class AsuraScans : - MangaThemesia( - "Asura Scans", - "https://asuratoon.com", - "en", - dateFormat = SimpleDateFormat("MMM d, yyyy", Locale.US), - ), - ConfigurableSource { - - private val preferences by lazy { - Injekt.get().getSharedPreferences("source_$id", 0x0000) +class AsuraScans : MangaThemesiaAlt( + "Asura Scans", + "https://asuratoon.com", + "en", + dateFormat = SimpleDateFormat("MMM d, yyyy", Locale.US), + randomUrlPrefKey = "pref_permanent_manga_url_2_en", +) { + init { + // remove legacy preferences + preferences.run { + if (contains("pref_url_map")) { + edit().remove("pref_url_map").apply() + } + if (contains("pref_base_url_host")) { + edit().remove("pref_base_url_host").apply() + } + } } - override val baseUrl by lazy { - preferences.baseUrlHost.let { "https://$it" } - } - - override val client: OkHttpClient = super.client.newBuilder() - .addInterceptor(::urlChangeInterceptor) - .addInterceptor(::domainChangeIntercept) - .rateLimit(1, 3, TimeUnit.SECONDS) + override val client = super.client.newBuilder() + .rateLimit(1, 3) .apply { val interceptors = interceptors() val index = interceptors.indexOfFirst { "Brotli" in it.javaClass.simpleName } @@ -64,19 +46,6 @@ class AsuraScans : override val pageSelector = "div.rdminimal > img, div.rdminimal > p > img, div.rdminimal > a > img, div.rdminimal > p > a > img, " + "div.rdminimal > noscript > img, div.rdminimal > p > noscript > img, div.rdminimal > a > noscript > img, div.rdminimal > p > a > noscript > img" - // 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 @@ -93,232 +62,10 @@ class AsuraScans : .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/" - } - // Skip scriptPages override fun pageListParse(document: Document): List { return document.select(pageSelector) .filterNot { it.attr("src").isNullOrEmpty() } .mapIndexed { i, img -> Page(i, document.location(), img.attr("abs:src")) } } - - 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 Asura to Asura") - - 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, "") - } - - private var lastDomain = "" - - private fun domainChangeIntercept(chain: Interceptor.Chain): Response { - val request = chain.request() - - if (request.url.host !in listOf(preferences.baseUrlHost, lastDomain)) { - return chain.proceed(request) - } - - if (lastDomain.isNotEmpty()) { - val newUrl = request.url.newBuilder() - .host(preferences.baseUrlHost) - .build() - - return chain.proceed( - request.newBuilder() - .url(newUrl) - .build(), - ) - } - - val response = chain.proceed(request) - - if (request.url.host == response.request.url.host) return response - - response.close() - - preferences.baseUrlHost = response.request.url.host - - lastDomain = request.url.host - - val newUrl = request.url.newBuilder() - .host(response.request.url.host) - .build() - - return chain.proceed( - request.newBuilder() - .url(newUrl) - .build(), - ) - } - - 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) - } - - 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() - } - - private var SharedPreferences.baseUrlHost - get() = getString(BASE_URL_PREF, defaultBaseUrlHost) ?: defaultBaseUrlHost - set(newHost) { - edit().putString(BASE_URL_PREF, newHost).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 const val BASE_URL_PREF = "pref_base_url_host" - private const val defaultBaseUrlHost = "asuratoon.com" - private val TEMP_TO_PERM_REGEX = Regex("""^\d+-""") - private val titleSpecialCharactersRegex = Regex("""[^a-z0-9]+""") - private val trailingPlusRegex = Regex("""\++$""") - } } diff --git a/src/en/flamecomics/build.gradle b/src/en/flamecomics/build.gradle index b948ecc02..e6110125b 100644 --- a/src/en/flamecomics/build.gradle +++ b/src/en/flamecomics/build.gradle @@ -3,7 +3,7 @@ ext { extClass = '.FlameComics' themePkg = 'mangathemesia' baseUrl = 'https://flamecomics.com' - overrideVersionCode = 0 + overrideVersionCode = 1 } apply from: "$rootDir/common.gradle" diff --git a/src/en/flamecomics/src/eu/kanade/tachiyomi/extension/en/flamecomics/FlameComics.kt b/src/en/flamecomics/src/eu/kanade/tachiyomi/extension/en/flamecomics/FlameComics.kt index 74a6fcbc7..35aa6d717 100644 --- a/src/en/flamecomics/src/eu/kanade/tachiyomi/extension/en/flamecomics/FlameComics.kt +++ b/src/en/flamecomics/src/eu/kanade/tachiyomi/extension/en/flamecomics/FlameComics.kt @@ -1,47 +1,30 @@ package eu.kanade.tachiyomi.extension.en.flamecomics -import android.app.Application import android.graphics.Bitmap import android.graphics.BitmapFactory import android.graphics.Canvas import android.graphics.Rect -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.ConfigurableSource -import eu.kanade.tachiyomi.source.model.FilterList -import eu.kanade.tachiyomi.source.model.MangasPage import eu.kanade.tachiyomi.source.model.Page -import eu.kanade.tachiyomi.source.model.SChapter -import eu.kanade.tachiyomi.source.model.SManga import okhttp3.Interceptor import okhttp3.MediaType.Companion.toMediaType import okhttp3.Protocol import okhttp3.Response import okhttp3.ResponseBody.Companion.toResponseBody import org.jsoup.nodes.Document -import rx.Observable -import uy.kohesive.injekt.Injekt -import uy.kohesive.injekt.api.get import java.io.ByteArrayOutputStream -class FlameComics : - MangaThemesia( - "Flame Comics", - "https://flamecomics.com", - "en", - mangaUrlDirectory = "/series", - ), - ConfigurableSource { +class FlameComics : MangaThemesia( + "Flame Comics", + "https://flamecomics.com", + "en", + mangaUrlDirectory = "/series", +) { // Flame Scans -> Flame Comics override val id = 6350607071566689772 - private val preferences by lazy { - Injekt.get().getSharedPreferences("source_$id", 0x0000) - } - override val client = super.client.newBuilder() .rateLimit(2, 7) .addInterceptor(::composedImageIntercept) @@ -130,114 +113,8 @@ class FlameComics : } // Split Image Fixer End - // Permanent Url start - 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() - } - - private fun Observable.tempUrlToPermIfNeeded(): Observable { - return this.map { mangasPage -> - MangasPage( - mangasPage.mangas.map { it.tempUrlToPermIfNeeded() }, - mangasPage.hasNextPage, - ) - } - } - - private fun SManga.tempUrlToPermIfNeeded(): SManga { - val turnTempUrlToPerm = preferences.getBoolean(getPermanentMangaUrlPreferenceKey(), true) - if (!turnTempUrlToPerm) return this - - val path = this.url.removePrefix("/").removeSuffix("/").split("/") - path.lastOrNull()?.let { slug -> this.url = "$mangaUrlDirectory/${deobfuscateSlug(slug)}/" } - - return this - } - - override fun fetchChapterList(manga: SManga) = super.fetchChapterList(manga.tempUrlToPermIfNeeded()) - .map { sChapterList -> sChapterList.map { it.tempUrlToPermIfNeeded() } } - - private fun SChapter.tempUrlToPermIfNeeded(): SChapter { - val turnTempUrlToPerm = preferences.getBoolean(getPermanentChapterUrlPreferenceKey(), true) - if (!turnTempUrlToPerm) return this - - val path = this.url.removePrefix("/").removeSuffix("/").split("/") - path.lastOrNull()?.let { slug -> this.url = "/${deobfuscateSlug(slug)}/" } - return this - } - - override fun setupPreferenceScreen(screen: PreferenceScreen) { - val permanentMangaUrlPref = SwitchPreferenceCompat(screen.context).apply { - key = getPermanentMangaUrlPreferenceKey() - title = PREF_PERM_MANGA_URL_TITLE - summary = PREF_PERM_MANGA_URL_SUMMARY - setDefaultValue(true) - - setOnPreferenceChangeListener { _, newValue -> - val checkValue = newValue as Boolean - preferences.edit() - .putBoolean(getPermanentMangaUrlPreferenceKey(), checkValue) - .commit() - } - } - val permanentChapterUrlPref = SwitchPreferenceCompat(screen.context).apply { - key = getPermanentChapterUrlPreferenceKey() - title = PREF_PERM_CHAPTER_URL_TITLE - summary = PREF_PERM_CHAPTER_URL_SUMMARY - setDefaultValue(true) - - setOnPreferenceChangeListener { _, newValue -> - val checkValue = newValue as Boolean - preferences.edit() - .putBoolean(getPermanentChapterUrlPreferenceKey(), checkValue) - .commit() - } - } - screen.addPreference(permanentMangaUrlPref) - screen.addPreference(permanentChapterUrlPref) - } - - private fun getPermanentMangaUrlPreferenceKey(): String { - return PREF_PERM_MANGA_URL_KEY_PREFIX + lang - } - - private fun getPermanentChapterUrlPreferenceKey(): String { - return PREF_PERM_CHAPTER_URL_KEY_PREFIX + lang - } - // Permanent Url for Manga/Chapter End - companion object { private const val COMPOSED_SUFFIX = "?comp" - - private const val PREF_PERM_MANGA_URL_KEY_PREFIX = "pref_permanent_manga_url_" - 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_PERM_CHAPTER_URL_KEY_PREFIX = "pref_permanent_chapter_url" - private const val PREF_PERM_CHAPTER_URL_TITLE = "Permanent Chapter URL" - private const val PREF_PERM_CHAPTER_URL_SUMMARY = "Turns all chapter urls into permanent ones." - - /** - * - * De-obfuscates the slug of a series or chapter to the permanent slug - * * For a series: "12345678-this-is-a-series" -> "this-is-a-series" - * * For a chapter: "12345678-this-is-a-series-chapter-1" -> "this-is-a-series-chapter-1" - * - * @param obfuscated_slug the obfuscated slug of a series or chapter - * - * @return - */ - private fun deobfuscateSlug(obfuscated_slug: String) = obfuscated_slug - .replaceFirst(Regex("""^\d+-"""), "") - private val MEDIA_TYPE = "image/png".toMediaType() } } diff --git a/src/en/luminousscans/build.gradle b/src/en/luminousscans/build.gradle index cb75c28e7..8cb7d2d9d 100644 --- a/src/en/luminousscans/build.gradle +++ b/src/en/luminousscans/build.gradle @@ -3,7 +3,7 @@ ext { extClass = '.LuminousScans' themePkg = 'mangathemesia' baseUrl = 'https://lumitoon.com' - overrideVersionCode = 3 + overrideVersionCode = 4 } apply from: "$rootDir/common.gradle" diff --git a/src/en/luminousscans/src/eu/kanade/tachiyomi/extension/en/luminousscans/LuminousScans.kt b/src/en/luminousscans/src/eu/kanade/tachiyomi/extension/en/luminousscans/LuminousScans.kt index 5b218aaf5..8dc39e197 100644 --- a/src/en/luminousscans/src/eu/kanade/tachiyomi/extension/en/luminousscans/LuminousScans.kt +++ b/src/en/luminousscans/src/eu/kanade/tachiyomi/extension/en/luminousscans/LuminousScans.kt @@ -1,57 +1,30 @@ 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.multisrc.mangathemesia.MangaThemesiaAlt import eu.kanade.tachiyomi.network.interceptor.rateLimit -import eu.kanade.tachiyomi.source.ConfigurableSource 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://lumitoon.com", - "en", - mangaUrlDirectory = "/series", - ), - ConfigurableSource { +class LuminousScans : MangaThemesiaAlt( + "Luminous Scans", + "https://lumitoon.com", + "en", + mangaUrlDirectory = "/series", + randomUrlPrefKey = "pref_permanent_manga_url_2_en", +) { + init { + // remove legacy preferences + preferences.run { + if (contains("pref_url_map")) { + edit().remove("pref_url_map").apply() + } + } + } 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 @@ -67,176 +40,4 @@ class LuminousScans : .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) - } - - 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/src/en/rizzcomic/build.gradle b/src/en/rizzcomic/build.gradle index 1353b1ee2..5d7c88f69 100644 --- a/src/en/rizzcomic/build.gradle +++ b/src/en/rizzcomic/build.gradle @@ -3,7 +3,7 @@ ext { extClass = '.RizzComic' themePkg = 'mangathemesia' baseUrl = 'https://rizzcomic.com' - overrideVersionCode = 0 + overrideVersionCode = 1 } apply from: "$rootDir/common.gradle" diff --git a/src/en/rizzcomic/src/eu/kanade/tachiyomi/extension/en/rizzcomic/Filters.kt b/src/en/rizzcomic/src/eu/kanade/tachiyomi/extension/en/rizzcomic/Filters.kt index fd9426d51..87a370120 100644 --- a/src/en/rizzcomic/src/eu/kanade/tachiyomi/extension/en/rizzcomic/Filters.kt +++ b/src/en/rizzcomic/src/eu/kanade/tachiyomi/extension/en/rizzcomic/Filters.kt @@ -26,53 +26,49 @@ abstract class SelectFilter( class SortFilter(defaultOrder: String? = null) : SelectFilter("Sort By", sort, defaultOrder) { override val formParameter = "OrderValue" companion object { - private val sort = listOf( - Pair("Default", "all"), - Pair("A-Z", "title"), - Pair("Z-A", "titlereverse"), - Pair("Latest Update", "update"), - Pair("Latest Added", "latest"), - Pair("Popular", "popular"), - ) - val POPULAR = FilterList(StatusFilter(), TypeFilter(), SortFilter("popular")) val LATEST = FilterList(StatusFilter(), TypeFilter(), SortFilter("update")) } } +private val sort = listOf( + Pair("Default", "all"), + Pair("A-Z", "title"), + Pair("Z-A", "titlereverse"), + Pair("Latest Update", "update"), + Pair("Latest Added", "latest"), + Pair("Popular", "popular"), +) + class StatusFilter : SelectFilter("Status", status) { override val formParameter = "StatusValue" - companion object { - private val status = listOf( - Pair("All", "all"), - Pair("Ongoing", "ongoing"), - Pair("Complete", "completed"), - Pair("Hiatus", "hiatus"), - ) - } } +private val status = listOf( + Pair("All", "all"), + Pair("Ongoing", "ongoing"), + Pair("Complete", "completed"), + Pair("Hiatus", "hiatus"), +) + class TypeFilter : SelectFilter("Type", type) { override val formParameter = "TypeValue" - companion object { - private val type = listOf( - Pair("All", "all"), - Pair("Manga", "Manga"), - Pair("Manhwa", "Manhwa"), - Pair("Manhua", "Manhua"), - Pair("Comic", "Comic"), - ) - } } +private val type = listOf( + Pair("All", "all"), + Pair("Manga", "Manga"), + Pair("Manhwa", "Manhwa"), + Pair("Manhua", "Manhua"), + Pair("Comic", "Comic"), +) + class CheckBoxFilter( name: String, val value: String, ) : Filter.CheckBox(name) -class GenreFilter( - genres: List>, -) : FormBodyFilter, Filter.Group( +class GenreFilter : FormBodyFilter, Filter.Group( "Genre", genres.map { CheckBoxFilter(it.first, it.second) }, ) { @@ -82,3 +78,34 @@ class GenreFilter( } } } + +val genres = listOf( + Pair("Abilities", "2"), + Pair("Action", "3"), + Pair("Adaptation", "4"), + Pair("Adventure", "5"), + Pair("Another Chance", "6"), + Pair("Apocalypse", "7"), + Pair("Based On A Novel", "8"), + Pair("Cheat", "9"), + Pair("Comedy", "10"), + Pair("Conspiracy", "11"), + Pair("Cultivation", "12"), + Pair("Demon", "13"), + Pair("Demon King", "14"), + Pair("Dragon", "15"), + Pair("Drama", "16"), + Pair("Drop", "17"), + Pair("Dungeon", "18"), + Pair("Dungeons", "19"), + Pair("Fantasy", "20"), + Pair("Game", "21"), + Pair("Genius", "22"), + Pair("Ghosts", "23"), + Pair("Harem", "24"), + Pair("Hero", "25"), + Pair("Hidden Identity", "26"), + Pair("HighFantasy", "27"), + Pair("Historical", "28"), + Pair("Horror", "29"), +) diff --git a/src/en/rizzcomic/src/eu/kanade/tachiyomi/extension/en/rizzcomic/RizzComic.kt b/src/en/rizzcomic/src/eu/kanade/tachiyomi/extension/en/rizzcomic/RizzComic.kt index 62d1f5835..4c687f3fd 100644 --- a/src/en/rizzcomic/src/eu/kanade/tachiyomi/extension/en/rizzcomic/RizzComic.kt +++ b/src/en/rizzcomic/src/eu/kanade/tachiyomi/extension/en/rizzcomic/RizzComic.kt @@ -1,6 +1,7 @@ package eu.kanade.tachiyomi.extension.en.rizzcomic -import eu.kanade.tachiyomi.multisrc.mangathemesia.MangaThemesia +import androidx.preference.PreferenceScreen +import eu.kanade.tachiyomi.multisrc.mangathemesia.MangaThemesiaAlt import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.POST import eu.kanade.tachiyomi.network.asObservableSuccess @@ -9,9 +10,7 @@ import eu.kanade.tachiyomi.source.model.Filter import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.MangasPage import eu.kanade.tachiyomi.source.model.Page -import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SManga -import eu.kanade.tachiyomi.util.asJsoup import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import kotlinx.serialization.decodeFromString @@ -22,7 +21,7 @@ import rx.Observable import java.text.SimpleDateFormat import java.util.Locale -class RizzComic : MangaThemesia( +class RizzComic : MangaThemesiaAlt( "Rizz Comic", "https://rizzcomic.com", "en", @@ -40,43 +39,12 @@ class RizzComic : MangaThemesia( .build() } - private var urlPrefix: String? = null - private var genreCache: List> = emptyList() - private var attempts = 0 + override val versionId = 2 - private fun updateCache() { - if ((urlPrefix.isNullOrEmpty() || genreCache.isEmpty()) && attempts < 3) { - runCatching { - val document = client.newCall(GET("$baseUrl$mangaUrlDirectory", headers)) - .execute().use { it.asJsoup() } + override val slugRegex = Regex("""^r\d+-""") - urlPrefix = document.selectFirst(".listupd a") - ?.attr("href") - ?.substringAfter("$mangaUrlDirectory/") - ?.substringBefore("-") - - genreCache = document.selectFirst(".filter .genrez") - ?.select("li") - .orEmpty() - .map { - val name = it.select("label").text() - val id = it.select("input").attr("value") - - Pair(name, id) - } - } - - attempts++ - } - } - - private fun getUrlPrefix(): String { - if (urlPrefix.isNullOrEmpty()) { - updateCache() - } - - return urlPrefix ?: throw Exception("Unable to update dynamic urls") - } + // don't allow disabling random part setting + override fun setupPreferenceScreen(screen: PreferenceScreen) { } override fun popularMangaRequest(page: Int) = searchMangaRequest(page, "", SortFilter.POPULAR) override fun popularMangaParse(response: Response) = searchMangaParse(response) @@ -103,30 +71,17 @@ class RizzComic : MangaThemesia( } override fun getFilterList(): FilterList { - val filters: MutableList> = mutableListOf( + return FilterList( Filter.Header("Filters don't work with text search"), SortFilter(), StatusFilter(), TypeFilter(), + GenreFilter(), ) - - filters += if (genreCache.isEmpty()) { - listOf( - Filter.Separator(), - Filter.Header("Press reset to attempt to load genres"), - ) - } else { - listOf( - GenreFilter(genreCache), - ) - } - - return FilterList(filters) } @Serializable class Comic( - val id: Int, val title: String, @SerialName("image_url") val cover: String? = null, @SerialName("long_description") val synopsis: String? = null, @@ -150,13 +105,11 @@ class RizzComic : MangaThemesia( } override fun searchMangaParse(response: Response): MangasPage { - updateCache() - val result = response.parseAs>() val entries = result.map { comic -> SManga.create().apply { - url = "${comic.slug}#${comic.id}" + url = "$mangaUrlDirectory/${comic.slug}/" title = comic.title description = comic.synopsis author = listOfNotNull(comic.author, comic.serialization).joinToString() @@ -166,7 +119,7 @@ class RizzComic : MangaThemesia( genre = buildList { add(comic.type?.capitalize()) comic.genreIds?.onEach { gId -> - add(genreCache.firstOrNull { it.second == gId }?.first) + add(genres.firstOrNull { it.second == gId }?.first) } }.filterNotNull().joinToString() initialized = true @@ -182,37 +135,6 @@ class RizzComic : MangaThemesia( .map { mangaDetailsParse(it).apply { description = manga.description } } } - override fun mangaDetailsRequest(manga: SManga): Request { - val slug = manga.url.substringBefore("#") - val randomPart = getUrlPrefix() - - return GET("$baseUrl/series/$randomPart-$slug", headers) - } - - override fun getMangaUrl(manga: SManga): String { - val slug = manga.url.substringBefore("#") - - val urlPart = urlPrefix?.let { "$it-" } ?: "" - - return "$baseUrl/series/$urlPart$slug" - } - - override fun chapterListRequest(manga: SManga) = mangaDetailsRequest(manga) - - override fun chapterListParse(response: Response): List { - return super.chapterListParse(response).map { chapter -> - chapter.apply { - url = url.removeSuffix("/") - .substringAfter("/") - .substringAfter("-") - } - } - } - - override fun pageListRequest(chapter: SChapter): Request { - return GET("$baseUrl/chapter/${getUrlPrefix()}-${chapter.url}", headers) - } - override fun imageRequest(page: Page): Request { val newHeaders = headersBuilder() .set("Accept", "image/avif,image/webp,image/png,image/jpeg,*/*")