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
|
||||
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;
|
||||
|
||||
@@ -37,6 +37,10 @@ subprojects {
|
||||
dependsOn("formatKotlin")
|
||||
kotlinOptions {
|
||||
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-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",
|
||||
|
||||
@@ -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<String> = ImageUtil.ImageType.entries.map { it.mime }
|
||||
}
|
||||
|
||||
@@ -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<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
|
||||
suspend fun Call.await(): Response {
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
private suspend fun Call.await(callStack: Array<StackTraceElement>): 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<Response> {
|
||||
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 <reified T> Response.parseAs(): T {
|
||||
// Avoiding Injekt.get<Json>() due to compiler issues
|
||||
val json = Injekt.getInstance<Json>(fullType<Json>().type)
|
||||
this.use {
|
||||
val responseBody = it.body.string()
|
||||
return json.decodeFromString(responseBody)
|
||||
return decodeFromJsonResponse(serializer(), this)
|
||||
}
|
||||
|
||||
context(Json)
|
||||
@OptIn(ExperimentalSerializationApi::class)
|
||||
fun <T> decodeFromJsonResponse(
|
||||
deserializer: DeserializationStrategy<T>,
|
||||
response: Response
|
||||
): T {
|
||||
return response.body.source().use {
|
||||
decodeFromBufferedSource(deserializer, it)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+90
-29
@@ -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<Long>(permits)
|
||||
private val rateLimitMillis = unit.toMillis(period)
|
||||
private val requestQueue = ArrayDeque<Long>(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
|
||||
}
|
||||
}
|
||||
|
||||
+50
-52
@@ -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<Long>(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))
|
||||
|
||||
@@ -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<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 query the search query.
|
||||
* @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.
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
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
|
||||
|
||||
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<Application>().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<Application>().getSharedPreferences(preferenceKey(), Context.MODE_PRIVATE)
|
||||
|
||||
@@ -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<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.
|
||||
* @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<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.
|
||||
*
|
||||
* @since extensions-lib 1.5
|
||||
* @param chapter the chapter.
|
||||
* @return the pages for the chapter.
|
||||
*/
|
||||
@Suppress("DEPRECATION")
|
||||
suspend fun getPageList(chapter: SChapter): List<Page> {
|
||||
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)
|
||||
|
||||
@@ -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<MangasPage> {
|
||||
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
|
||||
|
||||
@@ -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<MangasPage> {
|
||||
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<MangasPage> {
|
||||
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<MangasPage> {
|
||||
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<SManga> {
|
||||
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<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>> {
|
||||
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<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>> {
|
||||
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<String> {
|
||||
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<Response> {
|
||||
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")
|
||||
|
||||
@@ -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.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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<String>,
|
||||
@@ -336,6 +340,7 @@ data class MultiSelectListPreference(
|
||||
val key: String,
|
||||
val title: String?,
|
||||
val summary: String?,
|
||||
val visible: Boolean,
|
||||
val currentValue: List<String>?,
|
||||
val default: List<String>?,
|
||||
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(),
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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<Application>().getSharedPreferences(source.getPreferenceKey(), Context.MODE_PRIVATE)
|
||||
val sourceShardPreferences = source.sourcePreferences()
|
||||
|
||||
val screen = PreferenceScreen(context)
|
||||
screen.sharedPreferences = sourceShardPreferences
|
||||
|
||||
+1
-1
@@ -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()
|
||||
}
|
||||
|
||||
+10
-6
@@ -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<List<ExtensionJsonObject>>()
|
||||
.toExtensions()
|
||||
return with(json) {
|
||||
response
|
||||
.parseAs<List<ExtensionJsonObject>>()
|
||||
.toExtensions()
|
||||
}
|
||||
}
|
||||
|
||||
fun getApkUrl(extension: ExtensionDataClass): String {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<MangasPage> {
|
||||
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> {
|
||||
return Observable.error(getSourceNotInstalledException())
|
||||
}
|
||||
|
||||
@Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getLatestUpdates"))
|
||||
override fun fetchLatestUpdates(page: Int): Observable<MangasPage> {
|
||||
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<SManga> {
|
||||
return Observable.error(getSourceNotInstalledException())
|
||||
}
|
||||
|
||||
@Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getChapterList"))
|
||||
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
|
||||
return Observable.error(getSourceNotInstalledException())
|
||||
}
|
||||
|
||||
@Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getPageList"))
|
||||
override fun fetchPageList(chapter: SChapter): Observable<List<Page>> {
|
||||
return Observable.error(getSourceNotInstalledException())
|
||||
}
|
||||
|
||||
@@ -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<JsonObject>(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
|
||||
}
|
||||
)
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 <T> Observable<T>.awaitSingleRepeat(): T {
|
||||
private suspend fun <T> repeat(block: suspend () -> T): T {
|
||||
for (i in 1..2) {
|
||||
try {
|
||||
return awaitSingle()
|
||||
return block()
|
||||
} catch (e: Exception) {}
|
||||
}
|
||||
return awaitSingle()
|
||||
return block()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,6 +38,7 @@ class SearchTest : ApplicationTest() {
|
||||
class FakeSearchableSource(id: Long) : StubSource(id) {
|
||||
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> {
|
||||
return Observable.just(MangasPage(mangas, false))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user