MangaThemesia: add class to handle dynamic urls in sources (#1793)

* MangaThemesia: add alternative class to handle dynamic urls

* use MangaThemesiaAlt on Asura & Luminous

* use MangaThemesiaAlt on Rizz

* don't update in getMangaUrl

* small cleanup

* remove other pref as well
LuminousScans

* wording

* remove from FlameComics, since they no longer appear to do it

* review comments

* lint

* actual old pref key

Co-authored-by: stevenyomi <95685115+stevenyomi@users.noreply.github.com>

* actual old pref key x2

Co-authored-by: stevenyomi <95685115+stevenyomi@users.noreply.github.com>

---------

Co-authored-by: stevenyomi <95685115+stevenyomi@users.noreply.github.com>
This commit is contained in:
AwkwardPeak7 2024-03-14 22:47:56 +05:00 committed by GitHub
parent cc6e67f0e2
commit d3f33327c2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 275 additions and 739 deletions

View File

@ -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<Application>().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<SManga>.toPermanentMangaUrls(): List<SManga> {
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<T : Any>(
private val initializer: suspend () -> T,
) {
private val mutex = Mutex()
private var cachedValue: SoftReference<T>? = 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() }
}
}

View File

@ -3,7 +3,7 @@ ext {
extClass = '.AsuraScans' extClass = '.AsuraScans'
themePkg = 'mangathemesia' themePkg = 'mangathemesia'
baseUrl = 'https://asuratoon.com' baseUrl = 'https://asuratoon.com'
overrideVersionCode = 1 overrideVersionCode = 2
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"

View File

@ -1,53 +1,35 @@
package eu.kanade.tachiyomi.extension.en.asurascans package eu.kanade.tachiyomi.extension.en.asurascans
import android.app.Application import eu.kanade.tachiyomi.multisrc.mangathemesia.MangaThemesiaAlt
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.network.interceptor.rateLimit
import eu.kanade.tachiyomi.source.ConfigurableSource
import eu.kanade.tachiyomi.source.model.FilterList 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.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.Request
import okhttp3.Response
import org.jsoup.nodes.Document 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.text.SimpleDateFormat
import java.util.Locale import java.util.Locale
import java.util.concurrent.TimeUnit
class AsuraScans : class AsuraScans : MangaThemesiaAlt(
MangaThemesia( "Asura Scans",
"Asura Scans", "https://asuratoon.com",
"https://asuratoon.com", "en",
"en", dateFormat = SimpleDateFormat("MMM d, yyyy", Locale.US),
dateFormat = SimpleDateFormat("MMM d, yyyy", Locale.US), randomUrlPrefKey = "pref_permanent_manga_url_2_en",
), ) {
ConfigurableSource { init {
// remove legacy preferences
private val preferences by lazy { preferences.run {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000) 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 { override val client = super.client.newBuilder()
preferences.baseUrlHost.let { "https://$it" } .rateLimit(1, 3)
}
override val client: OkHttpClient = super.client.newBuilder()
.addInterceptor(::urlChangeInterceptor)
.addInterceptor(::domainChangeIntercept)
.rateLimit(1, 3, TimeUnit.SECONDS)
.apply { .apply {
val interceptors = interceptors() val interceptors = interceptors()
val index = interceptors.indexOfFirst { "Brotli" in it.javaClass.simpleName } 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, " + 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" "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<MangasPage> {
return super.fetchPopularManga(page).tempUrlToPermIfNeeded()
}
override fun fetchLatestUpdates(page: Int): Observable<MangasPage> {
return super.fetchLatestUpdates(page).tempUrlToPermIfNeeded()
}
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
return super.fetchSearchManga(page, query, filters).tempUrlToPermIfNeeded()
}
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val request = super.searchMangaRequest(page, query, filters) val request = super.searchMangaRequest(page, query, filters)
if (query.isBlank()) return request if (query.isBlank()) return request
@ -93,232 +62,10 @@ class AsuraScans :
.build() .build()
} }
// Temp Url for manga/chapter
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
val newManga = manga.titleToUrlFrag()
return super.fetchChapterList(newManga)
}
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
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 // Skip scriptPages
override fun pageListParse(document: Document): List<Page> { override fun pageListParse(document: Document): List<Page> {
return document.select(pageSelector) return document.select(pageSelector)
.filterNot { it.attr("src").isNullOrEmpty() } .filterNot { it.attr("src").isNullOrEmpty() }
.mapIndexed { i, img -> Page(i, document.location(), img.attr("abs:src")) } .mapIndexed { i, img -> Page(i, document.location(), img.attr("abs:src")) }
} }
private fun Observable<MangasPage>.tempUrlToPermIfNeeded(): Observable<MangasPage> {
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<String, String>
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("""\++$""")
}
} }

