From 6dec7d9876fb8a8ea2f1b151dc145f845d9b48ef Mon Sep 17 00:00:00 2001 From: AntsyLich <59261191+AntsyLich@users.noreply.github.com> Date: Tue, 28 Jan 2025 02:03:45 +0600 Subject: [PATCH 01/10] Bump maximum supported lib --- .../java/eu/kanade/tachiyomi/extension/util/ExtensionLoader.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionLoader.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionLoader.kt index 739ce15cc9..936d3e3cc7 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionLoader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionLoader.kt @@ -51,7 +51,7 @@ internal object ExtensionLoader { private const val METADATA_SOURCE_FACTORY = "tachiyomi.extension.factory" private const val METADATA_NSFW = "tachiyomi.extension.nsfw" const val LIB_VERSION_MIN = 1.4 - const val LIB_VERSION_MAX = 1.5 + const val LIB_VERSION_MAX = 1.6 @Suppress("DEPRECATION") private val PACKAGE_FLAGS = PackageManager.GET_CONFIGURATIONS or From f5d8cbe8ab826d75cb82ae9c60822f0f0eec5584 Mon Sep 17 00:00:00 2001 From: AntsyLich <59261191+AntsyLich@users.noreply.github.com> Date: Mon, 27 Jan 2025 01:50:30 +0600 Subject: [PATCH 02/10] Implement new rate limit API --- .../data/track/anilist/AnilistApi.kt | 4 +- .../main/kotlin/mihon.code.lint.gradle.kts | 1 + .../interceptor/RateLimitInterceptor.kt | 104 +--------------- .../SpecificHostRateLimitInterceptor.kt | 54 +------- .../kotlin/mihon/core/network/rateLimit.kt | 117 ++++++++++++++++++ .../kotlin/mihonx/network/rateLimit.kt | 87 +++++++++++++ 6 files changed, 217 insertions(+), 150 deletions(-) create mode 100644 core/common/src/main/kotlin/mihon/core/network/rateLimit.kt create mode 100644 source-api/src/commonMain/kotlin/mihonx/network/rateLimit.kt diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistApi.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistApi.kt index 3695b6f25d..6e5a6e18c7 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistApi.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistApi.kt @@ -11,7 +11,6 @@ import eu.kanade.tachiyomi.data.track.anilist.dto.ALUserListMangaQueryResult import eu.kanade.tachiyomi.data.track.model.TrackSearch import eu.kanade.tachiyomi.network.POST import eu.kanade.tachiyomi.network.awaitSuccess -import eu.kanade.tachiyomi.network.interceptor.rateLimit import eu.kanade.tachiyomi.network.jsonMime import eu.kanade.tachiyomi.network.parseAs import kotlinx.serialization.json.Json @@ -20,6 +19,7 @@ import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.json.put import kotlinx.serialization.json.putJsonObject +import mihon.core.network.rateLimit import okhttp3.OkHttpClient import okhttp3.RequestBody.Companion.toRequestBody import tachiyomi.core.common.util.lang.withIOContext @@ -36,7 +36,7 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) { private val authClient = client.newBuilder() .addInterceptor(interceptor) - .rateLimit(permits = 85, period = 1.minutes) + .rateLimit(85, 1.minutes) .build() suspend fun addLibManga(track: Track): Track { diff --git a/buildSrc/src/main/kotlin/mihon.code.lint.gradle.kts b/buildSrc/src/main/kotlin/mihon.code.lint.gradle.kts index 3e604c8c06..d8376cddc3 100644 --- a/buildSrc/src/main/kotlin/mihon.code.lint.gradle.kts +++ b/buildSrc/src/main/kotlin/mihon.code.lint.gradle.kts @@ -27,6 +27,7 @@ spotless { "ktlint_function_naming_ignore_when_annotated_with" to "Composable", "ktlint_standard_class-signature" to "disabled", "ktlint_standard_discouraged-comment-location" to "disabled", + "ktlint_standard_filename" to "disabled", "ktlint_standard_function-expression-body" to "disabled", "ktlint_standard_function-signature" to "disabled", )) diff --git a/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/interceptor/RateLimitInterceptor.kt b/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/interceptor/RateLimitInterceptor.kt index eab5ec302a..5dc118259d 100644 --- a/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/interceptor/RateLimitInterceptor.kt +++ b/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/interceptor/RateLimitInterceptor.kt @@ -1,17 +1,12 @@ +@file:Suppress("UNUSED") + package eu.kanade.tachiyomi.network.interceptor -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 +import mihon.core.network.rateLimit as actualRateLimit /** * An OkHttp interceptor that handles rate limiting. @@ -30,99 +25,8 @@ import kotlin.time.toDurationUnit * @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(null, permits, period.toDuration(unit.toDurationUnit()))) - -/** - * 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: Duration, -) : Interceptor { - - private val requestQueue = ArrayDeque(permits) - private val rateLimitMillis = period.inWholeMilliseconds - private val fairLock = Semaphore(1, true) - - override fun intercept(chain: Interceptor.Chain): Response { - val call = chain.call() - if (call.isCanceled()) throw IOException("Canceled") - - 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() && 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 - } - } - } - - // add request to queue - timestamp = SystemClock.elapsedRealtime() - requestQueue.addLast(timestamp) - } - } 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 response - } -} +): OkHttpClient.Builder = actualRateLimit(permits, period.toDuration(unit.toDurationUnit())) diff --git a/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/interceptor/SpecificHostRateLimitInterceptor.kt b/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/interceptor/SpecificHostRateLimitInterceptor.kt index 9f860faab8..9dd2661475 100644 --- a/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/interceptor/SpecificHostRateLimitInterceptor.kt +++ b/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/interceptor/SpecificHostRateLimitInterceptor.kt @@ -1,13 +1,13 @@ +@file:Suppress("UNUSED") + package eu.kanade.tachiyomi.network.interceptor import okhttp3.HttpUrl -import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import okhttp3.OkHttpClient import java.util.concurrent.TimeUnit -import kotlin.time.Duration -import kotlin.time.Duration.Companion.seconds import kotlin.time.toDuration import kotlin.time.toDurationUnit +import mihon.core.network.rateLimit as actualRateLimit /** * An OkHttp interceptor that handles given url host's rate limiting. @@ -17,8 +17,8 @@ import kotlin.time.toDurationUnit * * 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 = "api.manga.example".toHttpUrlOrNull(), permits = 5, period = 1, unit = seconds => 5 requests per second to api.manga.example + * httpUrl = "imagecdn.manga.example".toHttpUrlOrNull(), permits = 10, period = 2, unit = minutes => 10 requests per 2 minutes to imagecdn.manga.example * * @since extension-lib 1.3 * @@ -27,51 +27,9 @@ import kotlin.time.toDurationUnit * @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( - RateLimitInterceptor(httpUrl.host, permits, period.toDuration(unit.toDurationUnit())), -) - -/** - * 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. - */ -@Suppress("UNUSED") -fun OkHttpClient.Builder.rateLimitHost( - httpUrl: HttpUrl, - permits: Int, - period: Duration = 1.seconds, -) = addInterceptor(RateLimitInterceptor(httpUrl.host, permits, period)) - -/** - * 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. - */ -@Suppress("UNUSED") -fun OkHttpClient.Builder.rateLimitHost(url: String, permits: Int, period: Duration = 1.seconds) = - addInterceptor(RateLimitInterceptor(url.toHttpUrlOrNull()?.host, permits, period)) +): OkHttpClient.Builder = actualRateLimit(permits, period.toDuration(unit.toDurationUnit())) { it.host == httpUrl.host } diff --git a/core/common/src/main/kotlin/mihon/core/network/rateLimit.kt b/core/common/src/main/kotlin/mihon/core/network/rateLimit.kt new file mode 100644 index 0000000000..2d1135cb60 --- /dev/null +++ b/core/common/src/main/kotlin/mihon/core/network/rateLimit.kt @@ -0,0 +1,117 @@ +package mihon.core.network + +import android.os.SystemClock +import okhttp3.HttpUrl +import okhttp3.Interceptor +import okhttp3.OkHttpClient +import okhttp3.Response +import java.io.IOException +import java.util.ArrayDeque +import java.util.concurrent.Semaphore +import kotlin.time.Duration +import kotlin.time.Duration.Companion.seconds + +/** + * An OkHttp interceptor that enforces rate limiting across all requests. + * + * Examples: + * + * permits = 5, period = 1.seconds => 5 requests per second + * permits = 10, period = 2.minutes => 10 requests per 2 minutes + * + * @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, +): OkHttpClient.Builder = rateLimit(permits, period) { true } + +/** + * An OkHttp interceptor that enforces conditional rate limiting based on a given condition. + * + * Examples: + * + * permits = 5, period = 1.seconds, shouldLimit = { it.host == "api.manga.example" } => 5 requests per second to api.manga.example. + * permits = 10, period = 2.minutes, shouldLimit = { it.encodedPath.startsWith("/images/") } => 10 requests per 2 minutes to paths starting with "/images/". + * + * @param permits [Int] Number of requests allowed within a period of units. + * @param period [Duration] The limiting duration. Defaults to 1.seconds. + * @param shouldLimit A predicate to determine whether the rate limit should apply to a given request. + */ +fun OkHttpClient.Builder.rateLimit( + permits: Int, + period: Duration = 1.seconds, + shouldLimit: (HttpUrl) -> Boolean, +): OkHttpClient.Builder = addInterceptor(RateLimitInterceptor(permits, period, shouldLimit)) + +/** We can probably accept domains or wildcards by comparing with [endsWith], etc. */ +@Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN") +internal class RateLimitInterceptor( + private val permits: Int, + period: Duration, + private val shouldLimit: (HttpUrl) -> Boolean, +) : Interceptor { + + private val requestQueue = ArrayDeque(permits) + private val rateLimitMillis = period.inWholeMilliseconds + private val fairLock = Semaphore(1, true) + + override fun intercept(chain: Interceptor.Chain): Response { + val call = chain.call() + if (call.isCanceled()) throw IOException("Canceled") + + val request = chain.request() + if (!shouldLimit(request.url)) 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() && 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 + } + } + } + + // add request to queue + timestamp = SystemClock.elapsedRealtime() + requestQueue.addLast(timestamp) + } + } 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 response + } +} diff --git a/source-api/src/commonMain/kotlin/mihonx/network/rateLimit.kt b/source-api/src/commonMain/kotlin/mihonx/network/rateLimit.kt new file mode 100644 index 0000000000..ad4a41cff6 --- /dev/null +++ b/source-api/src/commonMain/kotlin/mihonx/network/rateLimit.kt @@ -0,0 +1,87 @@ +@file:Suppress("UNUSED") + +package mihonx.network + +import okhttp3.HttpUrl +import okhttp3.OkHttpClient +import kotlin.time.Duration +import kotlin.time.Duration.Companion.seconds +import mihon.core.network.rateLimit as actualRateLimit + +/** + * An OkHttp interceptor that enforces rate limiting across all requests. + * + * Examples: + * + * permits = 5, period = 1.seconds => 5 requests per second + * permits = 10, period = 2.minutes => 10 requests per 2 minutes + * + * @since extension-lib 1.6 + * + * @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, +): OkHttpClient.Builder = actualRateLimit(permits, period) + +/** + * An OkHttp interceptor that handles given url host's rate limiting. + * + * Examples: + * + * url = "https://api.manga.example", permits = 5, period = 1.seconds => 5 requests per second to any url starting with "https://api.manga.example" + * url = "https://cdn.manga.example/image", permits = 10, period = 2.minutes => 10 requests per 2 minutes to any url starting with "https://cdn.manga.example/image" + * + * @since extension-lib 1.6 + * + * @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.rateLimit( + url: String, + permits: Int, + period: Duration = 1.seconds, +): OkHttpClient.Builder = actualRateLimit(permits, period) { it.toString().startsWith(url) } + +/** + * An OkHttp interceptor that handles given url host's rate limiting. + * + * Examples: + * + * httpUrl = "https://api.manga.example".toHttpUrlOrNull(), permits = 5, period = 1.seconds => 5 requests per second to any url starting with "https://api.manga.example" + * httpUrl = "https://cdn.manga.example/image".toHttpUrlOrNull(), permits = 10, period = 2.minutes => 10 requests per 2 minutes to any url starting with "https://cdn.manga.example/image" + * + * @since extension-lib 1.6 + * + * @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.rateLimit( + httpUrl: HttpUrl, + permits: Int, + period: Duration = 1.seconds, +): OkHttpClient.Builder = rateLimit(httpUrl.toString(), permits, period) + +/** + * An OkHttp interceptor that enforces conditional rate limiting based on a given condition. + * + * Examples: + * + * permits = 5, period = 1.seconds, shouldLimit = { it.host == "api.manga.example" } => 5 requests per second to api.manga.example. + * permits = 10, period = 2.minutes, shouldLimit = { it.encodedPath.startsWith("/images/") } => 10 requests per 2 minutes to paths starting with "/images/". + * + * @since extension-lib 1.6 + * + * @param permits [Int] Number of requests allowed within a period of units. + * @param period [Duration] The limiting duration. Defaults to 1.seconds. + * @param shouldLimit A predicate to determine whether the rate limit should apply to a given request. + */ +fun OkHttpClient.Builder.rateLimit( + permits: Int, + period: Duration = 1.seconds, + shouldLimit: (HttpUrl) -> Boolean, +): OkHttpClient.Builder = actualRateLimit(permits, period, shouldLimit) From 4d841b3a5c5106d34321d5bad136c893bf53dcbc Mon Sep 17 00:00:00 2001 From: AntsyLich <59261191+AntsyLich@users.noreply.github.com> Date: Mon, 27 Jan 2025 02:05:00 +0600 Subject: [PATCH 03/10] Add UserAgentType --- .../mihonx/source/model/UserAgentType.kt | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 source-api/src/commonMain/kotlin/mihonx/source/model/UserAgentType.kt diff --git a/source-api/src/commonMain/kotlin/mihonx/source/model/UserAgentType.kt b/source-api/src/commonMain/kotlin/mihonx/source/model/UserAgentType.kt new file mode 100644 index 0000000000..838fe91c13 --- /dev/null +++ b/source-api/src/commonMain/kotlin/mihonx/source/model/UserAgentType.kt @@ -0,0 +1,38 @@ +@file:Suppress("UNUSED") + +package mihonx.source.model + +/** + * Type of UserAgent a source needs + * + * @since extensions-lib 1.6 + */ +sealed interface UserAgentType { + /** + * Supports both Desktop or Mobile UserAgent + * + * @since extensions-lib 1.6 + */ + data object Universal : UserAgentType + + /** + * Requires Desktop UserAgent + * + * @since extensions-lib 1.6 + */ + data object Desktop : UserAgentType + + /** + * Requires Mobile UserAgent + * + * @since extensions-lib 1.6 + */ + data object Mobile : UserAgentType + + /** + * Extension manages its own UserAgent + * + * @since extensions-lib 1.6 + */ + data object Managed : UserAgentType +} From e9896cd478d283eb17a085cd75a633ad7094d5e6 Mon Sep 17 00:00:00 2001 From: AntsyLich <59261191+AntsyLich@users.noreply.github.com> Date: Mon, 27 Jan 2025 02:17:37 +0600 Subject: [PATCH 04/10] Implement new preferences API --- .../creators/PreferenceBackupCreator.kt | 6 +-- .../restore/restorers/PreferenceRestorer.kt | 2 +- .../tachiyomi/data/track/kavita/Kavita.kt | 5 +-- .../details/SourcePreferencesScreen.kt | 2 +- .../tachiyomi/source/ConfigurableSource.kt | 24 ----------- .../kotlin/mihonx/source/utils/preferences.kt | 40 +++++++++++++++++++ 6 files changed, 47 insertions(+), 32 deletions(-) create mode 100644 source-api/src/commonMain/kotlin/mihonx/source/utils/preferences.kt diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/creators/PreferenceBackupCreator.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/creators/PreferenceBackupCreator.kt index 5b72ce3846..5573dc23e9 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/creators/PreferenceBackupCreator.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/creators/PreferenceBackupCreator.kt @@ -9,8 +9,8 @@ import eu.kanade.tachiyomi.data.backup.models.LongPreferenceValue import eu.kanade.tachiyomi.data.backup.models.StringPreferenceValue import eu.kanade.tachiyomi.data.backup.models.StringSetPreferenceValue import eu.kanade.tachiyomi.source.ConfigurableSource -import eu.kanade.tachiyomi.source.preferenceKey -import eu.kanade.tachiyomi.source.sourcePreferences +import mihonx.source.utils.preferencesKey +import mihonx.source.utils.sourcePreferences import tachiyomi.core.common.preference.Preference import tachiyomi.core.common.preference.PreferenceStore import tachiyomi.domain.source.service.SourceManager @@ -32,7 +32,7 @@ class PreferenceBackupCreator( .filterIsInstance() .map { BackupSourcePreferences( - it.preferenceKey(), + it.preferencesKey(), it.sourcePreferences().all.toBackupPreferences() .withPrivatePreferences(includePrivatePreferences), ) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/restorers/PreferenceRestorer.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/restorers/PreferenceRestorer.kt index b70280c557..62ff5f5179 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/restorers/PreferenceRestorer.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/restorers/PreferenceRestorer.kt @@ -11,7 +11,7 @@ import eu.kanade.tachiyomi.data.backup.models.LongPreferenceValue import eu.kanade.tachiyomi.data.backup.models.StringPreferenceValue import eu.kanade.tachiyomi.data.backup.models.StringSetPreferenceValue import eu.kanade.tachiyomi.data.library.LibraryUpdateJob -import eu.kanade.tachiyomi.source.sourcePreferences +import mihonx.source.utils.sourcePreferences import tachiyomi.core.common.preference.AndroidPreferenceStore import tachiyomi.core.common.preference.PreferenceStore import uy.kohesive.injekt.Injekt diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/kavita/Kavita.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/kavita/Kavita.kt index a9aed629b0..75e2b81a83 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/kavita/Kavita.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/kavita/Kavita.kt @@ -7,11 +7,10 @@ import eu.kanade.tachiyomi.data.database.models.Track import eu.kanade.tachiyomi.data.track.BaseTracker import eu.kanade.tachiyomi.data.track.EnhancedTracker import eu.kanade.tachiyomi.data.track.model.TrackSearch -import eu.kanade.tachiyomi.source.ConfigurableSource import eu.kanade.tachiyomi.source.Source -import eu.kanade.tachiyomi.source.sourcePreferences import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf +import mihonx.source.utils.sourcePreferences import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.source.service.SourceManager import tachiyomi.i18n.MR @@ -124,7 +123,7 @@ class Kavita(id: Long) : BaseTracker(id, "Kavita"), EnhancedTracker { (0..7).map { bytes[it].toLong() and 0xff shl 8 * (7 - it) } .reduce(Long::or) and Long.MAX_VALUE } - val preferences = (sourceManager.get(sourceId) as ConfigurableSource).sourcePreferences() + val preferences = sourceManager.get(sourceId)?.sourcePreferences() ?: continue val prefApiUrl = preferences.getString("APIURL", "") val prefApiKey = preferences.getString("APIKEY", "") diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/SourcePreferencesScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/SourcePreferencesScreen.kt index c144caa2a4..399f473dab 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/SourcePreferencesScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/SourcePreferencesScreen.kt @@ -37,8 +37,8 @@ import eu.kanade.presentation.util.Screen import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.preference.SharedPreferencesDataStore import eu.kanade.tachiyomi.source.ConfigurableSource -import eu.kanade.tachiyomi.source.sourcePreferences import eu.kanade.tachiyomi.widget.TachiyomiTextInputEditText.Companion.setIncognito +import mihonx.source.utils.sourcePreferences import tachiyomi.domain.source.service.SourceManager import tachiyomi.presentation.core.components.material.Scaffold import tachiyomi.presentation.core.screens.LoadingScreen diff --git a/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/ConfigurableSource.kt b/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/ConfigurableSource.kt index db9a985200..3cfd6309cd 100644 --- a/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/ConfigurableSource.kt +++ b/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/ConfigurableSource.kt @@ -1,29 +1,5 @@ package eu.kanade.tachiyomi.source -import android.app.Application -import android.content.Context -import android.content.SharedPreferences -import uy.kohesive.injekt.Injekt -import uy.kohesive.injekt.api.get - interface ConfigurableSource : Source { - - /** - * Gets instance of [SharedPreferences] scoped to the specific source. - * - * @since extensions-lib 1.5 - */ - fun getSourcePreferences(): SharedPreferences = - Injekt.get().getSharedPreferences(preferenceKey(), Context.MODE_PRIVATE) - fun setupPreferenceScreen(screen: PreferenceScreen) } - -fun ConfigurableSource.preferenceKey(): String = "source_$id" - -// TODO: use getSourcePreferences once all extensions are on ext-lib 1.5 -fun ConfigurableSource.sourcePreferences(): SharedPreferences = - Injekt.get().getSharedPreferences(preferenceKey(), Context.MODE_PRIVATE) - -fun sourcePreferences(key: String): SharedPreferences = - Injekt.get().getSharedPreferences(key, Context.MODE_PRIVATE) diff --git a/source-api/src/commonMain/kotlin/mihonx/source/utils/preferences.kt b/source-api/src/commonMain/kotlin/mihonx/source/utils/preferences.kt new file mode 100644 index 0000000000..b279f99c49 --- /dev/null +++ b/source-api/src/commonMain/kotlin/mihonx/source/utils/preferences.kt @@ -0,0 +1,40 @@ +package mihonx.source.utils + +import android.app.Application +import android.content.Context +import android.content.SharedPreferences +import eu.kanade.tachiyomi.source.Source +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get + +/** + * Gets preference key for source with [id]. + */ +fun preferencesKey(id: Long) = "source_$id" + +/** + * Gets preference key for source. + */ +fun Source.preferencesKey(): String = preferencesKey(id) + +/** + * Gets instance of [SharedPreferences] scoped to the specific source key. + */ +fun sourcePreferences(key: String): SharedPreferences = + Injekt.get().getSharedPreferences(key, Context.MODE_PRIVATE) + +/** + * Gets instance of [SharedPreferences] scoped to the specific source. + * + * @since extensions-lib 1.6 + */ +fun Source.sourcePreferences(): SharedPreferences = sourcePreferences(preferencesKey()) + +/** + * Gets instance of [SharedPreferences] scoped to the specific source id. + * + * @since extensions-lib 1.6 + * + * @param id source id which the [SharedPreferences] is scoped to. + */ +fun sourcePreferences(id: Long): SharedPreferences = sourcePreferences(preferencesKey(id)) From d53ab31fd25929a6c9cdc7864f44e41cd795f790 Mon Sep 17 00:00:00 2001 From: AntsyLich <59261191+AntsyLich@users.noreply.github.com> Date: Mon, 27 Jan 2025 02:21:12 +0600 Subject: [PATCH 05/10] Implement Json API --- .../commonMain/kotlin/mihonx/utils/json.kt | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 source-api/src/commonMain/kotlin/mihonx/utils/json.kt diff --git a/source-api/src/commonMain/kotlin/mihonx/utils/json.kt b/source-api/src/commonMain/kotlin/mihonx/utils/json.kt new file mode 100644 index 0000000000..b20cb9faa1 --- /dev/null +++ b/source-api/src/commonMain/kotlin/mihonx/utils/json.kt @@ -0,0 +1,20 @@ +@file:Suppress("UNUSED") + +package mihonx.utils + +import kotlinx.serialization.json.Json +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get + +/** + * App provided default [Json] instance. Configured as + * ``` + * Json { + * ignoreUnknownKeys = true + * explicitNulls = false + * } + * ``` + * + * @since extensions-lib 1.6 + */ +val defaultJson: Json = Injekt.get() From e7485e33d727354a3e4d75ad6686a0825121412f Mon Sep 17 00:00:00 2001 From: AntsyLich <59261191+AntsyLich@users.noreply.github.com> Date: Mon, 27 Jan 2025 05:49:58 +0600 Subject: [PATCH 06/10] Implement new Source API (basic metadata and API) --- .../browse/ExtensionDetailsScreen.kt | 2 +- .../presentation/browse/GlobalSearchScreen.kt | 2 +- .../tachiyomi/extension/model/Extension.kt | 2 +- .../extension/util/ExtensionLoader.kt | 2 +- .../tachiyomi/source/AndroidSourceManager.kt | 2 +- .../tachiyomi/source/SourceExtensions.kt | 2 +- .../details/ExtensionDetailsScreenModel.kt | 5 +- .../search/MigrateSearchScreenModel.kt | 2 +- .../source/browse/BrowseSourceScreen.kt | 3 +- .../source/globalsearch/SearchScreenModel.kt | 8 +- .../ui/library/LibraryScreenModel.kt | 2 +- .../data/source/SourcePagingSource.kt | 6 +- .../data/source/SourceRepositoryImpl.kt | 4 +- .../data/source/StubSourceRepositoryImpl.kt | 2 +- .../domain/source/model/StubSource.kt | 34 ++-- .../tachiyomi/source/CatalogueSource.kt | 26 +-- .../eu/kanade/tachiyomi/source/Source.kt | 167 ++++++++++++++---- .../tachiyomi/source/online/HttpSource.kt | 4 +- .../tachiyomi/source/local/LocalSource.kt | 27 ++- 19 files changed, 214 insertions(+), 88 deletions(-) diff --git a/app/src/main/java/eu/kanade/presentation/browse/ExtensionDetailsScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/ExtensionDetailsScreen.kt index c2219c3955..5aca46f14b 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/ExtensionDetailsScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/ExtensionDetailsScreen.kt @@ -426,7 +426,7 @@ private fun SourceSwitchPreference( title = if (source.labelAsName) { source.source.toString() } else { - LocaleHelper.getSourceDisplayName(source.source.lang, context) + LocaleHelper.getSourceDisplayName(source.source.language, context) }, widget = { Row( diff --git a/app/src/main/java/eu/kanade/presentation/browse/GlobalSearchScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/GlobalSearchScreen.kt index da4db5e98d..ba905bb3fd 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/GlobalSearchScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/GlobalSearchScreen.kt @@ -78,7 +78,7 @@ internal fun GlobalSearchContent( title = fromSourceId?.let { "▶ ${source.name}".takeIf { source.id == fromSourceId } } ?: source.name, - subtitle = LocaleHelper.getLocalizedDisplayName(source.lang), + subtitle = LocaleHelper.getLocalizedDisplayName(source.language), onClick = { onClickSource(source) }, modifier = Modifier.animateItem(), ) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/model/Extension.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/model/Extension.kt index a8e80d0a52..cd9df8fa35 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/model/Extension.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/model/Extension.kt @@ -54,8 +54,8 @@ sealed class Extension { fun toStubSource(): StubSource { return StubSource( id = this.id, - lang = this.lang, name = this.name, + language = this.lang, ) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionLoader.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionLoader.kt index 936d3e3cc7..9b247137ff 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionLoader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionLoader.kt @@ -302,7 +302,7 @@ internal object ExtensionLoader { } val langs = sources.filterIsInstance() - .map { it.lang } + .map { it.language } .toSet() val lang = when (langs.size) { 0 -> "" diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/AndroidSourceManager.kt b/app/src/main/java/eu/kanade/tachiyomi/source/AndroidSourceManager.kt index 0fa40ba9d6..1114dac2af 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/source/AndroidSourceManager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/source/AndroidSourceManager.kt @@ -103,7 +103,7 @@ class AndroidSourceManager( scope.launch { val dbSource = sourceRepository.getStubSource(source.id) if (dbSource == source) return@launch - sourceRepository.upsertStubSource(source.id, source.lang, source.name) + sourceRepository.upsertStubSource(source.id, source.language, source.name) if (dbSource != null) { downloadManager.renameSource(dbSource, source) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/SourceExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/source/SourceExtensions.kt index bac6ed5cd4..c0fb072b34 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/source/SourceExtensions.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/source/SourceExtensions.kt @@ -11,7 +11,7 @@ fun Source.getNameForMangaInfo(): String { val enabledLanguages = preferences.enabledLanguages().get() .filterNot { it in listOf("all", "other") } val hasOneActiveLanguages = enabledLanguages.size == 1 - val isInEnabledLanguages = lang in enabledLanguages + val isInEnabledLanguages = language in enabledLanguages return when { // For edge cases where user disables a source they got manga of in their library. hasOneActiveLanguages && !isInEnabledLanguages -> toString() diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/ExtensionDetailsScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/ExtensionDetailsScreenModel.kt index 7997f72626..0b22035875 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/ExtensionDetailsScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/ExtensionDetailsScreenModel.kt @@ -71,7 +71,10 @@ class ExtensionDetailsScreenModel( { !it.enabled }, { item -> item.source.name.takeIf { item.labelAsName } - ?: LocaleHelper.getSourceDisplayName(item.source.lang, context).lowercase() + ?: LocaleHelper.getSourceDisplayName( + item.source.language, + context, + ).lowercase() }, ), ) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/MigrateSearchScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/MigrateSearchScreenModel.kt index d22756c780..83e5d523db 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/MigrateSearchScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/MigrateSearchScreenModel.kt @@ -35,7 +35,7 @@ class MigrateSearchScreenModel( compareBy( { it.id != state.value.fromSourceId }, { "${it.id}" !in pinnedSources }, - { "${it.name.lowercase()} (${it.lang})" }, + { "${it.name.lowercase()} (${it.language})" }, ), ) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreen.kt index f4c6fb98ba..01e273efe5 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreen.kt @@ -43,7 +43,6 @@ import eu.kanade.presentation.category.components.ChangeCategoryDialog import eu.kanade.presentation.manga.DuplicateMangaDialog import eu.kanade.presentation.util.AssistContentScreen import eu.kanade.presentation.util.Screen -import eu.kanade.tachiyomi.source.CatalogueSource import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.ui.browse.extension.details.SourcePreferencesScreen import eu.kanade.tachiyomi.ui.browse.migration.search.MigrateDialog @@ -162,7 +161,7 @@ data class BrowseSourceScreen( Text(text = stringResource(MR.strings.popular)) }, ) - if ((screenModel.source as CatalogueSource).supportsLatest) { + if (screenModel.source.hasLatestListing) { FilterChip( selected = state.listing == Listing.Latest, onClick = { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/SearchScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/SearchScreenModel.kt index 1cb9ba3ff4..6b46909e03 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/SearchScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/SearchScreenModel.kt @@ -59,7 +59,7 @@ abstract class SearchScreenModel( compareBy( { (map[it] as? SearchItemResult.Success)?.isEmpty ?: true }, { "${it.id}" !in pinnedSources }, - { "${it.name.lowercase()} (${it.lang})" }, + { "${it.name.lowercase()} (${it.language})" }, ) } @@ -84,11 +84,11 @@ abstract class SearchScreenModel( open fun getEnabledSources(): List { return sourceManager.getCatalogueSources() - .filter { it.lang in enabledLanguages && "${it.id}" !in disabledSources } + .filter { it.language in enabledLanguages && "${it.id}" !in disabledSources } .sortedWith( compareBy( { "${it.id}" !in pinnedSources }, - { "${it.name.lowercase()} (${it.lang})" }, + { "${it.name.lowercase()} (${it.language})" }, ), ) } @@ -162,7 +162,7 @@ abstract class SearchScreenModel( try { val page = withContext(coroutineDispatcher) { - source.getSearchManga(1, query, source.getFilterList()) + source.getMangaList(query, source.getFilterList(), 1) } val titles = page.mangas.map { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryScreenModel.kt index 7d6350a63e..4b4501490b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryScreenModel.kt @@ -373,7 +373,7 @@ class LibraryScreenModel( unreadCount = libraryManga.unreadCount, isLocal = if (prefs.localBadge) libraryManga.manga.isLocal() else false, sourceLanguage = if (prefs.languageBadge) { - sourceManager.getOrStub(libraryManga.manga.source).lang + sourceManager.getOrStub(libraryManga.manga.source).language } else { "" }, diff --git a/data/src/main/java/tachiyomi/data/source/SourcePagingSource.kt b/data/src/main/java/tachiyomi/data/source/SourcePagingSource.kt index fc091c4917..89541e2fca 100644 --- a/data/src/main/java/tachiyomi/data/source/SourcePagingSource.kt +++ b/data/src/main/java/tachiyomi/data/source/SourcePagingSource.kt @@ -11,19 +11,19 @@ import tachiyomi.domain.source.repository.SourcePagingSourceType class SourceSearchPagingSource(source: CatalogueSource, val query: String, val filters: FilterList) : SourcePagingSource(source) { override suspend fun requestNextPage(currentPage: Int): MangasPage { - return source.getSearchManga(currentPage, query, filters) + return source.getMangaList(query, filters, currentPage) } } class SourcePopularPagingSource(source: CatalogueSource) : SourcePagingSource(source) { override suspend fun requestNextPage(currentPage: Int): MangasPage { - return source.getPopularManga(currentPage) + return source.getDefaultMangaList(currentPage) } } class SourceLatestPagingSource(source: CatalogueSource) : SourcePagingSource(source) { override suspend fun requestNextPage(currentPage: Int): MangasPage { - return source.getLatestUpdates(currentPage) + return source.getLatestMangaList(currentPage) } } diff --git a/data/src/main/java/tachiyomi/data/source/SourceRepositoryImpl.kt b/data/src/main/java/tachiyomi/data/source/SourceRepositoryImpl.kt index f2b2e0e057..dfdd645d5b 100644 --- a/data/src/main/java/tachiyomi/data/source/SourceRepositoryImpl.kt +++ b/data/src/main/java/tachiyomi/data/source/SourceRepositoryImpl.kt @@ -24,7 +24,7 @@ class SourceRepositoryImpl( return sourceManager.catalogueSources.map { sources -> sources.map { mapSourceToDomainSource(it).copy( - supportsLatest = it.supportsLatest, + supportsLatest = it.hasLatestListing, ) } } @@ -89,7 +89,7 @@ class SourceRepositoryImpl( private fun mapSourceToDomainSource(source: Source): DomainSource = DomainSource( id = source.id, - lang = source.lang, + lang = source.language, name = source.name, supportsLatest = false, isStub = false, diff --git a/data/src/main/java/tachiyomi/data/source/StubSourceRepositoryImpl.kt b/data/src/main/java/tachiyomi/data/source/StubSourceRepositoryImpl.kt index 157e1eb2f1..ebf3a739f6 100644 --- a/data/src/main/java/tachiyomi/data/source/StubSourceRepositoryImpl.kt +++ b/data/src/main/java/tachiyomi/data/source/StubSourceRepositoryImpl.kt @@ -25,5 +25,5 @@ class StubSourceRepositoryImpl( id: Long, lang: String, name: String, - ): StubSource = StubSource(id = id, lang = lang, name = name) + ): StubSource = StubSource(id = id, name = name, language = lang) } diff --git a/domain/src/main/java/tachiyomi/domain/source/model/StubSource.kt b/domain/src/main/java/tachiyomi/domain/source/model/StubSource.kt index 0fee2af183..e4fb12ac22 100644 --- a/domain/src/main/java/tachiyomi/domain/source/model/StubSource.kt +++ b/domain/src/main/java/tachiyomi/domain/source/model/StubSource.kt @@ -1,32 +1,46 @@ package tachiyomi.domain.source.model import eu.kanade.tachiyomi.source.Source +import eu.kanade.tachiyomi.source.model.FilterList +import eu.kanade.tachiyomi.source.model.MangasPage import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SManga class StubSource( override val id: Long, - override val lang: String, override val name: String, + override val language: String, ) : Source { - private val isInvalid: Boolean = name.isBlank() || lang.isBlank() + private val isInvalid: Boolean = name.isBlank() || language.isBlank() - override suspend fun getMangaDetails(manga: SManga): SManga = - throw SourceNotInstalledException() + override val hasSearchFilters: Boolean get() = throw SourceNotInstalledException() - override suspend fun getChapterList(manga: SManga): List = - throw SourceNotInstalledException() - override suspend fun getPageList(chapter: SChapter): List = + override val hasLatestListing: Boolean get() = throw SourceNotInstalledException() + + override suspend fun getSearchFilters(): FilterList = throw SourceNotInstalledException() + + override suspend fun getDefaultMangaList(page: Int): MangasPage = throw SourceNotInstalledException() + + override suspend fun getLatestMangaList(page: Int): MangasPage = throw SourceNotInstalledException() + + override suspend fun getMangaList(query: String, filters: FilterList, page: Int): MangasPage = throw SourceNotInstalledException() - override fun toString(): String = - if (!isInvalid) "$name (${lang.uppercase()})" else id.toString() + override suspend fun getMangaDetails( + manga: SManga, + updateManga: Boolean, + fetchChapters: Boolean, + ): Pair> = throw SourceNotInstalledException() + + override suspend fun getPageList(chapter: SChapter): List = throw SourceNotInstalledException() + + override fun toString(): String = if (!isInvalid) "$name (${language.uppercase()})" else id.toString() companion object { fun from(source: Source): StubSource { - return StubSource(id = source.id, lang = source.lang, name = source.name) + return StubSource(id = source.id, name = source.name, language = source.language) } } } diff --git a/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/CatalogueSource.kt b/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/CatalogueSource.kt index 9be6735411..5dc2dc402e 100644 --- a/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/CatalogueSource.kt +++ b/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/CatalogueSource.kt @@ -10,12 +10,16 @@ interface CatalogueSource : Source { /** * An ISO 639-1 compliant language code (two letters in lower case). */ - override val lang: String + val lang: String get() = throw UnsupportedOperationException() + + override val language: String get() = lang /** * Whether the source has support for latest updates. */ - val supportsLatest: Boolean + val supportsLatest: Boolean get() = throw UnsupportedOperationException() + + override val hasLatestListing: Boolean get() = supportsLatest /** * Get a page with a list of manga. @@ -24,32 +28,32 @@ interface CatalogueSource : Source { * @param page the page number to retrieve. */ @Suppress("DEPRECATION") - suspend fun getPopularManga(page: Int): MangasPage { + override suspend fun getDefaultMangaList(page: Int): MangasPage { return fetchPopularManga(page).awaitSingle() } /** - * Get a page with a list of manga. + * Get a page with a list of latest manga updates. * * @since extensions-lib 1.5 * @param page the page number to retrieve. - * @param query the search query. - * @param filters the list of filters to apply. */ @Suppress("DEPRECATION") - suspend fun getSearchManga(page: Int, query: String, filters: FilterList): MangasPage { - return fetchSearchManga(page, query, filters).awaitSingle() + override suspend fun getLatestMangaList(page: Int): MangasPage { + return fetchLatestUpdates(page).awaitSingle() } /** - * Get a page with a list of latest manga updates. + * Get a page with a list of manga. * * @since extensions-lib 1.5 * @param page the page number to retrieve. + * @param query the search query. + * @param filters the list of filters to apply. */ @Suppress("DEPRECATION") - suspend fun getLatestUpdates(page: Int): MangasPage { - return fetchLatestUpdates(page).awaitSingle() + override suspend fun getMangaList(query: String, filters: FilterList, page: Int): MangasPage { + return fetchSearchManga(page, query, filters).awaitSingle() } /** diff --git a/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/Source.kt b/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/Source.kt index 83fcd79624..38a9a8081e 100644 --- a/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/Source.kt +++ b/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/Source.kt @@ -1,9 +1,13 @@ package eu.kanade.tachiyomi.source +import eu.kanade.tachiyomi.source.model.FilterList +import eu.kanade.tachiyomi.source.model.MangasPage import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.util.awaitSingle +import kotlinx.coroutines.async +import kotlinx.coroutines.supervisorScope import rx.Observable /** @@ -12,7 +16,7 @@ import rx.Observable interface Source { /** - * ID for the source. Must be unique. + * Id for the source. Must be unique. */ val id: Long @@ -21,8 +25,131 @@ interface Source { */ val name: String - val lang: String - get() = "" + /** + * Represents an IETF BCP 47 compliant language tag. + * Special cases include: + * - [Language.MULTI]: Indicates multiple languages. + * - [Language.OTHER]: Refers to a language not explicitly defined by the source (e.g. text less, unofficially supported language). + * - 'all': Indicates multiple language. + * + * Usage of 'all' is highly discouraged and is only supported due to legacy reasons. + * + * @since extensions-lib 1.6 + */ + val language: String + + /** + * Indicates if the source supports search filters + * + * @since extensions-lib 1.6 + */ + val hasSearchFilters: Boolean + + /** + * Whether the source has a list for latest updates + * + * @since extensions-lib 1.6 + */ + val hasLatestListing: Boolean + + /** + * Returns the list of filters for the source. + * + * @since extensions-lib 1.6 + */ + suspend fun getSearchFilters(): FilterList + + /** + * Get a page with a list of manga. + * + * @since extensions-lib 1.6 + * + * @param page the page number to retrieve. + */ + suspend fun getDefaultMangaList(page: Int): MangasPage + + /** + * Get a page with a list of latest manga updates. + * + * @since extensions-lib 1.6 + * + * @param page the page number to retrieve. + */ + suspend fun getLatestMangaList(page: Int): MangasPage = throw UnsupportedOperationException() + + /** + * Get a page with a list of manga. + * + * @since extensions-lib 1.6 + * + * @param query the search query. + * @param filters the list of filters to apply. + * @param page the page number to retrieve. + */ + suspend fun getMangaList(query: String, filters: FilterList, page: Int): MangasPage + + /** + * Get the updated details for a manga and its chapters + * + * @since extensions-lib 1.6 + * + * @param manga manga to get details and chapters for. + * @param updateManga whether to update the manga details or not + * @param fetchChapters whether to fetch chapters or not. + */ + suspend fun getMangaDetails( + manga: SManga, + updateManga: Boolean, + fetchChapters: Boolean, + ): Pair> { + return supervisorScope { + val mangaAsync = async { if (updateManga) fetchMangaDetails(manga).awaitSingle() else manga } + val chaptersAsync = async { if (fetchChapters) fetchChapterList(manga).awaitSingle() else emptyList() } + mangaAsync.await() to chaptersAsync.await() + } + } + + /** + * Get the list of pages a chapter has. Pages should be returned + * in the expected order; the index is ignored. + * + * @since extensions-lib 1.6 + * + * @param chapter the chapter. + */ + suspend fun getPageList(chapter: SChapter): List = throw RuntimeException("Stub!") + + override fun toString(): String + + /** + * Object for holding the special cases supported by [Source.language]. + * + * @since extensions-lib 1.6 + */ + object Language { + /** + * Indicates multiple languages. + * + * @since extensions-lib 1.6 + */ + const val MULTI = "multi" + + /** + * Refers to a language not explicitly defined by the source (e.g. text less, unofficially supported language) + * + * @since extensions-lib 1.6 + */ + const val OTHER = "other" + } + + @Deprecated("Use the new suspend API instead", ReplaceWith("getMangaDetails(manga, true, false)")) + fun fetchMangaDetails(manga: SManga): Observable = throw UnsupportedOperationException() + + @Deprecated("Use the new suspend API instead", ReplaceWith("getMangaDetails(manga, false, true)")) + fun fetchChapterList(manga: SManga): Observable> = throw UnsupportedOperationException() + + @Deprecated("Use the new suspend API instead", ReplaceWith("getPageList")) + fun fetchPageList(chapter: SChapter): Observable> = throw UnsupportedOperationException() /** * Get the updated details for a manga. @@ -47,38 +174,4 @@ interface Source { suspend fun getChapterList(manga: SManga): List { return fetchChapterList(manga).awaitSingle() } - - /** - * Get the list of pages a chapter has. Pages should be returned - * in the expected order; the index is ignored. - * - * @since extensions-lib 1.5 - * @param chapter the chapter. - * @return the pages for the chapter. - */ - @Suppress("DEPRECATION") - suspend fun getPageList(chapter: SChapter): List { - return fetchPageList(chapter).awaitSingle() - } - - @Deprecated( - "Use the non-RxJava API instead", - ReplaceWith("getMangaDetails"), - ) - fun fetchMangaDetails(manga: SManga): Observable = - throw IllegalStateException("Not used") - - @Deprecated( - "Use the non-RxJava API instead", - ReplaceWith("getChapterList"), - ) - fun fetchChapterList(manga: SManga): Observable> = - throw IllegalStateException("Not used") - - @Deprecated( - "Use the non-RxJava API instead", - ReplaceWith("getPageList"), - ) - fun fetchPageList(chapter: SChapter): Observable> = - throw IllegalStateException("Not used") } diff --git a/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/online/HttpSource.kt b/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/online/HttpSource.kt index 712b281b31..19e1aafe34 100644 --- a/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/online/HttpSource.kt +++ b/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/online/HttpSource.kt @@ -54,7 +54,7 @@ abstract class HttpSource : CatalogueSource { * * Note: the generated ID sets the sign bit to `0`. */ - override val id by lazy { generateId(name, lang, versionId) } + override val id by lazy { generateId(name, language, versionId) } /** * Headers used for requests. @@ -100,7 +100,7 @@ abstract class HttpSource : CatalogueSource { /** * Visible name of the source. */ - override fun toString() = "$name (${lang.uppercase()})" + override fun toString() = "$name (${language.uppercase()})" /** * Returns an observable containing a page with a list of manga. Normally it's not needed to diff --git a/source-local/src/androidMain/kotlin/tachiyomi/source/local/LocalSource.kt b/source-local/src/androidMain/kotlin/tachiyomi/source/local/LocalSource.kt index 17fb4492a5..84b51c72b1 100644 --- a/source-local/src/androidMain/kotlin/tachiyomi/source/local/LocalSource.kt +++ b/source-local/src/androidMain/kotlin/tachiyomi/source/local/LocalSource.kt @@ -13,6 +13,7 @@ import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.supervisorScope import kotlinx.serialization.json.Json import kotlinx.serialization.json.decodeFromStream import logcat.LogPriority @@ -65,18 +66,18 @@ actual class LocalSource( override val id: Long = ID - override val lang: String = "other" + override val language: String = Source.Language.OTHER override fun toString() = name - override val supportsLatest: Boolean = true + override val hasLatestListing: Boolean = true // Browse related - override suspend fun getPopularManga(page: Int) = getSearchManga(page, "", PopularFilters) + override suspend fun getDefaultMangaList(page: Int) = getMangaList("", PopularFilters, page) - override suspend fun getLatestUpdates(page: Int) = getSearchManga(page, "", LatestFilters) + override suspend fun getLatestMangaList(page: Int) = getMangaList("", LatestFilters, page) - override suspend fun getSearchManga(page: Int, query: String, filters: FilterList): MangasPage = withIOContext { + override suspend fun getMangaList(query: String, filters: FilterList, page: Int): MangasPage = withIOContext { val lastModifiedLimit = if (filters === LatestFilters) { System.currentTimeMillis() - LATEST_THRESHOLD } else { @@ -138,8 +139,20 @@ actual class LocalSource( MangasPage(mangas, false) } + override suspend fun getMangaDetails( + manga: SManga, + updateManga: Boolean, + fetchChapters: Boolean, + ): Pair> { + return supervisorScope { + val mangaAsync = async { if (updateManga) getMangaDetails(manga) else manga } + val chaptersAsync = async { if (fetchChapters) getChapterList(manga) else emptyList() } + mangaAsync.await() to chaptersAsync.await() + } + } + // Manga details related - override suspend fun getMangaDetails(manga: SManga): SManga = withIOContext { + suspend fun getMangaDetails(manga: SManga): SManga = withIOContext { coverManager.find(manga.url)?.let { manga.thumbnail_url = it.uri.toString() } @@ -234,7 +247,7 @@ actual class LocalSource( } // Chapters - override suspend fun getChapterList(manga: SManga): List = withIOContext { + suspend fun getChapterList(manga: SManga): List = withIOContext { val chapters = fileSystem.getFilesInMangaDirectory(manga.url) // Only keep supported formats .filter { it.isDirectory || Archive.isSupported(it) || it.extension.equals("epub", true) } From 790d016f1906cede6913bd6260d1dd994e704a41 Mon Sep 17 00:00:00 2001 From: AntsyLich <59261191+AntsyLich@users.noreply.github.com> Date: Mon, 27 Jan 2025 05:51:13 +0600 Subject: [PATCH 07/10] Implement new Source API (getMangaDetails) --- .../data/library/LibraryUpdateJob.kt | 14 +-- .../data/library/MetadataUpdateJob.kt | 1 + .../browse/migration/search/MigrateDialog.kt | 1 + .../ui/deeplink/DeepLinkScreenModel.kt | 1 + .../kanade/tachiyomi/ui/manga/MangaScreen.kt | 2 +- .../tachiyomi/ui/manga/MangaScreenModel.kt | 98 +++++++------------ .../eu/kanade/tachiyomi/source/Source.kt | 28 +----- 7 files changed, 53 insertions(+), 92 deletions(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateJob.kt b/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateJob.kt index 6f3cfe0d8a..42f326b206 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateJob.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateJob.kt @@ -337,13 +337,15 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet private suspend fun updateManga(manga: Manga, fetchWindow: Pair): List { val source = sourceManager.getOrStub(manga.source) - // Update manga metadata if needed - if (libraryPreferences.autoUpdateMetadata().get()) { - val networkManga = source.getMangaDetails(manga.toSManga()) - updateManga.awaitUpdateFromSource(manga, networkManga, manualFetch = false, coverCache) - } + val fetchManga = libraryPreferences.autoUpdateMetadata().get() + + val (networkManga, chapters) = source.getMangaDetails( + manga = manga.toSManga(), + updateManga = fetchManga, + fetchChapters = true, + ) - val chapters = source.getChapterList(manga.toSManga()) + if (fetchManga) updateManga.awaitUpdateFromSource(manga, networkManga, manualFetch = false, coverCache) // Get manga from database to account for if it was removed during the update and // to get latest data so it doesn't get overwritten later on diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/library/MetadataUpdateJob.kt b/app/src/main/java/eu/kanade/tachiyomi/data/library/MetadataUpdateJob.kt index c6401ef728..94a7f59a1f 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/library/MetadataUpdateJob.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/library/MetadataUpdateJob.kt @@ -15,6 +15,7 @@ import eu.kanade.domain.manga.model.copyFrom import eu.kanade.domain.manga.model.toSManga import eu.kanade.tachiyomi.data.cache.CoverCache import eu.kanade.tachiyomi.data.notification.Notifications +import eu.kanade.tachiyomi.source.getMangaDetails import eu.kanade.tachiyomi.util.prepUpdateCover import eu.kanade.tachiyomi.util.system.isRunning import eu.kanade.tachiyomi.util.system.setForegroundSafely diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/MigrateDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/MigrateDialog.kt index d3b35bcf42..4537be0bd2 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/MigrateDialog.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/MigrateDialog.kt @@ -29,6 +29,7 @@ import eu.kanade.tachiyomi.data.download.DownloadManager import eu.kanade.tachiyomi.data.track.EnhancedTracker import eu.kanade.tachiyomi.data.track.TrackerManager import eu.kanade.tachiyomi.source.Source +import eu.kanade.tachiyomi.source.getChapterList import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.ui.browse.migration.MigrationFlags import kotlinx.coroutines.flow.update diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/deeplink/DeepLinkScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/deeplink/DeepLinkScreenModel.kt index 5bef14675b..e72205e32b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/deeplink/DeepLinkScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/deeplink/DeepLinkScreenModel.kt @@ -7,6 +7,7 @@ import eu.kanade.domain.chapter.interactor.SyncChaptersWithSource import eu.kanade.domain.manga.model.toDomainManga import eu.kanade.domain.manga.model.toSManga import eu.kanade.tachiyomi.source.Source +import eu.kanade.tachiyomi.source.getChapterList import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.online.ResolvableSource diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreen.kt index 9d71a8f511..1aee2c9bc9 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreen.kt @@ -151,7 +151,7 @@ class MangaScreen( }, onTagSearch = { scope.launch { performGenreSearch(navigator, it, screenModel.source!!) } }, onFilterButtonClicked = screenModel::showSettingsDialog, - onRefresh = screenModel::fetchAllFromSource, + onRefresh = screenModel::refresh, onContinueReading = { continueReading(context, screenModel.getNextUnreadChapter()) }, onSearch = { query, global -> scope.launch { performSearch(navigator, query, global) } }, onCoverClicked = screenModel::showCoverDialog, diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreenModel.kt index 53665552db..3ba026ca9f 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreenModel.kt @@ -35,7 +35,6 @@ import eu.kanade.tachiyomi.data.download.DownloadManager import eu.kanade.tachiyomi.data.download.model.Download import eu.kanade.tachiyomi.data.track.EnhancedTracker import eu.kanade.tachiyomi.data.track.TrackerManager -import eu.kanade.tachiyomi.network.HttpException import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences import eu.kanade.tachiyomi.util.chapter.getNextUnread @@ -43,8 +42,6 @@ import eu.kanade.tachiyomi.util.removeCovers import eu.kanade.tachiyomi.util.system.toast import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList -import kotlinx.coroutines.async -import kotlinx.coroutines.awaitAll import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.combine @@ -236,11 +233,11 @@ class MangaScreenModel( // Fetch info-chapters when needed if (screenModelScope.isActive) { - val fetchFromSourceTasks = listOf( - async { if (needRefreshInfo) fetchMangaFromSource() }, - async { if (needRefreshChapter) fetchChaptersFromSource() }, + fetchAllFromSource( + fetchManga = needRefreshInfo, + fetchChapters = needRefreshChapter, + manualFetch = false, ) - fetchFromSourceTasks.awaitAll() } // Initial loading finished @@ -248,41 +245,56 @@ class MangaScreenModel( } } - fun fetchAllFromSource(manualFetch: Boolean = true) { + fun refresh() { screenModelScope.launch { updateSuccessState { it.copy(isRefreshingData = true) } - val fetchFromSourceTasks = listOf( - async { fetchMangaFromSource(manualFetch) }, - async { fetchChaptersFromSource(manualFetch) }, - ) - fetchFromSourceTasks.awaitAll() + fetchAllFromSource() updateSuccessState { it.copy(isRefreshingData = false) } } } - // Manga info - start - - /** - * Fetch manga information from source. - */ - private suspend fun fetchMangaFromSource(manualFetch: Boolean = false) { + private suspend fun fetchAllFromSource( + fetchManga: Boolean = true, + fetchChapters: Boolean = true, + manualFetch: Boolean = true, + ) { val state = successState ?: return try { withIOContext { - val networkManga = state.source.getMangaDetails(state.manga.toSManga()) + val (networkManga, chapters) = state.source.getMangaDetails( + manga = state.manga.toSManga(), + updateManga = fetchManga, + fetchChapters = fetchChapters, + ) + updateManga.awaitUpdateFromSource(state.manga, networkManga, manualFetch) + + val newChapters = syncChaptersWithSource.await( + chapters, + state.manga, + state.source, + manualFetch, + ) + + if (manualFetch) { + downloadNewChapters(newChapters) + } + } + } catch (e: Exception) { + val message = if (e is NoChaptersException) { + context.stringResource(MR.strings.no_chapters_error) + } else { + logcat(LogPriority.ERROR, e) + with(context) { e.formattedMessage } } - } catch (e: Throwable) { - // Ignore early hints "errors" that aren't handled by OkHttp - if (e is HttpException && e.code == 103) return - logcat(LogPriority.ERROR, e) screenModelScope.launch { - snackbarHostState.showSnackbar(message = with(context) { e.formattedMessage }) + snackbarHostState.showSnackbar(message = message) } } } + // Manga info - start fun toggleFavorite() { toggleFavorite( onRemoved = { @@ -541,42 +553,6 @@ class MangaScreenModel( } } - /** - * Requests an updated list of chapters from the source. - */ - private suspend fun fetchChaptersFromSource(manualFetch: Boolean = false) { - val state = successState ?: return - try { - withIOContext { - val chapters = state.source.getChapterList(state.manga.toSManga()) - - val newChapters = syncChaptersWithSource.await( - chapters, - state.manga, - state.source, - manualFetch, - ) - - if (manualFetch) { - downloadNewChapters(newChapters) - } - } - } catch (e: Throwable) { - val message = if (e is NoChaptersException) { - context.stringResource(MR.strings.no_chapters_error) - } else { - logcat(LogPriority.ERROR, e) - with(context) { e.formattedMessage } - } - - screenModelScope.launch { - snackbarHostState.showSnackbar(message = message) - } - val newManga = mangaRepository.getMangaById(mangaId) - updateSuccessState { it.copy(manga = newManga, isRefreshingData = false) } - } - } - /** * @throws IllegalStateException if the swipe action is [LibraryPreferences.ChapterSwipeAction.Disabled] */ diff --git a/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/Source.kt b/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/Source.kt index 38a9a8081e..0a90f69147 100644 --- a/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/Source.kt +++ b/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/Source.kt @@ -97,6 +97,7 @@ interface Source { * @param updateManga whether to update the manga details or not * @param fetchChapters whether to fetch chapters or not. */ + @Suppress("DEPRECATION") suspend fun getMangaDetails( manga: SManga, updateManga: Boolean, @@ -150,28 +151,7 @@ interface Source { @Deprecated("Use the new suspend API instead", ReplaceWith("getPageList")) fun fetchPageList(chapter: SChapter): Observable> = throw UnsupportedOperationException() - - /** - * Get the updated details for a manga. - * - * @since extensions-lib 1.5 - * @param manga the manga to update. - * @return the updated manga. - */ - @Suppress("DEPRECATION") - suspend fun getMangaDetails(manga: SManga): SManga { - return fetchMangaDetails(manga).awaitSingle() - } - - /** - * 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 { - return fetchChapterList(manga).awaitSingle() - } } + +suspend fun Source.getMangaDetails(manga: SManga): SManga = getMangaDetails(manga, true, false).first +suspend fun Source.getChapterList(manga: SManga): List = getMangaDetails(manga, false, true).second From ad8044247e75f00cea1c2134e2f477c70c016842 Mon Sep 17 00:00:00 2001 From: AntsyLich <59261191+AntsyLich@users.noreply.github.com> Date: Mon, 27 Jan 2025 11:44:13 +0600 Subject: [PATCH 08/10] Implement new Source API (filters) --- .../source/browse/BrowseSourceScreen.kt | 2 +- .../source/browse/BrowseSourceScreenModel.kt | 49 ++++++++----------- .../source/globalsearch/SearchScreenModel.kt | 2 +- .../eu/kanade/tachiyomi/source/Source.kt | 3 ++ 4 files changed, 26 insertions(+), 30 deletions(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreen.kt index 01e273efe5..5731ebc406 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreen.kt @@ -181,7 +181,7 @@ data class BrowseSourceScreen( }, ) } - if (state.filters.isNotEmpty()) { + if (state.hasFilters) { FilterChip( selected = state.listing is Listing.Search, onClick = screenModel::openFilterSheet, diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreenModel.kt index e3da069fb9..81be009594 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreenModel.kt @@ -21,7 +21,6 @@ import eu.kanade.domain.source.service.SourcePreferences import eu.kanade.domain.track.interactor.AddTracks import eu.kanade.presentation.util.ioCoroutineScope import eu.kanade.tachiyomi.data.cache.CoverCache -import eu.kanade.tachiyomi.source.CatalogueSource import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.util.removeCovers import kotlinx.collections.immutable.ImmutableList @@ -71,7 +70,7 @@ class BrowseSourceScreenModel( private val networkToLocalManga: NetworkToLocalManga = Injekt.get(), private val updateManga: UpdateManga = Injekt.get(), private val addTracks: AddTracks = Injekt.get(), - private val getIncognitoState: GetIncognitoState = Injekt.get(), + getIncognitoState: GetIncognitoState = Injekt.get(), ) : StateScreenModel(State(Listing.valueOf(listingQuery))) { var displayMode by sourcePreferences.sourceDisplayMode().asState(screenModelScope) @@ -79,20 +78,21 @@ class BrowseSourceScreenModel( val source = sourceManager.getOrStub(sourceId) init { - if (source is CatalogueSource) { + screenModelScope.launchIO { mutableState.update { - var query: String? = null - var listing = it.listing - - if (listing is Listing.Search) { - query = listing.query - listing = Listing.Search(query, source.getFilterList()) - } + val hasFilters = source.hasSearchFilters + val filters = if (hasFilters) source.getSearchFilters() else FilterList() it.copy( - listing = listing, - filters = source.getFilterList(), - toolbarQuery = query, + listing = if (it.listing is Listing.Search) { + it.listing.copy(filters = filters) + } else { + it.listing + }, + hasFilters = hasFilters, + filters = filters, + defaultFilters = filters, + toolbarQuery = it.listing.query, ) } } @@ -135,9 +135,7 @@ class BrowseSourceScreenModel( } fun resetFilters() { - if (source !is CatalogueSource) return - - mutableState.update { it.copy(filters = source.getFilterList()) } + mutableState.update { it.copy(filters = it.defaultFilters) } } fun setListing(listing: Listing) { @@ -145,20 +143,15 @@ class BrowseSourceScreenModel( } fun setFilters(filters: FilterList) { - if (source !is CatalogueSource) return - mutableState.update { - it.copy( - filters = filters, - ) + it.copy(filters = filters) } } fun search(query: String? = null, filters: FilterList? = null) { - if (source !is CatalogueSource) return - - val input = state.value.listing as? Listing.Search - ?: Listing.Search(query = null, filters = source.getFilterList()) + val input = state.value.let { state -> + (state.listing as? Listing.Search) ?: Listing.Search(query = null, filters = state.defaultFilters) + } mutableState.update { it.copy( @@ -172,9 +165,7 @@ class BrowseSourceScreenModel( } fun searchGenre(genreName: String) { - if (source !is CatalogueSource) return - - val defaultFilters = source.getFilterList() + val defaultFilters = state.value.defaultFilters var genreExists = false filter@ for (sourceFilter in defaultFilters) { @@ -351,7 +342,9 @@ class BrowseSourceScreenModel( @Immutable data class State( val listing: Listing, + val hasFilters: Boolean = false, val filters: FilterList = FilterList(), + val defaultFilters: FilterList = FilterList(), val toolbarQuery: String? = null, val dialog: Dialog? = null, ) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/SearchScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/SearchScreenModel.kt index 6b46909e03..241ad9ad68 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/SearchScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/SearchScreenModel.kt @@ -162,7 +162,7 @@ abstract class SearchScreenModel( try { val page = withContext(coroutineDispatcher) { - source.getMangaList(query, source.getFilterList(), 1) + source.getMangaList(query, source.getSearchFilters(), 1) } val titles = page.mangas.map { diff --git a/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/Source.kt b/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/Source.kt index 0a90f69147..c8c097d1ae 100644 --- a/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/Source.kt +++ b/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/Source.kt @@ -153,5 +153,8 @@ interface Source { fun fetchPageList(chapter: SChapter): Observable> = throw UnsupportedOperationException() } +@Suppress("BooleanLiteralArgument") suspend fun Source.getMangaDetails(manga: SManga): SManga = getMangaDetails(manga, true, false).first + +@Suppress("BooleanLiteralArgument") suspend fun Source.getChapterList(manga: SManga): List = getMangaDetails(manga, false, true).second From d5de7a7d566a963e4b714a54f39dc865f412ca37 Mon Sep 17 00:00:00 2001 From: AntsyLich <59261191+AntsyLich@users.noreply.github.com> Date: Mon, 27 Jan 2025 12:04:43 +0600 Subject: [PATCH 09/10] Remove all usage of 'CatalogueSource' --- .../presentation/browse/GlobalSearchScreen.kt | 8 +- .../browse/MigrateSearchScreen.kt | 4 +- .../settings/screen/SettingsTrackingScreen.kt | 2 +- .../creators/PreferenceBackupCreator.kt | 2 +- .../extension/util/ExtensionLoader.kt | 3 +- .../tachiyomi/source/AndroidSourceManager.kt | 12 +-- .../search/MigrateSearchScreenModel.kt | 4 +- .../globalsearch/GlobalSearchScreenModel.kt | 4 +- .../source/globalsearch/SearchScreenModel.kt | 19 ++-- .../ui/deeplink/DeepLinkScreenModel.kt | 2 +- .../data/source/SourcePagingSource.kt | 10 +- .../data/source/SourceRepositoryImpl.kt | 13 ++- .../domain/source/service/SourceManager.kt | 9 +- .../tachiyomi/source/CatalogueSource.kt | 93 +++++++++---------- 14 files changed, 88 insertions(+), 97 deletions(-) diff --git a/app/src/main/java/eu/kanade/presentation/browse/GlobalSearchScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/GlobalSearchScreen.kt index ba905bb3fd..9fa58be5b5 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/GlobalSearchScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/GlobalSearchScreen.kt @@ -10,7 +10,7 @@ import eu.kanade.presentation.browse.components.GlobalSearchErrorResultItem import eu.kanade.presentation.browse.components.GlobalSearchLoadingResultItem import eu.kanade.presentation.browse.components.GlobalSearchResultItem import eu.kanade.presentation.browse.components.GlobalSearchToolbar -import eu.kanade.tachiyomi.source.CatalogueSource +import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SearchItemResult import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SearchScreenModel import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SourceFilter @@ -27,7 +27,7 @@ fun GlobalSearchScreen( onChangeSearchFilter: (SourceFilter) -> Unit, onToggleResults: () -> Unit, getManga: @Composable (Manga) -> State, - onClickSource: (CatalogueSource) -> Unit, + onClickSource: (Source) -> Unit, onClickItem: (Manga) -> Unit, onLongClickItem: (Manga) -> Unit, ) { @@ -61,10 +61,10 @@ fun GlobalSearchScreen( @Composable internal fun GlobalSearchContent( - items: Map, + items: Map, contentPadding: PaddingValues, getManga: @Composable (Manga) -> State, - onClickSource: (CatalogueSource) -> Unit, + onClickSource: (Source) -> Unit, onClickItem: (Manga) -> Unit, onLongClickItem: (Manga) -> Unit, fromSourceId: Long? = null, diff --git a/app/src/main/java/eu/kanade/presentation/browse/MigrateSearchScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/MigrateSearchScreen.kt index 31abb596c7..a7d1f8f189 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/MigrateSearchScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/MigrateSearchScreen.kt @@ -3,7 +3,7 @@ package eu.kanade.presentation.browse import androidx.compose.runtime.Composable import androidx.compose.runtime.State import eu.kanade.presentation.browse.components.GlobalSearchToolbar -import eu.kanade.tachiyomi.source.CatalogueSource +import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SearchScreenModel import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SourceFilter import tachiyomi.domain.manga.model.Manga @@ -19,7 +19,7 @@ fun MigrateSearchScreen( onChangeSearchFilter: (SourceFilter) -> Unit, onToggleResults: () -> Unit, getManga: @Composable (Manga) -> State, - onClickSource: (CatalogueSource) -> Unit, + onClickSource: (Source) -> Unit, onClickItem: (Manga) -> Unit, onLongClickItem: (Manga) -> Unit, ) { diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsTrackingScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsTrackingScreen.kt index 2a7231112b..ebee2b5485 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsTrackingScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsTrackingScreen.kt @@ -112,7 +112,7 @@ object SettingsTrackingScreen : SearchableSettings { .filter { it is EnhancedTracker } .partition { service -> val acceptedSources = (service as EnhancedTracker).getAcceptedSources() - sourceManager.getCatalogueSources().any { it::class.qualifiedName in acceptedSources } + sourceManager.getSources().any { it::class.qualifiedName in acceptedSources } } var enhancedTrackerInfo = stringResource(MR.strings.enhanced_tracking_info) if (enhancedTrackers.second.isNotEmpty()) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/creators/PreferenceBackupCreator.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/creators/PreferenceBackupCreator.kt index 5573dc23e9..e2da24ab53 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/creators/PreferenceBackupCreator.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/creators/PreferenceBackupCreator.kt @@ -28,7 +28,7 @@ class PreferenceBackupCreator( } fun createSource(includePrivatePreferences: Boolean): List { - return sourceManager.getCatalogueSources() + return sourceManager.getSources() .filterIsInstance() .map { BackupSourcePreferences( diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionLoader.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionLoader.kt index 9b247137ff..a2eea4fc5d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionLoader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionLoader.kt @@ -10,7 +10,6 @@ import eu.kanade.domain.extension.interactor.TrustExtension import eu.kanade.domain.source.service.SourcePreferences import eu.kanade.tachiyomi.extension.model.Extension import eu.kanade.tachiyomi.extension.model.LoadResult -import eu.kanade.tachiyomi.source.CatalogueSource import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.SourceFactory import eu.kanade.tachiyomi.util.lang.Hash @@ -301,7 +300,7 @@ internal object ExtensionLoader { } } - val langs = sources.filterIsInstance() + val langs = sources .map { it.language } .toSet() val lang = when (langs.size) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/AndroidSourceManager.kt b/app/src/main/java/eu/kanade/tachiyomi/source/AndroidSourceManager.kt index 1114dac2af..1c0287d500 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/source/AndroidSourceManager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/source/AndroidSourceManager.kt @@ -41,9 +41,7 @@ class AndroidSourceManager( private val stubSourcesMap = ConcurrentHashMap() - override val catalogueSources: Flow> = sourcesMapFlow.map { - it.values.filterIsInstance() - } + override val sources: Flow> = sourcesMapFlow.map { it.values.toList() } init { scope.launch { @@ -90,15 +88,15 @@ class AndroidSourceManager( } } - override fun getOnlineSources() = sourcesMapFlow.value.values.filterIsInstance() - - override fun getCatalogueSources() = sourcesMapFlow.value.values.filterIsInstance() + override fun getSources(): List = sourcesMapFlow.value.values.toList() override fun getStubSources(): List { val onlineSourceIds = getOnlineSources().map { it.id } return stubSourcesMap.values.filterNot { it.id in onlineSourceIds } } + override fun getOnlineSources() = sourcesMapFlow.value.values.filterIsInstance() + private fun registerStubSource(source: StubSource) { scope.launch { val dbSource = sourceRepository.getStubSource(source.id) @@ -118,6 +116,6 @@ class AndroidSourceManager( registerStubSource(it) return it } - return StubSource(id = id, lang = "", name = "") + return StubSource(id = id, name = "", language = "") } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/MigrateSearchScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/MigrateSearchScreenModel.kt index 83e5d523db..39c8d2193e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/MigrateSearchScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/MigrateSearchScreenModel.kt @@ -1,7 +1,7 @@ package eu.kanade.tachiyomi.ui.browse.migration.search import cafe.adriel.voyager.core.model.screenModelScope -import eu.kanade.tachiyomi.source.CatalogueSource +import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SearchScreenModel import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SourceFilter import kotlinx.coroutines.flow.update @@ -28,7 +28,7 @@ class MigrateSearchScreenModel( } } - override fun getEnabledSources(): List { + override fun getEnabledSources(): List { return super.getEnabledSources() .filter { state.value.sourceFilter != SourceFilter.PinnedOnly || "${it.id}" in pinnedSources } .sortedWith( diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchScreenModel.kt index 33c7157681..161fbc3601 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchScreenModel.kt @@ -1,6 +1,6 @@ package eu.kanade.tachiyomi.ui.browse.source.globalsearch -import eu.kanade.tachiyomi.source.CatalogueSource +import eu.kanade.tachiyomi.source.Source class GlobalSearchScreenModel( initialQuery: String = "", @@ -18,7 +18,7 @@ class GlobalSearchScreenModel( } } - override fun getEnabledSources(): List { + override fun getEnabledSources(): List { return super.getEnabledSources() .filter { state.value.sourceFilter != SourceFilter.PinnedOnly || "${it.id}" in pinnedSources } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/SearchScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/SearchScreenModel.kt index 241ad9ad68..e47e60aa98 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/SearchScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/SearchScreenModel.kt @@ -9,7 +9,7 @@ import eu.kanade.domain.manga.model.toDomainManga import eu.kanade.domain.source.service.SourcePreferences import eu.kanade.presentation.util.ioCoroutineScope import eu.kanade.tachiyomi.extension.ExtensionManager -import eu.kanade.tachiyomi.source.CatalogueSource +import eu.kanade.tachiyomi.source.Source import kotlinx.collections.immutable.PersistentMap import kotlinx.collections.immutable.mutate import kotlinx.collections.immutable.persistentMapOf @@ -55,8 +55,8 @@ abstract class SearchScreenModel( protected var extensionFilter: String? = null - private val sortComparator = { map: Map -> - compareBy( + private val sortComparator = { map: Map -> + compareBy( { (map[it] as? SearchItemResult.Success)?.isEmpty ?: true }, { "${it.id}" !in pinnedSources }, { "${it.name.lowercase()} (${it.language})" }, @@ -82,8 +82,8 @@ abstract class SearchScreenModel( } } - open fun getEnabledSources(): List { - return sourceManager.getCatalogueSources() + open fun getEnabledSources(): List { + return sourceManager.getSources() .filter { it.language in enabledLanguages && "${it.id}" !in disabledSources } .sortedWith( compareBy( @@ -93,7 +93,7 @@ abstract class SearchScreenModel( ) } - private fun getSelectedSources(): List { + private fun getSelectedSources(): List { val enabledSources = getEnabledSources() val filter = extensionFilter @@ -104,7 +104,6 @@ abstract class SearchScreenModel( return extensionManager.installedExtensionsFlow.value .filter { it.pkgName == filter } .flatMap { it.sources } - .filterIsInstance() .filter { it in enabledSources } } @@ -183,7 +182,7 @@ abstract class SearchScreenModel( } } - private fun updateItems(items: PersistentMap) { + private fun updateItems(items: PersistentMap) { mutableState.update { it.copy( items = items @@ -193,7 +192,7 @@ abstract class SearchScreenModel( } } - private fun updateItem(source: CatalogueSource, result: SearchItemResult) { + private fun updateItem(source: Source, result: SearchItemResult) { val newItems = state.value.items.mutate { it[source] = result } @@ -206,7 +205,7 @@ abstract class SearchScreenModel( val searchQuery: String? = null, val sourceFilter: SourceFilter = SourceFilter.PinnedOnly, val onlyShowHasResults: Boolean = false, - val items: PersistentMap = persistentMapOf(), + val items: PersistentMap = persistentMapOf(), ) { val progress: Int = items.count { it.value !is SearchItemResult.Loading } val total: Int = items.size diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/deeplink/DeepLinkScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/deeplink/DeepLinkScreenModel.kt index e72205e32b..80bdded64c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/deeplink/DeepLinkScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/deeplink/DeepLinkScreenModel.kt @@ -34,7 +34,7 @@ class DeepLinkScreenModel( init { screenModelScope.launchIO { - val source = sourceManager.getCatalogueSources() + val source = sourceManager.getSources() .filterIsInstance() .firstOrNull { it.getUriType(query) != UriType.Unknown } diff --git a/data/src/main/java/tachiyomi/data/source/SourcePagingSource.kt b/data/src/main/java/tachiyomi/data/source/SourcePagingSource.kt index 89541e2fca..2f171df9fa 100644 --- a/data/src/main/java/tachiyomi/data/source/SourcePagingSource.kt +++ b/data/src/main/java/tachiyomi/data/source/SourcePagingSource.kt @@ -1,34 +1,34 @@ package tachiyomi.data.source import androidx.paging.PagingState -import eu.kanade.tachiyomi.source.CatalogueSource +import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.MangasPage import eu.kanade.tachiyomi.source.model.SManga import tachiyomi.core.common.util.lang.withIOContext import tachiyomi.domain.source.repository.SourcePagingSourceType -class SourceSearchPagingSource(source: CatalogueSource, val query: String, val filters: FilterList) : +class SourceSearchPagingSource(source: Source, val query: String, val filters: FilterList) : SourcePagingSource(source) { override suspend fun requestNextPage(currentPage: Int): MangasPage { return source.getMangaList(query, filters, currentPage) } } -class SourcePopularPagingSource(source: CatalogueSource) : SourcePagingSource(source) { +class SourcePopularPagingSource(source: Source) : SourcePagingSource(source) { override suspend fun requestNextPage(currentPage: Int): MangasPage { return source.getDefaultMangaList(currentPage) } } -class SourceLatestPagingSource(source: CatalogueSource) : SourcePagingSource(source) { +class SourceLatestPagingSource(source: Source) : SourcePagingSource(source) { override suspend fun requestNextPage(currentPage: Int): MangasPage { return source.getLatestMangaList(currentPage) } } abstract class SourcePagingSource( - protected val source: CatalogueSource, + protected val source: Source, ) : SourcePagingSourceType() { abstract suspend fun requestNextPage(currentPage: Int): MangasPage diff --git a/data/src/main/java/tachiyomi/data/source/SourceRepositoryImpl.kt b/data/src/main/java/tachiyomi/data/source/SourceRepositoryImpl.kt index dfdd645d5b..38e5a53838 100644 --- a/data/src/main/java/tachiyomi/data/source/SourceRepositoryImpl.kt +++ b/data/src/main/java/tachiyomi/data/source/SourceRepositoryImpl.kt @@ -1,6 +1,5 @@ package tachiyomi.data.source -import eu.kanade.tachiyomi.source.CatalogueSource import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.online.HttpSource @@ -21,7 +20,7 @@ class SourceRepositoryImpl( ) : SourceRepository { override fun getSources(): Flow> { - return sourceManager.catalogueSources.map { sources -> + return sourceManager.sources.map { sources -> sources.map { mapSourceToDomainSource(it).copy( supportsLatest = it.hasLatestListing, @@ -31,7 +30,7 @@ class SourceRepositoryImpl( } override fun getOnlineSources(): Flow> { - return sourceManager.catalogueSources.map { sources -> + return sourceManager.sources.map { sources -> sources .filterIsInstance() .map(::mapSourceToDomainSource) @@ -41,7 +40,7 @@ class SourceRepositoryImpl( override fun getSourcesWithFavoriteCount(): Flow>> { return combine( handler.subscribeToList { mangasQueries.getSourceIdWithFavoriteCount() }, - sourceManager.catalogueSources, + sourceManager.sources, ) { sourceIdWithFavoriteCount, _ -> sourceIdWithFavoriteCount } .map { it.map { (sourceId, count) -> @@ -73,17 +72,17 @@ class SourceRepositoryImpl( query: String, filterList: FilterList, ): SourcePagingSourceType { - val source = sourceManager.get(sourceId) as CatalogueSource + val source = sourceManager.get(sourceId) as Source return SourceSearchPagingSource(source, query, filterList) } override fun getPopular(sourceId: Long): SourcePagingSourceType { - val source = sourceManager.get(sourceId) as CatalogueSource + val source = sourceManager.get(sourceId) as Source return SourcePopularPagingSource(source) } override fun getLatest(sourceId: Long): SourcePagingSourceType { - val source = sourceManager.get(sourceId) as CatalogueSource + val source = sourceManager.get(sourceId) as Source return SourceLatestPagingSource(source) } diff --git a/domain/src/main/java/tachiyomi/domain/source/service/SourceManager.kt b/domain/src/main/java/tachiyomi/domain/source/service/SourceManager.kt index da4fb39291..00eecee1a9 100644 --- a/domain/src/main/java/tachiyomi/domain/source/service/SourceManager.kt +++ b/domain/src/main/java/tachiyomi/domain/source/service/SourceManager.kt @@ -1,6 +1,5 @@ package tachiyomi.domain.source.service -import eu.kanade.tachiyomi.source.CatalogueSource import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.online.HttpSource import kotlinx.coroutines.flow.Flow @@ -11,15 +10,15 @@ interface SourceManager { val isInitialized: StateFlow - val catalogueSources: Flow> + val sources: Flow> fun get(sourceKey: Long): Source? fun getOrStub(sourceKey: Long): Source - fun getOnlineSources(): List - - fun getCatalogueSources(): List + fun getSources(): List fun getStubSources(): List + + fun getOnlineSources(): List } diff --git a/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/CatalogueSource.kt b/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/CatalogueSource.kt index 5dc2dc402e..aff3d00248 100644 --- a/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/CatalogueSource.kt +++ b/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/CatalogueSource.kt @@ -1,84 +1,81 @@ +@file:Suppress("DEPRECATION") + package eu.kanade.tachiyomi.source import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.MangasPage +import eu.kanade.tachiyomi.util.awaitSingle import rx.Observable -import tachiyomi.core.common.util.lang.awaitSingle +@Deprecated( + message = "Use the base Source class instead", + replaceWith = ReplaceWith( + expression = "Source", + imports = ["eu.kanade.tachiyomi.source.Source"], + ), +) interface CatalogueSource : Source { + override val language: String get() = lang + + override val hasSearchFilters: Boolean get() = getFilterList().isNotEmpty() + + override val hasLatestListing: Boolean get() = supportsLatest + + override suspend fun getSearchFilters(): FilterList = getFilterList() + + override suspend fun getDefaultMangaList(page: Int): MangasPage = fetchPopularManga(page).awaitSingle() + + override suspend fun getLatestMangaList(page: Int): MangasPage = fetchLatestUpdates(page).awaitSingle() + + override suspend fun getMangaList(query: String, filters: FilterList, page: Int): MangasPage = + fetchSearchManga(page, query, filters).awaitSingle() + /** * An ISO 639-1 compliant language code (two letters in lower case). */ + @Deprecated("Use language instead", ReplaceWith("language")) val lang: String get() = throw UnsupportedOperationException() - override val language: String get() = lang - /** * Whether the source has support for latest updates. */ + @Deprecated("Use hasLatestListing instead", ReplaceWith("hasLatestListing")) val supportsLatest: Boolean get() = throw UnsupportedOperationException() - override val hasLatestListing: Boolean get() = supportsLatest + /** + * Returns the list of filters for the source. + */ + @Deprecated("Use the new suspend API instead", ReplaceWith("getSearchFilters")) + fun getFilterList(): FilterList = throw UnsupportedOperationException() /** - * Get a page with a list of manga. + * Returns an observable containing a page with a list of manga. * - * @since extensions-lib 1.5 * @param page the page number to retrieve. */ - @Suppress("DEPRECATION") - override suspend fun getDefaultMangaList(page: Int): MangasPage { - return fetchPopularManga(page).awaitSingle() - } + @Deprecated("Use the new suspend API instead", ReplaceWith("getDefaultMangaList")) + fun fetchPopularManga(page: Int): Observable = throw UnsupportedOperationException() /** - * Get a page with a list of latest manga updates. + * Returns an observable containing a page with a list of latest manga updates. * - * @since extensions-lib 1.5 * @param page the page number to retrieve. */ - @Suppress("DEPRECATION") - override suspend fun getLatestMangaList(page: Int): MangasPage { - return fetchLatestUpdates(page).awaitSingle() - } + @Deprecated("Use the new suspend API instead", ReplaceWith("getLatestMangaList")) + fun fetchLatestUpdates(page: Int): Observable = throw UnsupportedOperationException() /** - * Get a page with a list of manga. + * Returns an observable containing a page with a list of manga. * - * @since extensions-lib 1.5 * @param page the page number to retrieve. * @param query the search query. * @param filters the list of filters to apply. */ - @Suppress("DEPRECATION") - override suspend fun getMangaList(query: String, filters: FilterList, page: Int): MangasPage { - return fetchSearchManga(page, query, filters).awaitSingle() - } - - /** - * Returns the list of filters for the source. - */ - fun getFilterList(): FilterList - - @Deprecated( - "Use the non-RxJava API instead", - ReplaceWith("getPopularManga"), - ) - fun fetchPopularManga(page: Int): Observable = - throw IllegalStateException("Not used") - - @Deprecated( - "Use the non-RxJava API instead", - ReplaceWith("getSearchManga"), - ) - fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable = - throw IllegalStateException("Not used") - - @Deprecated( - "Use the non-RxJava API instead", - ReplaceWith("getLatestUpdates"), - ) - fun fetchLatestUpdates(page: Int): Observable = - throw IllegalStateException("Not used") + @Deprecated("Use the new suspend API instead", ReplaceWith("getMangaList")) + fun fetchSearchManga( + page: Int, + query: String, + filters: FilterList, + ): Observable = throw UnsupportedOperationException() } From 352543bfe4d8ded45c3d35cd11cf0818b2afde88 Mon Sep 17 00:00:00 2001 From: AntsyLich <59261191+AntsyLich@users.noreply.github.com> Date: Mon, 27 Jan 2025 12:16:40 +0600 Subject: [PATCH 10/10] Sync implementation of HttpSource --- .../eu/kanade/tachiyomi/source/Source.kt | 3 +- .../tachiyomi/source/online/HttpSource.kt | 402 ++++++++---------- .../source/online/ParsedHttpSource.kt | 1 + 3 files changed, 183 insertions(+), 223 deletions(-) diff --git a/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/Source.kt b/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/Source.kt index c8c097d1ae..5951a2b6c7 100644 --- a/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/Source.kt +++ b/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/Source.kt @@ -118,7 +118,8 @@ interface Source { * * @param chapter the chapter. */ - suspend fun getPageList(chapter: SChapter): List = throw RuntimeException("Stub!") + @Suppress("DEPRECATION") + suspend fun getPageList(chapter: SChapter): List = fetchPageList(chapter).awaitSingle() override fun toString(): String diff --git a/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/online/HttpSource.kt b/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/online/HttpSource.kt index 19e1aafe34..dfe516c8b1 100644 --- a/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/online/HttpSource.kt +++ b/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/online/HttpSource.kt @@ -1,3 +1,5 @@ +@file:Suppress("UNUSED", "UnusedReceiverParameter", "DEPRECATION") + package eu.kanade.tachiyomi.source.online import eu.kanade.tachiyomi.network.GET @@ -11,12 +13,13 @@ import eu.kanade.tachiyomi.source.model.MangasPage import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SManga +import eu.kanade.tachiyomi.util.awaitSingle +import mihonx.source.model.UserAgentType import okhttp3.Headers import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.Response import rx.Observable -import tachiyomi.core.common.util.lang.awaitSingle import uy.kohesive.injekt.injectLazy import java.net.URI import java.net.URISyntaxException @@ -25,14 +28,8 @@ import java.security.MessageDigest /** * A simple implementation for sources from a website. */ -@Suppress("unused") abstract class HttpSource : CatalogueSource { - /** - * Network service. - */ - protected val network: NetworkHelper by injectLazy() - /** * Base url of the website without the trailing slash, like: http://mysite.com */ @@ -42,30 +39,14 @@ abstract class HttpSource : CatalogueSource { * Version id used to generate the source id. If the site completely changes and urls are * incompatible, you may increase this value and it'll be considered as a new source. */ - open val versionId = 1 + open val versionId: Int = 1 /** - * ID of the source. By default it uses a generated id using the first 16 characters (64 bits) - * 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`. + * 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. */ - override val id by lazy { generateId(name, language, versionId) } - - /** - * Headers used for requests. - */ - val headers: Headers by lazy { headersBuilder().build() } - - /** - * Default network client for doing requests. - */ - open val client: OkHttpClient - get() = network.client + override val id: Long by lazy { generateId(name, language, versionId) } /** * Generates a unique ID for the source based on the provided [name], [lang] and @@ -90,25 +71,125 @@ abstract class HttpSource : CatalogueSource { return (0..7).map { bytes[it].toLong() and 0xff shl 8 * (7 - it) }.reduce(Long::or) and Long.MAX_VALUE } + /** + * Network service. + */ + protected val network: NetworkHelper by injectLazy() + + /** + * Headers used for requests. + */ + val headers: Headers by lazy { headersBuilder().build() } + + /** + * Default network client for doing requests. + */ + open val client: OkHttpClient get() = network.client + + /** + * Type of UserAgent a source needs + */ + protected open val supportedUserAgentType: UserAgentType = UserAgentType.Universal + + /** + * @since extensions-lib 1.6 + */ + // TODO(antsy): Implement + @Suppress("MemberVisibilityCanBePrivate") + protected fun getUserAgent(): String = network.defaultUserAgentProvider() + /** * Headers builder for requests. Implementations can override this method for custom headers. */ - protected open fun headersBuilder() = Headers.Builder().apply { - add("User-Agent", network.defaultUserAgentProvider()) + protected open fun headersBuilder(): Headers.Builder = Headers.Builder().apply { + add("User-Agent", getUserAgent()) } /** - * Visible name of the source. + * Returns the image url for the provided [page]. The function is only called if [Page.imageUrl] is null. + * + * @param page the page whose source image has to be fetched. */ - override fun toString() = "$name (${language.uppercase()})" + open suspend fun getImageUrl(page: Page): String = fetchImageUrl(page).awaitSingle() /** - * Returns an observable containing a page with a list of manga. Normally it's not needed to - * override this method. + * Returns the request for getting the source image. Override only if it's needed to override + * the url, send different headers or request method like POST. * - * @param page the page number to retrieve. + * @param page the chapter whose page list has to be fetched + */ + open fun imageRequest(page: Page): Request = GET(page.imageUrl!!, headers) + + open suspend fun getImage(page: Page): Response { + return client.newCachelessCallWithProgress(imageRequest(page), page).awaitSuccess() + } + + /** + * Assigns the url of the chapter without the scheme and domain. It saves some redundancy from + * database and the urls could still work after a domain change. + * + * @param url the full url to the chapter. + */ + fun SChapter.setUrlWithoutDomain(url: String) { + this.url = getUrlWithoutDomain(url) + } + + /** + * Assigns the url of the manga without the scheme and domain. It saves some redundancy from + * database and the urls could still work after a domain change. + * + * @param url the full url to the manga. */ - @Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getPopularManga")) + fun SManga.setUrlWithoutDomain(url: String) { + this.url = getUrlWithoutDomain(url) + } + + /** + * Returns the url of the given string without the scheme and domain. + * + * @param orig the full url. + */ + private fun getUrlWithoutDomain(orig: String): String { + return try { + val uri = URI(orig.replace(" ", "%20")) + var out = uri.path + if (uri.query != null) { + out += "?" + uri.query + } + if (uri.fragment != null) { + out += "#" + uri.fragment + } + out + } catch (e: URISyntaxException) { + orig + } + } + + /** + * Returns the url of the provided manga + * + * @since extensions-lib 1.4 + * @param manga the manga + * @return url of the manga + */ + open fun getMangaUrl(manga: SManga): String { + return mangaDetailsRequest(manga).url.toString() + } + + /** + * Returns the url of the provided chapter + * + * @since extensions-lib 1.4 + * @param chapter the chapter + * @return url of the chapter + */ + open fun getChapterUrl(chapter: SChapter): String { + return pageListRequest(chapter).url.toString() + } + + override fun toString(): String = "$name (${language.uppercase()})" + + @Deprecated("Use the new suspend API instead", replaceWith = ReplaceWith("getDefaultMangaList")) override fun fetchPopularManga(page: Int): Observable { return client.newCall(popularMangaRequest(page)) .asObservableSuccess() @@ -122,24 +203,47 @@ abstract class HttpSource : CatalogueSource { * * @param page the page number to retrieve. */ - protected abstract fun popularMangaRequest(page: Int): Request + @Suppress("DeprecatedCallableAddReplaceWith") + @Deprecated("Directly implement inside [getDefaultMangaList]") + open fun popularMangaRequest(page: Int): Request = throw UnsupportedOperationException() /** * Parses the response from the site and returns a [MangasPage] object. * * @param response the response from the site. */ - protected abstract fun popularMangaParse(response: Response): MangasPage + @Suppress("DeprecatedCallableAddReplaceWith") + @Deprecated("Directly implement inside [getDefaultMangaList]") + open fun popularMangaParse(response: Response): MangasPage = throw UnsupportedOperationException() + + @Deprecated("Use the new suspend API instead", replaceWith = ReplaceWith("getLatestMangaList")) + override fun fetchLatestUpdates(page: Int): Observable { + return client.newCall(latestUpdatesRequest(page)) + .asObservableSuccess() + .map { response -> + latestUpdatesParse(response) + } + } /** - * Returns an observable containing a page with a list of manga. Normally it's not needed to - * override this method. + * Returns the request for latest manga given the page. * * @param page the page number to retrieve. - * @param query the search query. - * @param filters the list of filters to apply. */ - @Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getSearchManga")) + @Suppress("DeprecatedCallableAddReplaceWith") + @Deprecated("Directly implement inside [getLatestMangaList]") + open fun latestUpdatesRequest(page: Int): Request = throw UnsupportedOperationException() + + /** + * Parses the response from the site and returns a [MangasPage] object. + * + * @param response the response from the site. + */ + @Suppress("DeprecatedCallableAddReplaceWith") + @Deprecated("Directly implement inside [getLatestMangaList]") + open fun latestUpdatesParse(response: Response): MangasPage = throw UnsupportedOperationException() + + @Deprecated("Use the new suspend API instead", replaceWith = ReplaceWith("getMangaList")) override fun fetchSearchManga( page: Int, query: String, @@ -166,60 +270,24 @@ abstract class HttpSource : CatalogueSource { * @param query the search query. * @param filters the list of filters to apply. */ - protected abstract fun searchMangaRequest( + @Suppress("DeprecatedCallableAddReplaceWith") + @Deprecated("Directly implement inside [getMangaList]") + open fun searchMangaRequest( page: Int, query: String, filters: FilterList, - ): Request + ): Request = throw UnsupportedOperationException() /** * Parses the response from the site and returns a [MangasPage] object. * * @param response the response from the site. */ - protected abstract fun searchMangaParse(response: Response): MangasPage + @Suppress("DeprecatedCallableAddReplaceWith") + @Deprecated("Directly implement inside [getMangaList]") + open fun searchMangaParse(response: Response): MangasPage = throw RuntimeException("Stub!") - /** - * Returns an observable containing a page with a list of latest manga updates. - * - * @param page the page number to retrieve. - */ - @Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getLatestUpdates")) - override fun fetchLatestUpdates(page: Int): Observable { - return client.newCall(latestUpdatesRequest(page)) - .asObservableSuccess() - .map { response -> - latestUpdatesParse(response) - } - } - - /** - * Returns the request for latest manga given the page. - * - * @param page the page number to retrieve. - */ - protected abstract fun latestUpdatesRequest(page: Int): Request - - /** - * Parses the response from the site and returns a [MangasPage] object. - * - * @param response the response from the site. - */ - protected abstract fun latestUpdatesParse(response: Response): MangasPage - - /** - * Get the updated details for a manga. - * Normally it's not needed to override this method. - * - * @param manga the manga to update. - * @return the updated manga. - */ - @Suppress("DEPRECATION") - override suspend fun getMangaDetails(manga: SManga): SManga { - return fetchMangaDetails(manga).awaitSingle() - } - - @Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getMangaDetails")) + @Deprecated("Use the new suspend API instead", replaceWith = ReplaceWith("getMangaDetails(manga, true, false)")) override fun fetchMangaDetails(manga: SManga): Observable { return client.newCall(mangaDetailsRequest(manga)) .asObservableSuccess() @@ -234,6 +302,8 @@ abstract class HttpSource : CatalogueSource { * * @param manga the manga to be updated. */ + @Suppress("DeprecatedCallableAddReplaceWith") + @Deprecated("Directly implement inside [getMangaDetails]") open fun mangaDetailsRequest(manga: SManga): Request { return GET(baseUrl + manga.url, headers) } @@ -243,21 +313,11 @@ abstract class HttpSource : CatalogueSource { * * @param response the response from the site. */ - protected abstract fun mangaDetailsParse(response: Response): SManga + @Suppress("DeprecatedCallableAddReplaceWith") + @Deprecated("Directly implement inside [getMangaDetails]") + open fun mangaDetailsParse(response: Response): SManga = throw RuntimeException("Stub!") - /** - * Get all the available chapters for a manga. - * Normally it's not needed to override this method. - * - * @param manga the manga to update. - * @return the chapters for the manga. - */ - @Suppress("DEPRECATION") - override suspend fun getChapterList(manga: SManga): List { - return fetchChapterList(manga).awaitSingle() - } - - @Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getChapterList")) + @Deprecated("Use the new suspend API instead", replaceWith = ReplaceWith("getMangaDetails(manga, false, true)")) override fun fetchChapterList(manga: SManga): Observable> { return client.newCall(chapterListRequest(manga)) .asObservableSuccess() @@ -272,7 +332,9 @@ abstract class HttpSource : CatalogueSource { * * @param manga the manga to look for chapters. */ - protected open fun chapterListRequest(manga: SManga): Request { + @Suppress("DeprecatedCallableAddReplaceWith") + @Deprecated("Directly implement inside [getMangaDetails]") + open fun chapterListRequest(manga: SManga): Request { return GET(baseUrl + manga.url, headers) } @@ -281,28 +343,11 @@ abstract class HttpSource : CatalogueSource { * * @param response the response from the site. */ - protected abstract fun chapterListParse(response: Response): List - - /** - * Parses the response from the site and returns a SChapter Object. - * - * @param response the response from the site. - */ - protected abstract fun chapterPageParse(response: Response): SChapter - - /** - * Get the list of pages a chapter has. Pages should be returned - * in the expected order; the index is ignored. - * - * @param chapter the chapter. - * @return the pages for the chapter. - */ - @Suppress("DEPRECATION") - override suspend fun getPageList(chapter: SChapter): List { - return fetchPageList(chapter).awaitSingle() - } + @Suppress("DeprecatedCallableAddReplaceWith") + @Deprecated("Directly implement inside [getMangaDetails]") + open fun chapterListParse(response: Response): List = throw RuntimeException("Stub!") - @Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getPageList")) + @Deprecated("Use the new suspend API instead", ReplaceWith("getPageList")) override fun fetchPageList(chapter: SChapter): Observable> { return client.newCall(pageListRequest(chapter)) .asObservableSuccess() @@ -317,6 +362,8 @@ abstract class HttpSource : CatalogueSource { * * @param chapter the chapter whose page list has to be fetched. */ + @Suppress("DeprecatedCallableAddReplaceWith") + @Deprecated("Directly implement inside [getPageList]") protected open fun pageListRequest(chapter: SChapter): Request { return GET(baseUrl + chapter.url, headers) } @@ -326,21 +373,17 @@ abstract class HttpSource : CatalogueSource { * * @param response the response from the site. */ - protected abstract fun pageListParse(response: Response): List + @Suppress("DeprecatedCallableAddReplaceWith") + @Deprecated("Directly implement inside [getPageList]") + protected open fun pageListParse(response: Response): List = throw UnsupportedOperationException() /** * Returns an observable with the page containing the source url of the image. If there's any * error, it will return null instead of throwing an exception. * - * @since extensions-lib 1.5 * @param page the page whose source image has to be fetched. */ - @Suppress("DEPRECATION") - open suspend fun getImageUrl(page: Page): String { - return fetchImageUrl(page).awaitSingle() - } - - @Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getImageUrl")) + @Deprecated("Use the new suspend API instead", ReplaceWith("getImageUrl")) open fun fetchImageUrl(page: Page): Observable { return client.newCall(imageUrlRequest(page)) .asObservableSuccess() @@ -353,6 +396,8 @@ abstract class HttpSource : CatalogueSource { * * @param page the chapter whose page list has to be fetched */ + @Suppress("DeprecatedCallableAddReplaceWith") + @Deprecated("Directly implement inside [getImageUrl]") protected open fun imageUrlRequest(page: Page): Request { return GET(page.url, headers) } @@ -362,92 +407,9 @@ abstract class HttpSource : CatalogueSource { * * @param response the response from the site. */ - protected abstract fun imageUrlParse(response: Response): String - - /** - * Returns the response of the source image. - * Typically does not need to be overridden. - * - * @since extensions-lib 1.5 - * @param page the page whose source image has to be downloaded. - */ - open suspend fun getImage(page: Page): Response { - return client.newCachelessCallWithProgress(imageRequest(page), page) - .awaitSuccess() - } - - /** - * Returns the request for getting the source image. Override only if it's needed to override - * the url, send different headers or request method like POST. - * - * @param page the chapter whose page list has to be fetched - */ - protected open fun imageRequest(page: Page): Request { - return GET(page.imageUrl!!, headers) - } - - /** - * Assigns the url of the chapter without the scheme and domain. It saves some redundancy from - * database and the urls could still work after a domain change. - * - * @param url the full url to the chapter. - */ - fun SChapter.setUrlWithoutDomain(url: String) { - this.url = getUrlWithoutDomain(url) - } - - /** - * Assigns the url of the manga without the scheme and domain. It saves some redundancy from - * database and the urls could still work after a domain change. - * - * @param url the full url to the manga. - */ - fun SManga.setUrlWithoutDomain(url: String) { - this.url = getUrlWithoutDomain(url) - } - - /** - * Returns the url of the given string without the scheme and domain. - * - * @param orig the full url. - */ - private fun getUrlWithoutDomain(orig: String): String { - return try { - val uri = URI(orig.replace(" ", "%20")) - var out = uri.path - if (uri.query != null) { - out += "?" + uri.query - } - if (uri.fragment != null) { - out += "#" + uri.fragment - } - out - } catch (e: URISyntaxException) { - orig - } - } - - /** - * Returns the url of the provided manga - * - * @since extensions-lib 1.4 - * @param manga the manga - * @return url of the manga - */ - open fun getMangaUrl(manga: SManga): String { - return mangaDetailsRequest(manga).url.toString() - } - - /** - * Returns the url of the provided chapter - * - * @since extensions-lib 1.4 - * @param chapter the chapter - * @return url of the chapter - */ - open fun getChapterUrl(chapter: SChapter): String { - return pageListRequest(chapter).url.toString() - } + @Suppress("DeprecatedCallableAddReplaceWith") + @Deprecated("Directly implement inside [getImageUrl]") + protected open fun imageUrlParse(response: Response): String = throw UnsupportedOperationException() /** * Called before inserting a new chapter into database. Use it if you need to override chapter @@ -456,10 +418,6 @@ abstract class HttpSource : CatalogueSource { * @param chapter the chapter to be added. * @param manga the manga of the chapter. */ + @Deprecated("All these modification should be done when constructing the chapter") open fun prepareNewChapter(chapter: SChapter, manga: SManga) {} - - /** - * Returns the list of filters for the source. - */ - override fun getFilterList() = FilterList() } diff --git a/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/online/ParsedHttpSource.kt b/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/online/ParsedHttpSource.kt index 34376f847e..c62c6b8543 100644 --- a/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/online/ParsedHttpSource.kt +++ b/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/online/ParsedHttpSource.kt @@ -13,6 +13,7 @@ import org.jsoup.nodes.Element * A simple implementation for sources from a website using Jsoup, an HTML parser. */ @Suppress("unused") +@Deprecated("Use your own implementation made with Ksoup") abstract class ParsedHttpSource : HttpSource() { /**