From 2cf2fcfc4f85dc9e0b4d3e3abe304a03df3c4353 Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Wed, 1 Jan 2025 10:28:00 +0700 Subject: [PATCH] refactor: WIP delegated source refactor J2K only handles deep link, which was disabled when I forked it as Yokai... Might gonna re-introduce it for some sources I used later (mainly Cubari tbh) --- .../kanade/tachiyomi/source/SourceManager.kt | 40 +- .../source/online/DelegatedHttpSource.kt | 47 --- .../tachiyomi/source/online/all/Cubari.kt | 29 +- .../tachiyomi/source/online/all/MangaDex.kt | 32 +- .../source/online/english/FoolSlide.kt | 110 ----- .../source/online/english/KireiCake.kt | 28 -- .../source/online/english/MangaPlus.kt | 41 +- .../tachiyomi/ui/reader/ReaderViewModel.kt | 10 +- .../source/online/DelegatedHttpSource.kt | 378 ++++++++++++++++++ .../tachiyomi/source/online/HttpSource.kt | 20 +- 10 files changed, 460 insertions(+), 275 deletions(-) delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/source/online/DelegatedHttpSource.kt delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/source/online/english/FoolSlide.kt delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/source/online/english/KireiCake.kt create mode 100644 source/api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/online/DelegatedHttpSource.kt diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/SourceManager.kt b/app/src/main/java/eu/kanade/tachiyomi/source/SourceManager.kt index 407aa4a491..b503491fc7 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/source/SourceManager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/source/SourceManager.kt @@ -1,20 +1,13 @@ package eu.kanade.tachiyomi.source import android.content.Context -import eu.kanade.tachiyomi.R -import yokai.i18n.MR -import yokai.util.lang.getString -import dev.icerock.moko.resources.compose.stringResource import eu.kanade.tachiyomi.extension.ExtensionManager 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.online.DelegatedHttpSource import eu.kanade.tachiyomi.source.online.HttpSource -import eu.kanade.tachiyomi.source.online.all.Cubari -import eu.kanade.tachiyomi.source.online.all.MangaDex -import eu.kanade.tachiyomi.source.online.english.KireiCake -import eu.kanade.tachiyomi.source.online.english.MangaPlus +import java.util.concurrent.ConcurrentHashMap import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job @@ -24,7 +17,8 @@ import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking -import java.util.concurrent.ConcurrentHashMap +import yokai.i18n.MR +import yokai.util.lang.getString class SourceManager( private val context: Context, @@ -40,28 +34,8 @@ class SourceManager( val catalogueSources: Flow> = sourcesMapFlow.map { it.values.filterIsInstance() } val onlineSources: Flow> = catalogueSources.map { it.filterIsInstance() } - private val delegatedSources = listOf( - DelegatedSource( - "reader.kireicake.com", - 5509224355268673176, - KireiCake(), - ), - DelegatedSource( - "mangadex.org", - 2499283573021220255, - MangaDex(), - ), - DelegatedSource( - "mangaplus.shueisha.co.jp", - 1998944621602463790, - MangaPlus(), - ), - DelegatedSource( - "cubari.moe", - 6338219619148105941, - Cubari(), - ), - ).associateBy { it.sourceId } + // FIXME: Delegated source, unused at the moment, J2K only delegate deep links + private val delegatedSources = emptyList().associateBy { it.sourceId } init { scope.launch { @@ -71,8 +45,8 @@ class SourceManager( extensions.forEach { extension -> extension.sources.forEach { mutableMap[it.id] = it - delegatedSources[it.id]?.delegatedHttpSource?.delegate = it as? HttpSource -// registerStubSource(it) + //delegatedSources[it.id]?.delegatedHttpSource?.delegate = it as? HttpSource + //registerStubSource(it) } } sourcesMapFlow.value = mutableMap diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/online/DelegatedHttpSource.kt b/app/src/main/java/eu/kanade/tachiyomi/source/online/DelegatedHttpSource.kt deleted file mode 100644 index 1680498ce0..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/source/online/DelegatedHttpSource.kt +++ /dev/null @@ -1,47 +0,0 @@ -package eu.kanade.tachiyomi.source.online - -import android.net.Uri -import eu.kanade.tachiyomi.data.database.models.Chapter -import eu.kanade.tachiyomi.data.database.models.MangaImpl -import eu.kanade.tachiyomi.data.database.models.create -import eu.kanade.tachiyomi.domain.manga.models.Manga -import eu.kanade.tachiyomi.network.NetworkHelper -import eu.kanade.tachiyomi.source.model.SChapter -import uy.kohesive.injekt.injectLazy -import yokai.domain.chapter.interactor.GetChapter -import yokai.domain.manga.interactor.GetManga - -abstract class DelegatedHttpSource { - - var delegate: HttpSource? = null - abstract val domainName: String - - protected val getChapter: GetChapter by injectLazy() - protected val getManga: GetManga by injectLazy() - - protected val network: NetworkHelper by injectLazy() - - abstract fun canOpenUrl(uri: Uri): Boolean - abstract fun chapterUrl(uri: Uri): String? - open fun pageNumber(uri: Uri): Int? = uri.pathSegments.lastOrNull()?.toIntOrNull() - abstract suspend fun fetchMangaFromChapterUrl(uri: Uri): Triple>? - - protected open suspend fun getMangaInfo(url: String): Manga? { - val id = delegate?.id ?: return null - val manga = Manga.create(url, "", id) - val networkManga = delegate?.getMangaDetails(manga.copy()) ?: return null - val newManga = MangaImpl().apply { - this.url = url - title = try { networkManga.title } catch (e: Exception) { "" } - source = id - } - newManga.copyFrom(networkManga) - return newManga - } - - suspend fun getChapters(url: String): List? { - val id = delegate?.id ?: return null - val manga = Manga.create(url, "", id) - return delegate?.getChapterList(manga) - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/online/all/Cubari.kt b/app/src/main/java/eu/kanade/tachiyomi/source/online/all/Cubari.kt index 78816561fe..a0d0db6da4 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/source/online/all/Cubari.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/source/online/all/Cubari.kt @@ -1,22 +1,31 @@ package eu.kanade.tachiyomi.source.online.all import android.net.Uri -import eu.kanade.tachiyomi.data.database.models.Chapter import eu.kanade.tachiyomi.data.database.models.toChapter import eu.kanade.tachiyomi.data.preference.PreferencesHelper -import eu.kanade.tachiyomi.domain.manga.models.Manga import eu.kanade.tachiyomi.source.model.SChapter +import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.online.DelegatedHttpSource +import eu.kanade.tachiyomi.source.online.HttpSource import java.util.Locale import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async import kotlinx.coroutines.withContext import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get +import yokai.domain.chapter.interactor.GetChapter +import yokai.domain.manga.interactor.GetManga import yokai.i18n.MR import yokai.util.lang.getString -class Cubari : DelegatedHttpSource() { +class Cubari(delegate: HttpSource) : + DelegatedHttpSource(delegate) { + + private val getManga: GetManga = Injekt.get() + private val getChapter: GetChapter = Injekt.get() + + override val lang = "all" + override val domainName: String = "cubari" override fun canOpenUrl(uri: Uri): Boolean = true @@ -24,24 +33,24 @@ class Cubari : DelegatedHttpSource() { override fun pageNumber(uri: Uri): Int? = uri.pathSegments.getOrNull(4)?.toIntOrNull() - override suspend fun fetchMangaFromChapterUrl(uri: Uri): Triple>? { + override suspend fun fetchMangaFromChapterUrl(uri: Uri): Triple>? { val cubariType = uri.pathSegments.getOrNull(1)?.lowercase(Locale.ROOT) ?: return null val cubariPath = uri.pathSegments.getOrNull(2) ?: return null val chapterNumber = uri.pathSegments.getOrNull(3)?.replace("-", ".")?.toFloatOrNull() ?: return null val mangaUrl = "/read/$cubariType/$cubariPath" return withContext(Dispatchers.IO) { val deferredManga = async { - getManga.awaitByUrlAndSource(mangaUrl, delegate?.id!!) ?: getMangaInfo(mangaUrl) + getManga.awaitByUrlAndSource(mangaUrl, delegate.id) ?: getMangaDetailsByUrl(mangaUrl) } val deferredChapters = async { - getManga.awaitByUrlAndSource(mangaUrl, delegate?.id!!)?.let { manga -> + getManga.awaitByUrlAndSource(mangaUrl, delegate.id)?.let { manga -> val chapters = getChapter.awaitAll(manga, false) val chapter = findChapter(chapters, cubariType, chapterNumber) if (chapter != null) { return@async chapters } } - getChapters(mangaUrl) + getChapterListByUrl(mangaUrl) } val manga = deferredManga.await() val chapters = deferredChapters.await() @@ -50,11 +59,7 @@ class Cubari : DelegatedHttpSource() { ?: error( context.getString(MR.strings.chapter_not_found), ) - if (manga != null) { - Triple(trueChapter, manga, chapters.orEmpty()) - } else { - null - } + Triple(trueChapter, manga, chapters) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/online/all/MangaDex.kt b/app/src/main/java/eu/kanade/tachiyomi/source/online/all/MangaDex.kt index 15f011c18e..f87714d5d0 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/source/online/all/MangaDex.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/source/online/all/MangaDex.kt @@ -1,15 +1,15 @@ package eu.kanade.tachiyomi.source.online.all import android.net.Uri -import eu.kanade.tachiyomi.data.database.models.Chapter import eu.kanade.tachiyomi.data.database.models.toChapter import eu.kanade.tachiyomi.data.preference.PreferencesHelper -import eu.kanade.tachiyomi.domain.manga.models.Manga import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.await import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.model.SChapter +import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.online.DelegatedHttpSource +import eu.kanade.tachiyomi.source.online.HttpSource import java.util.Locale import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async @@ -20,10 +20,15 @@ import okhttp3.CacheControl import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get import uy.kohesive.injekt.injectLazy +import yokai.domain.manga.interactor.GetManga import yokai.i18n.MR import yokai.util.lang.getString -class MangaDex : DelegatedHttpSource() { +class MangaDex(delegate: HttpSource) : DelegatedHttpSource(delegate) { + + private val getManga: GetManga = Injekt.get() + + override val lang: String = "all" override val domainName: String = "mangadex" @@ -42,13 +47,13 @@ class MangaDex : DelegatedHttpSource() { return uri.pathSegments.getOrNull(2)?.toIntOrNull() } - override suspend fun fetchMangaFromChapterUrl(uri: Uri): Triple>? { + override suspend fun fetchMangaFromChapterUrl(uri: Uri): Triple>? { val url = chapterUrl(uri) ?: return null val request = GET("https:///api.mangadex.org/v2$url", delegate!!.headers, CacheControl.FORCE_NETWORK) val response = network.client.newCall(request).await() if (response.code != 200) throw Exception("HTTP error ${response.code}") - val body = response.body.string().orEmpty() + val body = response.body.string() if (body.isEmpty()) { throw Exception("Null Response") } @@ -56,28 +61,19 @@ class MangaDex : DelegatedHttpSource() { val jsonObject = Json.decodeFromString(body) val dataObject = jsonObject.data ?: throw Exception("Chapter not found") val mangaId = dataObject.mangaId ?: throw Exception("No manga associated with chapter") - val langCode = getRealLangCode(dataObject.language ?: "en").uppercase(Locale.getDefault()) - // Use the correct MangaDex source based on the language code, or the api will not return - // the correct chapter list - delegate = sourceManager.getOnlineSources().find { it.toString() == "MangaDex ($langCode)" } - ?: error("Source not found") val mangaUrl = "/manga/$mangaId/" return withContext(Dispatchers.IO) { val deferredManga = async { - getManga.awaitByUrlAndSource(mangaUrl, delegate?.id!!) ?: getMangaInfo(mangaUrl) + getManga.awaitByUrlAndSource(mangaUrl, delegate.id) ?: getMangaDetailsByUrl(mangaUrl) } - val deferredChapters = async { getChapters(mangaUrl) } + val deferredChapters = async { getChapterListByUrl(mangaUrl) } val manga = deferredManga.await() val chapters = deferredChapters.await() val context = Injekt.get().context - val trueChapter = chapters?.find { it.url == "/api$url" }?.toChapter() ?: error( + val trueChapter = chapters.find { it.url == "/api$url" }?.toChapter() ?: error( context.getString(MR.strings.chapter_not_found), ) - if (manga != null) { - Triple(trueChapter, manga, chapters.orEmpty()) - } else { - null - } + Triple(trueChapter, manga, chapters) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/online/english/FoolSlide.kt b/app/src/main/java/eu/kanade/tachiyomi/source/online/english/FoolSlide.kt deleted file mode 100644 index adeec2e93c..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/source/online/english/FoolSlide.kt +++ /dev/null @@ -1,110 +0,0 @@ -package eu.kanade.tachiyomi.source.online.english - -import android.net.Uri -import eu.kanade.tachiyomi.data.database.models.Chapter -import eu.kanade.tachiyomi.data.database.models.MangaImpl -import eu.kanade.tachiyomi.data.database.models.toChapter -import eu.kanade.tachiyomi.data.preference.PreferencesHelper -import eu.kanade.tachiyomi.domain.manga.models.Manga -import eu.kanade.tachiyomi.network.GET -import eu.kanade.tachiyomi.network.POST -import eu.kanade.tachiyomi.network.await -import eu.kanade.tachiyomi.source.model.SChapter -import eu.kanade.tachiyomi.source.online.DelegatedHttpSource -import eu.kanade.tachiyomi.util.asJsoup -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.async -import kotlinx.coroutines.withContext -import okhttp3.FormBody -import okhttp3.Request -import uy.kohesive.injekt.Injekt -import uy.kohesive.injekt.api.get -import yokai.i18n.MR -import yokai.util.lang.getString - -open class FoolSlide(override val domainName: String, private val urlModifier: String = "") : - DelegatedHttpSource - () { - - override fun canOpenUrl(uri: Uri): Boolean = true - - override fun chapterUrl(uri: Uri): String? { - val offset = if (urlModifier.isEmpty()) 0 else 1 - val mangaName = uri.pathSegments.getOrNull(1 + offset) ?: return null - val lang = uri.pathSegments.getOrNull(2 + offset) ?: return null - val volume = uri.pathSegments.getOrNull(3 + offset) ?: return null - val chapterNumber = uri.pathSegments.getOrNull(4 + offset) ?: return null - val subChapterNumber = uri.pathSegments.getOrNull(5 + offset)?.toIntOrNull()?.toString() - return "$urlModifier/read/" + listOfNotNull( - mangaName, - lang, - volume, - chapterNumber, - subChapterNumber, - ).joinToString("/") + "/" - } - - override fun pageNumber(uri: Uri): Int? { - val count = uri.pathSegments.count() - if (count > 2 && uri.pathSegments[count - 2] == "page") { - return super.pageNumber(uri) - } - return null - } - - override suspend fun fetchMangaFromChapterUrl(uri: Uri): Triple>? { - val offset = if (urlModifier.isEmpty()) 0 else 1 - val mangaName = uri.pathSegments.getOrNull(1 + offset) ?: return null - var chapterNumber = uri.pathSegments.getOrNull(4 + offset) ?: return null - val subChapterNumber = uri.pathSegments.getOrNull(5 + offset)?.toIntOrNull() - if (subChapterNumber != null) { - chapterNumber += ".$subChapterNumber" - } - return withContext(Dispatchers.IO) { - val mangaUrl = "$urlModifier/series/$mangaName/" - val sourceId = delegate?.id ?: return@withContext null - val deferredManga = async { - getManga.awaitByUrlAndSource(mangaUrl, sourceId) ?: getManga(mangaUrl) - } - val chapterUrl = chapterUrl(uri) - val deferredChapters = async { getChapters(mangaUrl) } - val manga = deferredManga.await() - val chapters = deferredChapters.await() - val context = Injekt.get().context - val trueChapter = chapters?.find { it.url == chapterUrl }?.toChapter() ?: error( - context.getString(MR.strings.chapter_not_found), - ) - if (manga != null) Triple(trueChapter, manga, chapters) else null - } - } - - open suspend fun getManga(url: String): Manga? { - val request = GET("${delegate!!.baseUrl}$url") - val document = network.client.newCall(allowAdult(request)).await().asJsoup() - val mangaDetailsInfoSelector = "div.info" - val infoElement = document.select(mangaDetailsInfoSelector).first()?.text() ?: return null - return MangaImpl().apply { - this.url = url - source = delegate?.id ?: -1 - title = infoElement.substringAfter("Title:").substringBefore("Author:").trim() - author = infoElement.substringAfter("Author:").substringBefore("Artist:").trim() - artist = infoElement.substringAfter("Artist:").substringBefore("Synopsis:").trim() - description = infoElement.substringAfter("Synopsis:").trim() - thumbnail_url = document.select("div.thumbnail img").firstOrNull()?.attr("abs:src")?.trim() - } - } - - /** - * Transform a GET request into a POST request that automatically authorizes all adult content - */ - private fun allowAdult(request: Request) = allowAdult(request.url.toString()) - - private fun allowAdult(url: String): Request { - return POST( - url, - body = FormBody.Builder() - .add("adult", "true") - .build(), - ) - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/online/english/KireiCake.kt b/app/src/main/java/eu/kanade/tachiyomi/source/online/english/KireiCake.kt deleted file mode 100644 index ecad868775..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/source/online/english/KireiCake.kt +++ /dev/null @@ -1,28 +0,0 @@ -package eu.kanade.tachiyomi.source.online.english - -import eu.kanade.tachiyomi.data.database.models.MangaImpl -import eu.kanade.tachiyomi.domain.manga.models.Manga -import eu.kanade.tachiyomi.network.GET -import eu.kanade.tachiyomi.network.await -import eu.kanade.tachiyomi.util.asJsoup -import eu.kanade.tachiyomi.util.lang.capitalizeWords - -class KireiCake : FoolSlide("kireicake") { - - override suspend fun getManga(url: String): Manga? { - val request = GET("${delegate!!.baseUrl}$url") - val document = network.client.newCall(request).await().asJsoup() - val mangaDetailsInfoSelector = "div.info" - return MangaImpl().apply { - this.url = url - source = delegate?.id ?: -1 - title = document.select("$mangaDetailsInfoSelector li:has(b:contains(title))").first() - ?.ownText()?.substringAfter(":")?.trim() - ?: url.split("/").last().replace("_", " " + "").capitalizeWords() - description = - document.select("$mangaDetailsInfoSelector li:has(b:contains(description))").first() - ?.ownText()?.substringAfter(":") - thumbnail_url = document.select("div.thumbnail img").firstOrNull()?.attr("abs:src") - } - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/online/english/MangaPlus.kt b/app/src/main/java/eu/kanade/tachiyomi/source/online/english/MangaPlus.kt index 710b6e4793..ebcbc527ce 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/source/online/english/MangaPlus.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/source/online/english/MangaPlus.kt @@ -1,24 +1,31 @@ package eu.kanade.tachiyomi.source.online.english import android.net.Uri -import eu.kanade.tachiyomi.data.database.models.Chapter import eu.kanade.tachiyomi.data.database.models.toChapter import eu.kanade.tachiyomi.data.preference.PreferencesHelper -import eu.kanade.tachiyomi.domain.manga.models.Manga import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.await import eu.kanade.tachiyomi.source.model.SChapter +import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.online.DelegatedHttpSource +import eu.kanade.tachiyomi.source.online.HttpSource import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async import kotlinx.coroutines.withContext import okhttp3.CacheControl import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get +import yokai.domain.manga.interactor.GetManga import yokai.i18n.MR import yokai.util.lang.getString -class MangaPlus : DelegatedHttpSource() { +class MangaPlus(delegate: HttpSource) : + DelegatedHttpSource(delegate) { + + private val getManga: GetManga = Injekt.get() + + override val lang: String get() = delegate.lang + override val domainName: String = "jumpg-webapi.tokyo-cdn" private val titleIdRegex = @@ -34,11 +41,11 @@ class MangaPlus : DelegatedHttpSource() { override fun pageNumber(uri: Uri): Int? = null - override suspend fun fetchMangaFromChapterUrl(uri: Uri): Triple>? { + override suspend fun fetchMangaFromChapterUrl(uri: Uri): Triple>? { val url = chapterUrl(uri) ?: return null val request = GET( chapterUrlTemplate.replace("##", uri.pathSegments[1]), - delegate!!.headers, + delegate.headers, CacheControl.FORCE_NETWORK, ) return withContext(Dispatchers.IO) { @@ -53,26 +60,22 @@ class MangaPlus : DelegatedHttpSource() { val trimmedTitle = title.substring(0, title.length - 1) val mangaUrl = "#/titles/$titleId" val deferredManga = async { - getManga.awaitByUrlAndSource(mangaUrl, delegate?.id!!) ?: getMangaInfo(mangaUrl) + getManga.awaitByUrlAndSource(mangaUrl, delegate.id) ?: getMangaDetailsByUrl(mangaUrl) } - val deferredChapters = async { getChapters(mangaUrl) } + val deferredChapters = async { getChapterListByUrl(mangaUrl) } val manga = deferredManga.await() val chapters = deferredChapters.await() val context = Injekt.get().context - val trueChapter = chapters?.find { it.url == url }?.toChapter() ?: error( + val trueChapter = chapters.find { it.url == url }?.toChapter() ?: error( context.getString(MR.strings.chapter_not_found), ) - if (manga != null) { - Triple( - trueChapter, - manga.apply { - this.title = trimmedTitle - }, - chapters, - ) - } else { - null - } + Triple( + trueChapter, + manga.apply { + this.title = trimmedTitle + }, + chapters, + ) } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderViewModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderViewModel.kt index cf0a856541..e2ec854e73 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderViewModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderViewModel.kt @@ -12,6 +12,7 @@ import com.hippo.unifile.UniFile import eu.kanade.tachiyomi.data.cache.CoverCache import eu.kanade.tachiyomi.data.database.models.Chapter import eu.kanade.tachiyomi.data.database.models.History +import eu.kanade.tachiyomi.data.database.models.create import eu.kanade.tachiyomi.data.database.models.defaultReaderType import eu.kanade.tachiyomi.data.database.models.orientationType import eu.kanade.tachiyomi.data.database.models.readingModeType @@ -307,6 +308,7 @@ class ReaderViewModel( return delegatedSource.pageNumber(url)?.minus(1) } + // FIXME: Unused at the moment, handles J2K's delegated deep link, refactor or remove later suspend fun loadChapterURL(url: Uri) { val host = url.host ?: return val context = Injekt.get() @@ -314,9 +316,7 @@ class ReaderViewModel( context.getString(MR.strings.source_not_installed), ) val chapterUrl = delegatedSource.chapterUrl(url) - val sourceId = delegatedSource.delegate?.id ?: error( - context.getString(MR.strings.source_not_installed), - ) + val sourceId = delegatedSource.delegate.id if (chapterUrl != null) { val dbChapter = getChapter.awaitAllByUrl(chapterUrl, false).find { val source = getManga.awaitById(it.manga_id!!)?.source ?: return@find false @@ -334,7 +334,9 @@ class ReaderViewModel( } val info = delegatedSource.fetchMangaFromChapterUrl(url) if (info != null) { - val (chapter, manga, chapters) = info + val (sChapter, sManga, chapters) = info + val manga = Manga.create(sourceId).apply { copyFrom(sManga) } + val chapter = Chapter.create().apply { copyFrom(sChapter) } val id = insertManga.await(manga) manga.id = id ?: manga.id chapter.manga_id = manga.id diff --git a/source/api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/online/DelegatedHttpSource.kt b/source/api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/online/DelegatedHttpSource.kt new file mode 100644 index 0000000000..cbcf769461 --- /dev/null +++ b/source/api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/online/DelegatedHttpSource.kt @@ -0,0 +1,378 @@ +package eu.kanade.tachiyomi.source.online + +import android.net.Uri +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.OkHttpClient +import okhttp3.Request +import okhttp3.Response +import rx.Observable + +abstract class DelegatedHttpSource(val delegate: HttpSource): HttpSource() { + /** + * Returns the request for the popular manga given the page. + * + * @param page the page number to retrieve. + */ + override fun popularMangaRequest(page: Int) = + throw UnsupportedOperationException("Should never be called!") + + /** + * Parses the response from the site and returns a [MangasPage] object. + * + * @param response the response from the site. + */ + override fun popularMangaParse(response: Response) = + throw UnsupportedOperationException("Should never be called!") + + /** + * Returns the request for the search manga given the page. + * + * @param page the page number to retrieve. + * @param query the search query. + * @param filters the list of filters to apply. + */ + override fun searchMangaRequest(page: Int, query: String, filters: FilterList) = + throw UnsupportedOperationException("Should never be called!") + + /** + * Parses the response from the site and returns a [MangasPage] object. + * + * @param response the response from the site. + */ + override fun searchMangaParse(response: Response) = + throw UnsupportedOperationException("Should never be called!") + + /** + * Returns the request for latest manga given the page. + * + * @param page the page number to retrieve. + */ + override fun latestUpdatesRequest(page: Int) = + throw UnsupportedOperationException("Should never be called!") + + /** + * Parses the response from the site and returns a [MangasPage] object. + * + * @param response the response from the site. + */ + override fun latestUpdatesParse(response: Response) = + throw UnsupportedOperationException("Should never be called!") + + /** + * Parses the response from the site and returns the details of a manga. + * + * @param response the response from the site. + */ + override fun mangaDetailsParse(response: Response) = + throw UnsupportedOperationException("Should never be called!") + + /** + * Parses the response from the site and returns a list of chapters. + * + * @param response the response from the site. + */ + override fun chapterListParse(response: Response) = + throw UnsupportedOperationException("Should never be called!") + + /** + * Parses the response from the site and returns a SChapter Object. + * + * @param response the response from the site. + */ + override fun chapterPageParse(response: Response) = + throw UnsupportedOperationException("Should never be called!") + + /** + * Parses the response from the site and returns a list of pages. + * + * @param response the response from the site. + */ + override fun pageListParse(response: Response) = + throw UnsupportedOperationException("Should never be called!") + + /** + * Parses the response from the site and returns the absolute url to the source image. + * + * @param response the response from the site. + */ + override fun imageUrlParse(response: Response) = + throw UnsupportedOperationException("Should never be called!") + + abstract val domainName: String + + /** + * Base url of the website without the trailing slash, like: http://mysite.com + */ + override val baseUrl get() = delegate.baseUrl + + /** + * Headers used for requests. + */ + override val headers get() = delegate.headers + + /** + * Whether the source has support for latest updates. + */ + override val supportsLatest get() = delegate.supportsLatest + + /** + * Name of the source. + */ + final override val name get() = delegate.name + + // ===> OPTIONAL FIELDS + + /** + * Id of the source. By default it uses a generated id using the first 16 characters (64 bits) + * of the MD5 of the string: sourcename/language/versionId + * Note the generated id sets the sign bit to 0. + */ + override val id get() = delegate.id + + /** + * Default network client for doing requests. + */ + final override val client get() = delegate.client + + /** + * You must NEVER call super.client if you override this! + */ + open val baseHttpClient: OkHttpClient? = null + open val networkHttpClient: OkHttpClient get() = network.client + + /** + * Visible name of the source. + */ + override fun toString() = delegate.toString() + + /** + * Returns an observable containing a page with a list of manga. Normally it's not needed to + * override this method. + * + * @param page the page number to retrieve. + */ + @Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getPopularManga")) + override fun fetchPopularManga(page: Int): Observable { + ensureDelegateCompatible() + return delegate.fetchPopularManga(page) + } + + override suspend fun getPopularManga(page: Int): MangasPage { + ensureDelegateCompatible() + return delegate.getPopularManga(page) + } + + /** + * Returns an observable containing a page with a list of manga. Normally it's not needed to + * override this method. + * + * @param page the page number to retrieve. + * @param query the search query. + * @param filters the list of filters to apply. + */ + @Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getSearchManga")) + override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable { + ensureDelegateCompatible() + return delegate.fetchSearchManga(page, query, filters) + } + + override suspend fun getSearchManga(page: Int, query: String, filters: FilterList): MangasPage { + ensureDelegateCompatible() + return delegate.getSearchManga(page, query, filters) + } + + /** + * Returns an observable containing a page with a list of latest manga updates. + * + * @param page the page number to retrieve. + */ + @Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getLatestUpdates")) + override fun fetchLatestUpdates(page: Int): Observable { + ensureDelegateCompatible() + return delegate.fetchLatestUpdates(page) + } + + override suspend fun getLatestUpdates(page: Int): MangasPage { + ensureDelegateCompatible() + return delegate.getLatestUpdates(page) + } + + /** + * Returns an observable with the updated details for a manga. Normally it's not needed to + * override this method. + * + * @param manga the manga to be updated. + */ + @Deprecated("Use the 1.x API instead", replaceWith = ReplaceWith("getMangaDetails")) + override fun fetchMangaDetails(manga: SManga): Observable { + ensureDelegateCompatible() + return delegate.fetchMangaDetails(manga) + } + + /** + * [1.x API] Get the updated details for a manga. + */ + override suspend fun getMangaDetails(manga: SManga): SManga { + ensureDelegateCompatible() + return delegate.getMangaDetails(manga) + } + + /** + * Returns the request for the details of a manga. Override only if it's needed to change the + * url, send different headers or request method like POST. + * + * @param manga the manga to be updated. + */ + override fun mangaDetailsRequest(manga: SManga): Request { + ensureDelegateCompatible() + return delegate.mangaDetailsRequest(manga) + } + + /** + * Returns an observable with the updated chapter list for a manga. Normally it's not needed to + * override this method. If a manga is licensed an empty chapter list observable is returned + * + * @param manga the manga to look for chapters. + */ + @Deprecated("Use the 1.x API instead", replaceWith = ReplaceWith("getChapterList")) + override fun fetchChapterList(manga: SManga): Observable> { + ensureDelegateCompatible() + return delegate.fetchChapterList(manga) + } + + /** + * [1.x API] Get all the available chapters for a manga. + */ + override suspend fun getChapterList(manga: SManga): List { + ensureDelegateCompatible() + return delegate.getChapterList(manga) + } + + /** + * Returns an observable with the page list for a chapter. + * + * @param chapter the chapter whose page list has to be fetched. + */ + @Deprecated("Use the 1.x API instead", replaceWith = ReplaceWith("getPageList")) + override fun fetchPageList(chapter: SChapter): Observable> { + ensureDelegateCompatible() + return delegate.fetchPageList(chapter) + } + + /** + * [1.x API] Get the list of pages a chapter has. + */ + override suspend fun getPageList(chapter: SChapter): List { + ensureDelegateCompatible() + return delegate.getPageList(chapter) + } + + /** + * Returns an observable with the page containing the source url of the image. If there's any + * error, it will return null instead of throwing an exception. + * + * @param page the page whose source image has to be fetched. + */ + @Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getImageUrl")) + override fun fetchImageUrl(page: Page): Observable { + ensureDelegateCompatible() + return delegate.fetchImageUrl(page) + } + + override suspend fun getImageUrl(page: Page): String { + ensureDelegateCompatible() + return delegate.getImageUrl(page) + } + + /** + * Returns the response of the source image. + * + * @param page the page whose source image has to be downloaded. + */ + override suspend fun getImage(page: Page): Response { + ensureDelegateCompatible() + return delegate.getImage(page) + } + + /** + * Returns the url of the provided manga + * + * @since extensions-lib 1.4 + * @param manga the manga + * @return url of the manga + */ + override fun getMangaUrl(manga: SManga): String { + ensureDelegateCompatible() + return delegate.getMangaUrl(manga) + } + + /** + * Returns the url of the provided chapter + * + * @since extensions-lib 1.4 + * @param chapter the chapter + * @return url of the chapter + */ + override fun getChapterUrl(chapter: SChapter): String { + ensureDelegateCompatible() + return delegate.getChapterUrl(chapter) + } + + /** + * Called before inserting a new chapter into database. Use it if you need to override chapter + * fields, like the title or the chapter number. Do not change anything to [manga]. + * + * @param chapter the chapter to be added. + * @param manga the manga of the chapter. + */ + override fun prepareNewChapter(chapter: SChapter, manga: SManga) { + ensureDelegateCompatible() + return delegate.prepareNewChapter(chapter, manga) + } + + /** + * Returns the list of filters for the source. + */ + override fun getFilterList() = delegate.getFilterList() + + abstract fun canOpenUrl(uri: Uri): Boolean + abstract fun chapterUrl(uri: Uri): String? + open fun pageNumber(uri: Uri): Int? = uri.pathSegments.lastOrNull()?.toIntOrNull() + abstract suspend fun fetchMangaFromChapterUrl(uri: Uri): Triple>? + + open suspend fun getMangaDetailsByUrl(url: String): SManga { + val manga = SManga.create().apply { + this.url = url + this.title = "" + } + return delegate.getMangaDetails(manga.copy()) + } + + open suspend fun getChapterListByUrl(url: String): List { + val manga = SManga.create().apply { + this.url = url + this.title = "" + } + return delegate.getChapterList(manga) + } + + protected open fun ensureDelegateCompatible() { + if (versionId != delegate.versionId || lang != delegate.lang) { + throw IncompatibleDelegateException( + "Delegate source is not compatible (" + + "versionId: $versionId <=> ${delegate.versionId}, lang: $lang <=> ${delegate.lang}" + + ")!", + ) + } + } + + class IncompatibleDelegateException(message: String) : RuntimeException(message) + + init { + delegate.bindDelegate(this) + } +} diff --git a/source/api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/online/HttpSource.kt b/source/api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/online/HttpSource.kt index 05494e6062..4faaffe463 100644 --- a/source/api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/online/HttpSource.kt +++ b/source/api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/online/HttpSource.kt @@ -12,21 +12,22 @@ 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.awaitSingle +import java.net.URI +import java.net.URISyntaxException +import java.security.MessageDigest import okhttp3.Headers import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.Response import rx.Observable import uy.kohesive.injekt.injectLazy -import java.net.URI -import java.net.URISyntaxException -import java.security.MessageDigest /** * A simple implementation for sources from a website. */ @Suppress("unused") abstract class HttpSource : CatalogueSource { + private var delegate: DelegatedHttpSource? = null /** * Network service. @@ -59,7 +60,7 @@ abstract class HttpSource : CatalogueSource { /** * Headers used for requests. */ - val headers: Headers by lazy { headersBuilder().build() } + open val headers: Headers by lazy { headersBuilder().build() } /** * Default network client for doing requests. @@ -67,6 +68,10 @@ abstract class HttpSource : CatalogueSource { open val client: OkHttpClient get() = network.client + fun bindDelegate(delegate: DelegatedHttpSource) { + this.delegate = delegate + } + /** * Generates a unique ID for the source based on the provided [name], [lang] and * [versionId]. It will use the first 16 characters (64 bits) of the MD5 of the string @@ -283,6 +288,13 @@ abstract class HttpSource : CatalogueSource { */ protected abstract fun chapterListParse(response: Response): List + /** + * Parses the response from the site and returns a SChapter Object. + * + * @param response the response from the site. + */ + protected abstract fun chapterPageParse(response: Response): SChapter + /** * Get the list of pages a chapter has. Pages should be returned * in the expected order; the index is ignored.