View File

@ -3,7 +3,7 @@ ext {
extClass = '.FlameComics' extClass = '.FlameComics'
themePkg = 'mangathemesia' themePkg = 'mangathemesia'
baseUrl = 'https://flamecomics.com' baseUrl = 'https://flamecomics.com'
overrideVersionCode = 0 overrideVersionCode = 1
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"

View File

@ -1,47 +1,30 @@
package eu.kanade.tachiyomi.extension.en.flamecomics package eu.kanade.tachiyomi.extension.en.flamecomics
import android.app.Application
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.BitmapFactory import android.graphics.BitmapFactory
import android.graphics.Canvas import android.graphics.Canvas
import android.graphics.Rect import android.graphics.Rect
import androidx.preference.PreferenceScreen
import androidx.preference.SwitchPreferenceCompat
import eu.kanade.tachiyomi.multisrc.mangathemesia.MangaThemesia import eu.kanade.tachiyomi.multisrc.mangathemesia.MangaThemesia
import eu.kanade.tachiyomi.network.interceptor.rateLimit 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.Page
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import okhttp3.Interceptor import okhttp3.Interceptor
import okhttp3.MediaType.Companion.toMediaType import okhttp3.MediaType.Companion.toMediaType
import okhttp3.Protocol import okhttp3.Protocol
import okhttp3.Response import okhttp3.Response
import okhttp3.ResponseBody.Companion.toResponseBody import okhttp3.ResponseBody.Companion.toResponseBody
import org.jsoup.nodes.Document import org.jsoup.nodes.Document
import rx.Observable
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
class FlameComics : class FlameComics : MangaThemesia(
MangaThemesia( "Flame Comics",
"Flame Comics", "https://flamecomics.com",
"https://flamecomics.com", "en",
"en", mangaUrlDirectory = "/series",
mangaUrlDirectory = "/series", ) {
),
ConfigurableSource {
// Flame Scans -> Flame Comics // Flame Scans -> Flame Comics
override val id = 6350607071566689772 override val id = 6350607071566689772
private val preferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
override val client = super.client.newBuilder() override val client = super.client.newBuilder()
.rateLimit(2, 7) .rateLimit(2, 7)
.addInterceptor(::composedImageIntercept) .addInterceptor(::composedImageIntercept)
@ -130,114 +113,8 @@ class FlameComics :
} }
// Split Image Fixer End // Split Image Fixer End
// Permanent Url start
override fun fetchPopularManga(page: Int): Observable<MangasPage> {
return super.fetchPopularManga(page).tempUrlToPermIfNeeded()
}
override fun fetchLatestUpdates(page: Int): Observable<MangasPage> {
return super.fetchLatestUpdates(page).tempUrlToPermIfNeeded()
}
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
return super.fetchSearchManga(page, query, filters).tempUrlToPermIfNeeded()
}
private fun Observable<MangasPage>.tempUrlToPermIfNeeded(): Observable<MangasPage> {
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 { companion object {
private const val COMPOSED_SUFFIX = "?comp" 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() private val MEDIA_TYPE = "image/png".toMediaType()
} }
} }

View File

