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)
This commit is contained in:
Ahmad Ansori Palembani 2025-01-01 10:28:00 +07:00
parent b4377a4609
commit 2cf2fcfc4f
Signed by: null2264
GPG key ID: BA64F8B60AF3EFB6
10 changed files with 460 additions and 275 deletions

View file

@ -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<List<CatalogueSource>> = sourcesMapFlow.map { it.values.filterIsInstance<CatalogueSource>() }
val onlineSources: Flow<List<HttpSource>> = catalogueSources.map { it.filterIsInstance<HttpSource>() }
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<DelegatedSource>().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

View file

@ -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<Chapter, Manga, List<SChapter>>?
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<SChapter>? {
val id = delegate?.id ?: return null
val manga = Manga.create(url, "", id)
return delegate?.getChapterList(manga)
}
}

View file

@ -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<Chapter, Manga, List<SChapter>>? {
override suspend fun fetchMangaFromChapterUrl(uri: Uri): Triple<SChapter, SManga, List<SChapter>>? {
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)
}
}

View file

@ -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<Chapter, Manga, List<SChapter>>? {
override suspend fun fetchMangaFromChapterUrl(uri: Uri): Triple<SChapter, SManga, List<SChapter>>? {
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<MangaDexChapterData>(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<PreferencesHelper>().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)
}
}

View file

@ -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<Chapter, Manga, List<SChapter>>? {
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<PreferencesHelper>().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(),
)
}
}

View file

@ -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")
}
}
}

View file

@ -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<Chapter, Manga, List<SChapter>>? {
override suspend fun fetchMangaFromChapterUrl(uri: Uri): Triple<SChapter, SManga, List<SChapter>>? {
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<PreferencesHelper>().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,
)
}
}
}

View file

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

View file

@ -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<MangasPage> {
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<MangasPage> {
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<MangasPage> {
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<SManga> {
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<List<SChapter>> {
ensureDelegateCompatible()
return delegate.fetchChapterList(manga)
}
/**
* [1.x API] Get all the available chapters for a manga.
*/
override suspend fun getChapterList(manga: SManga): List<SChapter> {
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<List<Page>> {
ensureDelegateCompatible()
return delegate.fetchPageList(chapter)
}
/**
* [1.x API] Get the list of pages a chapter has.
*/
override suspend fun getPageList(chapter: SChapter): List<Page> {
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<String> {
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<SChapter, SManga, List<SChapter>>?
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<SChapter> {
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)
}
}

View file

@ -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<SChapter>
/**
* 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.