mirror of
https://github.com/Suwayomi/Suwayomi-Server.git
synced 2026-07-10 22:34:33 -05:00
Extension API 1.6 (#2120)
* Non-Extension Index changes for 1.6 * Changelog * Minor fixes * Implement extension store * Test build fix * Docs * Simplify fetching manga and chapters * Use EMPTY JsonObject * Update docs/Configuring-Suwayomi‐Server.md Co-authored-by: Constantin Piber <59023762+cpiber@users.noreply.github.com> * Improve Fetch Extension Store * Fixes * Simplify deprecated isNsfw in SourceQuery * Simplify ContentRating in Source.kt * Simplify isNsfw in SourceType * No magic numbers for ContentRating, improves safety for future versions of extension api * Fix SearchTest * Lint * Lint * Optimize imports and fix unchecked cast warning * Proper extension store queries * Optimize import fixes * Add ContentRatingFilter * Improve extension store sync * fix: re-sync (#2121) * Lint * Add ExtenionStores to the fetchExtensions result since its possible for the stores to change. * Use a single version of ContentRating * Exclude ServerConfig.extensionStores from GraphQL * Use syncDbToPrefs in ExtensionStoreMutation * Optimize Imports * Update server/server-config/src/main/kotlin/suwayomi/tachidesk/server/ServerConfig.kt Co-authored-by: Constantin Piber <59023762+cpiber@users.noreply.github.com> * Remove replaceWith and add specific description for GQL APIs * Include OkHttp ZSTD * Update to latest Mihon extension lib * Fix latest Mihon Extension Lib * Lint * Optimize imports * Lint * Review fixes * Add a index to extesnion table store url * Lint --------- Co-authored-by: Constantin Piber <59023762+cpiber@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,15 @@
|
||||
package eu.kanade.tachiyomi.network
|
||||
|
||||
import okhttp3.Response
|
||||
|
||||
/**
|
||||
* Exception that handles HTTP codes considered not successful by OkHttp.
|
||||
* Use it to have a standardized error message in the app across the extensions.
|
||||
*
|
||||
* @see Response.isSuccessful
|
||||
* @since tachiyomix 1.6
|
||||
* @param code [Int] the HTTP status code
|
||||
*/
|
||||
class HttpException(
|
||||
val code: Int,
|
||||
) : IllegalStateException("HTTP error $code")
|
||||
@@ -2,26 +2,25 @@ package eu.kanade.tachiyomi.network
|
||||
|
||||
import android.content.Context
|
||||
import app.cash.quickjs.QuickJs
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import eu.kanade.tachiyomi.util.lang.withIOContext
|
||||
|
||||
/**
|
||||
* Util for evaluating JavaScript in sources.
|
||||
*/
|
||||
@Suppress("UNUSED", "UNCHECKED_CAST")
|
||||
class JavaScriptEngine(
|
||||
@Suppress("UNUSED_PARAMETER") context: Context,
|
||||
context: Context,
|
||||
) {
|
||||
/**
|
||||
* Evaluate arbitrary JavaScript code and get the result as a primitive type
|
||||
* (e.g., String, Int).
|
||||
*
|
||||
* @since extensions-lib 1.4
|
||||
* @since tachiyomix 1.4
|
||||
* @param script JavaScript to execute.
|
||||
* @return Result of JavaScript code as a primitive type.
|
||||
*/
|
||||
@Suppress("UNUSED", "UNCHECKED_CAST")
|
||||
suspend fun <T> evaluate(script: String): T =
|
||||
withContext(Dispatchers.IO) {
|
||||
withIOContext {
|
||||
QuickJs.create().use {
|
||||
it.evaluate(script) as T
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ package eu.kanade.tachiyomi.network
|
||||
|
||||
import android.content.Context
|
||||
import eu.kanade.tachiyomi.network.interceptor.CloudflareInterceptor
|
||||
import eu.kanade.tachiyomi.network.interceptor.IgnoreGzipInterceptor
|
||||
import eu.kanade.tachiyomi.network.interceptor.UncaughtExceptionInterceptor
|
||||
import eu.kanade.tachiyomi.network.interceptor.UserAgentInterceptor
|
||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
@@ -22,7 +21,6 @@ import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import okhttp3.Cache
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.brotli.BrotliInterceptor
|
||||
import okhttp3.logging.HttpLoggingInterceptor
|
||||
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource
|
||||
import java.net.CookieHandler
|
||||
@@ -84,8 +82,6 @@ class NetworkHelper(
|
||||
),
|
||||
).addInterceptor(UncaughtExceptionInterceptor())
|
||||
.addInterceptor(UserAgentInterceptor(::defaultUserAgentProvider))
|
||||
.addNetworkInterceptor(IgnoreGzipInterceptor())
|
||||
.addNetworkInterceptor(BrotliInterceptor)
|
||||
|
||||
// if (preferences.verboseLogging().get()) {
|
||||
val httpLoggingInterceptor =
|
||||
@@ -128,5 +124,7 @@ class NetworkHelper(
|
||||
// val client by lazy { baseClientBuilder.cache(Cache(cacheDir, cacheSize)).build() }
|
||||
val client by lazy { baseClientBuilder.build() }
|
||||
|
||||
@Deprecated("The regular client handles Cloudflare by default")
|
||||
@Suppress("UNUSED")
|
||||
val cloudflareClient by lazy { client }
|
||||
}
|
||||
|
||||
@@ -15,11 +15,14 @@ import rx.Observable
|
||||
import rx.Producer
|
||||
import rx.Subscription
|
||||
import java.io.IOException
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import kotlin.concurrent.atomics.AtomicBoolean
|
||||
import kotlin.concurrent.atomics.ExperimentalAtomicApi
|
||||
import kotlin.coroutines.resumeWithException
|
||||
|
||||
val jsonMime = "application/json; charset=utf-8".toMediaType()
|
||||
|
||||
@OptIn(ExperimentalAtomicApi::class)
|
||||
@Deprecated("Use suspend APIs instead")
|
||||
fun Call.asObservable(): Observable<Response> {
|
||||
return Observable.unsafeCreate { subscriber ->
|
||||
// Since Call is a one-shot type, clone it for each new subscriber.
|
||||
@@ -27,9 +30,11 @@ fun Call.asObservable(): Observable<Response> {
|
||||
|
||||
// Wrap the call in a helper which handles both unsubscription and backpressure.
|
||||
val requestArbiter =
|
||||
object : AtomicBoolean(), Producer, Subscription {
|
||||
object : Producer, Subscription {
|
||||
val boolean = AtomicBoolean(false)
|
||||
|
||||
override fun request(n: Long) {
|
||||
if (n == 0L || !compareAndSet(false, true)) return
|
||||
if (n == 0L || !boolean.compareAndSet(expectedValue = false, newValue = true)) return
|
||||
|
||||
try {
|
||||
val response = call.execute()
|
||||
@@ -37,15 +42,15 @@ fun Call.asObservable(): Observable<Response> {
|
||||
subscriber.onNext(response)
|
||||
subscriber.onCompleted()
|
||||
}
|
||||
} catch (error: Exception) {
|
||||
} catch (e: Exception) {
|
||||
if (!subscriber.isUnsubscribed) {
|
||||
subscriber.onError(error)
|
||||
subscriber.onError(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun unsubscribe() {
|
||||
// call.cancel()
|
||||
call.cancel()
|
||||
}
|
||||
|
||||
override fun isUnsubscribed(): Boolean = call.isCanceled()
|
||||
@@ -56,50 +61,50 @@ fun Call.asObservable(): Observable<Response> {
|
||||
}
|
||||
}
|
||||
|
||||
fun Call.asObservableSuccess(): Observable<Response> =
|
||||
asObservable()
|
||||
.doOnNext { response ->
|
||||
if (!response.isSuccessful) {
|
||||
response.close()
|
||||
throw HttpException(response.code)
|
||||
@Deprecated("Use suspend APIs instead")
|
||||
fun Call.asObservableSuccess(): Observable<Response> {
|
||||
@Suppress("DEPRECATION")
|
||||
return asObservable().doOnNext { response ->
|
||||
if (!response.isSuccessful) {
|
||||
response.close()
|
||||
throw HttpException(response.code)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Based on https://github.com/square/okhttp/blob/master/okhttp-coroutines/src/main/kotlin/okhttp3/coroutines/ExecuteAsync.kt
|
||||
// and https://github.com/gildor/kotlin-coroutines-okhttp
|
||||
private suspend fun Call.await(callStack: Array<StackTraceElement>): Response {
|
||||
return suspendCancellableCoroutine { continuation ->
|
||||
continuation.invokeOnCancellation {
|
||||
try {
|
||||
this.cancel()
|
||||
} catch (_: Throwable) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
// Based on https://github.com/gildor/kotlin-coroutines-okhttp
|
||||
private suspend fun Call.await(callStack: Array<StackTraceElement>): Response {
|
||||
return suspendCancellableCoroutine { continuation ->
|
||||
val callback =
|
||||
this.enqueue(
|
||||
object : Callback {
|
||||
override fun onResponse(
|
||||
call: Call,
|
||||
response: Response,
|
||||
) {
|
||||
continuation.resume(response) { _, resourceToClose, _ ->
|
||||
response.body.close()
|
||||
resourceToClose.close()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFailure(
|
||||
call: Call,
|
||||
e: IOException,
|
||||
) {
|
||||
// Don't bother with resuming the continuation if it is already cancelled.
|
||||
if (continuation.isCancelled) return
|
||||
val exception = IOException(e.message, e).apply { stackTrace = callStack }
|
||||
continuation.resumeWithException(exception)
|
||||
}
|
||||
}
|
||||
|
||||
enqueue(callback)
|
||||
|
||||
continuation.invokeOnCancellation {
|
||||
try {
|
||||
cancel()
|
||||
} catch (ex: Throwable) {
|
||||
// Ignore cancel exception
|
||||
}
|
||||
}
|
||||
override fun onResponse(
|
||||
call: Call,
|
||||
response: Response,
|
||||
) {
|
||||
continuation.resume(response) { _, value, _ ->
|
||||
value.close()
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,7 +114,7 @@ suspend fun Call.await(): Response {
|
||||
}
|
||||
|
||||
/**
|
||||
* @since extensions-lib 1.5
|
||||
* Similar to [await] but throws [HttpException] if [Response.isSuccessful] returns false
|
||||
*/
|
||||
suspend fun Call.awaitSuccess(): Response {
|
||||
val callStack = Exception().stackTrace.run { copyOfRange(1, size) }
|
||||
@@ -150,7 +155,3 @@ fun <T> decodeFromJsonResponse(
|
||||
response.body.source().use {
|
||||
json.decodeFromBufferedSource(deserializer, it)
|
||||
}
|
||||
|
||||
class HttpException(
|
||||
val code: Int,
|
||||
) : IllegalStateException("HTTP error $code")
|
||||
|
||||
@@ -35,7 +35,11 @@ class ProgressResponseBody(
|
||||
val bytesRead = super.read(sink, byteCount)
|
||||
// read() returns the number of bytes read, or -1 if this source is exhausted.
|
||||
totalBytesRead += if (bytesRead != -1L) bytesRead else 0
|
||||
progressListener.update(totalBytesRead, responseBody.contentLength(), bytesRead == -1L)
|
||||
progressListener.update(
|
||||
totalBytesRead,
|
||||
responseBody.contentLength(),
|
||||
bytesRead == -1L,
|
||||
)
|
||||
return bytesRead
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import okhttp3.CacheControl
|
||||
import okhttp3.FormBody
|
||||
import okhttp3.Headers
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody
|
||||
import java.util.concurrent.TimeUnit.MINUTES
|
||||
@@ -18,13 +19,7 @@ fun GET(
|
||||
url: String,
|
||||
headers: Headers = DEFAULT_HEADERS,
|
||||
cache: CacheControl = DEFAULT_CACHE_CONTROL,
|
||||
): Request =
|
||||
Request
|
||||
.Builder()
|
||||
.url(url)
|
||||
.headers(headers)
|
||||
.cacheControl(cache)
|
||||
.build()
|
||||
): Request = GET(url.toHttpUrl(), headers, cache)
|
||||
|
||||
/**
|
||||
* @since extensions-lib 1.4
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
package eu.kanade.tachiyomi.network.interceptor
|
||||
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.Response
|
||||
|
||||
/**
|
||||
* To use [okhttp3.brotli.BrotliInterceptor] as a network interceptor,
|
||||
* add [IgnoreGzipInterceptor] right before it.
|
||||
*
|
||||
* This nullifies the transparent gzip of [okhttp3.internal.http.BridgeInterceptor]
|
||||
* so gzip and Brotli are explicitly handled by the [okhttp3.brotli.BrotliInterceptor].
|
||||
*/
|
||||
class IgnoreGzipInterceptor : Interceptor {
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
var request = chain.request()
|
||||
if (request.header("Accept-Encoding") == "gzip") {
|
||||
request = request.newBuilder().removeHeader("Accept-Encoding").build()
|
||||
}
|
||||
return chain.proceed(request)
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,12 @@ package eu.kanade.tachiyomi.source
|
||||
|
||||
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.source.model.SMangaUpdate
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.supervisorScope
|
||||
import rx.Observable
|
||||
import suwayomi.tachidesk.manga.impl.util.lang.awaitSingle
|
||||
|
||||
@@ -11,68 +17,62 @@ interface CatalogueSource : Source {
|
||||
*/
|
||||
override val lang: String
|
||||
|
||||
/**
|
||||
* Whether the source has support for latest updates.
|
||||
*/
|
||||
val supportsLatest: Boolean
|
||||
|
||||
/**
|
||||
* Get a page with a list of manga.
|
||||
*
|
||||
* @since extensions-lib 1.5
|
||||
* @param page the page number to retrieve.
|
||||
*/
|
||||
@Suppress("DEPRECATION")
|
||||
suspend fun getPopularManga(page: Int): MangasPage = fetchPopularManga(page).awaitSingle()
|
||||
override suspend fun getPopularManga(page: Int): MangasPage = fetchPopularManga(page).awaitSingle()
|
||||
|
||||
/**
|
||||
* Get a page with a list of manga.
|
||||
*
|
||||
* @since extensions-lib 1.5
|
||||
* @param page the page number to retrieve.
|
||||
* @param query the search query.
|
||||
* @param filters the list of filters to apply.
|
||||
*/
|
||||
@Suppress("DEPRECATION")
|
||||
suspend fun getSearchManga(
|
||||
override suspend fun getLatestUpdates(page: Int): MangasPage = fetchLatestUpdates(page).awaitSingle()
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
override suspend fun getSearchManga(
|
||||
page: Int,
|
||||
query: String,
|
||||
filters: FilterList,
|
||||
): MangasPage = fetchSearchManga(page, query, filters).awaitSingle()
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
override suspend fun getMangaUpdate(
|
||||
manga: SManga,
|
||||
chapters: List<SChapter>,
|
||||
fetchDetails: Boolean,
|
||||
fetchChapters: Boolean,
|
||||
): SMangaUpdate =
|
||||
supervisorScope {
|
||||
val asyncManga = if (fetchDetails) async { fetchMangaDetails(manga).awaitSingle() } else null
|
||||
val asyncChapters = if (fetchChapters) async { fetchChapterList(manga).awaitSingle() } else null
|
||||
SMangaUpdate(asyncManga?.await() ?: manga, asyncChapters?.await() ?: chapters)
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
override suspend fun getPageList(chapter: SChapter): List<Page> = fetchPageList(chapter).awaitSingle()
|
||||
|
||||
/**
|
||||
* Get a page with a list of latest manga updates.
|
||||
* Returns an observable containing a page with a list of manga.
|
||||
*
|
||||
* @since extensions-lib 1.5
|
||||
* @param page the page number to retrieve.
|
||||
*/
|
||||
@Suppress("DEPRECATION")
|
||||
suspend fun getLatestUpdates(page: Int): MangasPage = fetchLatestUpdates(page).awaitSingle()
|
||||
@Deprecated("Use the suspend API instead", ReplaceWith("getPopularManga"))
|
||||
fun fetchPopularManga(page: Int): Observable<MangasPage> = throw UnsupportedOperationException()
|
||||
|
||||
/**
|
||||
* Returns the list of filters for the source.
|
||||
* Returns an observable containing a page with a list of manga.
|
||||
*
|
||||
* @param page the page number to retrieve.
|
||||
* @param query the search query.
|
||||
* @param filters the list of filters to apply.
|
||||
*/
|
||||
fun getFilterList(): FilterList
|
||||
|
||||
@Deprecated(
|
||||
"Use the non-RxJava API instead",
|
||||
ReplaceWith("getPopularManga"),
|
||||
)
|
||||
fun fetchPopularManga(page: Int): Observable<MangasPage> = throw IllegalStateException("Not used")
|
||||
|
||||
@Deprecated(
|
||||
"Use the non-RxJava API instead",
|
||||
ReplaceWith("getSearchManga"),
|
||||
)
|
||||
@Deprecated("Use the suspend API instead", ReplaceWith("getSearchManga"))
|
||||
fun fetchSearchManga(
|
||||
page: Int,
|
||||
query: String,
|
||||
filters: FilterList,
|
||||
): Observable<MangasPage> = throw IllegalStateException("Not used")
|
||||
): Observable<MangasPage> = throw UnsupportedOperationException()
|
||||
|
||||
@Deprecated(
|
||||
"Use the non-RxJava API instead",
|
||||
ReplaceWith("getLatestUpdates"),
|
||||
)
|
||||
fun fetchLatestUpdates(page: Int): Observable<MangasPage> = throw IllegalStateException("Not used")
|
||||
/**
|
||||
* Returns an observable containing a page with a list of latest manga updates.
|
||||
*
|
||||
* @param page the page number to retrieve.
|
||||
*/
|
||||
@Deprecated("Use the suspend API instead", ReplaceWith("getLatestUpdates"))
|
||||
fun fetchLatestUpdates(page: Int): Observable<MangasPage> = throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
package eu.kanade.tachiyomi.source
|
||||
|
||||
@Suppress("unused")
|
||||
typealias PreferenceScreen = androidx.preference.PreferenceScreen
|
||||
@@ -1,10 +1,12 @@
|
||||
package eu.kanade.tachiyomi.source
|
||||
|
||||
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.source.model.SMangaUpdate
|
||||
import rx.Observable
|
||||
import suwayomi.tachidesk.manga.impl.util.lang.awaitSingle
|
||||
|
||||
/**
|
||||
* A basic interface for creating a source. It could be an online source, a local source, etc.
|
||||
@@ -24,53 +26,86 @@ interface Source {
|
||||
get() = ""
|
||||
|
||||
/**
|
||||
* Get the updated details for a manga.
|
||||
*
|
||||
* @since extensions-lib 1.5
|
||||
* @param manga the manga to update.
|
||||
* @return the updated manga.
|
||||
* Whether the source has support for latest updates.
|
||||
*/
|
||||
@Suppress("DEPRECATION")
|
||||
suspend fun getMangaDetails(manga: SManga): SManga = fetchMangaDetails(manga).awaitSingle()
|
||||
val supportsLatest: Boolean
|
||||
|
||||
/**
|
||||
* Get all the available chapters for a manga.
|
||||
*
|
||||
* @since extensions-lib 1.5
|
||||
* @param manga the manga to update.
|
||||
* @return the chapters for the manga.
|
||||
* Returns the list of filters for the source.
|
||||
*/
|
||||
@Suppress("DEPRECATION")
|
||||
suspend fun getChapterList(manga: SManga): List<SChapter> = fetchChapterList(manga).awaitSingle()
|
||||
fun getFilterList(): FilterList = FilterList()
|
||||
|
||||
/**
|
||||
* Get a page with a list of manga.
|
||||
*
|
||||
* @since tachiyomix 1.6
|
||||
* @param page the page number to retrieve.
|
||||
*/
|
||||
suspend fun getPopularManga(page: Int): MangasPage
|
||||
|
||||
/**
|
||||
* Get a page with a list of latest manga updates.
|
||||
*
|
||||
* @since tachiyomix 1.6
|
||||
* @param page the page number to retrieve.
|
||||
*/
|
||||
suspend fun getLatestUpdates(page: Int): MangasPage
|
||||
|
||||
/**
|
||||
* Get a page with a list of manga.
|
||||
*
|
||||
* @since tachiyomix 1.6
|
||||
* @param page the page number to retrieve.
|
||||
* @param query the search query.
|
||||
* @param filters the list of filters to apply.
|
||||
*/
|
||||
suspend fun getSearchManga(
|
||||
page: Int,
|
||||
query: String,
|
||||
filters: FilterList,
|
||||
): MangasPage
|
||||
|
||||
/**
|
||||
* Fetches updated information for a manga.
|
||||
*
|
||||
* Depending on the provided flags or source availability, this may include
|
||||
* updated manga metadata, available chapters, or both.
|
||||
*
|
||||
* If a value is not requested, the existing provided value can be returned as-is.
|
||||
* The host app may apply any returned updates regardless of the flags,
|
||||
* so care should be taken to only return accurate and intentional changes.
|
||||
*
|
||||
* @since tachiyomix 1.6
|
||||
* @param manga The manga to fetch updates for.
|
||||
* @param chapters Existing chapters of the manga
|
||||
* @param fetchDetails Whether to fetch updated manga details.
|
||||
* @param fetchChapters Whether to fetch available chapters.
|
||||
*/
|
||||
suspend fun getMangaUpdate(
|
||||
manga: SManga,
|
||||
chapters: List<SChapter>,
|
||||
fetchDetails: Boolean,
|
||||
fetchChapters: Boolean,
|
||||
): SMangaUpdate
|
||||
|
||||
/**
|
||||
* Get the list of pages a chapter has. Pages should be returned
|
||||
* in the expected order; the index is ignored.
|
||||
*
|
||||
* @since extensions-lib 1.5
|
||||
* @since tachiyomix 1.6
|
||||
* @param chapter the chapter.
|
||||
* @return the pages for the chapter.
|
||||
*/
|
||||
@Suppress("DEPRECATION")
|
||||
suspend fun getPageList(chapter: SChapter): List<Page> = fetchPageList(chapter).awaitSingle()
|
||||
suspend fun getPageList(chapter: SChapter): List<Page>
|
||||
|
||||
@Deprecated(
|
||||
"Use the non-RxJava API instead",
|
||||
ReplaceWith("getMangaDetails"),
|
||||
)
|
||||
fun fetchMangaDetails(manga: SManga): Observable<SManga> = throw IllegalStateException("Not used")
|
||||
@Deprecated("Use the combined suspend API instead", ReplaceWith("getMangaUpdate"))
|
||||
fun fetchMangaDetails(manga: SManga): Observable<SManga> = throw UnsupportedOperationException()
|
||||
|
||||
@Deprecated(
|
||||
"Use the non-RxJava API instead",
|
||||
ReplaceWith("getChapterList"),
|
||||
)
|
||||
fun fetchChapterList(manga: SManga): Observable<List<SChapter>> = throw IllegalStateException("Not used")
|
||||
@Deprecated("Use the combined suspend API instead", ReplaceWith("getMangaUpdate"))
|
||||
fun fetchChapterList(manga: SManga): Observable<List<SChapter>> = throw UnsupportedOperationException()
|
||||
|
||||
@Deprecated(
|
||||
"Use the non-RxJava API instead",
|
||||
ReplaceWith("getPageList"),
|
||||
)
|
||||
fun fetchPageList(chapter: SChapter): Observable<List<Page>> = throw IllegalStateException("Not used")
|
||||
@Deprecated("Use the suspend API instead", ReplaceWith("getPageList"))
|
||||
fun fetchPageList(chapter: SChapter): Observable<List<Page>> = throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
// fun Source.icon(): Drawable? = Injekt.get<ExtensionManager>().getAppIconForSource(this)
|
||||
|
||||
@@ -23,12 +23,15 @@ 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.source.model.SMangaUpdate
|
||||
import eu.kanade.tachiyomi.util.chapter.ChapterRecognition
|
||||
import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder
|
||||
import eu.kanade.tachiyomi.util.storage.EpubFile
|
||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.supervisorScope
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.decodeFromStream
|
||||
@@ -167,8 +170,20 @@ class LocalSource(
|
||||
return MangasPage(mangas.toList(), false)
|
||||
}
|
||||
|
||||
override suspend fun getMangaUpdate(
|
||||
manga: SManga,
|
||||
chapters: List<SChapter>,
|
||||
fetchDetails: Boolean,
|
||||
fetchChapters: Boolean,
|
||||
): SMangaUpdate =
|
||||
supervisorScope {
|
||||
val asyncManga = if (fetchDetails) async { getMangaDetails(manga) } else null
|
||||
val asyncChapters = if (fetchChapters) async { getChapterList(manga) } else null
|
||||
SMangaUpdate(asyncManga?.await() ?: manga, asyncChapters?.await() ?: chapters)
|
||||
}
|
||||
|
||||
// Manga details related
|
||||
override suspend fun getMangaDetails(manga: SManga): SManga =
|
||||
private suspend fun getMangaDetails(manga: SManga): SManga =
|
||||
withContext(Dispatchers.IO) {
|
||||
coverManager.find(manga.url)?.let {
|
||||
manga.thumbnail_url = it.absolutePath
|
||||
@@ -289,7 +304,7 @@ class LocalSource(
|
||||
}
|
||||
|
||||
// Chapters
|
||||
override suspend fun getChapterList(manga: SManga): List<SChapter> =
|
||||
private suspend fun getChapterList(manga: SManga): List<SChapter> =
|
||||
fileSystem
|
||||
.getFilesInMangaDirectory(manga.url)
|
||||
// Only keep supported formats
|
||||
@@ -467,7 +482,8 @@ class LocalSource(
|
||||
it[versionName] = "1.2"
|
||||
it[versionCode] = 0
|
||||
it[lang] = LANG
|
||||
it[isNsfw] = false
|
||||
it[extensionLib] = "1.2"
|
||||
it[contentWarning] = 0
|
||||
it[isInstalled] = true
|
||||
}
|
||||
|
||||
@@ -476,7 +492,6 @@ class LocalSource(
|
||||
it[name] = NAME
|
||||
it[lang] = LANG
|
||||
it[extension] = extensionId
|
||||
it[isNsfw] = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,22 @@
|
||||
package eu.kanade.tachiyomi.source.model
|
||||
|
||||
data class MangasPage(
|
||||
class MangasPage(
|
||||
val mangas: List<SManga>,
|
||||
val hasNextPage: Boolean,
|
||||
)
|
||||
) {
|
||||
@Deprecated("MangasPage is now a regular class")
|
||||
operator fun component1(): List<SManga> = mangas
|
||||
|
||||
@Deprecated("MangasPage is now a regular class")
|
||||
operator fun component2(): Boolean = hasNextPage
|
||||
|
||||
@Deprecated("MangasPage is now a regular class")
|
||||
fun copy(
|
||||
mangas: List<SManga> = this.mangas,
|
||||
hasNextPage: Boolean = this.hasNextPage,
|
||||
): MangasPage =
|
||||
MangasPage(
|
||||
mangas = mangas,
|
||||
hasNextPage = hasNextPage,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -27,12 +27,4 @@ open class Page(
|
||||
-1
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val QUEUE = 0
|
||||
const val LOAD_PAGE = 1
|
||||
const val DOWNLOAD_IMAGE = 2
|
||||
const val READY = 3
|
||||
const val ERROR = 4
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
package eu.kanade.tachiyomi.source.model
|
||||
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import java.io.Serializable
|
||||
|
||||
interface SChapter : Serializable {
|
||||
@@ -9,12 +10,25 @@ interface SChapter : Serializable {
|
||||
|
||||
var name: String
|
||||
|
||||
var date_upload: Long
|
||||
|
||||
var chapter_number: Float
|
||||
|
||||
var scanlator: String?
|
||||
|
||||
var date_upload: Long
|
||||
|
||||
/**
|
||||
* Extra metadata associated with the chapter.
|
||||
*
|
||||
* The JSON object is not visible to users and intended for internal or source-specific
|
||||
* purposes. Apps may define their own namespaced keys (e.g., `"mihon.*"`) for sources to populate.
|
||||
*
|
||||
* This allows apps to attach and ask for custom information without affecting the visible
|
||||
* chapter data.
|
||||
*
|
||||
* @since tachiyomix 1.6
|
||||
*/
|
||||
var memo: JsonObject
|
||||
|
||||
fun copyFrom(other: SChapter) {
|
||||
name = other.name
|
||||
url = other.url
|
||||
|
||||
@@ -2,14 +2,19 @@
|
||||
|
||||
package eu.kanade.tachiyomi.source.model
|
||||
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import suwayomi.tachidesk.manga.impl.util.lang.EMPTY
|
||||
|
||||
class SChapterImpl : SChapter {
|
||||
override lateinit var url: String
|
||||
|
||||
override lateinit var name: String
|
||||
|
||||
override var date_upload: Long = 0
|
||||
|
||||
override var chapter_number: Float = -1f
|
||||
|
||||
override var scanlator: String? = null
|
||||
|
||||
override var date_upload: Long = 0
|
||||
|
||||
override var memo: JsonObject = JsonObject.EMPTY
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
package eu.kanade.tachiyomi.source.model
|
||||
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import java.io.Serializable
|
||||
|
||||
interface SManga : Serializable {
|
||||
@@ -9,22 +10,58 @@ interface SManga : Serializable {
|
||||
|
||||
var title: String
|
||||
|
||||
var thumbnail_url: String?
|
||||
|
||||
var artist: String?
|
||||
|
||||
var author: String?
|
||||
|
||||
var status: Int
|
||||
|
||||
var description: String?
|
||||
|
||||
var genre: String?
|
||||
|
||||
var status: Int
|
||||
|
||||
var thumbnail_url: String?
|
||||
|
||||
var update_strategy: UpdateStrategy
|
||||
|
||||
var initialized: Boolean
|
||||
|
||||
/**
|
||||
* Extra metadata associated with the manga.
|
||||
*
|
||||
* The JSON object is not visible to users and intended for internal or source-specific
|
||||
* purposes. Apps may define their own namespaced keys (e.g., `"mihon.*"`) for sources to populate.
|
||||
*
|
||||
* This allows apps to attach and ask for custom information without affecting the visible
|
||||
* manga data.
|
||||
*
|
||||
* @since tachiyomix 1.6
|
||||
*/
|
||||
var memo: JsonObject
|
||||
|
||||
fun getGenres(): List<String>? {
|
||||
if (genre.isNullOrBlank()) return null
|
||||
return genre
|
||||
?.split(", ")
|
||||
?.map { it.trim() }
|
||||
?.filterNot { it.isBlank() }
|
||||
?.distinct()
|
||||
}
|
||||
|
||||
fun copy() =
|
||||
create().also {
|
||||
it.url = url
|
||||
it.title = title
|
||||
it.artist = artist
|
||||
it.author = author
|
||||
it.description = description
|
||||
it.genre = genre
|
||||
it.status = status
|
||||
it.thumbnail_url = thumbnail_url
|
||||
it.update_strategy = update_strategy
|
||||
it.initialized = initialized
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val UNKNOWN = 0
|
||||
const val ONGOING = 1
|
||||
@@ -37,30 +74,3 @@ interface SManga : Serializable {
|
||||
fun create(): SManga = SMangaImpl()
|
||||
}
|
||||
}
|
||||
|
||||
// fun SManga.toMangaInfo(): MangaInfo {
|
||||
// return MangaInfo(
|
||||
// key = this.url,
|
||||
// title = this.title,
|
||||
// artist = this.artist ?: "",
|
||||
// author = this.author ?: "",
|
||||
// description = this.description ?: "",
|
||||
// genres = this.genre?.split(", ") ?: emptyList(),
|
||||
// status = this.status,
|
||||
// cover = this.thumbnail_url ?: ""
|
||||
// )
|
||||
// }
|
||||
//
|
||||
// fun MangaInfo.toSManga(): SManga {
|
||||
// val mangaInfo = this
|
||||
// return SManga.create().apply {
|
||||
// url = mangaInfo.key
|
||||
// title = mangaInfo.title
|
||||
// artist = mangaInfo.artist
|
||||
// author = mangaInfo.author
|
||||
// description = mangaInfo.description
|
||||
// genre = mangaInfo.genres.joinToString(", ")
|
||||
// status = mangaInfo.status
|
||||
// thumbnail_url = mangaInfo.cover
|
||||
// }
|
||||
// }
|
||||
|
||||
@@ -2,24 +2,29 @@
|
||||
|
||||
package eu.kanade.tachiyomi.source.model
|
||||
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import suwayomi.tachidesk.manga.impl.util.lang.EMPTY
|
||||
|
||||
class SMangaImpl : SManga {
|
||||
override lateinit var url: String
|
||||
|
||||
override lateinit var title: String
|
||||
|
||||
override var thumbnail_url: String? = null
|
||||
|
||||
override var artist: String? = null
|
||||
|
||||
override var author: String? = null
|
||||
|
||||
override var status: Int = 0
|
||||
|
||||
override var description: String? = null
|
||||
|
||||
override var genre: String? = null
|
||||
|
||||
override var status: Int = 0
|
||||
|
||||
override var thumbnail_url: String? = null
|
||||
|
||||
override var update_strategy: UpdateStrategy = UpdateStrategy.ALWAYS_UPDATE
|
||||
|
||||
override var initialized: Boolean = false
|
||||
|
||||
override var memo: JsonObject = JsonObject.EMPTY
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
package eu.kanade.tachiyomi.source.model
|
||||
|
||||
@Suppress("UNUSED")
|
||||
class SMangaUpdate(
|
||||
val manga: SManga,
|
||||
val chapters: List<SChapter>,
|
||||
)
|
||||
@@ -1,6 +1,22 @@
|
||||
package eu.kanade.tachiyomi.source.model
|
||||
|
||||
/**
|
||||
* Define the update strategy for a single [SManga].
|
||||
* The strategy used will only take effect on the library update.
|
||||
*
|
||||
* @since extensions-lib 1.4
|
||||
*/
|
||||
enum class UpdateStrategy {
|
||||
/**
|
||||
* Series marked as always update will be included in the library
|
||||
* update if they aren't excluded by additional restrictions.
|
||||
*/
|
||||
ALWAYS_UPDATE,
|
||||
|
||||
/**
|
||||
* Series marked as only fetch once will be automatically skipped
|
||||
* during library updates. Useful for cases where the series is previously
|
||||
* known to be finished and have only a single chapter, for example.
|
||||
*/
|
||||
ONLY_FETCH_ONCE,
|
||||
}
|
||||
|
||||
@@ -25,7 +25,6 @@ import java.security.MessageDigest
|
||||
/**
|
||||
* A simple implementation for sources from a website.
|
||||
*/
|
||||
@Suppress("unused")
|
||||
abstract class HttpSource : CatalogueSource {
|
||||
/**
|
||||
* Network service.
|
||||
@@ -37,11 +36,24 @@ abstract class HttpSource : CatalogueSource {
|
||||
*/
|
||||
abstract val baseUrl: String
|
||||
|
||||
/**
|
||||
* Returns the base (home) URL of the website as a string.
|
||||
*
|
||||
* This is typically the root address that serves as the main entry point
|
||||
* to the site's content, such as "https://mihon.tech".
|
||||
*
|
||||
* This method is used in the browse screen to determine the URL
|
||||
* opened when tapping "Open in WebView".
|
||||
*
|
||||
* @return The website’s home page URL. Defaults to [baseUrl].
|
||||
*/
|
||||
open fun getHomeUrl(): String = baseUrl
|
||||
|
||||
/**
|
||||
* Version id used to generate the source id. If the site completely changes and urls are
|
||||
* incompatible, you may increase this value and it'll be considered as a new source.
|
||||
*/
|
||||
open val versionId = 1
|
||||
open val versionId: Int = 1
|
||||
|
||||
/**
|
||||
* ID of the source. By default it uses a generated id using the first 16 characters (64 bits)
|
||||
@@ -53,7 +65,7 @@ abstract class HttpSource : CatalogueSource {
|
||||
*
|
||||
* Note: the generated ID sets the sign bit to `0`.
|
||||
*/
|
||||
override val id by lazy { generateId() }
|
||||
override val id: Long by lazy { generateId(name, lang, versionId) }
|
||||
|
||||
/**
|
||||
* Headers used for requests.
|
||||
@@ -63,10 +75,7 @@ abstract class HttpSource : CatalogueSource {
|
||||
/**
|
||||
* Default network client for doing requests.
|
||||
*/
|
||||
open val client: OkHttpClient
|
||||
get() = network.client
|
||||
|
||||
private fun generateId(): Long = generateId("${name.lowercase()}/$lang/$versionId")
|
||||
open val client: OkHttpClient get() = network.client
|
||||
|
||||
/**
|
||||
* Generates a unique ID for the source based on the provided [name], [lang] and
|
||||
@@ -91,10 +100,6 @@ abstract class HttpSource : CatalogueSource {
|
||||
versionId: Int,
|
||||
): Long {
|
||||
val key = "${name.lowercase()}/$lang/$versionId"
|
||||
return generateId(key)
|
||||
}
|
||||
|
||||
private fun generateId(key: String): Long {
|
||||
val bytes = MessageDigest.getInstance("MD5").digest(key.toByteArray())
|
||||
return (0..7).map { bytes[it].toLong() and 0xff shl 8 * (7 - it) }.reduce(Long::or) and Long.MAX_VALUE
|
||||
}
|
||||
@@ -102,7 +107,7 @@ abstract class HttpSource : CatalogueSource {
|
||||
/**
|
||||
* Headers builder for requests. Implementations can override this method for custom headers.
|
||||
*/
|
||||
protected open fun headersBuilder() =
|
||||
protected open fun headersBuilder(): Headers.Builder =
|
||||
Headers.Builder().apply {
|
||||
add("User-Agent", network.defaultUserAgentProvider())
|
||||
}
|
||||
@@ -110,7 +115,7 @@ abstract class HttpSource : CatalogueSource {
|
||||
/**
|
||||
* Visible name of the source.
|
||||
*/
|
||||
override fun toString() = "$name (${lang.uppercase()})"
|
||||
override fun toString(): String = "$name (${lang.uppercase()})"
|
||||
|
||||
/**
|
||||
* Returns an observable containing a page with a list of manga. Normally it's not needed to
|
||||
@@ -118,7 +123,8 @@ abstract class HttpSource : CatalogueSource {
|
||||
*
|
||||
* @param page the page number to retrieve.
|
||||
*/
|
||||
@Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getPopularManga"))
|
||||
@Suppress("DEPRECATION")
|
||||
@Deprecated("Use the suspend API instead", ReplaceWith("getPopularManga"))
|
||||
override fun fetchPopularManga(page: Int): Observable<MangasPage> =
|
||||
client
|
||||
.newCall(popularMangaRequest(page))
|
||||
@@ -132,14 +138,24 @@ abstract class HttpSource : CatalogueSource {
|
||||
*
|
||||
* @param page the page number to retrieve.
|
||||
*/
|
||||
protected abstract fun popularMangaRequest(page: Int): Request
|
||||
@Deprecated(
|
||||
message =
|
||||
"The helper functions are inherently limiting and hides the underlying implementation. " +
|
||||
"Source developers should make their own implementation according to their needs.",
|
||||
)
|
||||
protected open fun popularMangaRequest(page: Int): Request = throw UnsupportedOperationException()
|
||||
|
||||
/**
|
||||
* Parses the response from the site and returns a [MangasPage] object.
|
||||
*
|
||||
* @param response the response from the site.
|
||||
*/
|
||||
protected abstract fun popularMangaParse(response: Response): MangasPage
|
||||
@Deprecated(
|
||||
message =
|
||||
"The helper functions are inherently limiting and hides the underlying implementation. " +
|
||||
"Source developers should make their own implementation according to their needs.",
|
||||
)
|
||||
protected open fun popularMangaParse(response: Response): MangasPage = throw UnsupportedOperationException()
|
||||
|
||||
/**
|
||||
* Returns an observable containing a page with a list of manga. Normally it's not needed to
|
||||
@@ -149,22 +165,17 @@ abstract class HttpSource : CatalogueSource {
|
||||
* @param query the search query.
|
||||
* @param filters the list of filters to apply.
|
||||
*/
|
||||
@Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getSearchManga"))
|
||||
@Suppress("DEPRECATION")
|
||||
@Deprecated("Use the suspend API instead", ReplaceWith("getSearchManga"))
|
||||
override fun fetchSearchManga(
|
||||
page: Int,
|
||||
query: String,
|
||||
filters: FilterList,
|
||||
): Observable<MangasPage> =
|
||||
Observable
|
||||
.defer {
|
||||
try {
|
||||
client.newCall(searchMangaRequest(page, query, filters)).asObservableSuccess()
|
||||
} catch (e: NoClassDefFoundError) {
|
||||
// RxJava doesn't handle Errors, which tends to happen during global searches
|
||||
// if an old extension using non-existent classes is still around
|
||||
throw RuntimeException(e)
|
||||
}
|
||||
}.map { response ->
|
||||
client
|
||||
.newCall(searchMangaRequest(page, query, filters))
|
||||
.asObservableSuccess()
|
||||
.map { response ->
|
||||
searchMangaParse(response)
|
||||
}
|
||||
|
||||
@@ -175,25 +186,36 @@ abstract class HttpSource : CatalogueSource {
|
||||
* @param query the search query.
|
||||
* @param filters the list of filters to apply.
|
||||
*/
|
||||
protected abstract fun searchMangaRequest(
|
||||
@Deprecated(
|
||||
message =
|
||||
"The helper functions are inherently limiting and hides the underlying implementation. " +
|
||||
"Source developers should make their own implementation according to their needs.",
|
||||
)
|
||||
protected open fun searchMangaRequest(
|
||||
page: Int,
|
||||
query: String,
|
||||
filters: FilterList,
|
||||
): Request
|
||||
): Request = throw UnsupportedOperationException()
|
||||
|
||||
/**
|
||||
* Parses the response from the site and returns a [MangasPage] object.
|
||||
*
|
||||
* @param response the response from the site.
|
||||
*/
|
||||
protected abstract fun searchMangaParse(response: Response): MangasPage
|
||||
@Deprecated(
|
||||
message =
|
||||
"The helper functions are inherently limiting and hides the underlying implementation. " +
|
||||
"Source developers should make their own implementation according to their needs.",
|
||||
)
|
||||
protected open fun searchMangaParse(response: Response): MangasPage = throw UnsupportedOperationException()
|
||||
|
||||
/**
|
||||
* 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"))
|
||||
@Suppress("DEPRECATION")
|
||||
@Deprecated("Use the suspend API instead", ReplaceWith("getLatestUpdates"))
|
||||
override fun fetchLatestUpdates(page: Int): Observable<MangasPage> =
|
||||
client
|
||||
.newCall(latestUpdatesRequest(page))
|
||||
@@ -207,26 +229,33 @@ abstract class HttpSource : CatalogueSource {
|
||||
*
|
||||
* @param page the page number to retrieve.
|
||||
*/
|
||||
protected abstract fun latestUpdatesRequest(page: Int): Request
|
||||
@Deprecated(
|
||||
message =
|
||||
"The helper functions are inherently limiting and hides the underlying implementation. " +
|
||||
"Source developers should make their own implementation according to their needs.",
|
||||
)
|
||||
protected open fun latestUpdatesRequest(page: Int): Request = throw UnsupportedOperationException()
|
||||
|
||||
/**
|
||||
* Parses the response from the site and returns a [MangasPage] object.
|
||||
*
|
||||
* @param response the response from the site.
|
||||
*/
|
||||
protected abstract fun latestUpdatesParse(response: Response): MangasPage
|
||||
@Deprecated(
|
||||
message =
|
||||
"The helper functions are inherently limiting and hides the underlying implementation. " +
|
||||
"Source developers should make their own implementation according to their needs.",
|
||||
)
|
||||
protected open fun latestUpdatesParse(response: Response): MangasPage = throw UnsupportedOperationException()
|
||||
|
||||
/**
|
||||
* Get the updated details for a manga.
|
||||
* Normally it's not needed to override this method.
|
||||
* Returns an observable with the updated details for a manga. Normally it's not needed to
|
||||
* override this method.
|
||||
*
|
||||
* @param manga the manga to update.
|
||||
* @return the updated manga.
|
||||
* @param manga the manga to be updated.
|
||||
*/
|
||||
@Suppress("DEPRECATION")
|
||||
override suspend fun getMangaDetails(manga: SManga): SManga = fetchMangaDetails(manga).awaitSingle()
|
||||
|
||||
@Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getMangaDetails"))
|
||||
@Deprecated("Use the combined suspend API instead", replaceWith = ReplaceWith("getMangaUpdate"))
|
||||
override fun fetchMangaDetails(manga: SManga): Observable<SManga> =
|
||||
client
|
||||
.newCall(mangaDetailsRequest(manga))
|
||||
@@ -241,6 +270,11 @@ abstract class HttpSource : CatalogueSource {
|
||||
*
|
||||
* @param manga the manga to be updated.
|
||||
*/
|
||||
@Deprecated(
|
||||
message =
|
||||
"The helper functions are inherently limiting and hides the underlying implementation. " +
|
||||
"Source developers should make their own implementation according to their needs.",
|
||||
)
|
||||
open fun mangaDetailsRequest(manga: SManga): Request = GET(baseUrl + manga.url, headers)
|
||||
|
||||
/**
|
||||
@@ -248,37 +282,28 @@ abstract class HttpSource : CatalogueSource {
|
||||
*
|
||||
* @param response the response from the site.
|
||||
*/
|
||||
protected abstract fun mangaDetailsParse(response: Response): SManga
|
||||
@Deprecated(
|
||||
message =
|
||||
"The helper functions are inherently limiting and hides the underlying implementation. " +
|
||||
"Source developers should make their own implementation according to their needs.",
|
||||
)
|
||||
protected open fun mangaDetailsParse(response: Response): SManga = throw UnsupportedOperationException()
|
||||
|
||||
/**
|
||||
* Get all the available chapters for a manga.
|
||||
* Normally it's not needed to override this method.
|
||||
* Returns an observable with the updated chapter list for a manga. Normally it's not needed to
|
||||
* override this method.
|
||||
*
|
||||
* @param manga the manga to update.
|
||||
* @return the chapters for the manga.
|
||||
* @throws LicensedMangaChaptersException if a manga is licensed and therefore no chapters are available.
|
||||
* @param manga the manga to look for chapters.
|
||||
*/
|
||||
@Suppress("DEPRECATION")
|
||||
override suspend fun getChapterList(manga: SManga): List<SChapter> {
|
||||
if (manga.status == SManga.LICENSED) {
|
||||
throw LicensedMangaChaptersException()
|
||||
}
|
||||
|
||||
return fetchChapterList(manga).awaitSingle()
|
||||
}
|
||||
|
||||
@Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getChapterList"))
|
||||
@Deprecated("Use the combined suspend API instead", replaceWith = ReplaceWith("getMangaUpdate"))
|
||||
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> =
|
||||
if (manga.status != SManga.LICENSED) {
|
||||
client
|
||||
.newCall(chapterListRequest(manga))
|
||||
.asObservableSuccess()
|
||||
.map { response ->
|
||||
chapterListParse(response)
|
||||
}
|
||||
} else {
|
||||
Observable.error(LicensedMangaChaptersException())
|
||||
}
|
||||
client
|
||||
.newCall(chapterListRequest(manga))
|
||||
.asObservableSuccess()
|
||||
.map { response ->
|
||||
chapterListParse(response)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the request for updating the chapter list. Override only if it's needed to override
|
||||
@@ -286,6 +311,11 @@ abstract class HttpSource : CatalogueSource {
|
||||
*
|
||||
* @param manga the manga to look for chapters.
|
||||
*/
|
||||
@Deprecated(
|
||||
message =
|
||||
"The helper functions are inherently limiting and hides the underlying implementation. " +
|
||||
"Source developers should make their own implementation according to their needs.",
|
||||
)
|
||||
protected open fun chapterListRequest(manga: SManga): Request = GET(baseUrl + manga.url, headers)
|
||||
|
||||
/**
|
||||
@@ -293,19 +323,20 @@ abstract class HttpSource : CatalogueSource {
|
||||
*
|
||||
* @param response the response from the site.
|
||||
*/
|
||||
protected abstract fun chapterListParse(response: Response): List<SChapter>
|
||||
@Deprecated(
|
||||
message =
|
||||
"The helper functions are inherently limiting and hides the underlying implementation. " +
|
||||
"Source developers should make their own implementation according to their needs.",
|
||||
)
|
||||
protected open fun chapterListParse(response: Response): List<SChapter> = throw UnsupportedOperationException()
|
||||
|
||||
/**
|
||||
* Get the list of pages a chapter has. Pages should be returned
|
||||
* in the expected order; the index is ignored.
|
||||
* Returns an observable with the page list for a chapter.
|
||||
*
|
||||
* @param chapter the chapter.
|
||||
* @return the pages for the chapter.
|
||||
* @param chapter the chapter whose page list has to be fetched.
|
||||
*/
|
||||
@Suppress("DEPRECATION")
|
||||
override suspend fun getPageList(chapter: SChapter): List<Page> = fetchPageList(chapter).awaitSingle()
|
||||
|
||||
@Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getPageList"))
|
||||
@Deprecated("Use the suspend API instead", ReplaceWith("getPageList"))
|
||||
override fun fetchPageList(chapter: SChapter): Observable<List<Page>> =
|
||||
client
|
||||
.newCall(pageListRequest(chapter))
|
||||
@@ -320,6 +351,11 @@ abstract class HttpSource : CatalogueSource {
|
||||
*
|
||||
* @param chapter the chapter whose page list has to be fetched.
|
||||
*/
|
||||
@Deprecated(
|
||||
message =
|
||||
"The helper functions are inherently limiting and hides the underlying implementation. " +
|
||||
"Source developers should make their own implementation according to their needs.",
|
||||
)
|
||||
protected open fun pageListRequest(chapter: SChapter): Request = GET(baseUrl + chapter.url, headers)
|
||||
|
||||
/**
|
||||
@@ -327,31 +363,47 @@ abstract class HttpSource : CatalogueSource {
|
||||
*
|
||||
* @param response the response from the site.
|
||||
*/
|
||||
protected abstract fun pageListParse(response: Response): List<Page>
|
||||
@Deprecated(
|
||||
message =
|
||||
"The helper functions are inherently limiting and hides the underlying implementation. " +
|
||||
"Source developers should make their own implementation according to their needs.",
|
||||
)
|
||||
protected open fun pageListParse(response: Response): List<Page> = throw UnsupportedOperationException()
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @since extensions-lib 1.5
|
||||
* @param page the page whose source image has to be fetched.
|
||||
*/
|
||||
@Suppress("DEPRECATION")
|
||||
open suspend fun getImageUrl(page: Page): String = fetchImageUrl(page).awaitSingle()
|
||||
|
||||
@Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getImageUrl"))
|
||||
@Deprecated("Use the suspend API instead", ReplaceWith("getImageUrl"))
|
||||
open fun fetchImageUrl(page: Page): Observable<String> =
|
||||
client
|
||||
.newCall(imageUrlRequest(page))
|
||||
.asObservableSuccess()
|
||||
.map { imageUrlParse(it) }
|
||||
|
||||
/**
|
||||
* Returns the image url for the provided [page]. The function is only called if [Page.imageUrl] is null.
|
||||
*
|
||||
* @since tachiyomix 1.6
|
||||
* @param page the page whose source image has to be fetched.
|
||||
*/
|
||||
@Suppress("DEPRECATION")
|
||||
open suspend fun getImageUrl(page: Page): String = fetchImageUrl(page).awaitSingle()
|
||||
|
||||
/**
|
||||
* Returns the request for getting the url to the source image. Override only if it's needed to
|
||||
* override the url, send different headers or request method like POST.
|
||||
*
|
||||
* @param page the chapter whose page list has to be fetched
|
||||
*/
|
||||
@Deprecated(
|
||||
message =
|
||||
"The helper functions are inherently limiting and hides the underlying implementation. " +
|
||||
"Source developers should make their own implementation according to their needs.",
|
||||
)
|
||||
protected open fun imageUrlRequest(page: Page): Request = GET(page.url, headers)
|
||||
|
||||
/**
|
||||
@@ -359,16 +411,14 @@ abstract class HttpSource : CatalogueSource {
|
||||
*
|
||||
* @param response the response from the site.
|
||||
*/
|
||||
protected abstract fun imageUrlParse(response: Response): String
|
||||
@Deprecated(
|
||||
message =
|
||||
"The helper functions are inherently limiting and hides the underlying implementation. " +
|
||||
"Source developers should make their own implementation according to their needs.",
|
||||
)
|
||||
protected open fun imageUrlParse(response: Response): String = throw UnsupportedOperationException()
|
||||
|
||||
/**
|
||||
* Returns the response of the source image.
|
||||
* Typically does not need to be overridden.
|
||||
*
|
||||
* @since extensions-lib 1.5
|
||||
* @param page the page whose source image has to be downloaded.
|
||||
*/
|
||||
open suspend fun getImage(page: Page): Response =
|
||||
suspend fun getImage(page: Page): Response =
|
||||
client
|
||||
.newCachelessCallWithProgress(imageRequest(page), page)
|
||||
.awaitSuccess()
|
||||
@@ -387,6 +437,7 @@ abstract class HttpSource : CatalogueSource {
|
||||
*
|
||||
* @param url the full url to the chapter.
|
||||
*/
|
||||
@Suppress("Unused")
|
||||
fun SChapter.setUrlWithoutDomain(url: String) {
|
||||
this.url = getUrlWithoutDomain(url)
|
||||
}
|
||||
@@ -397,6 +448,7 @@ abstract class HttpSource : CatalogueSource {
|
||||
*
|
||||
* @param url the full url to the manga.
|
||||
*/
|
||||
@Suppress("Unused")
|
||||
fun SManga.setUrlWithoutDomain(url: String) {
|
||||
this.url = getUrlWithoutDomain(url)
|
||||
}
|
||||
@@ -417,7 +469,7 @@ abstract class HttpSource : CatalogueSource {
|
||||
out += "#" + uri.fragment
|
||||
}
|
||||
out
|
||||
} catch (e: URISyntaxException) {
|
||||
} catch (_: URISyntaxException) {
|
||||
orig
|
||||
}
|
||||
|
||||
@@ -428,6 +480,7 @@ abstract class HttpSource : CatalogueSource {
|
||||
* @param manga the manga
|
||||
* @return url of the manga
|
||||
*/
|
||||
@Suppress("DEPRECATION")
|
||||
open fun getMangaUrl(manga: SManga): String = mangaDetailsRequest(manga).url.toString()
|
||||
|
||||
/**
|
||||
@@ -437,6 +490,7 @@ abstract class HttpSource : CatalogueSource {
|
||||
* @param chapter the chapter
|
||||
* @return url of the chapter
|
||||
*/
|
||||
@Suppress("DEPRECATION")
|
||||
open fun getChapterUrl(chapter: SChapter): String = pageListRequest(chapter).url.toString()
|
||||
|
||||
/**
|
||||
@@ -446,15 +500,9 @@ abstract class HttpSource : CatalogueSource {
|
||||
* @param chapter the chapter to be added.
|
||||
* @param manga the manga of the chapter.
|
||||
*/
|
||||
@Deprecated("All modifications should be done when constructing the chapter")
|
||||
open fun prepareNewChapter(
|
||||
chapter: SChapter,
|
||||
manga: SManga,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Returns the list of filters for the source.
|
||||
*/
|
||||
override fun getFilterList() = FilterList()
|
||||
}
|
||||
|
||||
class LicensedMangaChaptersException : Exception("Licensed - No chapters to show")
|
||||
|
||||
@@ -12,12 +12,20 @@ import org.jsoup.nodes.Element
|
||||
/**
|
||||
* A simple implementation for sources from a website using Jsoup, an HTML parser.
|
||||
*/
|
||||
@Deprecated(
|
||||
message =
|
||||
"In most cases sources only require a subset of the methods from this class. " +
|
||||
"Source developers should make their own implementation according to their needs.",
|
||||
)
|
||||
abstract class ParsedHttpSource : HttpSource() {
|
||||
/**
|
||||
* Parses the response from the site and returns a [MangasPage] object.
|
||||
*
|
||||
* @param response the response from the site.
|
||||
*/
|
||||
@Deprecated(
|
||||
"The helper functions are inherently limiting and hides the underlying implementation. Source developers should make their own implementation according to their needs.",
|
||||
)
|
||||
override fun popularMangaParse(response: Response): MangasPage {
|
||||
val document = response.asJsoup()
|
||||
|
||||
@@ -58,6 +66,9 @@ abstract class ParsedHttpSource : HttpSource() {
|
||||
*
|
||||
* @param response the response from the site.
|
||||
*/
|
||||
@Deprecated(
|
||||
"The helper functions are inherently limiting and hides the underlying implementation. Source developers should make their own implementation according to their needs.",
|
||||
)
|
||||
override fun searchMangaParse(response: Response): MangasPage {
|
||||
val document = response.asJsoup()
|
||||
|
||||
@@ -98,6 +109,9 @@ abstract class ParsedHttpSource : HttpSource() {
|
||||
*
|
||||
* @param response the response from the site.
|
||||
*/
|
||||
@Deprecated(
|
||||
"The helper functions are inherently limiting and hides the underlying implementation. Source developers should make their own implementation according to their needs.",
|
||||
)
|
||||
override fun latestUpdatesParse(response: Response): MangasPage {
|
||||
val document = response.asJsoup()
|
||||
|
||||
@@ -138,6 +152,9 @@ abstract class ParsedHttpSource : HttpSource() {
|
||||
*
|
||||
* @param response the response from the site.
|
||||
*/
|
||||
@Deprecated(
|
||||
"The helper functions are inherently limiting and hides the underlying implementation. Source developers should make their own implementation according to their needs.",
|
||||
)
|
||||
override fun mangaDetailsParse(response: Response): SManga = mangaDetailsParse(response.asJsoup())
|
||||
|
||||
/**
|
||||
@@ -152,6 +169,9 @@ abstract class ParsedHttpSource : HttpSource() {
|
||||
*
|
||||
* @param response the response from the site.
|
||||
*/
|
||||
@Deprecated(
|
||||
"The helper functions are inherently limiting and hides the underlying implementation. Source developers should make their own implementation according to their needs.",
|
||||
)
|
||||
override fun chapterListParse(response: Response): List<SChapter> {
|
||||
val document = response.asJsoup()
|
||||
return document.select(chapterListSelector()).map { chapterFromElement(it) }
|
||||
@@ -174,6 +194,9 @@ abstract class ParsedHttpSource : HttpSource() {
|
||||
*
|
||||
* @param response the response from the site.
|
||||
*/
|
||||
@Deprecated(
|
||||
"The helper functions are inherently limiting and hides the underlying implementation. Source developers should make their own implementation according to their needs.",
|
||||
)
|
||||
override fun pageListParse(response: Response): List<Page> = pageListParse(response.asJsoup())
|
||||
|
||||
/**
|
||||
@@ -188,6 +211,9 @@ abstract class ParsedHttpSource : HttpSource() {
|
||||
*
|
||||
* @param response the response from the site.
|
||||
*/
|
||||
@Deprecated(
|
||||
"The helper functions are inherently limiting and hides the underlying implementation. Source developers should make their own implementation according to their needs.",
|
||||
)
|
||||
override fun imageUrlParse(response: Response): String = imageUrlParse(response.asJsoup())
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,26 +1,44 @@
|
||||
package eu.kanade.tachiyomi.source.online
|
||||
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
|
||||
/**
|
||||
* A source that may handle opening an SManga for a given URI.
|
||||
* A source that may handle opening an SManga or SChapter for a given URI.
|
||||
*
|
||||
* @since extensions-lib 1.5
|
||||
*/
|
||||
@Suppress("unused")
|
||||
interface ResolvableSource : Source {
|
||||
/**
|
||||
* Whether this source may potentially handle the given URI.
|
||||
* Returns what the given URI may open.
|
||||
* Returns [UriType.Unknown] if the source is not able to resolve the URI.
|
||||
*
|
||||
* @since extensions-lib 1.5
|
||||
*/
|
||||
fun canResolveUri(uri: String): Boolean
|
||||
fun getUriType(uri: String): UriType
|
||||
|
||||
/**
|
||||
* Called if canHandleUri is true. Returns the corresponding SManga, if possible.
|
||||
* Called if [getUriType] is [UriType.Manga].
|
||||
* Returns the corresponding SManga, if possible.
|
||||
*
|
||||
* @since extensions-lib 1.5
|
||||
*/
|
||||
suspend fun getManga(uri: String): SManga?
|
||||
|
||||
/**
|
||||
* Called if [getUriType] is [UriType.Chapter].
|
||||
* Returns the corresponding SChapter, if possible.
|
||||
*
|
||||
* @since extensions-lib 1.5
|
||||
*/
|
||||
suspend fun getChapter(uri: String): SChapter?
|
||||
}
|
||||
|
||||
sealed interface UriType {
|
||||
data object Manga : UriType
|
||||
|
||||
data object Chapter : UriType
|
||||
|
||||
data object Unknown : UriType
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user