@ -3,7 +3,7 @@ ext {
extClass = '.LuminousScans' extClass = '.LuminousScans'
themePkg = 'mangathemesia' themePkg = 'mangathemesia'
baseUrl = 'https://lumitoon.com' baseUrl = 'https://lumitoon.com'
overrideVersionCode = 3 overrideVersionCode = 4
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"

View File

@ -1,57 +1,30 @@
package eu.kanade.tachiyomi.extension.en.luminousscans package eu.kanade.tachiyomi.extension.en.luminousscans
import android.app.Application import eu.kanade.tachiyomi.multisrc.mangathemesia.MangaThemesiaAlt
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.network.interceptor.rateLimit
import eu.kanade.tachiyomi.source.ConfigurableSource
import eu.kanade.tachiyomi.source.model.FilterList 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.Request
import okhttp3.Response
import rx.Observable
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.io.IOException
class LuminousScans : class LuminousScans : MangaThemesiaAlt(
MangaThemesia( "Luminous Scans",
"Luminous Scans", "https://lumitoon.com",
"https://lumitoon.com", "en",
"en", mangaUrlDirectory = "/series",
mangaUrlDirectory = "/series", randomUrlPrefKey = "pref_permanent_manga_url_2_en",
), ) {
ConfigurableSource { init {
// remove legacy preferences
preferences.run {
if (contains("pref_url_map")) {
edit().remove("pref_url_map").apply()
}
}
}
override val client = super.client.newBuilder() override val client = super.client.newBuilder()
.addInterceptor(::urlChangeInterceptor)
.rateLimit(2) .rateLimit(2)
.build() .build()
private val preferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
// Permanent Url for Manga/Chapter End
override fun fetchPopularManga(page: Int): Observable<MangasPage> {
return super.fetchPopularManga(page).tempUrlToPermIfNeeded()
}
override fun fetchLatestUpdates(page: Int): Observable<MangasPage> {
return super.fetchLatestUpdates(page).tempUrlToPermIfNeeded()
}
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
return super.fetchSearchManga(page, query, filters).tempUrlToPermIfNeeded()
}
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val request = super.searchMangaRequest(page, query, filters) val request = super.searchMangaRequest(page, query, filters)
if (query.isBlank()) return request if (query.isBlank()) return request
@ -67,176 +40,4 @@ class LuminousScans :
.url(url) .url(url)
.build() .build()
} }
// Temp Url for manga/chapter
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
val newManga = manga.titleToUrlFrag()
return super.fetchChapterList(newManga)
}
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
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<MangasPage>.tempUrlToPermIfNeeded(): Observable<MangasPage> {
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<String, String>
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("""\++$""")
}
} }

View File

