mirror of
https://github.com/Suwayomi/Suwayomi-Server.git
synced 2026-07-11 06:44:34 -05:00
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:
@@ -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;
|
||||||
|
|||||||
@@ -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"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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 }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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) }
|
|
||||||
}
|
|
||||||
@@ -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?
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
|||||||
@@ -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")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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))
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user