diff --git a/AndroidCompat/src/main/java/androidx/preference/Preference.java b/AndroidCompat/src/main/java/androidx/preference/Preference.java index 2fb71b2c..8b7a55bf 100644 --- a/AndroidCompat/src/main/java/androidx/preference/Preference.java +++ b/AndroidCompat/src/main/java/androidx/preference/Preference.java @@ -22,6 +22,7 @@ public class Preference { @JsonIgnore protected Context context; + private boolean isVisible; private String key; private CharSequence title; private CharSequence summary; @@ -100,6 +101,14 @@ public class Preference { return sharedPreferences; } + public void setVisible(boolean visible) { + isVisible = visible; + } + + public boolean getVisible() { + return isVisible; + } + /** Tachidesk specific API */ public void setSharedPreferences(SharedPreferences sharedPreferences) { this.sharedPreferences = sharedPreferences; diff --git a/build.gradle.kts b/build.gradle.kts index 93a920ea..d7e6efa4 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -37,6 +37,10 @@ subprojects { dependsOn("formatKotlin") kotlinOptions { jvmTarget = JavaVersion.VERSION_1_8.toString() + + freeCompilerArgs += listOf( + "-Xcontext-receivers" + ) } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9291ce03..1306df75 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -27,6 +27,7 @@ coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", ve # 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-xml-core = { module = "io.github.pdvrieze.xmlutil:core-jvm", version.ref = "xmlserialization" } serialization-xml = { module = "io.github.pdvrieze.xmlutil:serialization-jvm", version.ref = "xmlserialization" } @@ -167,6 +168,7 @@ shared = [ "coroutines-core", "coroutines-jdk8", "serialization-json", + "serialization-json-okio", "serialization-protobuf", "kodein", "slf4japi", diff --git a/server/src/main/kotlin/eu/kanade/tachiyomi/AppInfo.kt b/server/src/main/kotlin/eu/kanade/tachiyomi/AppInfo.kt index b0134c2d..b205b725 100644 --- a/server/src/main/kotlin/eu/kanade/tachiyomi/AppInfo.kt +++ b/server/src/main/kotlin/eu/kanade/tachiyomi/AppInfo.kt @@ -1,14 +1,33 @@ package eu.kanade.tachiyomi +import suwayomi.tachidesk.manga.impl.util.storage.ImageUtil + /** * Used by extensions. * * @since extension-lib 1.3 */ 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() - /** 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) + + /** + * A list of supported image MIME types by the reader. + * e.g. ["image/jpeg", "image/png", ...] + * + * @since extension-lib 1.5 + */ + fun getSupportedImageMimeTypes(): List = ImageUtil.ImageType.entries.map { it.mime } } diff --git a/server/src/main/kotlin/eu/kanade/tachiyomi/network/OkHttpExtensions.kt b/server/src/main/kotlin/eu/kanade/tachiyomi/network/OkHttpExtensions.kt index b817bfa6..8d6fe136 100644 --- a/server/src/main/kotlin/eu/kanade/tachiyomi/network/OkHttpExtensions.kt +++ b/server/src/main/kotlin/eu/kanade/tachiyomi/network/OkHttpExtensions.kt @@ -1,19 +1,20 @@ package eu.kanade.tachiyomi.network +import kotlinx.coroutines.ExperimentalCoroutinesApi 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.okio.decodeFromBufferedSource +import kotlinx.serialization.serializer import okhttp3.Call import okhttp3.Callback import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.Response -import okhttp3.internal.closeQuietly import rx.Observable import rx.Producer import rx.Subscription -import uy.kohesive.injekt.Injekt -import uy.kohesive.injekt.api.fullType import java.io.IOException import java.util.concurrent.atomic.AtomicBoolean import kotlin.coroutines.resumeWithException @@ -55,29 +56,37 @@ fun Call.asObservable(): Observable { } } +fun Call.asObservableSuccess(): Observable { + return asObservable() + .doOnNext { response -> + if (!response.isSuccessful) { + response.close() + throw HttpException(response.code) + } + } +} + // Based on https://github.com/gildor/kotlin-coroutines-okhttp -suspend fun Call.await(): Response { +@OptIn(ExperimentalCoroutinesApi::class) +private suspend fun Call.await(callStack: Array): Response { return suspendCancellableCoroutine { continuation -> - enqueue( + val callback = object : Callback { override fun onResponse(call: Call, response: Response) { - if (!response.isSuccessful) { - continuation.resumeWithException(HttpException(response.code)) - return - } - continuation.resume(response) { - response.body.closeQuietly() + response.body.close() } } override fun onFailure(call: Call, e: IOException) { // Don't bother with resuming the continuation if it is already cancelled. if (continuation.isCancelled) return - continuation.resumeWithException(e) + val exception = IOException(e.message, e).apply { stackTrace = callStack } + continuation.resumeWithException(exception) } } - ) + + enqueue(callback) continuation.invokeOnCancellation { try { @@ -89,32 +98,25 @@ suspend fun Call.await(): Response { } } -fun Call.asObservableSuccess(): Observable { - return asObservable() - .doOnNext { response -> - if (!response.isSuccessful) { - response.close() - throw HttpException(response.code) - } - } +suspend fun Call.await(): Response { + val callStack = Exception().stackTrace.run { copyOfRange(1, size) } + return await(callStack) } -// fun OkHttpClient.newCallWithProgress(request: Request, listener: ProgressListener): Call { -// val progressClient = newBuilder() -// .cache(nasObservableSuccessull) -// .addNetworkInterceptor { chain -> -// val originalResponse = chain.proceed(chain.request()) -// originalResponse.newBuilder() -// .body(ProgressResponseBody(originalResponse.body!!, listener)) -// .build() -// } -// .build() -// -// return progressClient.newCall(request) -// } +/** + * @since extensions-lib 1.5 + */ +suspend fun Call.awaitSuccess(): Response { + val callStack = Exception().stackTrace.run { copyOfRange(1, size) } + val response = await(callStack) + if (!response.isSuccessful) { + response.close() + throw HttpException(response.code).apply { stackTrace = callStack } + } + return response +} -@Suppress("UNUSED_PARAMETER") -fun OkHttpClient.newCallWithProgress(request: Request, listener: ProgressListener): Call { +fun OkHttpClient.newCachelessCallWithProgress(request: Request, listener: ProgressListener): Call { val progressClient = newBuilder() .cache(null) .addNetworkInterceptor { chain -> @@ -128,12 +130,19 @@ fun OkHttpClient.newCallWithProgress(request: Request, listener: ProgressListene return progressClient.newCall(request) } +context(Json) inline fun Response.parseAs(): T { - // Avoiding Injekt.get() due to compiler issues - val json = Injekt.getInstance(fullType().type) - this.use { - val responseBody = it.body.string() - return json.decodeFromString(responseBody) + return decodeFromJsonResponse(serializer(), this) +} + +context(Json) +@OptIn(ExperimentalSerializationApi::class) +fun decodeFromJsonResponse( + deserializer: DeserializationStrategy, + response: Response +): T { + return response.body.source().use { + decodeFromBufferedSource(deserializer, it) } } diff --git a/server/src/main/kotlin/eu/kanade/tachiyomi/network/interceptor/RateLimitInterceptor.kt b/server/src/main/kotlin/eu/kanade/tachiyomi/network/interceptor/RateLimitInterceptor.kt index 58587000..52d8cff1 100644 --- a/server/src/main/kotlin/eu/kanade/tachiyomi/network/interceptor/RateLimitInterceptor.kt +++ b/server/src/main/kotlin/eu/kanade/tachiyomi/network/interceptor/RateLimitInterceptor.kt @@ -4,11 +4,21 @@ import android.os.SystemClock import okhttp3.Interceptor import okhttp3.OkHttpClient import okhttp3.Response +import java.io.IOException +import java.util.ArrayDeque +import java.util.concurrent.Semaphore 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. * + * This uses `java.time` APIs and is the legacy method, kept + * for compatibility reasons with existing extensions. + * * Examples: * * permits = 5, period = 1, unit = seconds => 5 requests per second @@ -16,52 +26,103 @@ import java.util.concurrent.TimeUnit * * @since extension-lib 1.3 * - * @param permits {Int} Number of requests allowed within a period of units. - * @param period {Long} The limiting duration. Defaults to 1. - * @param unit {TimeUnit} The unit of time for the period. Defaults to seconds. + * @param permits [Int] Number of requests allowed within a period of units. + * @param period [Long] The limiting duration. Defaults to 1. + * @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( permits: Int, period: Long = 1, 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, - period: Long, - unit: TimeUnit + period: Duration ) : Interceptor { - private val requestQueue = ArrayList(permits) - private val rateLimitMillis = unit.toMillis(period) + private val requestQueue = ArrayDeque(permits) + private val rateLimitMillis = period.inWholeMilliseconds + private val fairLock = Semaphore(1, true) override fun intercept(chain: Interceptor.Chain): Response { - synchronized(requestQueue) { - val now = SystemClock.elapsedRealtime() - val waitTime = if (requestQueue.size < permits) { - 0 - } else { - val oldestReq = requestQueue[0] - val newestReq = requestQueue[permits - 1] + val call = chain.call() + if (call.isCanceled()) throw IOException("Canceled") - if (newestReq - oldestReq > rateLimitMillis) { - 0 - } else { - oldestReq + rateLimitMillis - now // Remaining time + val request = chain.request() + when (host) { + null, request.url.host -> {} // need rate limit + 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) { - requestQueue.removeAt(0) + // add request to queue + timestamp = SystemClock.elapsedRealtime() + requestQueue.addLast(timestamp) } - if (waitTime > 0) { - requestQueue.add(now + waitTime) - Thread.sleep(waitTime) // Sleep inside synchronized to pause queued requests - } else { - requestQueue.add(now) + } finally { + fairLock.release() + } + + 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 } } diff --git a/server/src/main/kotlin/eu/kanade/tachiyomi/network/interceptor/SpecificHostRateLimitInterceptor.kt b/server/src/main/kotlin/eu/kanade/tachiyomi/network/interceptor/SpecificHostRateLimitInterceptor.kt index 2851a9b8..0f079008 100644 --- a/server/src/main/kotlin/eu/kanade/tachiyomi/network/interceptor/SpecificHostRateLimitInterceptor.kt +++ b/server/src/main/kotlin/eu/kanade/tachiyomi/network/interceptor/SpecificHostRateLimitInterceptor.kt @@ -1,75 +1,73 @@ package eu.kanade.tachiyomi.network.interceptor -import android.os.SystemClock import okhttp3.HttpUrl -import okhttp3.Interceptor +import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import okhttp3.OkHttpClient -import okhttp3.Response 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. * * Examples: * - * httpUrl = "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://api.manga.com".toHttpUrlOrNull(), permits = 5, period = 1, unit = seconds => 5 requests per second to api.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 * - * @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 {Long} The limiting duration. Defaults to 1. - * @param unit {TimeUnit} The unit of time for the period. Defaults to seconds. + * @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 [Long] The limiting duration. Defaults to 1. + * @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( httpUrl: HttpUrl, permits: Int, period: Long = 1, 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, - private val permits: Int, - period: Long, - unit: TimeUnit -) : Interceptor { + permits: Int, + period: Duration = 1.seconds +): OkHttpClient.Builder = addInterceptor(RateLimitInterceptor(httpUrl.host, permits, period)) - private val requestQueue = ArrayList(permits) - private val rateLimitMillis = unit.toMillis(period) - private val host = httpUrl.host - - override fun intercept(chain: Interceptor.Chain): Response { - if (chain.request().url.host != host) { - return chain.proceed(chain.request()) - } - synchronized(requestQueue) { - val now = SystemClock.elapsedRealtime() - val waitTime = if (requestQueue.size < permits) { - 0 - } else { - val oldestReq = requestQueue[0] - val newestReq = requestQueue[permits - 1] - - if (newestReq - oldestReq > rateLimitMillis) { - 0 - } else { - 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()) - } -} +/** + * An OkHttp interceptor that handles given url host's rate limiting. + * + * Examples: + * + * url = "https://api.manga.com", permits = 5, period = 1.seconds => 5 requests per second to api.manga.com + * url = "https://imagecdn.manga.com", permits = 10, period = 2.minutes => 10 requests per 2 minutes to imagecdn.manga.com + * + * @since extension-lib 1.5 + * + * @param url [String] 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( + url: String, + permits: Int, + period: Duration = 1.seconds +): OkHttpClient.Builder = addInterceptor(RateLimitInterceptor(url.toHttpUrlOrNull()?.host, permits, period)) diff --git a/server/src/main/kotlin/eu/kanade/tachiyomi/source/CatalogueSource.kt b/server/src/main/kotlin/eu/kanade/tachiyomi/source/CatalogueSource.kt index c78033ea..71484c6f 100644 --- a/server/src/main/kotlin/eu/kanade/tachiyomi/source/CatalogueSource.kt +++ b/server/src/main/kotlin/eu/kanade/tachiyomi/source/CatalogueSource.kt @@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.source import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.MangasPage import rx.Observable +import suwayomi.tachidesk.manga.impl.util.lang.awaitSingle interface CatalogueSource : Source { @@ -17,30 +18,63 @@ interface CatalogueSource : Source { 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. */ - fun fetchPopularManga(page: Int): Observable + @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 query the search query. * @param filters the list of filters to apply. */ - fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable + @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. */ - fun fetchLatestUpdates(page: Int): Observable + @Suppress("DEPRECATION") + suspend fun getLatestUpdates(page: Int): MangasPage { + return fetchLatestUpdates(page).awaitSingle() + } /** * Returns the list of filters for the source. */ fun getFilterList(): FilterList + + @Deprecated( + "Use the non-RxJava API instead", + ReplaceWith("getPopularManga") + ) + fun fetchPopularManga(page: Int): Observable = + throw IllegalStateException("Not used") + + @Deprecated( + "Use the non-RxJava API instead", + ReplaceWith("getSearchManga") + ) + fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable = + throw IllegalStateException("Not used") + + @Deprecated( + "Use the non-RxJava API instead", + ReplaceWith("getLatestUpdates") + ) + fun fetchLatestUpdates(page: Int): Observable = + throw IllegalStateException("Not used") } diff --git a/server/src/main/kotlin/eu/kanade/tachiyomi/source/ConfigurableSource.kt b/server/src/main/kotlin/eu/kanade/tachiyomi/source/ConfigurableSource.kt index 4f5154fe..9f1abe2e 100644 --- a/server/src/main/kotlin/eu/kanade/tachiyomi/source/ConfigurableSource.kt +++ b/server/src/main/kotlin/eu/kanade/tachiyomi/source/ConfigurableSource.kt @@ -1,8 +1,27 @@ package eu.kanade.tachiyomi.source +import android.app.Application +import android.content.Context +import android.content.SharedPreferences import androidx.preference.PreferenceScreen +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get interface ConfigurableSource : Source { + /** + * Gets instance of [SharedPreferences] scoped to the specific source. + * + * @since extensions-lib 1.5 + */ + fun getSourcePreferences(): SharedPreferences = + Injekt.get().getSharedPreferences(preferenceKey(), Context.MODE_PRIVATE) + 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().getSharedPreferences(preferenceKey(), Context.MODE_PRIVATE) diff --git a/server/src/main/kotlin/eu/kanade/tachiyomi/source/Source.kt b/server/src/main/kotlin/eu/kanade/tachiyomi/source/Source.kt index 0856cf8c..5d126bba 100644 --- a/server/src/main/kotlin/eu/kanade/tachiyomi/source/Source.kt +++ b/server/src/main/kotlin/eu/kanade/tachiyomi/source/Source.kt @@ -22,41 +22,11 @@ interface Source { 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. - */ - @Deprecated( - "Use the 1.x API instead", - ReplaceWith("getMangaDetails") - ) - fun fetchMangaDetails(manga: SManga): Observable = 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> = 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> = Observable.empty() - - /** - * [1.x API] Get the updated details for a manga. + * @return the updated manga. */ @Suppress("DEPRECATION") 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") suspend fun getChapterList(manga: SManga): List { @@ -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. + * + * @since extensions-lib 1.5 + * @param chapter the chapter. + * @return the pages for the chapter. */ @Suppress("DEPRECATION") suspend fun getPageList(chapter: SChapter): List { return fetchPageList(chapter).awaitSingle() } + + @Deprecated( + "Use the non-RxJava API instead", + ReplaceWith("getMangaDetails") + ) + fun fetchMangaDetails(manga: SManga): Observable = throw IllegalStateException("Not used") + + @Deprecated( + "Use the non-RxJava API instead", + ReplaceWith("getChapterList") + ) + fun fetchChapterList(manga: SManga): Observable> = throw IllegalStateException("Not used") + + @Deprecated( + "Use the non-RxJava API instead", + ReplaceWith("getPageList") + ) + fun fetchPageList(chapter: SChapter): Observable> = Observable.empty() } // fun Source.icon(): Drawable? = Injekt.get().getAppIconForSource(this) diff --git a/server/src/main/kotlin/eu/kanade/tachiyomi/source/local/LocalSource.kt b/server/src/main/kotlin/eu/kanade/tachiyomi/source/local/LocalSource.kt index 5d0bfe82..6e257239 100644 --- a/server/src/main/kotlin/eu/kanade/tachiyomi/source/local/LocalSource.kt +++ b/server/src/main/kotlin/eu/kanade/tachiyomi/source/local/LocalSource.kt @@ -40,7 +40,6 @@ import org.jetbrains.exposed.sql.transactions.transaction import org.kodein.di.DI import org.kodein.di.conf.global import org.kodein.di.instance -import rx.Observable import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.registerCatalogueSource import suwayomi.tachidesk.manga.impl.util.storage.ImageUtil import suwayomi.tachidesk.manga.model.table.ExtensionTable @@ -76,11 +75,11 @@ class LocalSource( override val supportsLatest: Boolean = true // 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 { + override suspend fun getSearchManga(page: Int, query: String, filters: FilterList): MangasPage { val baseDirsFiles = fileSystem.getFilesInBaseDirectories() val lastModifiedLimit by lazy { if (filters === LATEST_FILTERS) System.currentTimeMillis() - LATEST_THRESHOLD else 0L } var mangaDirs = baseDirsFiles @@ -153,7 +152,7 @@ class LocalSource( } } - return Observable.just(MangasPage(mangas.toList(), false)) + return MangasPage(mangas.toList(), false) } // Manga details related diff --git a/server/src/main/kotlin/eu/kanade/tachiyomi/source/online/HttpSource.kt b/server/src/main/kotlin/eu/kanade/tachiyomi/source/online/HttpSource.kt index 69d88d86..165c3693 100644 --- a/server/src/main/kotlin/eu/kanade/tachiyomi/source/online/HttpSource.kt +++ b/server/src/main/kotlin/eu/kanade/tachiyomi/source/online/HttpSource.kt @@ -3,8 +3,9 @@ package eu.kanade.tachiyomi.source.online import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.NetworkHelper 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.newCallWithProgress +import eu.kanade.tachiyomi.network.newCachelessCallWithProgress import eu.kanade.tachiyomi.source.CatalogueSource import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.MangasPage @@ -16,6 +17,7 @@ import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.Response import rx.Observable +import suwayomi.tachidesk.manga.impl.util.lang.awaitSingle import uy.kohesive.injekt.injectLazy // import uy.kohesive.injekt.injectLazy import java.net.URI @@ -51,15 +53,16 @@ abstract class HttpSource : CatalogueSource { open val versionId = 1 /** - * Id of the source. By default it uses a generated id using the first 16 characters (64 bits) - * of the MD5 of the string: sourcename/language/versionId - * Note the generated id sets the sign bit to 0. + * ID of the source. By default it uses a generated id using the first 16 characters (64 bits) + * of the MD5 of the string `"${name.lowercase()}/$lang/$versionId"`. + * + * 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 { - 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 - } + override val id by lazy { generateId(name, lang, versionId) } /** * Headers used for requests. @@ -72,6 +75,28 @@ abstract class HttpSource : CatalogueSource { open val client: OkHttpClient 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. */ @@ -90,6 +115,7 @@ abstract class HttpSource : CatalogueSource { * * @param page the page number to retrieve. */ + @Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getPopularManga")) override fun fetchPopularManga(page: Int): Observable { return client.newCall(popularMangaRequest(page)) .asObservableSuccess() @@ -120,6 +146,7 @@ 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")) override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable { return client.newCall(searchMangaRequest(page, query, filters)) .asObservableSuccess() @@ -149,6 +176,7 @@ abstract class HttpSource : CatalogueSource { * * @param page the page number to retrieve. */ + @Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getLatestUpdates")) override fun fetchLatestUpdates(page: Int): Observable { return client.newCall(latestUpdatesRequest(page)) .asObservableSuccess() @@ -172,11 +200,18 @@ abstract class HttpSource : CatalogueSource { protected abstract fun latestUpdatesParse(response: Response): MangasPage /** - * Returns an observable with the updated details for a manga. Normally it's not needed to - * override this method. + * Get the updated details for a manga. + * 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 { return client.newCall(mangaDetailsRequest(manga)) .asObservableSuccess() @@ -203,11 +238,23 @@ abstract class HttpSource : CatalogueSource { protected abstract fun mangaDetailsParse(response: Response): SManga /** - * Returns an observable with the updated chapter list for a manga. Normally it's not needed to - * override this method. If a manga is licensed an empty chapter list observable is returned + * Get all the available chapters for a manga. + * 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 { + 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> { return if (manga.status != SManga.LICENSED) { client.newCall(chapterListRequest(manga)) @@ -216,7 +263,7 @@ abstract class HttpSource : CatalogueSource { chapterListParse(response) } } 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 /** - * 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 { + return fetchPageList(chapter).awaitSingle() + } + + @Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getPageList")) override fun fetchPageList(chapter: SChapter): Observable> { return client.newCall(pageListRequest(chapter)) .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 * 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 { + return fetchImageUrl(page).awaitSingle() + } + + @Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getImageUrl")) open fun fetchImageUrl(page: Page): Observable { return client.newCall(imageUrlRequest(page)) .asObservableSuccess() @@ -297,13 +359,15 @@ abstract class HttpSource : CatalogueSource { 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. */ - fun fetchImage(page: Page): Observable { - return client.newCallWithProgress(imageRequest(page), page) - .asObservableSuccess() + open suspend fun getImage(page: Page): Response { + return client.newCachelessCallWithProgress(imageRequest(page), page) + .awaitSuccess() } /** @@ -397,3 +461,5 @@ abstract class HttpSource : CatalogueSource { val DEFAULT_USER_AGENT by lazy { getWebViewUserAgent() } } } + +class LicensedMangaChaptersException : Exception("Licensed - No chapters to show") diff --git a/server/src/main/kotlin/eu/kanade/tachiyomi/source/online/HttpSourceFetcher.kt b/server/src/main/kotlin/eu/kanade/tachiyomi/source/online/HttpSourceFetcher.kt deleted file mode 100644 index 26969fc2..00000000 --- a/server/src/main/kotlin/eu/kanade/tachiyomi/source/online/HttpSourceFetcher.kt +++ /dev/null @@ -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 { - return fetchImageUrl(page) - .onErrorReturn { null } - .doOnNext { page.imageUrl = it } - .map { page } -} - -fun HttpSource.fetchAllImageUrlsFromPageList(pages: List): Observable { - return Observable.from(pages) - .filter { !it.imageUrl.isNullOrEmpty() } - .mergeWith(fetchRemainingImageUrlsFromPageList(pages)) -} - -fun HttpSource.fetchRemainingImageUrlsFromPageList(pages: List): Observable { - return Observable.from(pages) - .filter { it.imageUrl.isNullOrEmpty() } - .concatMap { getImageUrl(it) } -} diff --git a/server/src/main/kotlin/eu/kanade/tachiyomi/source/online/ResolvableSource.kt b/server/src/main/kotlin/eu/kanade/tachiyomi/source/online/ResolvableSource.kt new file mode 100644 index 00000000..6f785d71 --- /dev/null +++ b/server/src/main/kotlin/eu/kanade/tachiyomi/source/online/ResolvableSource.kt @@ -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? +} diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/SourceMutation.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/SourceMutation.kt index 6c378eea..a4811337 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/SourceMutation.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/SourceMutation.kt @@ -14,7 +14,6 @@ import suwayomi.tachidesk.graphql.types.preferenceOf import suwayomi.tachidesk.graphql.types.updateFilterList import suwayomi.tachidesk.manga.impl.MangaList.insertOrGet 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.model.table.MangaTable import suwayomi.tachidesk.server.JavalinSetup.future @@ -50,18 +49,18 @@ class SourceMutation { val source = GetCatalogueSource.getCatalogueSourceOrNull(sourceId)!! val mangasPage = when (type) { FetchSourceMangaType.SEARCH -> { - source.fetchSearchManga( + source.getSearchManga( page = page, query = query.orEmpty(), filters = updateFilterList(source, filters) - ).awaitSingle() + ) } FetchSourceMangaType.POPULAR -> { - source.fetchPopularManga(page).awaitSingle() + source.getPopularManga(page) } FetchSourceMangaType.LATEST -> { if (!source.supportsLatest) throw Exception("Source does not support latest") - source.fetchLatestUpdates(page).awaitSingle() + source.getLatestUpdates(page) } } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/SourceType.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/SourceType.kt index e57b33b7..df38728c 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/SourceType.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/SourceType.kt @@ -299,6 +299,7 @@ data class SwitchPreference( val key: String, val title: String, val summary: String?, + val visible: Boolean, val currentValue: Boolean?, val default: Boolean ) : Preference @@ -307,6 +308,7 @@ data class CheckBoxPreference( val key: String, val title: String, val summary: String?, + val visible: Boolean, val currentValue: Boolean?, val default: Boolean ) : Preference @@ -315,6 +317,7 @@ data class EditTextPreference( val key: String, val title: String?, val summary: String?, + val visible: Boolean, val currentValue: String?, val default: String?, val dialogTitle: String?, @@ -326,6 +329,7 @@ data class ListPreference( val key: String, val title: String?, val summary: String?, + val visible: Boolean, val currentValue: String?, val default: String?, val entries: List, @@ -336,6 +340,7 @@ data class MultiSelectListPreference( val key: String, val title: String?, val summary: String?, + val visible: Boolean, val currentValue: List?, val default: List?, val dialogTitle: String?, @@ -350,13 +355,15 @@ fun preferenceOf(preference: SourcePreference): Preference { preference.key, preference.title.toString(), preference.summary?.toString(), + preference.visible, preference.currentValue as Boolean, - preference.defaultValue as Boolean + preference.defaultValue as Boolean, ) is SourceCheckBoxPreference -> CheckBoxPreference( preference.key, preference.title.toString(), preference.summary?.toString(), + preference.visible, preference.currentValue as Boolean, preference.defaultValue as Boolean ) @@ -364,6 +371,7 @@ fun preferenceOf(preference: SourcePreference): Preference { preference.key, preference.title?.toString(), preference.summary?.toString(), + preference.visible, (preference.currentValue as CharSequence?)?.toString(), (preference.defaultValue as CharSequence?)?.toString(), preference.dialogTitle?.toString(), @@ -374,6 +382,7 @@ fun preferenceOf(preference: SourcePreference): Preference { preference.key, preference.title?.toString(), preference.summary?.toString(), + preference.visible, (preference.currentValue as CharSequence?)?.toString(), (preference.defaultValue as CharSequence?)?.toString(), preference.entries.map { it.toString() }, @@ -383,6 +392,7 @@ fun preferenceOf(preference: SourcePreference): Preference { preference.key, preference.title?.toString(), preference.summary?.toString(), + preference.visible, (preference.currentValue as Collection<*>?)?.map { it.toString() }, (preference.defaultValue as Collection<*>?)?.map { it.toString() }, preference.dialogTitle?.toString(), diff --git a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/MangaList.kt b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/MangaList.kt index f750aeb1..c1e7b22d 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/MangaList.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/MangaList.kt @@ -14,7 +14,6 @@ import org.jetbrains.exposed.sql.insertAndGetId import org.jetbrains.exposed.sql.select import org.jetbrains.exposed.sql.transactions.transaction 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.model.dataclass.MangaDataClass import suwayomi.tachidesk.manga.model.dataclass.PagedMangaListDataClass @@ -33,10 +32,10 @@ object MangaList { } val source = getCatalogueSourceOrStub(sourceId) val mangasPage = if (popular) { - source.fetchPopularManga(pageNum).awaitSingle() + source.getPopularManga(pageNum) } else { if (source.supportsLatest) { - source.fetchLatestUpdates(pageNum).awaitSingle() + source.getLatestUpdates(pageNum) } else { throw Exception("Source $source doesn't support latest") } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/Page.kt b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/Page.kt index 27d17248..8f96c6ee 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/Page.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/Page.kt @@ -17,7 +17,6 @@ import org.jetbrains.exposed.sql.select import org.jetbrains.exposed.sql.transactions.transaction import org.jetbrains.exposed.sql.update 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.storage.ImageResponse.getImageResponse import suwayomi.tachidesk.manga.impl.util.storage.ImageUtil @@ -34,7 +33,7 @@ object Page { */ suspend fun getTrueImageUrl(page: Page, source: HttpSource): String { if (page.imageUrl == null) { - page.imageUrl = source.fetchImageUrl(page).awaitSingle() + page.imageUrl = source.getImageUrl(page) } return page.imageUrl!! } @@ -100,7 +99,7 @@ object Page { // Note: don't care about invalidating cache because OS cache is not permanent return getImageResponse(cacheSaveDir, fileName) { - source.fetchImage(tachiyomiPage).awaitSingle() + source.getImage(tachiyomiPage) } } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/Search.kt b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/Search.kt index 84813b31..36c18ed9 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/Search.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/Search.kt @@ -16,21 +16,20 @@ import org.kodein.di.DI import org.kodein.di.conf.global import org.kodein.di.instance 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.model.dataclass.PagedMangaListDataClass object Search { suspend fun sourceSearch(sourceId: Long, searchTerm: String, pageNum: Int): PagedMangaListDataClass { 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) } suspend fun sourceFilter(sourceId: Long, pageNum: Int, filter: FilterData): PagedMangaListDataClass { val source = getCatalogueSourceOrStub(sourceId) 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) } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/Source.kt b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/Source.kt index 1c63cf59..0242b7d4 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/Source.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/Source.kt @@ -7,12 +7,10 @@ package suwayomi.tachidesk.manga.impl * 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/. */ -import android.app.Application -import android.content.Context import androidx.preference.Preference import androidx.preference.PreferenceScreen 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 mu.KotlinLogging 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.table.ExtensionTable import suwayomi.tachidesk.manga.model.table.SourceTable -import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get import xyz.nulldev.androidcompat.androidimpl.CustomContext @@ -106,8 +103,7 @@ object Source { val source = getCatalogueSourceOrStub(sourceId) if (source is ConfigurableSource) { - val sourceShardPreferences = - Injekt.get().getSharedPreferences(source.getPreferenceKey(), Context.MODE_PRIVATE) + val sourceShardPreferences = source.sourcePreferences() val screen = PreferenceScreen(context) screen.sharedPreferences = sourceShardPreferences diff --git a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/backup/proto/ProtoBackupImport.kt b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/backup/proto/ProtoBackupImport.kt index 4533a8ee..25f21bd5 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/backup/proto/ProtoBackupImport.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/backup/proto/ProtoBackupImport.kt @@ -48,7 +48,7 @@ object ProtoBackupImport : ProtoBackupBase() { private val backupMutex = Mutex() sealed class BackupRestoreState { - object Idle : BackupRestoreState() + data object Idle : BackupRestoreState() data class RestoringCategories(val totalManga: Int) : BackupRestoreState() data class RestoringManga(val current: Int, val totalManga: Int, val title: String) : BackupRestoreState() } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/extension/github/ExtensionGithubApi.kt b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/extension/github/ExtensionGithubApi.kt index cbaf29bd..c462c8b7 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/extension/github/ExtensionGithubApi.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/extension/github/ExtensionGithubApi.kt @@ -9,9 +9,10 @@ package suwayomi.tachidesk.manga.impl.extension.github import eu.kanade.tachiyomi.network.GET 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 kotlinx.serialization.Serializable +import kotlinx.serialization.json.Json import mu.KotlinLogging import suwayomi.tachidesk.manga.impl.util.PackageTools.LIB_VERSION_MAX 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 FALLBACK_REPO_URL_PREFIX = "https://gcore.jsdelivr.net/gh/tachiyomiorg/tachiyomi-extensions@repo/" private val logger = KotlinLogging.logger {} + private val json: Json by injectLazy() @Serializable private data class ExtensionJsonObject( @@ -52,7 +54,7 @@ object ExtensionGithubApi { null } else { try { - client.newCall(GET("${REPO_URL_PREFIX}index.min.json")).await() + client.newCall(GET("${REPO_URL_PREFIX}index.min.json")).awaitSuccess() } catch (e: Throwable) { logger.error(e) { "Failed to get extensions from GitHub" } requiresFallbackSource = true @@ -61,12 +63,14 @@ object ExtensionGithubApi { } 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 - .parseAs>() - .toExtensions() + return with(json) { + response + .parseAs>() + .toExtensions() + } } fun getApkUrl(extension: ExtensionDataClass): String { diff --git a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/util/PackageTools.kt b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/util/PackageTools.kt index 448f2c95..393aef22 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/util/PackageTools.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/util/PackageTools.kt @@ -41,7 +41,7 @@ object PackageTools { const val METADATA_SOURCE_FACTORY = "tachiyomi.extension.factory" const val METADATA_NSFW = "tachiyomi.extension.nsfw" 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 unofficialSignature = "64feb21075ba97ebc9cc981243645b331595c111cef1b0d084236a0403b00581" // ArMor's key diff --git a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/util/source/StubSource.kt b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/util/source/StubSource.kt index 49749c16..00220e3d 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/util/source/StubSource.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/util/source/StubSource.kt @@ -21,14 +21,17 @@ open class StubSource(override val id: Long) : CatalogueSource { override val name: String get() = id.toString() + @Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getPopularManga")) override fun fetchPopularManga(page: Int): Observable { return Observable.error(getSourceNotInstalledException()) } + @Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getSearchManga")) override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable { return Observable.error(getSourceNotInstalledException()) } + @Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getLatestUpdates")) override fun fetchLatestUpdates(page: Int): Observable { return Observable.error(getSourceNotInstalledException()) } @@ -37,14 +40,17 @@ open class StubSource(override val id: Long) : CatalogueSource { return FilterList() } + @Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getMangaDetails")) override fun fetchMangaDetails(manga: SManga): Observable { return Observable.error(getSourceNotInstalledException()) } + @Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getChapterList")) override fun fetchChapterList(manga: SManga): Observable> { return Observable.error(getSourceNotInstalledException()) } + @Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getPageList")) override fun fetchPageList(chapter: SChapter): Observable> { return Observable.error(getSourceNotInstalledException()) } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/server/util/WebInterfaceManager.kt b/server/src/main/kotlin/suwayomi/tachidesk/server/util/WebInterfaceManager.kt index 3e2f57ff..7f268bda 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/server/util/WebInterfaceManager.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/server/util/WebInterfaceManager.kt @@ -9,7 +9,7 @@ package suwayomi.tachidesk.server.util import eu.kanade.tachiyomi.network.GET 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.Dispatchers import kotlinx.coroutines.SupervisorJob @@ -408,7 +408,7 @@ object WebInterfaceManager { private suspend fun fetchMD5SumFor(version: String): String { return try { 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) { "" @@ -422,7 +422,7 @@ object WebInterfaceManager { private suspend fun fetchPreviewVersion(): String { 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(releaseInfoJson)["tag_name"]?.jsonPrimitive?.content ?: throw Exception("Failed to get the preview version tag") }) @@ -433,7 +433,7 @@ object WebInterfaceManager { KotlinLogging.logger("$logger fetchServerMappingFile"), { json.parseToJsonElement( - network.client.newCall(GET(WebUIFlavor.WEBUI.versionMappingUrl)).await().body.string() + network.client.newCall(GET(WebUIFlavor.WEBUI.versionMappingUrl)).awaitSuccess().body.string() ).jsonArray } ) diff --git a/server/src/test/kotlin/masstest/CloudFlareTest.kt b/server/src/test/kotlin/masstest/CloudFlareTest.kt index 7c768737..5305eb79 100644 --- a/server/src/test/kotlin/masstest/CloudFlareTest.kt +++ b/server/src/test/kotlin/masstest/CloudFlareTest.kt @@ -10,7 +10,6 @@ import org.junit.jupiter.api.TestInstance import suwayomi.tachidesk.manga.impl.Source import suwayomi.tachidesk.manga.impl.extension.Extension 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.server.applicationSetup import suwayomi.tachidesk.test.BASE_PATH @@ -51,7 +50,7 @@ class CloudFlareTest { @Test fun `test nhentai browse`() = runTest { - assert(nhentai.fetchPopularManga(1).awaitSingle().mangas.isNotEmpty()) { + assert(nhentai.getPopularManga(1).mangas.isNotEmpty()) { "NHentai results were empty" } } diff --git a/server/src/test/kotlin/masstest/TestExtensionCompatibility.kt b/server/src/test/kotlin/masstest/TestExtensionCompatibility.kt index 83266a25..3c943746 100644 --- a/server/src/test/kotlin/masstest/TestExtensionCompatibility.kt +++ b/server/src/test/kotlin/masstest/TestExtensionCompatibility.kt @@ -20,13 +20,11 @@ import mu.KotlinLogging import org.junit.jupiter.api.BeforeAll import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestInstance -import rx.Observable import suwayomi.tachidesk.manga.impl.Source.getSourceList import suwayomi.tachidesk.manga.impl.extension.Extension.installExtension import suwayomi.tachidesk.manga.impl.extension.Extension.uninstallExtension import suwayomi.tachidesk.manga.impl.extension.Extension.updateExtension 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.model.dataclass.ExtensionDataClass import suwayomi.tachidesk.server.applicationSetup @@ -89,8 +87,8 @@ class TestExtensionCompatibility { logger.info { "${popularCount.getAndIncrement()} - Now fetching popular manga from $source" } try { mangaToFetch += source to ( - source.fetchPopularManga(1) - .awaitSingleRepeat().mangas.firstOrNull() + repeat { source.getPopularManga(1) } + .mangas.firstOrNull() ?: throw Exception("Source returned no manga") ) } catch (e: Exception) { @@ -114,7 +112,7 @@ class TestExtensionCompatibility { semaphore.withPermit { logger.info { "${mangaCount.getAndIncrement()} - Now fetching manga from $source" } try { - manga.copyFrom(source.fetchMangaDetails(manga).awaitSingleRepeat()) + manga.copyFrom(repeat { source.getMangaDetails(manga) }) manga.initialized = true } catch (e: Exception) { logger.warn { @@ -143,7 +141,7 @@ class TestExtensionCompatibility { chaptersToFetch += Triple( source, 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) { logger.warn { @@ -174,7 +172,7 @@ class TestExtensionCompatibility { semaphore.withPermit { logger.info { "${pageListCount.getAndIncrement()} - Now fetching page list from $source" } try { - source.fetchPageList(chapter).awaitSingleRepeat() + repeat { source.getPageList(chapter) } } catch (e: Exception) { logger.warn { "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 Observable.awaitSingleRepeat(): T { + private suspend fun repeat(block: suspend () -> T): T { for (i in 1..2) { try { - return awaitSingle() + return block() } catch (e: Exception) {} } - return awaitSingle() + return block() } } diff --git a/server/src/test/kotlin/suwayomi/tachidesk/manga/impl/SearchTest.kt b/server/src/test/kotlin/suwayomi/tachidesk/manga/impl/SearchTest.kt index a7f75271..5b9793c9 100644 --- a/server/src/test/kotlin/suwayomi/tachidesk/manga/impl/SearchTest.kt +++ b/server/src/test/kotlin/suwayomi/tachidesk/manga/impl/SearchTest.kt @@ -38,6 +38,7 @@ class SearchTest : ApplicationTest() { class FakeSearchableSource(id: Long) : StubSource(id) { var mangas: List = emptyList() + @Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getSearchManga")) override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable { return Observable.just(MangasPage(mangas, false)) }