@ -3,7 +3,7 @@ ext {
extClass = '.RizzComic' extClass = '.RizzComic'
themePkg = 'mangathemesia' themePkg = 'mangathemesia'
baseUrl = 'https://rizzcomic.com' baseUrl = 'https://rizzcomic.com'
overrideVersionCode = 0 overrideVersionCode = 1
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"

View File

@ -26,53 +26,49 @@ abstract class SelectFilter(
class SortFilter(defaultOrder: String? = null) : SelectFilter("Sort By", sort, defaultOrder) { class SortFilter(defaultOrder: String? = null) : SelectFilter("Sort By", sort, defaultOrder) {
override val formParameter = "OrderValue" override val formParameter = "OrderValue"
companion object { 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 POPULAR = FilterList(StatusFilter(), TypeFilter(), SortFilter("popular"))
val LATEST = FilterList(StatusFilter(), TypeFilter(), SortFilter("update")) 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) { class StatusFilter : SelectFilter("Status", status) {
override val formParameter = "StatusValue" 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) { class TypeFilter : SelectFilter("Type", type) {
override val formParameter = "TypeValue" 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( class CheckBoxFilter(
name: String, name: String,
val value: String, val value: String,
) : Filter.CheckBox(name) ) : Filter.CheckBox(name)
class GenreFilter( class GenreFilter : FormBodyFilter, Filter.Group<CheckBoxFilter>(
genres: List<Pair<String, String>>,
) : FormBodyFilter, Filter.Group<CheckBoxFilter>(
"Genre", "Genre",
genres.map { CheckBoxFilter(it.first, it.second) }, 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"),
)

View File

@ -1,6 +1,7 @@
package eu.kanade.tachiyomi.extension.en.rizzcomic 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.GET
import eu.kanade.tachiyomi.network.POST import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.network.asObservableSuccess 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.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.Page 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.source.model.SManga
import eu.kanade.tachiyomi.util.asJsoup
import kotlinx.serialization.SerialName import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromString import kotlinx.serialization.decodeFromString
@ -22,7 +21,7 @@ import rx.Observable
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Locale import java.util.Locale
class RizzComic : MangaThemesia( class RizzComic : MangaThemesiaAlt(
"Rizz Comic", "Rizz Comic",
"https://rizzcomic.com", "https://rizzcomic.com",
"en", "en",
@ -40,43 +39,12 @@ class RizzComic : MangaThemesia(
.build() .build()
} }
private var urlPrefix: String? = null override val versionId = 2
private var genreCache: List<Pair<String, String>> = emptyList()
private var attempts = 0
private fun updateCache() { override val slugRegex = Regex("""^r\d+-""")
if ((urlPrefix.isNullOrEmpty() || genreCache.isEmpty()) && attempts < 3) {
runCatching {
val document = client.newCall(GET("$baseUrl$mangaUrlDirectory", headers))
.execute().use { it.asJsoup() }
urlPrefix = document.selectFirst(".listupd a") // don't allow disabling random part setting
?.attr("href") override fun setupPreferenceScreen(screen: PreferenceScreen) { }
?.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")
}
override fun popularMangaRequest(page: Int) = searchMangaRequest(page, "", SortFilter.POPULAR) override fun popularMangaRequest(page: Int) = searchMangaRequest(page, "", SortFilter.POPULAR)
override fun popularMangaParse(response: Response) = searchMangaParse(response) override fun popularMangaParse(response: Response) = searchMangaParse(response)
@ -103,30 +71,17 @@ class RizzComic : MangaThemesia(
} }
override fun getFilterList(): FilterList { override fun getFilterList(): FilterList {
val filters: MutableList<Filter<*>> = mutableListOf( return FilterList(
Filter.Header("Filters don't work with text search"), Filter.Header("Filters don't work with text search"),
SortFilter(), SortFilter(),
StatusFilter(), StatusFilter(),
TypeFilter(), 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 @Serializable
class Comic( class Comic(
val id: Int,
val title: String, val title: String,
@SerialName("image_url") val cover: String? = null, @SerialName("image_url") val cover: String? = null,
@SerialName("long_description") val synopsis: String? = null, @SerialName("long_description") val synopsis: String? = null,
@ -150,13 +105,11 @@ class RizzComic : MangaThemesia(
} }
override fun searchMangaParse(response: Response): MangasPage { override fun searchMangaParse(response: Response): MangasPage {
updateCache()
val result = response.parseAs<List<Comic>>() val result = response.parseAs<List<Comic>>()
val entries = result.map { comic -> val entries = result.map { comic ->
SManga.create().apply { SManga.create().apply {
url = "${comic.slug}#${comic.id}" url = "$mangaUrlDirectory/${comic.slug}/"
title = comic.title title = comic.title
description = comic.synopsis description = comic.synopsis
author = listOfNotNull(comic.author, comic.serialization).joinToString() author = listOfNotNull(comic.author, comic.serialization).joinToString()
@ -166,7 +119,7 @@ class RizzComic : MangaThemesia(
genre = buildList { genre = buildList {
add(comic.type?.capitalize()) add(comic.type?.capitalize())
comic.genreIds?.onEach { gId -> comic.genreIds?.onEach { gId ->
add(genreCache.firstOrNull { it.second == gId }?.first) add(genres.firstOrNull { it.second == gId }?.first)
} }
}.filterNotNull().joinToString() }.filterNotNull().joinToString()
initialized = true initialized = true
@ -182,37 +135,6 @@ class RizzComic : MangaThemesia(
.map { mangaDetailsParse(it).apply { description = manga.description } } .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<SChapter> {
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 { override fun imageRequest(page: Page): Request {
val newHeaders = headersBuilder() val newHeaders = headersBuilder()
.set("Accept", "image/avif,image/webp,image/png,image/jpeg,*/*") .set("Accept", "image/avif,image/webp,image/png,image/jpeg,*/*")