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'
themePkg = 'mangathemesia'
baseUrl = 'https://asuratoon.com'
overrideVersionCode = 1
overrideVersionCode = 2
}
apply from: "$rootDir/common.gradle"

View File

@ -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<Application>().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<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 {
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<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
override fun pageListParse(document: Document): List<Page> {
return document.select(pageSelector)
.filterNot { it.attr("src").isNullOrEmpty() }
.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'
themePkg = 'mangathemesia'
baseUrl = 'https://flamecomics.com'
overrideVersionCode = 0
overrideVersionCode = 1
}
apply from: "$rootDir/common.gradle"

View File

@ -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<Application>().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<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 {
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()
}
}

View File

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

View File

@ -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<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 {
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<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'
themePkg = 'mangathemesia'
baseUrl = 'https://rizzcomic.com'
overrideVersionCode = 0
overrideVersionCode = 1
}
apply from: "$rootDir/common.gradle"

View File

@ -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<Pair<String, String>>,
) : FormBodyFilter, Filter.Group<CheckBoxFilter>(
class GenreFilter : FormBodyFilter, Filter.Group<CheckBoxFilter>(
"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"),
)

View File

@ -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<Pair<String, String>> = 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<Filter<*>> = 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<List<Comic>>()
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<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 {
val newHeaders = headersBuilder()
.set("Accept", "image/avif,image/webp,image/png,image/jpeg,*/*")