Implement Non-Final 1.5 Extensions API (#699)

* Implement non-final 1.5 extensions API

* Bump lib version max

* Add visibility to preferences

* Add preference visibility
This commit is contained in:
Mitchell Syer
2023-10-04 22:01:45 -04:00
committed by GitHub
parent 354968fba7
commit c8865ad185
28 changed files with 491 additions and 261 deletions

View File

@@ -22,6 +22,7 @@ public class Preference {
@JsonIgnore @JsonIgnore
protected Context context; protected Context context;
private boolean isVisible;
private String key; private String key;
private CharSequence title; private CharSequence title;
private CharSequence summary; private CharSequence summary;
@@ -100,6 +101,14 @@ public class Preference {
return sharedPreferences; return sharedPreferences;
} }
public void setVisible(boolean visible) {
isVisible = visible;
}
public boolean getVisible() {
return isVisible;
}
/** Tachidesk specific API */ /** Tachidesk specific API */
public void setSharedPreferences(SharedPreferences sharedPreferences) { public void setSharedPreferences(SharedPreferences sharedPreferences) {
this.sharedPreferences = sharedPreferences; this.sharedPreferences = sharedPreferences;

View File

@@ -37,6 +37,10 @@ subprojects {
dependsOn("formatKotlin") dependsOn("formatKotlin")
kotlinOptions { kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8.toString() jvmTarget = JavaVersion.VERSION_1_8.toString()
freeCompilerArgs += listOf(
"-Xcontext-receivers"
)
} }
} }

View File

@@ -27,6 +27,7 @@ coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", ve
# Serialization # Serialization
serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "serialization" } serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "serialization" }
serialization-json-okio = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json-okio", version.ref = "serialization" }
serialization-protobuf = { module = "org.jetbrains.kotlinx:kotlinx-serialization-protobuf", version.ref = "serialization" } serialization-protobuf = { module = "org.jetbrains.kotlinx:kotlinx-serialization-protobuf", version.ref = "serialization" }
serialization-xml-core = { module = "io.github.pdvrieze.xmlutil:core-jvm", version.ref = "xmlserialization" } serialization-xml-core = { module = "io.github.pdvrieze.xmlutil:core-jvm", version.ref = "xmlserialization" }
serialization-xml = { module = "io.github.pdvrieze.xmlutil:serialization-jvm", version.ref = "xmlserialization" } serialization-xml = { module = "io.github.pdvrieze.xmlutil:serialization-jvm", version.ref = "xmlserialization" }
@@ -167,6 +168,7 @@ shared = [
"coroutines-core", "coroutines-core",
"coroutines-jdk8", "coroutines-jdk8",
"serialization-json", "serialization-json",
"serialization-json-okio",
"serialization-protobuf", "serialization-protobuf",
"kodein", "kodein",
"slf4japi", "slf4japi",

View File

@@ -1,14 +1,33 @@
package eu.kanade.tachiyomi package eu.kanade.tachiyomi
import suwayomi.tachidesk.manga.impl.util.storage.ImageUtil
/** /**
* Used by extensions. * Used by extensions.
* *
* @since extension-lib 1.3 * @since extension-lib 1.3
*/ */
object AppInfo { object AppInfo {
/** should be something like 74 */ /**
*
* should be something like 74
*
* @since extension-lib 1.3
*/
fun getVersionCode() = suwayomi.tachidesk.server.BuildConfig.REVISION.substring(1).toInt() fun getVersionCode() = suwayomi.tachidesk.server.BuildConfig.REVISION.substring(1).toInt()
/** should be something like "0.13.1" */ /**
* should be something like "0.13.1"
*
* @since extension-lib 1.3
*/
fun getVersionName() = suwayomi.tachidesk.server.BuildConfig.VERSION.substring(1) fun getVersionName() = suwayomi.tachidesk.server.BuildConfig.VERSION.substring(1)
/**
* A list of supported image MIME types by the reader.
* e.g. ["image/jpeg", "image/png", ...]
*
* @since extension-lib 1.5
*/
fun getSupportedImageMimeTypes(): List<String> = ImageUtil.ImageType.entries.map { it.mime }
} }

View File

@@ -1,19 +1,20 @@
package eu.kanade.tachiyomi.network package eu.kanade.tachiyomi.network
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.serialization.decodeFromString import kotlinx.serialization.DeserializationStrategy
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlinx.serialization.json.okio.decodeFromBufferedSource
import kotlinx.serialization.serializer
import okhttp3.Call import okhttp3.Call
import okhttp3.Callback import okhttp3.Callback
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import okhttp3.Response import okhttp3.Response
import okhttp3.internal.closeQuietly
import rx.Observable import rx.Observable
import rx.Producer import rx.Producer
import rx.Subscription import rx.Subscription
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.fullType
import java.io.IOException import java.io.IOException
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
import kotlin.coroutines.resumeWithException import kotlin.coroutines.resumeWithException
@@ -55,29 +56,37 @@ fun Call.asObservable(): Observable<Response> {
} }
} }
fun Call.asObservableSuccess(): Observable<Response> {
return asObservable()
.doOnNext { response ->
if (!response.isSuccessful) {
response.close()
throw HttpException(response.code)
}
}
}
// Based on https://github.com/gildor/kotlin-coroutines-okhttp // Based on https://github.com/gildor/kotlin-coroutines-okhttp
suspend fun Call.await(): Response { @OptIn(ExperimentalCoroutinesApi::class)
private suspend fun Call.await(callStack: Array<StackTraceElement>): Response {
return suspendCancellableCoroutine { continuation -> return suspendCancellableCoroutine { continuation ->
enqueue( val callback =
object : Callback { object : Callback {
override fun onResponse(call: Call, response: Response) { override fun onResponse(call: Call, response: Response) {
if (!response.isSuccessful) {
continuation.resumeWithException(HttpException(response.code))
return
}
continuation.resume(response) { continuation.resume(response) {
response.body.closeQuietly() response.body.close()
} }
} }
override fun onFailure(call: Call, e: IOException) { override fun onFailure(call: Call, e: IOException) {
// Don't bother with resuming the continuation if it is already cancelled. // Don't bother with resuming the continuation if it is already cancelled.
if (continuation.isCancelled) return if (continuation.isCancelled) return
continuation.resumeWithException(e) val exception = IOException(e.message, e).apply { stackTrace = callStack }
continuation.resumeWithException(exception)
} }
} }
)
enqueue(callback)
continuation.invokeOnCancellation { continuation.invokeOnCancellation {
try { try {
@@ -89,32 +98,25 @@ suspend fun Call.await(): Response {
} }
} }
fun Call.asObservableSuccess(): Observable<Response> { suspend fun Call.await(): Response {
return asObservable() val callStack = Exception().stackTrace.run { copyOfRange(1, size) }
.doOnNext { response -> return await(callStack)
if (!response.isSuccessful) {
response.close()
throw HttpException(response.code)
}
}
} }
// fun OkHttpClient.newCallWithProgress(request: Request, listener: ProgressListener): Call { /**
// val progressClient = newBuilder() * @since extensions-lib 1.5
// .cache(nasObservableSuccessull) */
// .addNetworkInterceptor { chain -> suspend fun Call.awaitSuccess(): Response {
// val originalResponse = chain.proceed(chain.request()) val callStack = Exception().stackTrace.run { copyOfRange(1, size) }
// originalResponse.newBuilder() val response = await(callStack)
// .body(ProgressResponseBody(originalResponse.body!!, listener)) if (!response.isSuccessful) {
// .build() response.close()
// } throw HttpException(response.code).apply { stackTrace = callStack }
// .build() }
// return response
// return progressClient.newCall(request) }
// }
@Suppress("UNUSED_PARAMETER") fun OkHttpClient.newCachelessCallWithProgress(request: Request, listener: ProgressListener): Call {
fun OkHttpClient.newCallWithProgress(request: Request, listener: ProgressListener): Call {
val progressClient = newBuilder() val progressClient = newBuilder()
.cache(null) .cache(null)
.addNetworkInterceptor { chain -> .addNetworkInterceptor { chain ->
@@ -128,12 +130,19 @@ fun OkHttpClient.newCallWithProgress(request: Request, listener: ProgressListene
return progressClient.newCall(request) return progressClient.newCall(request)
} }
context(Json)
inline fun <reified T> Response.parseAs(): T { inline fun <reified T> Response.parseAs(): T {
// Avoiding Injekt.get<Json>() due to compiler issues return decodeFromJsonResponse(serializer(), this)
val json = Injekt.getInstance<Json>(fullType<Json>().type) }
this.use {
val responseBody = it.body.string() context(Json)
return json.decodeFromString(responseBody) @OptIn(ExperimentalSerializationApi::class)
fun <T> decodeFromJsonResponse(
deserializer: DeserializationStrategy<T>,
response: Response
): T {
return response.body.source().use {
decodeFromBufferedSource(deserializer, it)
} }
} }

View File

@@ -4,11 +4,21 @@ import android.os.SystemClock
import okhttp3.Interceptor import okhttp3.Interceptor
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Response import okhttp3.Response
import java.io.IOException
import java.util.ArrayDeque
import java.util.concurrent.Semaphore
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds
import kotlin.time.toDuration
import kotlin.time.toDurationUnit
/** /**
* An OkHttp interceptor that handles rate limiting. * An OkHttp interceptor that handles rate limiting.
* *
* This uses `java.time` APIs and is the legacy method, kept
* for compatibility reasons with existing extensions.
*
* Examples: * Examples:
* *
* permits = 5, period = 1, unit = seconds => 5 requests per second * permits = 5, period = 1, unit = seconds => 5 requests per second
@@ -16,52 +26,103 @@ import java.util.concurrent.TimeUnit
* *
* @since extension-lib 1.3 * @since extension-lib 1.3
* *
* @param permits {Int} Number of requests allowed within a period of units. * @param permits [Int] Number of requests allowed within a period of units.
* @param period {Long} The limiting duration. Defaults to 1. * @param period [Long] The limiting duration. Defaults to 1.
* @param unit {TimeUnit} The unit of time for the period. Defaults to seconds. * @param unit [TimeUnit] The unit of time for the period. Defaults to seconds.
*/ */
@Deprecated("Use the version with kotlin.time APIs instead.")
fun OkHttpClient.Builder.rateLimit( fun OkHttpClient.Builder.rateLimit(
permits: Int, permits: Int,
period: Long = 1, period: Long = 1,
unit: TimeUnit = TimeUnit.SECONDS unit: TimeUnit = TimeUnit.SECONDS
) = addInterceptor(RateLimitInterceptor(permits, period, unit)) ) = addInterceptor(RateLimitInterceptor(null, permits, period.toDuration(unit.toDurationUnit())))
private class RateLimitInterceptor( /**
* An OkHttp interceptor that handles rate limiting.
*
* Examples:
*
* permits = 5, period = 1.seconds => 5 requests per second
* permits = 10, period = 2.minutes => 10 requests per 2 minutes
*
* @since extension-lib 1.5
*
* @param permits [Int] Number of requests allowed within a period of units.
* @param period [Duration] The limiting duration. Defaults to 1.seconds.
*/
fun OkHttpClient.Builder.rateLimit(permits: Int, period: Duration = 1.seconds) =
addInterceptor(RateLimitInterceptor(null, permits, period))
/** We can probably accept domains or wildcards by comparing with [endsWith], etc. */
@Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN")
internal class RateLimitInterceptor(
private val host: String?,
private val permits: Int, private val permits: Int,
period: Long, period: Duration
unit: TimeUnit
) : Interceptor { ) : Interceptor {
private val requestQueue = ArrayList<Long>(permits) private val requestQueue = ArrayDeque<Long>(permits)
private val rateLimitMillis = unit.toMillis(period) private val rateLimitMillis = period.inWholeMilliseconds
private val fairLock = Semaphore(1, true)
override fun intercept(chain: Interceptor.Chain): Response { override fun intercept(chain: Interceptor.Chain): Response {
synchronized(requestQueue) { val call = chain.call()
val now = SystemClock.elapsedRealtime() if (call.isCanceled()) throw IOException("Canceled")
val waitTime = if (requestQueue.size < permits) {
0
} else {
val oldestReq = requestQueue[0]
val newestReq = requestQueue[permits - 1]
if (newestReq - oldestReq > rateLimitMillis) { val request = chain.request()
0 when (host) {
} else { null, request.url.host -> {} // need rate limit
oldestReq + rateLimitMillis - now // Remaining time else -> return chain.proceed(request)
}
try {
fairLock.acquire()
} catch (e: InterruptedException) {
throw IOException(e)
}
val requestQueue = this.requestQueue
val timestamp: Long
try {
synchronized(requestQueue) {
while (requestQueue.size >= permits) { // queue is full, remove expired entries
val periodStart = SystemClock.elapsedRealtime() - rateLimitMillis
var hasRemovedExpired = false
while (requestQueue.isEmpty().not() && requestQueue.first <= periodStart) {
requestQueue.removeFirst()
hasRemovedExpired = true
}
if (call.isCanceled()) {
throw IOException("Canceled")
} else if (hasRemovedExpired) {
break
} else {
try { // wait for the first entry to expire, or notified by cached response
(requestQueue as Object).wait(requestQueue.first - periodStart)
} catch (_: InterruptedException) {
continue
}
}
} }
}
if (requestQueue.size == permits) { // add request to queue
requestQueue.removeAt(0) timestamp = SystemClock.elapsedRealtime()
requestQueue.addLast(timestamp)
} }
if (waitTime > 0) { } finally {
requestQueue.add(now + waitTime) fairLock.release()
Thread.sleep(waitTime) // Sleep inside synchronized to pause queued requests }
} else {
requestQueue.add(now) val response = chain.proceed(request)
if (response.networkResponse == null) { // response is cached, remove it from queue
synchronized(requestQueue) {
if (requestQueue.isEmpty() || timestamp < requestQueue.first()) return@synchronized
requestQueue.removeFirstOccurrence(timestamp)
(requestQueue as Object).notifyAll()
} }
} }
return chain.proceed(chain.request()) return response
} }
} }

View File

@@ -1,75 +1,73 @@
package eu.kanade.tachiyomi.network.interceptor package eu.kanade.tachiyomi.network.interceptor
import android.os.SystemClock
import okhttp3.HttpUrl import okhttp3.HttpUrl
import okhttp3.Interceptor import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Response
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds
import kotlin.time.toDuration
import kotlin.time.toDurationUnit
/** /**
* An OkHttp interceptor that handles given url host's rate limiting. * An OkHttp interceptor that handles given url host's rate limiting.
* *
* Examples: * Examples:
* *
* httpUrl = "api.manga.com".toHttpUrlOrNull(), permits = 5, period = 1, unit = seconds => 5 requests per second to api.manga.com * httpUrl = "https://api.manga.com".toHttpUrlOrNull(), permits = 5, period = 1, unit = seconds => 5 requests per second to api.manga.com
* httpUrl = "imagecdn.manga.com".toHttpUrlOrNull(), permits = 10, period = 2, unit = minutes => 10 requests per 2 minutes to imagecdn.manga.com * httpUrl = "https://imagecdn.manga.com".toHttpUrlOrNull(), permits = 10, period = 2, unit = minutes => 10 requests per 2 minutes to imagecdn.manga.com
* *
* @since extension-lib 1.3 * @since extension-lib 1.3
* *
* @param httpUrl {HttpUrl} The url host that this interceptor should handle. Will get url's host by using HttpUrl.host() * @param httpUrl [HttpUrl] The url host that this interceptor should handle. Will get url's host by using HttpUrl.host()
* @param permits {Int} Number of requests allowed within a period of units. * @param permits [Int] Number of requests allowed within a period of units.
* @param period {Long} The limiting duration. Defaults to 1. * @param period [Long] The limiting duration. Defaults to 1.
* @param unit {TimeUnit} The unit of time for the period. Defaults to seconds. * @param unit [TimeUnit] The unit of time for the period. Defaults to seconds.
*/ */
@Deprecated("Use the version with kotlin.time APIs instead.")
fun OkHttpClient.Builder.rateLimitHost( fun OkHttpClient.Builder.rateLimitHost(
httpUrl: HttpUrl, httpUrl: HttpUrl,
permits: Int, permits: Int,
period: Long = 1, period: Long = 1,
unit: TimeUnit = TimeUnit.SECONDS unit: TimeUnit = TimeUnit.SECONDS
) = addInterceptor(SpecificHostRateLimitInterceptor(httpUrl, permits, period, unit)) ) = addInterceptor(RateLimitInterceptor(httpUrl.host, permits, period.toDuration(unit.toDurationUnit())))
class SpecificHostRateLimitInterceptor( /**
* An OkHttp interceptor that handles given url host's rate limiting.
*
* Examples:
*
* httpUrl = "https://api.manga.com".toHttpUrlOrNull(), permits = 5, period = 1.seconds => 5 requests per second to api.manga.com
* httpUrl = "https://imagecdn.manga.com".toHttpUrlOrNull(), permits = 10, period = 2.minutes => 10 requests per 2 minutes to imagecdn.manga.com
*
* @since extension-lib 1.5
*
* @param httpUrl [HttpUrl] The url host that this interceptor should handle. Will get url's host by using HttpUrl.host()
* @param permits [Int] Number of requests allowed within a period of units.
* @param period [Duration] The limiting duration. Defaults to 1.seconds.
*/
fun OkHttpClient.Builder.rateLimitHost(
httpUrl: HttpUrl, httpUrl: HttpUrl,
private val permits: Int, permits: Int,
period: Long, period: Duration = 1.seconds
unit: TimeUnit ): OkHttpClient.Builder = addInterceptor(RateLimitInterceptor(httpUrl.host, permits, period))
) : Interceptor {
private val requestQueue = ArrayList<Long>(permits) /**
private val rateLimitMillis = unit.toMillis(period) * An OkHttp interceptor that handles given url host's rate limiting.
private val host = httpUrl.host *
* Examples:
override fun intercept(chain: Interceptor.Chain): Response { *
if (chain.request().url.host != host) { * url = "https://api.manga.com", permits = 5, period = 1.seconds => 5 requests per second to api.manga.com
return chain.proceed(chain.request()) * url = "https://imagecdn.manga.com", permits = 10, period = 2.minutes => 10 requests per 2 minutes to imagecdn.manga.com
} *
synchronized(requestQueue) { * @since extension-lib 1.5
val now = SystemClock.elapsedRealtime() *
val waitTime = if (requestQueue.size < permits) { * @param url [String] The url host that this interceptor should handle. Will get url's host by using HttpUrl.host()
0 * @param permits [Int] Number of requests allowed within a period of units.
} else { * @param period [Duration] The limiting duration. Defaults to 1.seconds.
val oldestReq = requestQueue[0] */
val newestReq = requestQueue[permits - 1] fun OkHttpClient.Builder.rateLimitHost(
url: String,
if (newestReq - oldestReq > rateLimitMillis) { permits: Int,
0 period: Duration = 1.seconds
} else { ): OkHttpClient.Builder = addInterceptor(RateLimitInterceptor(url.toHttpUrlOrNull()?.host, permits, period))
oldestReq + rateLimitMillis - now // Remaining time
}
}
if (requestQueue.size == permits) {
requestQueue.removeAt(0)
}
if (waitTime > 0) {
requestQueue.add(now + waitTime)
Thread.sleep(waitTime) // Sleep inside synchronized to pause queued requests
} else {
requestQueue.add(now)
}
}
return chain.proceed(chain.request())
}
}

View File

@@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.source
import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage import eu.kanade.tachiyomi.source.model.MangasPage
import rx.Observable import rx.Observable
import suwayomi.tachidesk.manga.impl.util.lang.awaitSingle
interface CatalogueSource : Source { interface CatalogueSource : Source {
@@ -17,30 +18,63 @@ interface CatalogueSource : Source {
val supportsLatest: Boolean val supportsLatest: Boolean
/** /**
* Returns an observable containing a page with a list of manga. * Get a page with a list of manga.
* *
* @since extensions-lib 1.5
* @param page the page number to retrieve. * @param page the page number to retrieve.
*/ */
fun fetchPopularManga(page: Int): Observable<MangasPage> @Suppress("DEPRECATION")
suspend fun getPopularManga(page: Int): MangasPage {
return fetchPopularManga(page).awaitSingle()
}
/** /**
* Returns an observable containing a page with a list of manga. * Get a page with a list of manga.
* *
* @since extensions-lib 1.5
* @param page the page number to retrieve. * @param page the page number to retrieve.
* @param query the search query. * @param query the search query.
* @param filters the list of filters to apply. * @param filters the list of filters to apply.
*/ */
fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> @Suppress("DEPRECATION")
suspend fun getSearchManga(page: Int, query: String, filters: FilterList): MangasPage {
return fetchSearchManga(page, query, filters).awaitSingle()
}
/** /**
* Returns an observable containing a page with a list of latest manga updates. * Get a page with a list of latest manga updates.
* *
* @since extensions-lib 1.5
* @param page the page number to retrieve. * @param page the page number to retrieve.
*/ */
fun fetchLatestUpdates(page: Int): Observable<MangasPage> @Suppress("DEPRECATION")
suspend fun getLatestUpdates(page: Int): MangasPage {
return fetchLatestUpdates(page).awaitSingle()
}
/** /**
* Returns the list of filters for the source. * Returns the list of filters for the source.
*/ */
fun getFilterList(): FilterList 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")
)
fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> =
throw IllegalStateException("Not used")
@Deprecated(
"Use the non-RxJava API instead",
ReplaceWith("getLatestUpdates")
)
fun fetchLatestUpdates(page: Int): Observable<MangasPage> =
throw IllegalStateException("Not used")
} }

View File

@@ -1,8 +1,27 @@
package eu.kanade.tachiyomi.source package eu.kanade.tachiyomi.source
import android.app.Application
import android.content.Context
import android.content.SharedPreferences
import androidx.preference.PreferenceScreen import androidx.preference.PreferenceScreen
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
interface ConfigurableSource : Source { interface ConfigurableSource : Source {
/**
* Gets instance of [SharedPreferences] scoped to the specific source.
*
* @since extensions-lib 1.5
*/
fun getSourcePreferences(): SharedPreferences =
Injekt.get<Application>().getSharedPreferences(preferenceKey(), Context.MODE_PRIVATE)
fun setupPreferenceScreen(screen: PreferenceScreen) fun setupPreferenceScreen(screen: PreferenceScreen)
} }
private fun ConfigurableSource.preferenceKey(): String = "source_$id"
// TODO: use getSourcePreferences once all extensions are on ext-lib 1.5
fun ConfigurableSource.sourcePreferences(): SharedPreferences =
Injekt.get<Application>().getSharedPreferences(preferenceKey(), Context.MODE_PRIVATE)

View File

@@ -22,41 +22,11 @@ interface Source {
val name: String val name: String
/** /**
* Returns an observable with the updated details for a manga. * Get the updated details for a manga.
* *
* @since extensions-lib 1.5
* @param manga the manga to update. * @param manga the manga to update.
*/ * @return the updated manga.
@Deprecated(
"Use the 1.x API instead",
ReplaceWith("getMangaDetails")
)
fun fetchMangaDetails(manga: SManga): Observable<SManga> = throw IllegalStateException("Not used")
/**
* Returns an observable with all the available chapters for a manga.
*
* @param manga the manga to update.
*/
@Deprecated(
"Use the 1.x API instead",
ReplaceWith("getChapterList")
)
fun fetchChapterList(manga: SManga): Observable<List<SChapter>> = throw IllegalStateException("Not used")
/**
* Returns an observable with the list of pages a chapter has. Pages should be returned
* in the expected order; the index is ignored.
*
* @param chapter the chapter.
*/
@Deprecated(
"Use the 1.x API instead",
ReplaceWith("getPageList")
)
fun fetchPageList(chapter: SChapter): Observable<List<Page>> = Observable.empty()
/**
* [1.x API] Get the updated details for a manga.
*/ */
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
suspend fun getMangaDetails(manga: SManga): SManga { suspend fun getMangaDetails(manga: SManga): SManga {
@@ -64,7 +34,11 @@ interface Source {
} }
/** /**
* [1.x API] Get all the available chapters for a manga. * 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.
*/ */
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
suspend fun getChapterList(manga: SManga): List<SChapter> { suspend fun getChapterList(manga: SManga): List<SChapter> {
@@ -72,13 +46,35 @@ interface Source {
} }
/** /**
* [1.x API] Get the list of pages a chapter has. Pages should be returned * Get the list of pages a chapter has. Pages should be returned
* in the expected order; the index is ignored. * in the expected order; the index is ignored.
*
* @since extensions-lib 1.5
* @param chapter the chapter.
* @return the pages for the chapter.
*/ */
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
suspend fun getPageList(chapter: SChapter): List<Page> { suspend fun getPageList(chapter: SChapter): List<Page> {
return fetchPageList(chapter).awaitSingle() return fetchPageList(chapter).awaitSingle()
} }
@Deprecated(
"Use the non-RxJava API instead",
ReplaceWith("getMangaDetails")
)
fun fetchMangaDetails(manga: SManga): Observable<SManga> = throw IllegalStateException("Not used")
@Deprecated(
"Use the non-RxJava API instead",
ReplaceWith("getChapterList")
)
fun fetchChapterList(manga: SManga): Observable<List<SChapter>> = throw IllegalStateException("Not used")
@Deprecated(
"Use the non-RxJava API instead",
ReplaceWith("getPageList")
)
fun fetchPageList(chapter: SChapter): Observable<List<Page>> = Observable.empty()
} }
// fun Source.icon(): Drawable? = Injekt.get<ExtensionManager>().getAppIconForSource(this) // fun Source.icon(): Drawable? = Injekt.get<ExtensionManager>().getAppIconForSource(this)

View File

@@ -40,7 +40,6 @@ import org.jetbrains.exposed.sql.transactions.transaction
import org.kodein.di.DI import org.kodein.di.DI
import org.kodein.di.conf.global import org.kodein.di.conf.global
import org.kodein.di.instance import org.kodein.di.instance
import rx.Observable
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.registerCatalogueSource import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.registerCatalogueSource
import suwayomi.tachidesk.manga.impl.util.storage.ImageUtil import suwayomi.tachidesk.manga.impl.util.storage.ImageUtil
import suwayomi.tachidesk.manga.model.table.ExtensionTable import suwayomi.tachidesk.manga.model.table.ExtensionTable
@@ -76,11 +75,11 @@ class LocalSource(
override val supportsLatest: Boolean = true override val supportsLatest: Boolean = true
// Browse related // Browse related
override fun fetchPopularManga(page: Int) = fetchSearchManga(page, "", POPULAR_FILTERS) override suspend fun getPopularManga(page: Int) = getSearchManga(page, "", POPULAR_FILTERS)
override fun fetchLatestUpdates(page: Int) = fetchSearchManga(page, "", LATEST_FILTERS) override suspend fun getLatestUpdates(page: Int) = getSearchManga(page, "", LATEST_FILTERS)
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> { override suspend fun getSearchManga(page: Int, query: String, filters: FilterList): MangasPage {
val baseDirsFiles = fileSystem.getFilesInBaseDirectories() val baseDirsFiles = fileSystem.getFilesInBaseDirectories()
val lastModifiedLimit by lazy { if (filters === LATEST_FILTERS) System.currentTimeMillis() - LATEST_THRESHOLD else 0L } val lastModifiedLimit by lazy { if (filters === LATEST_FILTERS) System.currentTimeMillis() - LATEST_THRESHOLD else 0L }
var mangaDirs = baseDirsFiles var mangaDirs = baseDirsFiles
@@ -153,7 +152,7 @@ class LocalSource(
} }
} }
return Observable.just(MangasPage(mangas.toList(), false)) return MangasPage(mangas.toList(), false)
} }
// Manga details related // Manga details related

View File

@@ -3,8 +3,9 @@ package eu.kanade.tachiyomi.source.online
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.NetworkHelper import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.network.asObservableSuccess import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.network.awaitSuccess
import eu.kanade.tachiyomi.network.interceptor.CFClearance.getWebViewUserAgent import eu.kanade.tachiyomi.network.interceptor.CFClearance.getWebViewUserAgent
import eu.kanade.tachiyomi.network.newCallWithProgress import eu.kanade.tachiyomi.network.newCachelessCallWithProgress
import eu.kanade.tachiyomi.source.CatalogueSource import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage import eu.kanade.tachiyomi.source.model.MangasPage
@@ -16,6 +17,7 @@ import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import okhttp3.Response import okhttp3.Response
import rx.Observable import rx.Observable
import suwayomi.tachidesk.manga.impl.util.lang.awaitSingle
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
// import uy.kohesive.injekt.injectLazy // import uy.kohesive.injekt.injectLazy
import java.net.URI import java.net.URI
@@ -51,15 +53,16 @@ abstract class HttpSource : CatalogueSource {
open val versionId = 1 open val versionId = 1
/** /**
* Id of the source. By default it uses a generated id using the first 16 characters (64 bits) * 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 * of the MD5 of the string `"${name.lowercase()}/$lang/$versionId"`.
* Note the generated id sets the sign bit to 0. *
* The ID is generated by the [generateId] function, which can be reused if needed
* to generate outdated IDs for cases where the source name or language needs to
* be changed but migrations can be avoided.
*
* Note: the generated ID sets the sign bit to `0`.
*/ */
override val id by lazy { override val id by lazy { generateId(name, lang, versionId) }
val key = "${name.lowercase()}/$lang/$versionId"
val bytes = MessageDigest.getInstance("MD5").digest(key.toByteArray())
(0..7).map { bytes[it].toLong() and 0xff shl 8 * (7 - it) }.reduce(Long::or) and Long.MAX_VALUE
}
/** /**
* Headers used for requests. * Headers used for requests.
@@ -72,6 +75,28 @@ abstract class HttpSource : CatalogueSource {
open val client: OkHttpClient open val client: OkHttpClient
get() = network.client get() = network.client
/**
* 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
* `"${name.lowercase()}/$lang/$versionId"`.
*
* Note: the generated ID sets the sign bit to `0`.
*
* Can be used to generate outdated IDs, such as when the source name or language
* needs to be changed but migrations can be avoided.
*
* @since extensions-lib 1.5
* @param name [String] the name of the source
* @param lang [String] the language of the source
* @param versionId [Int] the version ID of the source
* @return a unique ID for the source
*/
protected fun generateId(name: String, lang: String, versionId: Int): Long {
val key = "${name.lowercase()}/$lang/$versionId"
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
}
/** /**
* Headers builder for requests. Implementations can override this method for custom headers. * Headers builder for requests. Implementations can override this method for custom headers.
*/ */
@@ -90,6 +115,7 @@ abstract class HttpSource : CatalogueSource {
* *
* @param page the page number to retrieve. * @param page the page number to retrieve.
*/ */
@Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getPopularManga"))
override fun fetchPopularManga(page: Int): Observable<MangasPage> { override fun fetchPopularManga(page: Int): Observable<MangasPage> {
return client.newCall(popularMangaRequest(page)) return client.newCall(popularMangaRequest(page))
.asObservableSuccess() .asObservableSuccess()
@@ -120,6 +146,7 @@ abstract class HttpSource : CatalogueSource {
* @param query the search query. * @param query the search query.
* @param filters the list of filters to apply. * @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> { override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
return client.newCall(searchMangaRequest(page, query, filters)) return client.newCall(searchMangaRequest(page, query, filters))
.asObservableSuccess() .asObservableSuccess()
@@ -149,6 +176,7 @@ abstract class HttpSource : CatalogueSource {
* *
* @param page the page number to retrieve. * @param page the page number to retrieve.
*/ */
@Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getLatestUpdates"))
override fun fetchLatestUpdates(page: Int): Observable<MangasPage> { override fun fetchLatestUpdates(page: Int): Observable<MangasPage> {
return client.newCall(latestUpdatesRequest(page)) return client.newCall(latestUpdatesRequest(page))
.asObservableSuccess() .asObservableSuccess()
@@ -172,11 +200,18 @@ abstract class HttpSource : CatalogueSource {
protected abstract fun latestUpdatesParse(response: Response): MangasPage protected abstract fun latestUpdatesParse(response: Response): MangasPage
/** /**
* Returns an observable with the updated details for a manga. Normally it's not needed to * Get the updated details for a manga.
* override this method. * Normally it's not needed to override this method.
* *
* @param manga the manga to be updated. * @param manga the manga to update.
* @return the updated manga.
*/ */
@Suppress("DEPRECATION")
override suspend fun getMangaDetails(manga: SManga): SManga {
return fetchMangaDetails(manga).awaitSingle()
}
@Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getMangaDetails"))
override fun fetchMangaDetails(manga: SManga): Observable<SManga> { override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
return client.newCall(mangaDetailsRequest(manga)) return client.newCall(mangaDetailsRequest(manga))
.asObservableSuccess() .asObservableSuccess()
@@ -203,11 +238,23 @@ abstract class HttpSource : CatalogueSource {
protected abstract fun mangaDetailsParse(response: Response): SManga protected abstract fun mangaDetailsParse(response: Response): SManga
/** /**
* Returns an observable with the updated chapter list for a manga. Normally it's not needed to * Get all the available chapters for a manga.
* override this method. If a manga is licensed an empty chapter list observable is returned * Normally it's not needed to override this method.
* *
* @param manga the manga to look for chapters. * @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.
*/ */
@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"))
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> { override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
return if (manga.status != SManga.LICENSED) { return if (manga.status != SManga.LICENSED) {
client.newCall(chapterListRequest(manga)) client.newCall(chapterListRequest(manga))
@@ -216,7 +263,7 @@ abstract class HttpSource : CatalogueSource {
chapterListParse(response) chapterListParse(response)
} }
} else { } else {
Observable.error(Exception("Licensed - No chapters to show")) Observable.error(LicensedMangaChaptersException())
} }
} }
@@ -238,10 +285,18 @@ abstract class HttpSource : CatalogueSource {
protected abstract fun chapterListParse(response: Response): List<SChapter> protected abstract fun chapterListParse(response: Response): List<SChapter>
/** /**
* Returns an observable with the page list for a chapter. * Get the list of pages a chapter has. Pages should be returned
* in the expected order; the index is ignored.
* *
* @param chapter the chapter whose page list has to be fetched. * @param chapter the chapter.
* @return the pages for the chapter.
*/ */
@Suppress("DEPRECATION")
override suspend fun getPageList(chapter: SChapter): List<Page> {
return fetchPageList(chapter).awaitSingle()
}
@Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getPageList"))
override fun fetchPageList(chapter: SChapter): Observable<List<Page>> { override fun fetchPageList(chapter: SChapter): Observable<List<Page>> {
return client.newCall(pageListRequest(chapter)) return client.newCall(pageListRequest(chapter))
.asObservableSuccess() .asObservableSuccess()
@@ -271,8 +326,15 @@ abstract class HttpSource : CatalogueSource {
* Returns an observable with the page containing the source url of the image. If there's any * 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. * 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. * @param page the page whose source image has to be fetched.
*/ */
@Suppress("DEPRECATION")
open suspend fun getImageUrl(page: Page): String {
return fetchImageUrl(page).awaitSingle()
}
@Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getImageUrl"))
open fun fetchImageUrl(page: Page): Observable<String> { open fun fetchImageUrl(page: Page): Observable<String> {
return client.newCall(imageUrlRequest(page)) return client.newCall(imageUrlRequest(page))
.asObservableSuccess() .asObservableSuccess()
@@ -297,13 +359,15 @@ abstract class HttpSource : CatalogueSource {
protected abstract fun imageUrlParse(response: Response): String protected abstract fun imageUrlParse(response: Response): String
/** /**
* Returns an observable with the response of the source image. * 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. * @param page the page whose source image has to be downloaded.
*/ */
fun fetchImage(page: Page): Observable<Response> { open suspend fun getImage(page: Page): Response {
return client.newCallWithProgress(imageRequest(page), page) return client.newCachelessCallWithProgress(imageRequest(page), page)
.asObservableSuccess() .awaitSuccess()
} }
/** /**
@@ -397,3 +461,5 @@ abstract class HttpSource : CatalogueSource {
val DEFAULT_USER_AGENT by lazy { getWebViewUserAgent() } val DEFAULT_USER_AGENT by lazy { getWebViewUserAgent() }
} }
} }
class LicensedMangaChaptersException : Exception("Licensed - No chapters to show")

View File

@@ -1,23 +0,0 @@
package eu.kanade.tachiyomi.source.online
import eu.kanade.tachiyomi.source.model.Page
import rx.Observable
fun HttpSource.getImageUrl(page: Page): Observable<Page> {
return fetchImageUrl(page)
.onErrorReturn { null }
.doOnNext { page.imageUrl = it }
.map { page }
}
fun HttpSource.fetchAllImageUrlsFromPageList(pages: List<Page>): Observable<Page> {
return Observable.from(pages)
.filter { !it.imageUrl.isNullOrEmpty() }
.mergeWith(fetchRemainingImageUrlsFromPageList(pages))
}
fun HttpSource.fetchRemainingImageUrlsFromPageList(pages: List<Page>): Observable<Page> {
return Observable.from(pages)
.filter { it.imageUrl.isNullOrEmpty() }
.concatMap { getImageUrl(it) }
}

View File

@@ -0,0 +1,27 @@
package eu.kanade.tachiyomi.source.online
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.model.SManga
/**
* A source that may handle opening an SManga for a given URI.
*
* @since extensions-lib 1.5
*/
@Suppress("unused")
interface ResolvableSource : Source {
/**
* Whether this source may potentially handle the given URI.
*
* @since extensions-lib 1.5
*/
fun canResolveUri(uri: String): Boolean
/**
* Called if canHandleUri is true. Returns the corresponding SManga, if possible.
*
* @since extensions-lib 1.5
*/
suspend fun getManga(uri: String): SManga?
}

View File

@@ -14,7 +14,6 @@ import suwayomi.tachidesk.graphql.types.preferenceOf
import suwayomi.tachidesk.graphql.types.updateFilterList import suwayomi.tachidesk.graphql.types.updateFilterList
import suwayomi.tachidesk.manga.impl.MangaList.insertOrGet import suwayomi.tachidesk.manga.impl.MangaList.insertOrGet
import suwayomi.tachidesk.manga.impl.Source import suwayomi.tachidesk.manga.impl.Source
import suwayomi.tachidesk.manga.impl.util.lang.awaitSingle
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource
import suwayomi.tachidesk.manga.model.table.MangaTable import suwayomi.tachidesk.manga.model.table.MangaTable
import suwayomi.tachidesk.server.JavalinSetup.future import suwayomi.tachidesk.server.JavalinSetup.future
@@ -50,18 +49,18 @@ class SourceMutation {
val source = GetCatalogueSource.getCatalogueSourceOrNull(sourceId)!! val source = GetCatalogueSource.getCatalogueSourceOrNull(sourceId)!!
val mangasPage = when (type) { val mangasPage = when (type) {
FetchSourceMangaType.SEARCH -> { FetchSourceMangaType.SEARCH -> {
source.fetchSearchManga( source.getSearchManga(
page = page, page = page,
query = query.orEmpty(), query = query.orEmpty(),
filters = updateFilterList(source, filters) filters = updateFilterList(source, filters)
).awaitSingle() )
} }
FetchSourceMangaType.POPULAR -> { FetchSourceMangaType.POPULAR -> {
source.fetchPopularManga(page).awaitSingle() source.getPopularManga(page)
} }
FetchSourceMangaType.LATEST -> { FetchSourceMangaType.LATEST -> {
if (!source.supportsLatest) throw Exception("Source does not support latest") if (!source.supportsLatest) throw Exception("Source does not support latest")
source.fetchLatestUpdates(page).awaitSingle() source.getLatestUpdates(page)
} }
} }

View File

@@ -299,6 +299,7 @@ data class SwitchPreference(
val key: String, val key: String,
val title: String, val title: String,
val summary: String?, val summary: String?,
val visible: Boolean,
val currentValue: Boolean?, val currentValue: Boolean?,
val default: Boolean val default: Boolean
) : Preference ) : Preference
@@ -307,6 +308,7 @@ data class CheckBoxPreference(
val key: String, val key: String,
val title: String, val title: String,
val summary: String?, val summary: String?,
val visible: Boolean,
val currentValue: Boolean?, val currentValue: Boolean?,
val default: Boolean val default: Boolean
) : Preference ) : Preference
@@ -315,6 +317,7 @@ data class EditTextPreference(
val key: String, val key: String,
val title: String?, val title: String?,
val summary: String?, val summary: String?,
val visible: Boolean,
val currentValue: String?, val currentValue: String?,
val default: String?, val default: String?,
val dialogTitle: String?, val dialogTitle: String?,
@@ -326,6 +329,7 @@ data class ListPreference(
val key: String, val key: String,
val title: String?, val title: String?,
val summary: String?, val summary: String?,
val visible: Boolean,
val currentValue: String?, val currentValue: String?,
val default: String?, val default: String?,
val entries: List<String>, val entries: List<String>,
@@ -336,6 +340,7 @@ data class MultiSelectListPreference(
val key: String, val key: String,
val title: String?, val title: String?,
val summary: String?, val summary: String?,
val visible: Boolean,
val currentValue: List<String>?, val currentValue: List<String>?,
val default: List<String>?, val default: List<String>?,
val dialogTitle: String?, val dialogTitle: String?,
@@ -350,13 +355,15 @@ fun preferenceOf(preference: SourcePreference): Preference {
preference.key, preference.key,
preference.title.toString(), preference.title.toString(),
preference.summary?.toString(), preference.summary?.toString(),
preference.visible,
preference.currentValue as Boolean, preference.currentValue as Boolean,
preference.defaultValue as Boolean preference.defaultValue as Boolean,
) )
is SourceCheckBoxPreference -> CheckBoxPreference( is SourceCheckBoxPreference -> CheckBoxPreference(
preference.key, preference.key,
preference.title.toString(), preference.title.toString(),
preference.summary?.toString(), preference.summary?.toString(),
preference.visible,
preference.currentValue as Boolean, preference.currentValue as Boolean,
preference.defaultValue as Boolean preference.defaultValue as Boolean
) )
@@ -364,6 +371,7 @@ fun preferenceOf(preference: SourcePreference): Preference {
preference.key, preference.key,
preference.title?.toString(), preference.title?.toString(),
preference.summary?.toString(), preference.summary?.toString(),
preference.visible,
(preference.currentValue as CharSequence?)?.toString(), (preference.currentValue as CharSequence?)?.toString(),
(preference.defaultValue as CharSequence?)?.toString(), (preference.defaultValue as CharSequence?)?.toString(),
preference.dialogTitle?.toString(), preference.dialogTitle?.toString(),
@@ -374,6 +382,7 @@ fun preferenceOf(preference: SourcePreference): Preference {
preference.key, preference.key,
preference.title?.toString(), preference.title?.toString(),
preference.summary?.toString(), preference.summary?.toString(),
preference.visible,
(preference.currentValue as CharSequence?)?.toString(), (preference.currentValue as CharSequence?)?.toString(),
(preference.defaultValue as CharSequence?)?.toString(), (preference.defaultValue as CharSequence?)?.toString(),
preference.entries.map { it.toString() }, preference.entries.map { it.toString() },
@@ -383,6 +392,7 @@ fun preferenceOf(preference: SourcePreference): Preference {
preference.key, preference.key,
preference.title?.toString(), preference.title?.toString(),
preference.summary?.toString(), preference.summary?.toString(),
preference.visible,
(preference.currentValue as Collection<*>?)?.map { it.toString() }, (preference.currentValue as Collection<*>?)?.map { it.toString() },
(preference.defaultValue as Collection<*>?)?.map { it.toString() }, (preference.defaultValue as Collection<*>?)?.map { it.toString() },
preference.dialogTitle?.toString(), preference.dialogTitle?.toString(),

View File

@@ -14,7 +14,6 @@ import org.jetbrains.exposed.sql.insertAndGetId
import org.jetbrains.exposed.sql.select import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.transactions.transaction import org.jetbrains.exposed.sql.transactions.transaction
import suwayomi.tachidesk.manga.impl.Manga.getMangaMetaMap import suwayomi.tachidesk.manga.impl.Manga.getMangaMetaMap
import suwayomi.tachidesk.manga.impl.util.lang.awaitSingle
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.getCatalogueSourceOrStub import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.getCatalogueSourceOrStub
import suwayomi.tachidesk.manga.model.dataclass.MangaDataClass import suwayomi.tachidesk.manga.model.dataclass.MangaDataClass
import suwayomi.tachidesk.manga.model.dataclass.PagedMangaListDataClass import suwayomi.tachidesk.manga.model.dataclass.PagedMangaListDataClass
@@ -33,10 +32,10 @@ object MangaList {
} }
val source = getCatalogueSourceOrStub(sourceId) val source = getCatalogueSourceOrStub(sourceId)
val mangasPage = if (popular) { val mangasPage = if (popular) {
source.fetchPopularManga(pageNum).awaitSingle() source.getPopularManga(pageNum)
} else { } else {
if (source.supportsLatest) { if (source.supportsLatest) {
source.fetchLatestUpdates(pageNum).awaitSingle() source.getLatestUpdates(pageNum)
} else { } else {
throw Exception("Source $source doesn't support latest") throw Exception("Source $source doesn't support latest")
} }

View File

@@ -17,7 +17,6 @@ import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.transactions.transaction import org.jetbrains.exposed.sql.transactions.transaction
import org.jetbrains.exposed.sql.update import org.jetbrains.exposed.sql.update
import suwayomi.tachidesk.manga.impl.util.getChapterCachePath import suwayomi.tachidesk.manga.impl.util.getChapterCachePath
import suwayomi.tachidesk.manga.impl.util.lang.awaitSingle
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.getCatalogueSourceOrStub import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.getCatalogueSourceOrStub
import suwayomi.tachidesk.manga.impl.util.storage.ImageResponse.getImageResponse import suwayomi.tachidesk.manga.impl.util.storage.ImageResponse.getImageResponse
import suwayomi.tachidesk.manga.impl.util.storage.ImageUtil import suwayomi.tachidesk.manga.impl.util.storage.ImageUtil
@@ -34,7 +33,7 @@ object Page {
*/ */
suspend fun getTrueImageUrl(page: Page, source: HttpSource): String { suspend fun getTrueImageUrl(page: Page, source: HttpSource): String {
if (page.imageUrl == null) { if (page.imageUrl == null) {
page.imageUrl = source.fetchImageUrl(page).awaitSingle() page.imageUrl = source.getImageUrl(page)
} }
return page.imageUrl!! return page.imageUrl!!
} }
@@ -100,7 +99,7 @@ object Page {
// Note: don't care about invalidating cache because OS cache is not permanent // Note: don't care about invalidating cache because OS cache is not permanent
return getImageResponse(cacheSaveDir, fileName) { return getImageResponse(cacheSaveDir, fileName) {
source.fetchImage(tachiyomiPage).awaitSingle() source.getImage(tachiyomiPage)
} }
} }

View File

@@ -16,21 +16,20 @@ import org.kodein.di.DI
import org.kodein.di.conf.global import org.kodein.di.conf.global
import org.kodein.di.instance import org.kodein.di.instance
import suwayomi.tachidesk.manga.impl.MangaList.processEntries import suwayomi.tachidesk.manga.impl.MangaList.processEntries
import suwayomi.tachidesk.manga.impl.util.lang.awaitSingle
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.getCatalogueSourceOrStub import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.getCatalogueSourceOrStub
import suwayomi.tachidesk.manga.model.dataclass.PagedMangaListDataClass import suwayomi.tachidesk.manga.model.dataclass.PagedMangaListDataClass
object Search { object Search {
suspend fun sourceSearch(sourceId: Long, searchTerm: String, pageNum: Int): PagedMangaListDataClass { suspend fun sourceSearch(sourceId: Long, searchTerm: String, pageNum: Int): PagedMangaListDataClass {
val source = getCatalogueSourceOrStub(sourceId) val source = getCatalogueSourceOrStub(sourceId)
val searchManga = source.fetchSearchManga(pageNum, searchTerm, getFilterListOf(source)).awaitSingle() val searchManga = source.getSearchManga(pageNum, searchTerm, getFilterListOf(source))
return searchManga.processEntries(sourceId) return searchManga.processEntries(sourceId)
} }
suspend fun sourceFilter(sourceId: Long, pageNum: Int, filter: FilterData): PagedMangaListDataClass { suspend fun sourceFilter(sourceId: Long, pageNum: Int, filter: FilterData): PagedMangaListDataClass {
val source = getCatalogueSourceOrStub(sourceId) val source = getCatalogueSourceOrStub(sourceId)
val filterList = if (filter.filter != null) buildFilterList(sourceId, filter.filter) else source.getFilterList() val filterList = if (filter.filter != null) buildFilterList(sourceId, filter.filter) else source.getFilterList()
val searchManga = source.fetchSearchManga(pageNum, filter.searchTerm ?: "", filterList).awaitSingle() val searchManga = source.getSearchManga(pageNum, filter.searchTerm ?: "", filterList)
return searchManga.processEntries(sourceId) return searchManga.processEntries(sourceId)
} }

View File

@@ -7,12 +7,10 @@ package suwayomi.tachidesk.manga.impl
* License, v. 2.0. If a copy of the MPL was not distributed with this * License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import android.app.Application
import android.content.Context
import androidx.preference.Preference import androidx.preference.Preference
import androidx.preference.PreferenceScreen import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.source.ConfigurableSource import eu.kanade.tachiyomi.source.ConfigurableSource
import eu.kanade.tachiyomi.source.getPreferenceKey import eu.kanade.tachiyomi.source.sourcePreferences
import io.javalin.plugin.json.JsonMapper import io.javalin.plugin.json.JsonMapper
import mu.KotlinLogging import mu.KotlinLogging
import org.jetbrains.exposed.sql.select import org.jetbrains.exposed.sql.select
@@ -28,7 +26,6 @@ import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.unregisterCa
import suwayomi.tachidesk.manga.model.dataclass.SourceDataClass import suwayomi.tachidesk.manga.model.dataclass.SourceDataClass
import suwayomi.tachidesk.manga.model.table.ExtensionTable import suwayomi.tachidesk.manga.model.table.ExtensionTable
import suwayomi.tachidesk.manga.model.table.SourceTable import suwayomi.tachidesk.manga.model.table.SourceTable
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import xyz.nulldev.androidcompat.androidimpl.CustomContext import xyz.nulldev.androidcompat.androidimpl.CustomContext
@@ -106,8 +103,7 @@ object Source {
val source = getCatalogueSourceOrStub(sourceId) val source = getCatalogueSourceOrStub(sourceId)
if (source is ConfigurableSource) { if (source is ConfigurableSource) {
val sourceShardPreferences = val sourceShardPreferences = source.sourcePreferences()
Injekt.get<Application>().getSharedPreferences(source.getPreferenceKey(), Context.MODE_PRIVATE)
val screen = PreferenceScreen(context) val screen = PreferenceScreen(context)
screen.sharedPreferences = sourceShardPreferences screen.sharedPreferences = sourceShardPreferences

View File

@@ -48,7 +48,7 @@ object ProtoBackupImport : ProtoBackupBase() {
private val backupMutex = Mutex() private val backupMutex = Mutex()
sealed class BackupRestoreState { sealed class BackupRestoreState {
object Idle : BackupRestoreState() data object Idle : BackupRestoreState()
data class RestoringCategories(val totalManga: Int) : BackupRestoreState() data class RestoringCategories(val totalManga: Int) : BackupRestoreState()
data class RestoringManga(val current: Int, val totalManga: Int, val title: String) : BackupRestoreState() data class RestoringManga(val current: Int, val totalManga: Int, val title: String) : BackupRestoreState()
} }

View File

@@ -9,9 +9,10 @@ package suwayomi.tachidesk.manga.impl.extension.github
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.NetworkHelper import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.network.await import eu.kanade.tachiyomi.network.awaitSuccess
import eu.kanade.tachiyomi.network.parseAs import eu.kanade.tachiyomi.network.parseAs
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import mu.KotlinLogging import mu.KotlinLogging
import suwayomi.tachidesk.manga.impl.util.PackageTools.LIB_VERSION_MAX import suwayomi.tachidesk.manga.impl.util.PackageTools.LIB_VERSION_MAX
import suwayomi.tachidesk.manga.impl.util.PackageTools.LIB_VERSION_MIN import suwayomi.tachidesk.manga.impl.util.PackageTools.LIB_VERSION_MIN
@@ -22,6 +23,7 @@ object ExtensionGithubApi {
private const val REPO_URL_PREFIX = "https://raw.githubusercontent.com/tachiyomiorg/tachiyomi-extensions/repo/" private const val REPO_URL_PREFIX = "https://raw.githubusercontent.com/tachiyomiorg/tachiyomi-extensions/repo/"
private const val FALLBACK_REPO_URL_PREFIX = "https://gcore.jsdelivr.net/gh/tachiyomiorg/tachiyomi-extensions@repo/" private const val FALLBACK_REPO_URL_PREFIX = "https://gcore.jsdelivr.net/gh/tachiyomiorg/tachiyomi-extensions@repo/"
private val logger = KotlinLogging.logger {} private val logger = KotlinLogging.logger {}
private val json: Json by injectLazy()
@Serializable @Serializable
private data class ExtensionJsonObject( private data class ExtensionJsonObject(
@@ -52,7 +54,7 @@ object ExtensionGithubApi {
null null
} else { } else {
try { try {
client.newCall(GET("${REPO_URL_PREFIX}index.min.json")).await() client.newCall(GET("${REPO_URL_PREFIX}index.min.json")).awaitSuccess()
} catch (e: Throwable) { } catch (e: Throwable) {
logger.error(e) { "Failed to get extensions from GitHub" } logger.error(e) { "Failed to get extensions from GitHub" }
requiresFallbackSource = true requiresFallbackSource = true
@@ -61,12 +63,14 @@ object ExtensionGithubApi {
} }
val response = githubResponse ?: run { val response = githubResponse ?: run {
client.newCall(GET("${FALLBACK_REPO_URL_PREFIX}index.min.json")).await() client.newCall(GET("${FALLBACK_REPO_URL_PREFIX}index.min.json")).awaitSuccess()
} }
return response return with(json) {
.parseAs<List<ExtensionJsonObject>>() response
.toExtensions() .parseAs<List<ExtensionJsonObject>>()
.toExtensions()
}
} }
fun getApkUrl(extension: ExtensionDataClass): String { fun getApkUrl(extension: ExtensionDataClass): String {

View File

@@ -41,7 +41,7 @@ object PackageTools {
const val METADATA_SOURCE_FACTORY = "tachiyomi.extension.factory" const val METADATA_SOURCE_FACTORY = "tachiyomi.extension.factory"
const val METADATA_NSFW = "tachiyomi.extension.nsfw" const val METADATA_NSFW = "tachiyomi.extension.nsfw"
const val LIB_VERSION_MIN = 1.3 const val LIB_VERSION_MIN = 1.3
const val LIB_VERSION_MAX = 1.4 const val LIB_VERSION_MAX = 1.5
private const val officialSignature = "7ce04da7773d41b489f4693a366c36bcd0a11fc39b547168553c285bd7348e23" // inorichi's key private const val officialSignature = "7ce04da7773d41b489f4693a366c36bcd0a11fc39b547168553c285bd7348e23" // inorichi's key
private const val unofficialSignature = "64feb21075ba97ebc9cc981243645b331595c111cef1b0d084236a0403b00581" // ArMor's key private const val unofficialSignature = "64feb21075ba97ebc9cc981243645b331595c111cef1b0d084236a0403b00581" // ArMor's key

View File

@@ -21,14 +21,17 @@ open class StubSource(override val id: Long) : CatalogueSource {
override val name: String override val name: String
get() = id.toString() get() = id.toString()
@Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getPopularManga"))
override fun fetchPopularManga(page: Int): Observable<MangasPage> { override fun fetchPopularManga(page: Int): Observable<MangasPage> {
return Observable.error(getSourceNotInstalledException()) return Observable.error(getSourceNotInstalledException())
} }
@Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getSearchManga"))
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> { override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
return Observable.error(getSourceNotInstalledException()) return Observable.error(getSourceNotInstalledException())
} }
@Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getLatestUpdates"))
override fun fetchLatestUpdates(page: Int): Observable<MangasPage> { override fun fetchLatestUpdates(page: Int): Observable<MangasPage> {
return Observable.error(getSourceNotInstalledException()) return Observable.error(getSourceNotInstalledException())
} }
@@ -37,14 +40,17 @@ open class StubSource(override val id: Long) : CatalogueSource {
return FilterList() return FilterList()
} }
@Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getMangaDetails"))
override fun fetchMangaDetails(manga: SManga): Observable<SManga> { override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
return Observable.error(getSourceNotInstalledException()) return Observable.error(getSourceNotInstalledException())
} }
@Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getChapterList"))
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> { override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
return Observable.error(getSourceNotInstalledException()) return Observable.error(getSourceNotInstalledException())
} }
@Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getPageList"))
override fun fetchPageList(chapter: SChapter): Observable<List<Page>> { override fun fetchPageList(chapter: SChapter): Observable<List<Page>> {
return Observable.error(getSourceNotInstalledException()) return Observable.error(getSourceNotInstalledException())
} }

View File

@@ -9,7 +9,7 @@ package suwayomi.tachidesk.server.util
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.NetworkHelper import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.network.await import eu.kanade.tachiyomi.network.awaitSuccess
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
@@ -408,7 +408,7 @@ object WebInterfaceManager {
private suspend fun fetchMD5SumFor(version: String): String { private suspend fun fetchMD5SumFor(version: String): String {
return try { return try {
executeWithRetry(KotlinLogging.logger("${logger.name} fetchMD5SumFor($version)"), { executeWithRetry(KotlinLogging.logger("${logger.name} fetchMD5SumFor($version)"), {
network.client.newCall(GET("${getDownloadUrlFor(version)}/md5sum")).await().body.string().trim() network.client.newCall(GET("${getDownloadUrlFor(version)}/md5sum")).awaitSuccess().body.string().trim()
}) })
} catch (e: Exception) { } catch (e: Exception) {
"" ""
@@ -422,7 +422,7 @@ object WebInterfaceManager {
private suspend fun fetchPreviewVersion(): String { private suspend fun fetchPreviewVersion(): String {
return executeWithRetry(KotlinLogging.logger("${logger.name} fetchPreviewVersion"), { return executeWithRetry(KotlinLogging.logger("${logger.name} fetchPreviewVersion"), {
val releaseInfoJson = network.client.newCall(GET(WebUIFlavor.WEBUI.latestReleaseInfoUrl)).await().body.string() val releaseInfoJson = network.client.newCall(GET(WebUIFlavor.WEBUI.latestReleaseInfoUrl)).awaitSuccess().body.string()
Json.decodeFromString<JsonObject>(releaseInfoJson)["tag_name"]?.jsonPrimitive?.content Json.decodeFromString<JsonObject>(releaseInfoJson)["tag_name"]?.jsonPrimitive?.content
?: throw Exception("Failed to get the preview version tag") ?: throw Exception("Failed to get the preview version tag")
}) })
@@ -433,7 +433,7 @@ object WebInterfaceManager {
KotlinLogging.logger("$logger fetchServerMappingFile"), KotlinLogging.logger("$logger fetchServerMappingFile"),
{ {
json.parseToJsonElement( json.parseToJsonElement(
network.client.newCall(GET(WebUIFlavor.WEBUI.versionMappingUrl)).await().body.string() network.client.newCall(GET(WebUIFlavor.WEBUI.versionMappingUrl)).awaitSuccess().body.string()
).jsonArray ).jsonArray
} }
) )

View File

@@ -10,7 +10,6 @@ import org.junit.jupiter.api.TestInstance
import suwayomi.tachidesk.manga.impl.Source import suwayomi.tachidesk.manga.impl.Source
import suwayomi.tachidesk.manga.impl.extension.Extension import suwayomi.tachidesk.manga.impl.extension.Extension
import suwayomi.tachidesk.manga.impl.extension.ExtensionsList import suwayomi.tachidesk.manga.impl.extension.ExtensionsList
import suwayomi.tachidesk.manga.impl.util.lang.awaitSingle
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource
import suwayomi.tachidesk.server.applicationSetup import suwayomi.tachidesk.server.applicationSetup
import suwayomi.tachidesk.test.BASE_PATH import suwayomi.tachidesk.test.BASE_PATH
@@ -51,7 +50,7 @@ class CloudFlareTest {
@Test @Test
fun `test nhentai browse`() = runTest { fun `test nhentai browse`() = runTest {
assert(nhentai.fetchPopularManga(1).awaitSingle().mangas.isNotEmpty()) { assert(nhentai.getPopularManga(1).mangas.isNotEmpty()) {
"NHentai results were empty" "NHentai results were empty"
} }
} }

View File

@@ -20,13 +20,11 @@ import mu.KotlinLogging
import org.junit.jupiter.api.BeforeAll import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance import org.junit.jupiter.api.TestInstance
import rx.Observable
import suwayomi.tachidesk.manga.impl.Source.getSourceList import suwayomi.tachidesk.manga.impl.Source.getSourceList
import suwayomi.tachidesk.manga.impl.extension.Extension.installExtension import suwayomi.tachidesk.manga.impl.extension.Extension.installExtension
import suwayomi.tachidesk.manga.impl.extension.Extension.uninstallExtension import suwayomi.tachidesk.manga.impl.extension.Extension.uninstallExtension
import suwayomi.tachidesk.manga.impl.extension.Extension.updateExtension import suwayomi.tachidesk.manga.impl.extension.Extension.updateExtension
import suwayomi.tachidesk.manga.impl.extension.ExtensionsList.getExtensionList import suwayomi.tachidesk.manga.impl.extension.ExtensionsList.getExtensionList
import suwayomi.tachidesk.manga.impl.util.lang.awaitSingle
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.getCatalogueSourceOrNull import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.getCatalogueSourceOrNull
import suwayomi.tachidesk.manga.model.dataclass.ExtensionDataClass import suwayomi.tachidesk.manga.model.dataclass.ExtensionDataClass
import suwayomi.tachidesk.server.applicationSetup import suwayomi.tachidesk.server.applicationSetup
@@ -89,8 +87,8 @@ class TestExtensionCompatibility {
logger.info { "${popularCount.getAndIncrement()} - Now fetching popular manga from $source" } logger.info { "${popularCount.getAndIncrement()} - Now fetching popular manga from $source" }
try { try {
mangaToFetch += source to ( mangaToFetch += source to (
source.fetchPopularManga(1) repeat { source.getPopularManga(1) }
.awaitSingleRepeat().mangas.firstOrNull() .mangas.firstOrNull()
?: throw Exception("Source returned no manga") ?: throw Exception("Source returned no manga")
) )
} catch (e: Exception) { } catch (e: Exception) {
@@ -114,7 +112,7 @@ class TestExtensionCompatibility {
semaphore.withPermit { semaphore.withPermit {
logger.info { "${mangaCount.getAndIncrement()} - Now fetching manga from $source" } logger.info { "${mangaCount.getAndIncrement()} - Now fetching manga from $source" }
try { try {
manga.copyFrom(source.fetchMangaDetails(manga).awaitSingleRepeat()) manga.copyFrom(repeat { source.getMangaDetails(manga) })
manga.initialized = true manga.initialized = true
} catch (e: Exception) { } catch (e: Exception) {
logger.warn { logger.warn {
@@ -143,7 +141,7 @@ class TestExtensionCompatibility {
chaptersToFetch += Triple( chaptersToFetch += Triple(
source, source,
manga, manga,
source.fetchChapterList(manga).awaitSingleRepeat().firstOrNull() ?: throw Exception("Source returned no chapters") repeat { source.getChapterList(manga) }.firstOrNull() ?: throw Exception("Source returned no chapters")
) )
} catch (e: Exception) { } catch (e: Exception) {
logger.warn { logger.warn {
@@ -174,7 +172,7 @@ class TestExtensionCompatibility {
semaphore.withPermit { semaphore.withPermit {
logger.info { "${pageListCount.getAndIncrement()} - Now fetching page list from $source" } logger.info { "${pageListCount.getAndIncrement()} - Now fetching page list from $source" }
try { try {
source.fetchPageList(chapter).awaitSingleRepeat() repeat { source.getPageList(chapter) }
} catch (e: Exception) { } catch (e: Exception) {
logger.warn { logger.warn {
"Failed to fetch manga info from $source for ${manga.title} (${source.mangaDetailsRequest(manga).url}): ${e.message}" "Failed to fetch manga info from $source for ${manga.title} (${source.mangaDetailsRequest(manga).url}): ${e.message}"
@@ -195,12 +193,12 @@ class TestExtensionCompatibility {
} }
} }
private suspend fun <T> Observable<T>.awaitSingleRepeat(): T { private suspend fun <T> repeat(block: suspend () -> T): T {
for (i in 1..2) { for (i in 1..2) {
try { try {
return awaitSingle() return block()
} catch (e: Exception) {} } catch (e: Exception) {}
} }
return awaitSingle() return block()
} }
} }

View File

@@ -38,6 +38,7 @@ class SearchTest : ApplicationTest() {
class FakeSearchableSource(id: Long) : StubSource(id) { class FakeSearchableSource(id: Long) : StubSource(id) {
var mangas: List<SManga> = emptyList() var mangas: List<SManga> = emptyList()
@Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getSearchManga"))
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> { override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
return Observable.just(MangasPage(mangas, false)) return Observable.just(MangasPage(mangas, false))
} }