From d0b0e7c66c364a511ab422b78bbe7f2ceded2cb2 Mon Sep 17 00:00:00 2001 From: Jays2Kings Date: Wed, 4 Oct 2023 18:16:53 -0700 Subject: [PATCH] More work for ext 1.5 Updated the extension loader for support of it to match main (save for the private extension stuff, that's later) * Rename new method in ConfigurableSource to get preferences (afb1ee2) *Add more replacement suspend functions for source APIs (26c5d76) Co-Authored-By: arkon <4098258+arkon@users.noreply.github.com> --- app/build.gradle.kts | 33 ++- .../tachiyomi/data/download/Downloader.kt | 3 +- .../data/track/anilist/AnilistApi.kt | 129 +++++----- .../data/track/bangumi/BangumiApi.kt | 18 +- .../tachiyomi/data/track/kavita/Kavita.kt | 28 ++- .../tachiyomi/data/track/kavita/KavitaApi.kt | 26 +- .../tachiyomi/data/track/kitsu/KitsuApi.kt | 226 +++++++++-------- .../tachiyomi/data/track/komga/KomgaApi.kt | 49 ++-- .../track/mangaupdates/MangaUpdatesApi.kt | 92 +++---- .../data/track/myanimelist/MyAnimeListApi.kt | 127 +++++----- .../myanimelist/MyAnimeListInterceptor.kt | 20 +- .../data/track/shikimori/ShikimoriApi.kt | 58 +++-- .../data/track/suwayomi/TachideskApi.kt | 10 +- .../data/updater/AppUpdateChecker.kt | 73 +++--- .../extension/api/ExtensionGithubApi.kt | 10 +- .../tachiyomi/extension/model/Extension.kt | 1 + .../tachiyomi/extension/model/LoadResult.kt | 10 +- .../util/ExtensionInstallReceiver.kt | 2 +- .../extension/util/ExtensionLoader.kt | 231 ++++++++++++++---- .../tachiyomi/network/OkHttpExtensions.kt | 41 ++-- .../smartsearch/SmartSearchEngine.kt | 8 +- .../tachiyomi/source/CatalogueSource.kt | 46 +++- .../tachiyomi/source/ConfigurableSource.kt | 17 ++ .../eu/kanade/tachiyomi/source/LocalSource.kt | 11 +- .../java/eu/kanade/tachiyomi/source/Source.kt | 77 +++--- .../kanade/tachiyomi/source/SourceManager.kt | 27 +- .../tachiyomi/source/online/HttpSource.kt | 140 ++++++++--- .../source/online/HttpSourceFetcher.kt | 25 -- .../details/ExtensionDetailsController.kt | 9 +- .../ui/reader/loader/HttpPageLoader.kt | 3 +- .../ui/source/browse/BrowseSourcePager.kt | 9 +- .../ui/source/browse/LatestUpdatesPager.kt | 3 +- .../globalsearch/GlobalSearchPresenter.kt | 3 +- .../kanade/tachiyomi/util/JsoupExtensions.kt | 2 +- 34 files changed, 956 insertions(+), 611 deletions(-) delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/source/online/HttpSourceFetcher.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 5c45d8a7a1..98154f1083 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -296,21 +296,40 @@ dependencies { implementation("com.github.PhilJay:MPAndroidChart:v3.1.0") } - - tasks { // See https://kotlinlang.org/docs/reference/experimental.html#experimental-status-of-experimental-api(-markers) withType { kotlinOptions.freeCompilerArgs += listOf( - "-opt-in=kotlin.Experimental", - "-opt-in=kotlin.RequiresOptIn", - "-opt-in=kotlin.ExperimentalStdlibApi", + "-Xcontext-receivers", + "-opt-in=androidx.compose.foundation.layout.ExperimentalLayoutApi", + "-opt-in=androidx.compose.material.ExperimentalMaterialApi", + "-opt-in=androidx.compose.material3.ExperimentalMaterial3Api", + "-opt-in=androidx.compose.material.ExperimentalMaterialApi", + "-opt-in=androidx.compose.ui.ExperimentalComposeUiApi", + "-opt-in=androidx.compose.foundation.ExperimentalFoundationApi", + "-opt-in=androidx.compose.animation.ExperimentalAnimationApi", + "-opt-in=androidx.compose.animation.graphics.ExperimentalAnimationGraphicsApi", + "-opt-in=coil.annotation.ExperimentalCoilApi", + "-opt-in=com.google.accompanist.permissions.ExperimentalPermissionsApi", + "-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi", "-opt-in=kotlinx.coroutines.FlowPreview", "-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi", "-opt-in=kotlinx.coroutines.InternalCoroutinesApi", "-opt-in=kotlinx.serialization.ExperimentalSerializationApi", - "-opt-in=coil.annotation.ExperimentalCoilApi", ) + + if (project.findProperty("tachiyomi.enableComposeCompilerMetrics") == "true") { + kotlinOptions.freeCompilerArgs += listOf( + "-P", + "plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination=" + + project.buildDir.absolutePath + "/compose_metrics", + ) + kotlinOptions.freeCompilerArgs += listOf( + "-P", + "plugin:androidx.compose.compiler.plugins.kotlin:metricsDestination=" + + project.buildDir.absolutePath + "/compose_metrics", + ) + } } // Duplicating Hebrew string assets due to some locale code issues on different devices @@ -323,4 +342,4 @@ tasks { preBuild { dependsOn(formatKotlin, copyHebrewStrings) } -} \ No newline at end of file +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt b/app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt index bbe7ab25e4..2298d950bd 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt @@ -27,7 +27,6 @@ import eu.kanade.tachiyomi.util.storage.DiskUtil import eu.kanade.tachiyomi.util.storage.DiskUtil.NOMEDIA_FILE import eu.kanade.tachiyomi.util.storage.saveTo import eu.kanade.tachiyomi.util.system.ImageUtil -import eu.kanade.tachiyomi.util.system.awaitSingle import eu.kanade.tachiyomi.util.system.launchIO import eu.kanade.tachiyomi.util.system.launchNow import eu.kanade.tachiyomi.util.system.withIOContext @@ -388,7 +387,7 @@ class Downloader( pageList.filter { it.imageUrl.isNullOrEmpty() }.forEach { page -> page.status = Page.State.LOAD_PAGE try { - page.imageUrl = download.source.fetchImageUrl(page).awaitSingle() + page.imageUrl = download.source.getImageUrl(page) } catch (e: Throwable) { page.status = Page.State.ERROR } 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 4d6ead27ac..fe605fbbc5 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 @@ -25,11 +25,14 @@ import kotlinx.serialization.json.put import kotlinx.serialization.json.putJsonObject import okhttp3.OkHttpClient import okhttp3.RequestBody.Companion.toRequestBody +import uy.kohesive.injekt.injectLazy import java.util.Calendar import java.util.concurrent.TimeUnit class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) { + private val json: Json by injectLazy() + private val authClient = client.newBuilder() .addInterceptor(interceptor) .rateLimit(permits = 85, period = 1, unit = TimeUnit.MINUTES) @@ -51,14 +54,16 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) { } } } - authClient.newCall(POST(apiUrl, body = payload.toString().toRequestBody(jsonMime))) - .awaitSuccess() - .parseAs() - .let { - track.library_id = - it["data"]!!.jsonObject["SaveMediaListEntry"]!!.jsonObject["id"]!!.jsonPrimitive.long - track - } + with(json) { + authClient.newCall(POST(apiUrl, body = payload.toString().toRequestBody(jsonMime))) + .awaitSuccess() + .parseAs() + .let { + track.library_id = + it["data"]!!.jsonObject["SaveMediaListEntry"]!!.jsonObject["id"]!!.jsonPrimitive.long + track + } + } } } @@ -79,21 +84,23 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) { } } } - authClient.newCall(POST(apiUrl, body = payload.toString().toRequestBody(jsonMime))) - .awaitSuccess() - .parseAs() - .let { response -> - val media = response["data"]!!.jsonObject["SaveMediaListEntry"]!!.jsonObject - val startedDate = parseDate(media, "startedAt") - if (track.started_reading_date <= 0L || startedDate > 0) { - track.started_reading_date = startedDate + with(json) { + authClient.newCall(POST(apiUrl, body = payload.toString().toRequestBody(jsonMime))) + .awaitSuccess() + .parseAs() + .let { response -> + val media = response["data"]!!.jsonObject["SaveMediaListEntry"]!!.jsonObject + val startedDate = parseDate(media, "startedAt") + if (track.started_reading_date <= 0L || startedDate > 0) { + track.started_reading_date = startedDate + } + val finishedDate = parseDate(media, "completedAt") + if (track.finished_reading_date <= 0L || finishedDate > 0) { + track.finished_reading_date = finishedDate + } + track } - val finishedDate = parseDate(media, "completedAt") - if (track.finished_reading_date <= 0L || finishedDate > 0) { - track.finished_reading_date = finishedDate - } - track - } + } } } @@ -105,16 +112,18 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) { put("query", search) } } - authClient.newCall(POST(apiUrl, body = payload.toString().toRequestBody(jsonMime))) - .awaitSuccess() - .parseAs() - .let { response -> - val data = response["data"]!!.jsonObject - val page = data["Page"]!!.jsonObject - val media = page["media"]!!.jsonArray - val entries = media.map { jsonToALManga(it.jsonObject) } - entries.map { it.toTrack() } - } + with(json) { + authClient.newCall(POST(apiUrl, body = payload.toString().toRequestBody(jsonMime))) + .awaitSuccess() + .parseAs() + .let { response -> + val data = response["data"]!!.jsonObject + val page = data["Page"]!!.jsonObject + val media = page["media"]!!.jsonArray + val entries = media.map { jsonToALManga(it.jsonObject) } + entries.map { it.toTrack() } + } + } } } @@ -127,16 +136,18 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) { put("manga_id", track.media_id) } } - authClient.newCall(POST(apiUrl, body = payload.toString().toRequestBody(jsonMime))) - .awaitSuccess() - .parseAs() - .let { response -> - val data = response["data"]!!.jsonObject - val page = data["Page"]!!.jsonObject - val media = page["mediaList"]!!.jsonArray - val entries = media.map { jsonToALUserManga(it.jsonObject) } - entries.firstOrNull()?.toTrack() - } + with(json) { + authClient.newCall(POST(apiUrl, body = payload.toString().toRequestBody(jsonMime))) + .awaitSuccess() + .parseAs() + .let { response -> + val data = response["data"]!!.jsonObject + val page = data["Page"]!!.jsonObject + val media = page["mediaList"]!!.jsonArray + val entries = media.map { jsonToALUserManga(it.jsonObject) } + entries.firstOrNull()?.toTrack() + } + } } } @@ -168,22 +179,24 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) { val payload = buildJsonObject { put("query", currentUserQuery()) } - authClient.newCall( - POST( - apiUrl, - body = payload.toString().toRequestBody(jsonMime), - ), - ) - .awaitSuccess() - .parseAs() - .let { - val data = it["data"]!!.jsonObject - val viewer = data["Viewer"]!!.jsonObject - Pair( - viewer["id"]!!.jsonPrimitive.int, - viewer["mediaListOptions"]!!.jsonObject["scoreFormat"]!!.jsonPrimitive.content, - ) - } + with(json) { + authClient.newCall( + POST( + apiUrl, + body = payload.toString().toRequestBody(jsonMime), + ), + ) + .awaitSuccess() + .parseAs() + .let { + val data = it["data"]!!.jsonObject + val viewer = data["Viewer"]!!.jsonObject + Pair( + viewer["id"]!!.jsonPrimitive.int, + viewer["mediaListOptions"]!!.jsonObject["scoreFormat"]!!.jsonPrimitive.content, + ) + } + } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/BangumiApi.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/BangumiApi.kt index e38562266e..8462e86682 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/BangumiApi.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/BangumiApi.kt @@ -118,10 +118,12 @@ class BangumiApi(private val client: OkHttpClient, interceptor: BangumiIntercept suspend fun findLibManga(track: Track): Track? { return withIOContext { - authClient.newCall(GET("$apiUrl/subject/${track.media_id}")) - .awaitSuccess() - .parseAs() - .let { jsonToSearch(it) } + with(json) { + authClient.newCall(GET("$apiUrl/subject/${track.media_id}")) + .awaitSuccess() + .parseAs() + .let { jsonToSearch(it) } + } } } @@ -155,9 +157,11 @@ class BangumiApi(private val client: OkHttpClient, interceptor: BangumiIntercept suspend fun accessToken(code: String): OAuth { return withIOContext { - client.newCall(accessTokenRequest(code)) - .awaitSuccess() - .parseAs() + with(json) { + client.newCall(accessTokenRequest(code)) + .awaitSuccess() + .parseAs() + } } } 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 03f10e048a..d5be00f3ac 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 @@ -1,7 +1,6 @@ package eu.kanade.tachiyomi.data.track.kavita import android.content.Context -import android.content.SharedPreferences import android.graphics.Color import androidx.annotation.StringRes import eu.kanade.tachiyomi.R @@ -11,7 +10,11 @@ import eu.kanade.tachiyomi.data.track.EnhancedTrackService import eu.kanade.tachiyomi.data.track.TrackService import eu.kanade.tachiyomi.data.track.model.TrackSearch import eu.kanade.tachiyomi.data.track.updateNewTrackInfo +import eu.kanade.tachiyomi.source.ConfigurableSource import eu.kanade.tachiyomi.source.Source +import eu.kanade.tachiyomi.source.SourceManager +import eu.kanade.tachiyomi.source.sourcePreferences +import uy.kohesive.injekt.injectLazy import java.security.MessageDigest class Kavita(private val context: Context, id: Int) : TrackService(id), EnhancedTrackService { @@ -27,6 +30,8 @@ class Kavita(private val context: Context, id: Int) : TrackService(id), Enhanced private val interceptor by lazy { KavitaInterceptor(this) } val api by lazy { KavitaApi(client, interceptor) } + private val sourceManager: SourceManager by injectLazy() + @StringRes override fun nameRes() = R.string.kavita @@ -117,28 +122,29 @@ class Kavita(private val context: Context, id: Int) : TrackService(id), Enhanced fun loadOAuth() { val oauth = OAuth() - for (sourceId in 1..3) { - val authentication = oauth.authentications[sourceId - 1] - val sourceSuffixID by lazy { - val key = "kavita_$sourceId/all/1" // Hardcoded versionID to 1 + for (id in 1..3) { + val authentication = oauth.authentications[id - 1] + val sourceId by lazy { + val key = "kavita_$id/all/1" // Hardcoded versionID to 1 val bytes = MessageDigest.getInstance("MD5").digest(key.toByteArray()) (0..7).map { bytes[it].toLong() and 0xff shl 8 * (7 - it) } .reduce(Long::or) and Long.MAX_VALUE } - val preferences: SharedPreferences by lazy { - context.getSharedPreferences("source_$sourceSuffixID", 0x0000) - } - val prefApiUrl = preferences.getString("APIURL", "")!! - if (prefApiUrl.isEmpty()) { + val preferences = (sourceManager.get(sourceId) as ConfigurableSource).sourcePreferences() + + val prefApiUrl = preferences.getString("APIURL", "") + val prefApiKey = preferences.getString("APIKEY", "") + if (prefApiUrl.isNullOrEmpty() || prefApiKey.isNullOrEmpty()) { // Source not configured. Skip continue } - val prefApiKey = preferences.getString("APIKEY", "")!! + val token = api.getNewToken(apiUrl = prefApiUrl, apiKey = prefApiKey) if (token.isNullOrEmpty()) { // Source is not accessible. Skip continue } + authentication.apiUrl = prefApiUrl authentication.jwtToken = token.toString() } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/kavita/KavitaApi.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/kavita/KavitaApi.kt index c060c725d3..f2c13922b4 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/kavita/KavitaApi.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/kavita/KavitaApi.kt @@ -7,16 +7,20 @@ import eu.kanade.tachiyomi.network.POST import eu.kanade.tachiyomi.network.awaitSuccess import eu.kanade.tachiyomi.network.parseAs import eu.kanade.tachiyomi.util.system.withIOContext +import kotlinx.serialization.json.Json import okhttp3.Dns import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.OkHttpClient import okhttp3.RequestBody.Companion.toRequestBody import timber.log.Timber +import uy.kohesive.injekt.injectLazy import java.io.IOException import java.net.SocketTimeoutException class KavitaApi(private val client: OkHttpClient, interceptor: KavitaInterceptor) { + private val json: Json by injectLazy() + private val authClient = client.newBuilder() .dns(Dns.SYSTEM) .addInterceptor(interceptor) @@ -40,7 +44,7 @@ class KavitaApi(private val client: OkHttpClient, interceptor: KavitaInterceptor try { client.newCall(request).execute().use { when (it.code) { - 200 -> return it.parseAs().token + 200 -> return with(json) { it.parseAs().token } 401 -> { Timber.w("Unauthorized / api key not valid: Cleaned api URL: $apiUrl, Api key is empty: ${apiKey.isEmpty()}") throw IOException("Unauthorized / api key not valid") @@ -85,9 +89,11 @@ class KavitaApi(private val client: OkHttpClient, interceptor: KavitaInterceptor private fun getTotalChapters(url: String): Int { val requestUrl = getApiVolumesUrl(url) try { - val listVolumeDto = authClient.newCall(GET(requestUrl)) - .execute() - .parseAs>() + val listVolumeDto = with(json) { + authClient.newCall(GET(requestUrl)) + .execute() + .parseAs>() + } var volumeNumber = 0 var maxChapterNumber = 0 for (volume in listVolumeDto) { @@ -111,7 +117,9 @@ class KavitaApi(private val client: OkHttpClient, interceptor: KavitaInterceptor try { authClient.newCall(GET(requestUrl)).execute().use { if (it.code == 200) { - return it.parseAs().number!!.replace(",", ".").toFloat() + return with(json) { + it.parseAs().number!!.replace(",", ".").toFloat() + } } if (it.code == 204) { return 0F @@ -126,9 +134,11 @@ class KavitaApi(private val client: OkHttpClient, interceptor: KavitaInterceptor suspend fun getTrackSearch(url: String): TrackSearch = withIOContext { try { - val serieDto: SeriesDto = authClient.newCall(GET(url)) - .awaitSuccess() - .parseAs() + val serieDto: SeriesDto = with(json) { + authClient.newCall(GET(url)) + .awaitSuccess() + .parseAs() + } val track = serieDto.toTrack() track.apply { diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuApi.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuApi.kt index c75051d051..896cd651fa 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuApi.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuApi.kt @@ -9,6 +9,7 @@ import eu.kanade.tachiyomi.network.awaitSuccess import eu.kanade.tachiyomi.network.jsonMime import eu.kanade.tachiyomi.network.parseAs import eu.kanade.tachiyomi.util.system.withIOContext +import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.json.contentOrNull @@ -25,9 +26,12 @@ import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.RequestBody import okhttp3.RequestBody.Companion.toRequestBody +import uy.kohesive.injekt.injectLazy class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor) { + private val json: Json by injectLazy() + private val authClient = client.newBuilder().addInterceptor(interceptor).build() suspend fun addLibManga(track: Track, userId: String): Track { @@ -56,22 +60,25 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor) } } - authClient.newCall( - POST( - "${baseUrl}library-entries", - headers = headersOf( - "Content-Type", - "application/vnd.api+json", + with(json) { + authClient.newCall( + POST( + "${baseUrl}library-entries", + headers = headersOf( + "Content-Type", + "application/vnd.api+json", + ), + body = data.toString() + .toRequestBody("application/vnd.api+json".toMediaType()), ), - body = data.toString().toRequestBody("application/vnd.api+json".toMediaType()), - ), - ) - .awaitSuccess() - .parseAs() - .let { - track.media_id = it["data"]!!.jsonObject["id"]!!.jsonPrimitive.long - track - } + ) + .awaitSuccess() + .parseAs() + .let { + track.media_id = it["data"]!!.jsonObject["id"]!!.jsonPrimitive.long + track + } + } } } @@ -95,36 +102,42 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor) } } - authClient.newCall( - Request.Builder() - .url("${baseUrl}library-entries/${track.media_id}") - .headers( - headersOf( - "Content-Type", - "application/vnd.api+json", - ), - ) - .patch(data.toString().toRequestBody("application/vnd.api+json".toMediaType())) - .build(), - ) - .awaitSuccess() - .parseAs() - .let { - val manga = it["data"]?.jsonObject - if (manga != null) { - val startedAt = manga["attributes"]!!.jsonObject["startedAt"]?.jsonPrimitive?.contentOrNull - val finishedAt = manga["attributes"]!!.jsonObject["finishedAt"]?.jsonPrimitive?.contentOrNull - val startedDate = KitsuDateHelper.parse(startedAt) - if (track.started_reading_date <= 0L || startedDate > 0) { - track.started_reading_date = startedDate - } - val finishedDate = KitsuDateHelper.parse(finishedAt) - if (track.finished_reading_date <= 0L || finishedDate > 0) { - track.finished_reading_date = finishedDate + with(json) { + authClient.newCall( + Request.Builder() + .url("${baseUrl}library-entries/${track.media_id}") + .headers( + headersOf( + "Content-Type", + "application/vnd.api+json", + ), + ) + .patch( + data.toString().toRequestBody("application/vnd.api+json".toMediaType()), + ) + .build(), + ) + .awaitSuccess() + .parseAs() + .let { + val manga = it["data"]?.jsonObject + if (manga != null) { + val startedAt = + manga["attributes"]!!.jsonObject["startedAt"]?.jsonPrimitive?.contentOrNull + val finishedAt = + manga["attributes"]!!.jsonObject["finishedAt"]?.jsonPrimitive?.contentOrNull + val startedDate = KitsuDateHelper.parse(startedAt) + if (track.started_reading_date <= 0L || startedDate > 0) { + track.started_reading_date = startedDate + } + val finishedDate = KitsuDateHelper.parse(finishedAt) + if (track.finished_reading_date <= 0L || finishedDate > 0) { + track.finished_reading_date = finishedDate + } } + track } - track - } + } } } @@ -158,13 +171,15 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor) suspend fun search(query: String): List { return withIOContext { - authClient.newCall(GET(algoliaKeyUrl)) - .awaitSuccess() - .parseAs() - .let { - val key = it["media"]!!.jsonObject["key"]!!.jsonPrimitive.content - algoliaSearch(key, query) - } + with(json) { + authClient.newCall(GET(algoliaKeyUrl)) + .awaitSuccess() + .parseAs() + .let { + val key = it["media"]!!.jsonObject["key"]!!.jsonPrimitive.content + algoliaSearch(key, query) + } + } } } @@ -173,27 +188,28 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor) val jsonObject = buildJsonObject { put("params", "query=$query$algoliaFilter") } - - client.newCall( - POST( - algoliaUrl, - headers = headersOf( - "X-Algolia-Application-Id", - algoliaAppId, - "X-Algolia-API-Key", - key, + with(json) { + client.newCall( + POST( + algoliaUrl, + headers = headersOf( + "X-Algolia-Application-Id", + algoliaAppId, + "X-Algolia-API-Key", + key, + ), + body = jsonObject.toString().toRequestBody(jsonMime), ), - body = jsonObject.toString().toRequestBody(jsonMime), - ), - ) - .awaitSuccess() - .parseAs() - .let { - it["hits"]!!.jsonArray - .map { KitsuSearchManga(it.jsonObject) } - .filter { it.subType != "novel" } - .map { it.toTrack() } - } + ) + .awaitSuccess() + .parseAs() + .let { + it["hits"]!!.jsonArray + .map { KitsuSearchManga(it.jsonObject) } + .filter { it.subType != "novel" } + .map { it.toTrack() } + } + } } } @@ -203,18 +219,20 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor) .encodedQuery("filter[manga_id]=${track.media_id}&filter[user_id]=$userId") .appendQueryParameter("include", "manga") .build() - authClient.newCall(GET(url.toString())) - .awaitSuccess() - .parseAs() - .let { - val data = it["data"]!!.jsonArray - if (data.size > 0) { - val manga = it["included"]!!.jsonArray[0].jsonObject - KitsuLibManga(data[0].jsonObject, manga).toTrack() - } else { - null + with(json) { + authClient.newCall(GET(url.toString())) + .awaitSuccess() + .parseAs() + .let { + val data = it["data"]!!.jsonArray + if (data.size > 0) { + val manga = it["included"]!!.jsonArray[0].jsonObject + KitsuLibManga(data[0].jsonObject, manga).toTrack() + } else { + null + } } - } + } } } @@ -224,18 +242,20 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor) .encodedQuery("filter[id]=${track.media_id}") .appendQueryParameter("include", "manga") .build() - authClient.newCall(GET(url.toString())) - .awaitSuccess() - .parseAs() - .let { - val data = it["data"]!!.jsonArray - if (data.size > 0) { - val manga = it["included"]!!.jsonArray[0].jsonObject - KitsuLibManga(data[0].jsonObject, manga).toTrack() - } else { - throw Exception("Could not find manga") + with(json) { + authClient.newCall(GET(url.toString())) + .awaitSuccess() + .parseAs() + .let { + val data = it["data"]!!.jsonArray + if (data.size > 0) { + val manga = it["included"]!!.jsonArray[0].jsonObject + KitsuLibManga(data[0].jsonObject, manga).toTrack() + } else { + throw Exception("Could not find manga") + } } - } + } } } @@ -248,9 +268,11 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor) .add("client_id", clientId) .add("client_secret", clientSecret) .build() - client.newCall(POST(loginUrl, body = formBody)) - .awaitSuccess() - .parseAs() + with(json) { + client.newCall(POST(loginUrl, body = formBody)) + .awaitSuccess() + .parseAs() + } } } @@ -259,12 +281,14 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor) val url = "${baseUrl}users".toUri().buildUpon() .encodedQuery("filter[self]=true") .build() - authClient.newCall(GET(url.toString())) - .awaitSuccess() - .parseAs() - .let { - it["data"]!!.jsonArray[0].jsonObject["id"]!!.jsonPrimitive.content - } + with(json) { + authClient.newCall(GET(url.toString())) + .awaitSuccess() + .parseAs() + .let { + it["data"]!!.jsonArray[0].jsonObject["id"]!!.jsonPrimitive.content + } + } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/komga/KomgaApi.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/komga/KomgaApi.kt index 71cc56c8e3..075d09bb11 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/komga/KomgaApi.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/komga/KomgaApi.kt @@ -25,28 +25,41 @@ class KomgaApi(private val client: OkHttpClient) { suspend fun getTrackSearch(url: String): TrackSearch = withIOContext { try { - val track = if (url.contains(READLIST_API)) { - client.newCall(GET(url)) - .awaitSuccess() - .parseAs() - .toTrack() - } else { - client.newCall(GET(url)) - .awaitSuccess() - .parseAs() - .toTrack() - } - - val progress = client - .newCall(GET("${url.replace("/api/v1/series/", "/api/v2/series/")}/read-progress/tachiyomi")) - .awaitSuccess().let { - if (url.contains("/api/v1/series/")) { - it.parseAs() + val track = + with(json) { + if (url.contains(READLIST_API)) { + client.newCall(GET(url)) + .awaitSuccess() + .parseAs() + .toTrack() } else { - it.parseAs().toV2() + client.newCall(GET(url)) + .awaitSuccess() + .parseAs() + .toTrack() } } + val progress = with(json) { + client + .newCall( + GET( + "${ + url.replace( + "/api/v1/series/", + "/api/v2/series/", + ) + }/read-progress/tachiyomi", + ), + ) + .awaitSuccess().let { + if (url.contains("/api/v1/series/")) { + it.parseAs() + } else { + it.parseAs().toV2() + } + } + } track.apply { cover_url = "$url/thumbnail" tracking_url = url diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/MangaUpdatesApi.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/MangaUpdatesApi.kt index 603fd0f2c6..179a64f4a7 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/MangaUpdatesApi.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/MangaUpdatesApi.kt @@ -43,13 +43,15 @@ class MangaUpdatesApi( suspend fun getSeriesListItem(track: Track): Pair { val listItem = - authClient.newCall( - GET( - url = "$baseUrl/v1/lists/series/${track.media_id}", - ), - ) - .awaitSuccess() - .parseAs() + with(json) { + authClient.newCall( + GET( + url = "$baseUrl/v1/lists/series/${track.media_id}", + ), + ) + .awaitSuccess() + .parseAs() + } val rating = getSeriesRating(track) @@ -108,13 +110,15 @@ class MangaUpdatesApi( private suspend fun getSeriesRating(track: Track): Rating? { return try { - authClient.newCall( - GET( - url = "$baseUrl/v1/series/${track.media_id}/rating", - ), - ) - .awaitSuccess() - .parseAs() + with(json) { + authClient.newCall( + GET( + url = "$baseUrl/v1/series/${track.media_id}/rating", + ), + ) + .awaitSuccess() + .parseAs() + } } catch (e: Exception) { null } @@ -153,20 +157,22 @@ class MangaUpdatesApi( }, ) } - return client.newCall( - POST( - url = "$baseUrl/v1/series/search", - body = body.toString().toRequestBody(contentType), - ), - ) - .awaitSuccess() - .parseAs() - .let { obj -> - obj["results"]?.jsonArray?.map { element -> - json.decodeFromJsonElement(element.jsonObject["record"]!!) + return with(json) { + client.newCall( + POST( + url = "$baseUrl/v1/series/search", + body = body.toString().toRequestBody(contentType), + ), + ) + .awaitSuccess() + .parseAs() + .let { obj -> + obj["results"]?.jsonArray?.map { element -> + json.decodeFromJsonElement(element.jsonObject["record"]!!) + } } - } - .orEmpty() + .orEmpty() + } } suspend fun authenticate(username: String, password: String): Context? { @@ -174,21 +180,23 @@ class MangaUpdatesApi( put("username", username) put("password", password) } - return client.newCall( - PUT( - url = "$baseUrl/v1/account/login", - body = body.toString().toRequestBody(contentType), - ), - ) - .awaitSuccess() - .parseAs() - .let { obj -> - try { - json.decodeFromJsonElement(obj["context"]!!) - } catch (e: Exception) { - Timber.e(e) - null + return with(json) { + client.newCall( + PUT( + url = "$baseUrl/v1/account/login", + body = body.toString().toRequestBody(contentType), + ), + ) + .awaitSuccess() + .parseAs() + .let { obj -> + try { + json.decodeFromJsonElement(obj["context"]!!) + } catch (e: Exception) { + Timber.e(e) + null + } } - } + } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeListApi.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeListApi.kt index 4cac34f76e..7dd263d01f 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeListApi.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeListApi.kt @@ -13,6 +13,7 @@ import eu.kanade.tachiyomi.util.PkceUtil import eu.kanade.tachiyomi.util.system.withIOContext import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll +import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.boolean import kotlinx.serialization.json.contentOrNull @@ -28,12 +29,14 @@ import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.RequestBody import timber.log.Timber +import uy.kohesive.injekt.injectLazy import java.text.SimpleDateFormat import java.util.Locale class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListInterceptor) { private val authClient = client.newBuilder().addInterceptor(interceptor).build() + private val json: Json by injectLazy() suspend fun getAccessToken(authCode: String): OAuth { return withIOContext { @@ -43,9 +46,11 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI .add("code_verifier", codeVerifier) .add("grant_type", "authorization_code") .build() - client.newCall(POST("$baseOAuthUrl/token", body = formBody)) - .awaitSuccess() - .parseAs() + with(json) { + client.newCall(POST("$baseOAuthUrl/token", body = formBody)) + .awaitSuccess() + .parseAs() + } } } @@ -55,10 +60,12 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI .url("$baseApiUrl/users/@me") .get() .build() - authClient.newCall(request) - .awaitSuccess() - .parseAs() - .let { it["name"]!!.jsonPrimitive.content } + with(json) { + authClient.newCall(request) + .awaitSuccess() + .parseAs() + .let { it["name"]!!.jsonPrimitive.content } + } } } @@ -69,19 +76,21 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI .appendQueryParameter("q", query.take(64)) .appendQueryParameter("nsfw", "true") .build() - authClient.newCall(GET(url.toString())) - .awaitSuccess() - .parseAs() - .let { - it["data"]!!.jsonArray - .map { data -> data.jsonObject["node"]!!.jsonObject } - .map { node -> - val id = node["id"]!!.jsonPrimitive.int - async { getMangaDetails(id) } - } - .awaitAll() - .filter { trackSearch -> !trackSearch.publishing_type.contains("novel") } - } + with(json) { + authClient.newCall(GET(url.toString())) + .awaitSuccess() + .parseAs() + .let { + it["data"]!!.jsonArray + .map { data -> data.jsonObject["node"]!!.jsonObject } + .map { node -> + val id = node["id"]!!.jsonPrimitive.int + async { getMangaDetails(id) } + } + .awaitAll() + .filter { trackSearch -> !trackSearch.publishing_type.contains("novel") } + } + } } } @@ -91,28 +100,30 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI .appendPath(id.toString()) .appendQueryParameter("fields", "id,title,synopsis,num_chapters,main_picture,status,media_type,start_date") .build() - authClient.newCall(GET(url.toString())) - .awaitSuccess() - .parseAs() - .let { - val obj = it.jsonObject - TrackSearch.create(TrackManager.MYANIMELIST).apply { - media_id = obj["id"]!!.jsonPrimitive.long - title = obj["title"]!!.jsonPrimitive.content - summary = obj["synopsis"]?.jsonPrimitive?.content ?: "" - total_chapters = obj["num_chapters"]!!.jsonPrimitive.int - cover_url = obj["main_picture"]?.jsonObject?.get("large")?.jsonPrimitive?.content ?: "" - tracking_url = "https://myanimelist.net/manga/$media_id" - publishing_status = obj["status"]!!.jsonPrimitive.content.replace("_", " ") - publishing_type = obj["media_type"]!!.jsonPrimitive.content.replace("_", " ") - start_date = try { - val outputDf = SimpleDateFormat("yyyy-MM-dd", Locale.US) - outputDf.format(obj["start_date"]!!) - } catch (e: Exception) { - "" + with(json) { + authClient.newCall(GET(url.toString())) + .awaitSuccess() + .parseAs() + .let { + val obj = it.jsonObject + TrackSearch.create(TrackManager.MYANIMELIST).apply { + media_id = obj["id"]!!.jsonPrimitive.long + title = obj["title"]!!.jsonPrimitive.content + summary = obj["synopsis"]?.jsonPrimitive?.content ?: "" + total_chapters = obj["num_chapters"]!!.jsonPrimitive.int + cover_url = obj["main_picture"]?.jsonObject?.get("large")?.jsonPrimitive?.content ?: "" + tracking_url = "https://myanimelist.net/manga/$media_id" + publishing_status = obj["status"]!!.jsonPrimitive.content.replace("_", " ") + publishing_type = obj["media_type"]!!.jsonPrimitive.content.replace("_", " ") + start_date = try { + val outputDf = SimpleDateFormat("yyyy-MM-dd", Locale.US) + outputDf.format(obj["start_date"]!!) + } catch (e: Exception) { + "" + } } } - } + } } } @@ -134,10 +145,12 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI .url(mangaUrl(track.media_id).toString()) .put(formBodyBuilder.build()) .build() - authClient.newCall(request) - .awaitSuccess() - .parseAs() - .let { parseMangaItem(it, track) } + with(json) { + authClient.newCall(request) + .awaitSuccess() + .parseAs() + .let { parseMangaItem(it, track) } + } } } @@ -147,15 +160,17 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI .appendPath(track.media_id.toString()) .appendQueryParameter("fields", "num_chapters,my_list_status{start_date,finish_date}") .build() - authClient.newCall(GET(uri.toString())) - .awaitSuccess() - .parseAs() - .let { obj -> - track.total_chapters = obj["num_chapters"]!!.jsonPrimitive.int - obj.jsonObject["my_list_status"]?.jsonObject?.let { - parseMangaItem(it, track) + with(json) { + authClient.newCall(GET(uri.toString())) + .awaitSuccess() + .parseAs() + .let { obj -> + track.total_chapters = obj["num_chapters"]!!.jsonPrimitive.int + obj.jsonObject["my_list_status"]?.jsonObject?.let { + parseMangaItem(it, track) + } } - } + } } } @@ -199,9 +214,11 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI .url(urlBuilder.build().toString()) .get() .build() - authClient.newCall(request) - .awaitSuccess() - .parseAs() + with(json) { + authClient.newCall(request) + .awaitSuccess() + .parseAs() + } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeListInterceptor.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeListInterceptor.kt index 37ad63ce1c..7f0c2e4bc4 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeListInterceptor.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeListInterceptor.kt @@ -1,13 +1,17 @@ package eu.kanade.tachiyomi.data.track.myanimelist import eu.kanade.tachiyomi.network.parseAs +import kotlinx.serialization.json.Json import okhttp3.Interceptor import okhttp3.Response import okhttp3.internal.closeQuietly +import uy.kohesive.injekt.injectLazy import java.io.IOException class MyAnimeListInterceptor(private val myanimelist: MyAnimeList, private var token: String?) : Interceptor { + private val json: Json by injectLazy() + private var oauth: OAuth? = null override fun intercept(chain: Interceptor.Chain): Response { @@ -23,14 +27,16 @@ class MyAnimeListInterceptor(private val myanimelist: MyAnimeList, private var t if (oauth != null && (oauth!!.isExpired() || oauth!!.created_at == System.currentTimeMillis()) ) { - val newOauth = runCatching { - val oauthResponse = chain.proceed(MyAnimeListApi.refreshTokenRequest(oauth!!)) + val newOauth = with(json) { + runCatching { + val oauthResponse = chain.proceed(MyAnimeListApi.refreshTokenRequest(oauth!!)) - if (oauthResponse.isSuccessful) { - oauthResponse.parseAs() - } else { - oauthResponse.closeQuietly() - null + if (oauthResponse.isSuccessful) { + oauthResponse.parseAs() + } else { + oauthResponse.closeQuietly() + null + } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/ShikimoriApi.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/ShikimoriApi.kt index 8efe53577d..75314c1a26 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/ShikimoriApi.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/ShikimoriApi.kt @@ -11,6 +11,7 @@ import eu.kanade.tachiyomi.network.awaitSuccess import eu.kanade.tachiyomi.network.jsonMime import eu.kanade.tachiyomi.network.parseAs import eu.kanade.tachiyomi.util.system.withIOContext +import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonArray import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.buildJsonObject @@ -26,9 +27,12 @@ import okhttp3.FormBody import okhttp3.OkHttpClient import okhttp3.RequestBody.Companion.toRequestBody import timber.log.Timber +import uy.kohesive.injekt.injectLazy class ShikimoriApi(private val client: OkHttpClient, interceptor: ShikimoriInterceptor) { + private val json: Json by injectLazy() + private val authClient = client.newBuilder().addInterceptor(interceptor).build() suspend fun addLibManga(track: Track, user_id: String): Track { @@ -62,14 +66,16 @@ class ShikimoriApi(private val client: OkHttpClient, interceptor: ShikimoriInter .appendQueryParameter("search", search) .appendQueryParameter("limit", "20") .build() - authClient.newCall(GET(url.toString())) - .awaitSuccess() - .parseAs() - .let { response -> - response.map { - jsonToSearch(it.jsonObject) + with(json) { + authClient.newCall(GET(url.toString())) + .awaitSuccess() + .parseAs() + .let { response -> + response.map { + jsonToSearch(it.jsonObject) + } } - } + } } } @@ -120,9 +126,11 @@ class ShikimoriApi(private val client: OkHttpClient, interceptor: ShikimoriInter .appendQueryParameter("target_id", track.media_id.toString()) .appendQueryParameter("target_type", "Manga") .build() - return authClient.newCall(GET(url.toString())) - .execute() - .parseAs() + return with(json) { + authClient.newCall(GET(url.toString())) + .execute() + .parseAs() + } } suspend fun findLibManga(track: Track, user_id: String): Track? { @@ -130,9 +138,11 @@ class ShikimoriApi(private val client: OkHttpClient, interceptor: ShikimoriInter val urlMangas = "$apiUrl/mangas".toUri().buildUpon() .appendPath(track.media_id.toString()) .build() - val mangas = authClient.newCall(GET(urlMangas.toString())) - .awaitSuccess() - .parseAs() + val mangas = with(json) { + authClient.newCall(GET(urlMangas.toString())) + .awaitSuccess() + .parseAs() + } val entry = getUserRates(track, user_id) if (entry.size > 1) { @@ -146,20 +156,24 @@ class ShikimoriApi(private val client: OkHttpClient, interceptor: ShikimoriInter suspend fun getCurrentUser(): Int { return withIOContext { - authClient.newCall(GET("$apiUrl/users/whoami")) - .awaitSuccess() - .parseAs() - .let { - it["id"]!!.jsonPrimitive.int - } + with(json) { + authClient.newCall(GET("$apiUrl/users/whoami")) + .awaitSuccess() + .parseAs() + .let { + it["id"]!!.jsonPrimitive.int + } + } } } suspend fun accessToken(code: String): OAuth { return withIOContext { - client.newCall(accessTokenRequest(code)) - .awaitSuccess() - .parseAs() + with(json) { + client.newCall(accessTokenRequest(code)) + .awaitSuccess() + .parseAs() + } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/suwayomi/TachideskApi.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/suwayomi/TachideskApi.kt index 9844e23a14..c01be82015 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/suwayomi/TachideskApi.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/suwayomi/TachideskApi.kt @@ -11,6 +11,7 @@ import eu.kanade.tachiyomi.network.PUT import eu.kanade.tachiyomi.network.awaitSuccess import eu.kanade.tachiyomi.network.parseAs import eu.kanade.tachiyomi.util.system.withIOContext +import kotlinx.serialization.json.Json import okhttp3.Credentials import okhttp3.Dns import okhttp3.FormBody @@ -23,6 +24,7 @@ import java.nio.charset.Charset import java.security.MessageDigest class TachideskApi { + private val json: Json by injectLazy() private val network by injectLazy() val client: OkHttpClient = network.client.newBuilder() @@ -50,7 +52,9 @@ class TachideskApi { trackUrl } - val manga = client.newCall(GET("$url/full", headers)).awaitSuccess().parseAs() + val manga = with(json) { + client.newCall(GET("$url/full", headers)).awaitSuccess().parseAs() + } TrackSearch.create(TrackManager.SUWAYOMI).apply { title = manga.title @@ -70,7 +74,9 @@ class TachideskApi { suspend fun updateProgress(track: Track): Track { val url = track.tracking_url - val chapters = client.newCall(GET("$url/chapters", headers)).awaitSuccess().parseAs>() + val chapters = with(json) { + client.newCall(GET("$url/chapters", headers)).awaitSuccess().parseAs>() + } val lastChapterIndex = chapters.first { it.chapterNumber == track.last_chapter_read }.index client.newCall( diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/updater/AppUpdateChecker.kt b/app/src/main/java/eu/kanade/tachiyomi/data/updater/AppUpdateChecker.kt index d820a7d8c8..9ad9b2f8a0 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/updater/AppUpdateChecker.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/updater/AppUpdateChecker.kt @@ -10,12 +10,14 @@ import eu.kanade.tachiyomi.network.await import eu.kanade.tachiyomi.network.parseAs import eu.kanade.tachiyomi.util.system.localeContext import eu.kanade.tachiyomi.util.system.withIOContext +import kotlinx.serialization.json.Json import uy.kohesive.injekt.injectLazy import java.util.Date import java.util.concurrent.TimeUnit class AppUpdateChecker { + private val json: Json by injectLazy() private val networkService: NetworkHelper by injectLazy() private val preferences: PreferencesHelper by injectLazy() @@ -26,45 +28,48 @@ class AppUpdateChecker { } return withIOContext { - val result = if (preferences.checkForBetas().get()) { - networkService.client - .newCall(GET("https://api.github.com/repos/$GITHUB_REPO/releases")) - .await() - .parseAs>() - .let { githubReleases -> - val releases = githubReleases.take(10).filter { isNewVersion(it.version) } - // Check if any of the latest versions are newer than the current version - val release = releases - .maxWithOrNull { r1, r2 -> - when { - r1.version == r2.version -> 0 - isNewVersion(r2.version, r1.version) -> -1 - else -> 1 + val result = with(json) { + if (preferences.checkForBetas().get()) { + networkService.client + .newCall(GET("https://api.github.com/repos/$GITHUB_REPO/releases")) + .await() + .parseAs>() + .let { githubReleases -> + val releases = + githubReleases.take(10).filter { isNewVersion(it.version) } + // Check if any of the latest versions are newer than the current version + val release = releases + .maxWithOrNull { r1, r2 -> + when { + r1.version == r2.version -> 0 + isNewVersion(r2.version, r1.version) -> -1 + else -> 1 + } } + preferences.lastAppCheck().set(Date().time) + + if (release != null) { + AppUpdateResult.NewUpdate(release) + } else { + AppUpdateResult.NoNewUpdate } - preferences.lastAppCheck().set(Date().time) - - if (release != null) { - AppUpdateResult.NewUpdate(release) - } else { - AppUpdateResult.NoNewUpdate } - } - } else { - networkService.client - .newCall(GET("https://api.github.com/repos/$GITHUB_REPO/releases/latest")) - .await() - .parseAs() - .let { - preferences.lastAppCheck().set(Date().time) + } else { + networkService.client + .newCall(GET("https://api.github.com/repos/$GITHUB_REPO/releases/latest")) + .await() + .parseAs() + .let { + preferences.lastAppCheck().set(Date().time) - // Check if latest version is newer than the current version - if (isNewVersion(it.version)) { - AppUpdateResult.NewUpdate(it) - } else { - AppUpdateResult.NoNewUpdate + // Check if latest version is newer than the current version + if (isNewVersion(it.version)) { + AppUpdateResult.NewUpdate(it) + } else { + AppUpdateResult.NoNewUpdate + } } - } + } } if (doExtrasAfterNewUpdate && result is AppUpdateResult.NewUpdate) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/api/ExtensionGithubApi.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/api/ExtensionGithubApi.kt index 62ed005dca..f9c3ca9ba3 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/api/ExtensionGithubApi.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/api/ExtensionGithubApi.kt @@ -11,11 +11,13 @@ import eu.kanade.tachiyomi.network.await import eu.kanade.tachiyomi.network.parseAs import eu.kanade.tachiyomi.util.system.withIOContext import kotlinx.serialization.Serializable +import kotlinx.serialization.json.Json import timber.log.Timber import uy.kohesive.injekt.injectLazy internal class ExtensionGithubApi { + private val json: Json by injectLazy() private val networkService: NetworkHelper by injectLazy() private var requiresFallbackSource = false @@ -42,9 +44,11 @@ internal class ExtensionGithubApi { .await() } - val extensions = response - .parseAs>() - .toExtensions() + val extensions = with(json) { + response + .parseAs>() + .toExtensions() + } // Sanity check - a small number of extensions probably means something broke // with the repo generator 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 45d40e4509..aaedfa5167 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 @@ -32,6 +32,7 @@ sealed class Extension { val hasUpdate: Boolean = false, val isObsolete: Boolean = false, val isUnofficial: Boolean = false, + val isShared: Boolean, ) : Extension() data class Available( diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/model/LoadResult.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/model/LoadResult.kt index 0cf470fe85..bb1194871e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/model/LoadResult.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/model/LoadResult.kt @@ -1,10 +1,8 @@ package eu.kanade.tachiyomi.extension.model -sealed class LoadResult { +sealed interface LoadResult { - class Success(val extension: Extension.Installed) : LoadResult() - class Untrusted(val extension: Extension.Untrusted) : LoadResult() - class Error(val message: String? = null) : LoadResult() { - constructor(exception: Throwable) : this(exception.message) - } + data class Success(val extension: Extension.Installed) : LoadResult + data class Untrusted(val extension: Extension.Untrusted) : LoadResult + data object Error : LoadResult } diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionInstallReceiver.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionInstallReceiver.kt index a06323e547..14c0914caf 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionInstallReceiver.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionInstallReceiver.kt @@ -97,7 +97,7 @@ internal class ExtensionInstallReceiver(private val listener: Listener) : */ private suspend fun getExtensionFromIntent(context: Context, intent: Intent?): LoadResult { val pkgName = getPackageNameFromIntent(intent) - ?: return LoadResult.Error("Package name not found") + ?: return LoadResult.Error return GlobalScope.async(Dispatchers.Default, CoroutineStart.DEFAULT) { ExtensionLoader.loadExtensionFromPkgName(context, pkgName) }.await() } 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 70a27d2e02..ff2bc0db7d 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 @@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.extension.util import android.annotation.SuppressLint import android.content.Context +import android.content.pm.ApplicationInfo import android.content.pm.PackageInfo import android.content.pm.PackageManager import android.os.Build @@ -15,8 +16,8 @@ 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 -import eu.kanade.tachiyomi.util.system.getApplicationIcon import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll import kotlinx.coroutines.runBlocking import timber.log.Timber import uy.kohesive.injekt.injectLazy @@ -41,7 +42,11 @@ internal object ExtensionLoader { const val LIB_VERSION_MIN = 1.3 const val LIB_VERSION_MAX = 1.5 - private const val PACKAGE_FLAGS = PackageManager.GET_CONFIGURATIONS or PackageManager.GET_SIGNATURES + @Suppress("DEPRECATION") + private val PACKAGE_FLAGS = PackageManager.GET_CONFIGURATIONS or + PackageManager.GET_META_DATA or + PackageManager.GET_SIGNATURES or + (if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) PackageManager.GET_SIGNING_CERTIFICATES else 0) // inorichi's key private const val officialSignature = "7ce04da7773d41b489f4693a366c36bcd0a11fc39b547168553c285bd7348e23" @@ -49,7 +54,7 @@ internal object ExtensionLoader { /** * List of the trusted signatures. */ - var trustedSignatures = mutableSetOf() + preferences.trustedSignatures().get() + officialSignature + var trustedSignatures = mutableSetOf(officialSignature) + preferences.trustedSignatures().get() /** * Return a list of all the installed extensions initialized concurrently. @@ -58,17 +63,50 @@ internal object ExtensionLoader { */ fun loadExtensions(context: Context): List { val pkgManager = context.packageManager - val installedPkgs = pkgManager.getInstalledPackages(PACKAGE_FLAGS) - val extPkgs = installedPkgs.filter { isPackageAnExtension(it) } + + val installedPkgs = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + pkgManager.getInstalledPackages(PackageManager.PackageInfoFlags.of(PACKAGE_FLAGS.toLong())) + } else { + pkgManager.getInstalledPackages(PACKAGE_FLAGS) + } + + val sharedExtPkgs = installedPkgs + .asSequence() + .filter { isPackageAnExtension(it) } + .map { ExtensionInfo(packageInfo = it, isShared = true) } + +// val privateExtPkgs = getPrivateExtensionDir(context) +// .listFiles() +// ?.asSequence() +// ?.filter { it.isFile && it.extension == PRIVATE_EXTENSION_EXTENSION } +// ?.mapNotNull { +// val path = it.absolutePath +// pkgManager.getPackageArchiveInfo(path, PACKAGE_FLAGS) +// ?.apply { applicationInfo.fixBasePaths(path) } +// } +// ?.filter { isPackageAnExtension(it) } +// ?.map { ExtensionInfo(packageInfo = it, isShared = false) } +// ?: emptySequence() + + val extPkgs = (sharedExtPkgs) + // Remove duplicates. Shared takes priority than private by default + .distinctBy { it.packageInfo.packageName } + // Compare version number +// .mapNotNull { sharedPkg -> +// val privatePkg = privateExtPkgs +// .singleOrNull { it.packageInfo.packageName == sharedPkg.packageInfo.packageName } +// selectExtensionPackage(sharedPkg, privatePkg) +// } + .toList() if (extPkgs.isEmpty()) return emptyList() // Load each extension concurrently and wait for completion return runBlocking { val deferred = extPkgs.map { - async { loadExtension(context, it.packageName, it) } + async { loadExtension(context, it) } } - deferred.map { it.await() } + deferred.awaitAll() } } @@ -77,16 +115,48 @@ internal object ExtensionLoader { * contains the required feature flag before trying to load it. */ fun loadExtensionFromPkgName(context: Context, pkgName: String): LoadResult { - val pkgInfo = try { + val extensionPackage = getExtensionInfoFromPkgName(context, pkgName) + if (extensionPackage == null) { + Timber.e("Extension package is not found ($pkgName)") + return LoadResult.Error + } + return loadExtension(context, extensionPackage) + } + + fun getExtensionPackageInfoFromPkgName(context: Context, pkgName: String): PackageInfo? { + return getExtensionInfoFromPkgName(context, pkgName)?.packageInfo + } + + private fun getExtensionInfoFromPkgName(context: Context, pkgName: String): ExtensionInfo? { +// val privateExtensionFile = File(getPrivateExtensionDir(context), "$pkgName.$PRIVATE_EXTENSION_EXTENSION") +// val privatePkg = if (privateExtensionFile.isFile) { +// context.packageManager.getPackageArchiveInfo(privateExtensionFile.absolutePath, PACKAGE_FLAGS) +// ?.takeIf { isPackageAnExtension(it) } +// ?.let { +// it.applicationInfo.fixBasePaths(privateExtensionFile.absolutePath) +// ExtensionInfo( +// packageInfo = it, +// isShared = false, +// ) +// } +// } else { +// null +// } + + val sharedPkg = try { context.packageManager.getPackageInfo(pkgName, PACKAGE_FLAGS) + .takeIf { isPackageAnExtension(it) } + ?.let { + ExtensionInfo( + packageInfo = it, + isShared = true, + ) + } } catch (error: PackageManager.NameNotFoundException) { - // Unlikely, but the package may have been uninstalled at this point - return LoadResult.Error(error) + null } - if (!isPackageAnExtension(pkgInfo)) { - return LoadResult.Error("Tried to load a package that wasn't a extension") - } - return loadExtension(context, pkgName, pkgInfo) + + return sharedPkg // selectExtensionPackage(sharedPkg, privatePkg) } fun isExtensionInstalledByApp(context: Context, pkgName: String): Boolean { @@ -102,56 +172,56 @@ internal object ExtensionLoader { } /** - * Loads an extension given its package name. + * Loads an extension * * @param context The application context. - * @param pkgName The package name of the extension to load. - * @param pkgInfo The package info of the extension. + * @param extensionInfo The extension to load. */ - private fun loadExtension(context: Context, pkgName: String, pkgInfo: PackageInfo): LoadResult { + private fun loadExtension(context: Context, extensionInfo: ExtensionInfo): LoadResult { val pkgManager = context.packageManager - - val appInfo = try { - pkgManager.getApplicationInfo(pkgName, PackageManager.GET_META_DATA) - } catch (error: PackageManager.NameNotFoundException) { - // Unlikely, but the package may have been uninstalled at this point - return LoadResult.Error(error) - } + val pkgInfo = extensionInfo.packageInfo + val appInfo = pkgInfo.applicationInfo + val pkgName = pkgInfo.packageName val extName = pkgManager.getApplicationLabel(appInfo).toString().substringAfter("Tachiyomi: ") val versionName = pkgInfo.versionName val versionCode = PackageInfoCompat.getLongVersionCode(pkgInfo) if (versionName.isNullOrEmpty()) { - val exception = Exception("Missing versionName for extension $extName") - Timber.w(exception) - return LoadResult.Error(exception) + Timber.w("Missing versionName for extension $extName") + return LoadResult.Error } // Validate lib version - val libVersion = versionName.substringBeforeLast('.').toDouble() - if (libVersion < LIB_VERSION_MIN || libVersion > LIB_VERSION_MAX) { - val exception = Exception( - "Lib version is $libVersion, while only versions " + - "$LIB_VERSION_MIN to $LIB_VERSION_MAX are allowed", + val libVersion = versionName.substringBeforeLast('.').toDoubleOrNull() + if (libVersion == null || libVersion < LIB_VERSION_MIN || libVersion > LIB_VERSION_MAX) { + Timber.w( + "Lib version is $libVersion, while only versions $LIB_VERSION_MIN to $LIB_VERSION_MAX are allowed", ) - Timber.w(exception) - return LoadResult.Error(exception) + return LoadResult.Error } - val signatureHash = getSignatureHash(pkgInfo) - - if (signatureHash == null) { - return LoadResult.Error("Package $pkgName isn't signed") - } else if (signatureHash !in trustedSignatures) { - val extension = Extension.Untrusted(extName, pkgName, versionName, versionCode, libVersion, signatureHash) + val signatures = getSignatures(pkgInfo) + if (signatures.isNullOrEmpty()) { + Timber.w("Package $pkgName isn't signed") + return LoadResult.Error + } else if (!hasTrustedSignature(signatures)) { + val extension = Extension.Untrusted( + extName, + pkgName, + versionName, + versionCode, + libVersion, + signatures.last(), + ) Timber.w("Extension $pkgName isn't trusted") return LoadResult.Untrusted(extension) } val isNsfw = appInfo.metaData.getInt(METADATA_NSFW) == 1 if (!loadNsfwSource && isNsfw) { - return LoadResult.Error("NSFW extension $pkgName not allowed") + Timber.w("NSFW extension $pkgName not allowed") + return LoadResult.Error } val hasReadme = appInfo.metaData.getInt(METADATA_HAS_README, 0) == 1 @@ -171,14 +241,14 @@ internal object ExtensionLoader { } .flatMap { try { - when (val obj = Class.forName(it, false, classLoader).newInstance()) { + when (val obj = Class.forName(it, false, classLoader).getDeclaredConstructor().newInstance()) { is Source -> listOf(obj) is SourceFactory -> obj.createSources() else -> throw Exception("Unknown source class type! ${obj.javaClass}") } } catch (e: Throwable) { Timber.e(e, "Extension load error: $extName.") - return LoadResult.Error(e) + return LoadResult.Error } } @@ -203,12 +273,35 @@ internal object ExtensionLoader { hasChangelog = hasChangelog, sources = sources, pkgFactory = appInfo.metaData.getString(METADATA_SOURCE_FACTORY), - isUnofficial = signatureHash != officialSignature, - icon = context.getApplicationIcon(pkgName), + isUnofficial = !isOfficiallySigned(signatures), + icon = appInfo.loadIcon(pkgManager), + isShared = extensionInfo.isShared, ) return LoadResult.Success(extension) } + /** + * Choose which extension package to use based on version code + * + * @param shared extension installed to system + * @param private extension installed to data directory + */ + private fun selectExtensionPackage(shared: ExtensionInfo?, private: ExtensionInfo?): ExtensionInfo? { + when { + private == null && shared != null -> return shared + shared == null && private != null -> return private + shared == null && private == null -> return null + } + + return if (PackageInfoCompat.getLongVersionCode(shared!!.packageInfo) >= + PackageInfoCompat.getLongVersionCode(private!!.packageInfo) + ) { + shared + } else { + private + } + } + fun isPackageNameAnExtension(packageManager: PackageManager, pkgName: String): Boolean = isPackageAnExtension(packageManager.getPackageInfo(pkgName, PACKAGE_FLAGS)) @@ -221,16 +314,50 @@ internal object ExtensionLoader { pkgInfo.reqFeatures.orEmpty().any { it.name == EXTENSION_FEATURE } /** - * Returns the signature hash of the package or null if it's not signed. + * Returns the signatures of the package or null if it's not signed. * * @param pkgInfo The package info of the application. + * @return List SHA256 digest of the signatures */ - private fun getSignatureHash(pkgInfo: PackageInfo): String? { - val signatures = pkgInfo.signatures - return if (signatures != null && signatures.isNotEmpty()) { - Hash.sha256(signatures.first().toByteArray()) + private fun getSignatures(pkgInfo: PackageInfo): List? { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + val signingInfo = pkgInfo.signingInfo + if (signingInfo.hasMultipleSigners()) { + signingInfo.apkContentsSigners + } else { + signingInfo.signingCertificateHistory + } } else { - null + @Suppress("DEPRECATION") + pkgInfo.signatures + } + ?.map { Hash.sha256(it.toByteArray()) } + ?.toList() + } + + private fun hasTrustedSignature(signatures: List): Boolean { + return trustedSignatures.any { signatures.contains(it) } + } + + private fun isOfficiallySigned(signatures: List): Boolean { + return signatures.all { it == officialSignature } + } + + /** + * On Android 13+ the ApplicationInfo generated by getPackageArchiveInfo doesn't + * have sourceDir which breaks assets loading (used for getting icon here). + */ + private fun ApplicationInfo.fixBasePaths(apkPath: String) { + if (sourceDir == null) { + sourceDir = apkPath + } + if (publicSourceDir == null) { + publicSourceDir = apkPath } } + + private data class ExtensionInfo( + val packageInfo: PackageInfo, + val isShared: Boolean, + ) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/network/OkHttpExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/network/OkHttpExtensions.kt index f651317dc9..e6eec02f4a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/network/OkHttpExtensions.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/network/OkHttpExtensions.kt @@ -3,7 +3,6 @@ package eu.kanade.tachiyomi.network import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.serialization.DeserializationStrategy -import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.json.Json import kotlinx.serialization.json.okio.decodeFromBufferedSource import kotlinx.serialization.serializer @@ -16,8 +15,6 @@ import okhttp3.Response import rx.Observable import rx.Producer import rx.Subscription -import uy.kohesive.injekt.Injekt -import uy.kohesive.injekt.api.get import java.io.IOException import java.util.concurrent.atomic.AtomicBoolean import kotlin.coroutines.resumeWithException @@ -61,6 +58,15 @@ fun Call.asObservable(): Observable { } } +fun Call.asObservableSuccess(): Observable { + return asObservable().doOnNext { response -> + if (!response.isSuccessful) { + response.close() + throw HttpException(response.code) + } + } +} + // Based on https://github.com/gildor/kotlin-coroutines-okhttp @OptIn(ExperimentalCoroutinesApi::class) private suspend fun Call.await(callStack: Array): Response { @@ -98,6 +104,9 @@ suspend fun Call.await(): Response { return await(callStack) } +/** + * @since extensions-lib 1.5 + */ suspend fun Call.awaitSuccess(): Response { val callStack = Exception().stackTrace.run { copyOfRange(1, size) } val response = await(callStack) @@ -108,15 +117,6 @@ suspend fun Call.awaitSuccess(): Response { return response } -fun Call.asObservableSuccess(): Observable { - return asObservable().doOnNext { response -> - if (!response.isSuccessful) { - response.close() - throw HttpException(response.code) - } - } -} - fun OkHttpClient.newCachelessCallWithProgress(request: Request, listener: ProgressListener): Call { val progressClient = newBuilder() .cache(null) @@ -131,15 +131,26 @@ fun OkHttpClient.newCachelessCallWithProgress(request: Request, listener: Progre return progressClient.newCall(request) } +context(Json) inline fun Response.parseAs(): T { return decodeFromJsonResponse(serializer(), this) } -@OptIn(ExperimentalSerializationApi::class) -fun decodeFromJsonResponse(deserializer: DeserializationStrategy, response: Response): T { +context(Json) +fun decodeFromJsonResponse( + deserializer: DeserializationStrategy, + response: Response, +): T { return response.body.source().use { - Injekt.get().decodeFromBufferedSource(deserializer, it) + decodeFromBufferedSource(deserializer, it) } } +/** + * Exception that handles HTTP codes considered not successful by OkHttp. + * Use it to have a standardized error message in the app across the extensions. + * + * @since extensions-lib 1.5 + * @param code [Int] the HTTP status code + */ class HttpException(val code: Int) : IllegalStateException("HTTP error $code") diff --git a/app/src/main/java/eu/kanade/tachiyomi/smartsearch/SmartSearchEngine.kt b/app/src/main/java/eu/kanade/tachiyomi/smartsearch/SmartSearchEngine.kt index 33bb602a3b..56bce3f318 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/smartsearch/SmartSearchEngine.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/smartsearch/SmartSearchEngine.kt @@ -5,13 +5,11 @@ import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.source.CatalogueSource import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.util.lang.toNormalized -import eu.kanade.tachiyomi.util.system.await import info.debatty.java.stringsimilarity.NormalizedLevenshtein import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.supervisorScope -import rx.schedulers.Schedulers import uy.kohesive.injekt.injectLazy import kotlin.coroutines.CoroutineContext @@ -37,8 +35,7 @@ class SmartSearchEngine( "$query ${extraSearchParams.trim()}" } else query - val searchResults = source.fetchSearchManga(1, builtQuery, FilterList()) - .toSingle().await(Schedulers.io()) + val searchResults = source.getSearchManga(1, builtQuery, FilterList()) searchResults.mangas.map { val cleanedMangaTitle = cleanSmartSearchTitle(it.title) @@ -63,8 +60,7 @@ class SmartSearchEngine( titleNormalized } val searchResults = - source.fetchSearchManga(1, searchQuery, source.getFilterList()).toSingle() - .await(Schedulers.io()) + source.getSearchManga(1, searchQuery, source.getFilterList()) if (searchResults.mangas.size == 1) { return@supervisorScope listOf(SearchEntry(searchResults.mangas.first(), 0.0)) diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/CatalogueSource.kt b/app/src/main/java/eu/kanade/tachiyomi/source/CatalogueSource.kt index f9e416def6..01cc4a4fc9 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/source/CatalogueSource.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/source/CatalogueSource.kt @@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.source import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.MangasPage +import eu.kanade.tachiyomi.util.system.awaitSingle import rx.Observable interface CatalogueSource : Source { @@ -17,30 +18,63 @@ interface CatalogueSource : Source { val supportsLatest: Boolean /** - * Returns an observable containing a page with a list of manga. + * Get a page with a list of manga. * + * @since extensions-lib 1.5 * @param page the page number to retrieve. */ - fun fetchPopularManga(page: Int): Observable + @Suppress("DEPRECATION") + suspend fun getPopularManga(page: Int): MangasPage { + return fetchPopularManga(page).awaitSingle() + } /** - * Returns an observable containing a page with a list of manga. + * Get a page with a list of manga. * + * @since extensions-lib 1.5 * @param page the page number to retrieve. * @param query the search query. * @param filters the list of filters to apply. */ - fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable + @Suppress("DEPRECATION") + suspend fun getSearchManga(page: Int, query: String, filters: FilterList): MangasPage { + return fetchSearchManga(page, query, filters).awaitSingle() + } /** - * Returns an observable containing a page with a list of latest manga updates. + * Get a page with a list of latest manga updates. * + * @since extensions-lib 1.5 * @param page the page number to retrieve. */ - fun fetchLatestUpdates(page: Int): Observable + @Suppress("DEPRECATION") + suspend fun getLatestUpdates(page: Int): MangasPage { + return fetchLatestUpdates(page).awaitSingle() + } /** * Returns the list of filters for the source. */ fun getFilterList(): FilterList + + @Deprecated( + "Use the non-RxJava API instead", + ReplaceWith("getPopularManga"), + ) + fun fetchPopularManga(page: Int): Observable = + throw IllegalStateException("Not used") + + @Deprecated( + "Use the non-RxJava API instead", + ReplaceWith("getSearchManga"), + ) + fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable = + throw IllegalStateException("Not used") + + @Deprecated( + "Use the non-RxJava API instead", + ReplaceWith("getLatestUpdates"), + ) + fun fetchLatestUpdates(page: Int): Observable = + throw IllegalStateException("Not used") } diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/ConfigurableSource.kt b/app/src/main/java/eu/kanade/tachiyomi/source/ConfigurableSource.kt index 4f5154fe52..b74c96af7e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/source/ConfigurableSource.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/source/ConfigurableSource.kt @@ -1,8 +1,25 @@ package eu.kanade.tachiyomi.source +import android.app.Application +import android.content.Context +import android.content.SharedPreferences import androidx.preference.PreferenceScreen +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get interface ConfigurableSource : Source { + /** + * Gets instance of [SharedPreferences] scoped to the specific source. + * + * @since extensions-lib 1.5 + */ + fun getSourcePreferences(): SharedPreferences = + Injekt.get().getSharedPreferences(preferenceKey(), Context.MODE_PRIVATE) + fun setupPreferenceScreen(screen: PreferenceScreen) } + +// TODO: use getSourcePreferences once all extensions are on ext-lib 1.5 +fun ConfigurableSource.sourcePreferences(): SharedPreferences = + Injekt.get().getSharedPreferences(preferenceKey(), Context.MODE_PRIVATE) diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/LocalSource.kt b/app/src/main/java/eu/kanade/tachiyomi/source/LocalSource.kt index 2fb0ea116d..8a0303dfa9 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/source/LocalSource.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/source/LocalSource.kt @@ -18,7 +18,6 @@ import kotlinx.serialization.Serializable import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import kotlinx.serialization.json.decodeFromStream -import rx.Observable import timber.log.Timber import uy.kohesive.injekt.injectLazy import java.io.File @@ -101,13 +100,13 @@ class LocalSource(private val context: Context) : CatalogueSource, UnmeteredSour override fun toString() = name - override fun fetchPopularManga(page: Int) = fetchSearchManga(page, "", popularFilters) + override suspend fun getPopularManga(page: Int) = getSearchManga(page, "", popularFilters) - override fun fetchSearchManga( + override suspend fun getSearchManga( page: Int, query: String, filters: FilterList, - ): Observable { + ): MangasPage { val baseDirs = getBaseDirectories(context) val time = @@ -179,10 +178,10 @@ class LocalSource(private val context: Context) : CatalogueSource, UnmeteredSour } } - return Observable.just(MangasPage(mangas.toList(), false)) + return MangasPage(mangas.toList(), false) } - override fun fetchLatestUpdates(page: Int) = fetchSearchManga(page, "", latestFilters) + override suspend fun getLatestUpdates(page: Int) = getSearchManga(page, "", latestFilters) override suspend fun getMangaDetails(manga: SManga): SManga { val localDetails = getBaseDirectories(context) diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/Source.kt b/app/src/main/java/eu/kanade/tachiyomi/source/Source.kt index 0c08c41644..b2167ee94c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/source/Source.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/source/Source.kt @@ -12,12 +12,12 @@ import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get /** - * A basic interface for creating a source. It could be an online source, a local source, etc... + * A basic interface for creating a source. It could be an online source, a local source, etc. */ interface Source { /** - * Id for the source. Must be unique. + * ID for the source. Must be unique. */ val id: Long @@ -30,42 +30,11 @@ interface Source { get() = "" /** - * Returns an observable with the updated details for a manga. + * Get the updated details for a manga. * + * @since extensions-lib 1.5 * @param manga the manga to update. - */ - @Deprecated( - "Use the 1.x API instead", - ReplaceWith("getMangaDetails"), - ) - fun fetchMangaDetails(manga: SManga): Observable = throw IllegalStateException("Not used") - - /** - * Returns an observable with all the available chapters for a manga. - * - * @param manga the manga to update. - */ - @Deprecated( - "Use the 1.x API instead", - ReplaceWith("getChapterList"), - ) - fun fetchChapterList(manga: SManga): Observable> = throw IllegalStateException("Not used") - - // TODO: remove direct usages on this method - - /** - * Returns an observable with the list of pages a chapter has. - * - * @param chapter the chapter. - */ - @Deprecated( - "Use the 1.x API instead", - ReplaceWith("getPageList"), - ) - fun fetchPageList(chapter: SChapter): Observable> = Observable.empty() - - /** - * [1.x API] Get the updated details for a manga. + * @return the updated manga. */ @Suppress("DEPRECATION") suspend fun getMangaDetails(manga: SManga): SManga { @@ -73,7 +42,11 @@ interface Source { } /** - * [1.x API] Get all the available chapters for a manga. + * Get all the available chapters for a manga. + * + * @since extensions-lib 1.5 + * @param manga the manga to update. + * @return the chapters for the manga. */ @Suppress("DEPRECATION") suspend fun getChapterList(manga: SManga): List { @@ -81,7 +54,12 @@ interface Source { } /** - * [1.x API] Get the list of pages a chapter has. + * 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 { @@ -100,8 +78,29 @@ interface Source { fun nameBasedOnEnabledLanguages(enabledLanguages: Set, extensionManager: ExtensionManager? = null): String { return if (includeLangInName(enabledLanguages, extensionManager)) toString() else name } + + @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") } fun Source.icon(): Drawable? = Injekt.get().getAppIconForSource(this) -fun Source.getPreferenceKey(): String = "source_$id" +fun Source.preferenceKey(): String = "source_$id" diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/SourceManager.kt b/app/src/main/java/eu/kanade/tachiyomi/source/SourceManager.kt index 045ea680e6..e4f7464e5f 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/source/SourceManager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/source/SourceManager.kt @@ -21,7 +21,6 @@ import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking -import rx.Observable import java.util.concurrent.ConcurrentHashMap class SourceManager( @@ -116,38 +115,20 @@ class SourceManager( override val name: String get() = extensionManager.getStubSource(id)?.name ?: id.toString() - override suspend fun getMangaDetails(manga: SManga): SManga { + override suspend fun getMangaDetails(manga: SManga): SManga = throw getSourceNotInstalledException() - } - @Deprecated("Use the 1.x API instead", replaceWith = ReplaceWith("getMangaDetails")) - override fun fetchMangaDetails(manga: SManga): Observable { - return Observable.error(getSourceNotInstalledException()) - } - - override suspend fun getChapterList(manga: SManga): List { + override suspend fun getChapterList(manga: SManga): List = throw getSourceNotInstalledException() - } - @Deprecated("Use the 1.x API instead", replaceWith = ReplaceWith("getChapterList")) - override fun fetchChapterList(manga: SManga): Observable> { - return Observable.error(getSourceNotInstalledException()) - } - - override suspend fun getPageList(chapter: SChapter): List { + override suspend fun getPageList(chapter: SChapter): List = throw getSourceNotInstalledException() - } - - @Deprecated("Use the 1.x API instead", replaceWith = ReplaceWith("getPageList")) - override fun fetchPageList(chapter: SChapter): Observable> { - return Observable.error(getSourceNotInstalledException()) - } override fun toString(): String { return name } - fun getSourceNotInstalledException(): Exception { + private fun getSourceNotInstalledException(): Exception { return SourceNotFoundException( context.getString( R.string.source_not_installed_, diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/online/HttpSource.kt b/app/src/main/java/eu/kanade/tachiyomi/source/online/HttpSource.kt index 683aadd288..5fc6ba155b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/source/online/HttpSource.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/source/online/HttpSource.kt @@ -14,6 +14,7 @@ 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.lang.getUrlWithoutDomain +import eu.kanade.tachiyomi.util.system.awaitSingle import okhttp3.Headers import okhttp3.OkHttpClient import okhttp3.Request @@ -25,11 +26,11 @@ import uy.kohesive.injekt.injectLazy import java.net.URI import java.net.URISyntaxException import java.security.MessageDigest -import java.util.Locale /** * A simple implementation for sources from a website. */ +@Suppress("unused") abstract class HttpSource : CatalogueSource { /** @@ -49,15 +50,16 @@ abstract class HttpSource : CatalogueSource { open val versionId = 1 /** - * Id of the source. By default it uses a generated id using the first 16 characters (64 bits) - * of the MD5 of the string: sourcename/language/versionId - * Note the generated id sets the sign bit to 0. + * ID of the source. By default it uses a generated id using the first 16 characters (64 bits) + * of the MD5 of the string `"${name.lowercase()}/$lang/$versionId"`. + * + * The ID is generated by the [generateId] function, which can be reused if needed + * to generate outdated IDs for cases where the source name or language needs to + * be changed but migrations can be avoided. + * + * Note: the generated ID sets the sign bit to `0`. */ - override val id by lazy { - val key = "${name.lowercase(Locale.getDefault())}/$lang/$versionId" - val bytes = MessageDigest.getInstance("MD5").digest(key.toByteArray()) - (0..7).map { bytes[it].toLong() and 0xff shl 8 * (7 - it) }.reduce(Long::or) and Long.MAX_VALUE - } + override val id by lazy { generateId(name, lang, versionId) } /** * Headers used for requests. @@ -70,6 +72,29 @@ abstract class HttpSource : CatalogueSource { open val client: OkHttpClient get() = network.client + /** + * Generates a unique ID for the source based on the provided [name], [lang] and + * [versionId]. It will use the first 16 characters (64 bits) of the MD5 of the string + * `"${name.lowercase()}/$lang/$versionId"`. + * + * Note: the generated ID sets the sign bit to `0`. + * + * Can be used to generate outdated IDs, such as when the source name or language + * needs to be changed but migrations can be avoided. + * + * @since extensions-lib 1.5 + * @param name [String] the name of the source + * @param lang [String] the language of the source + * @param versionId [Int] the version ID of the source + * @return a unique ID for the source + */ + @Suppress("MemberVisibilityCanBePrivate") + protected fun generateId(name: String, lang: String, versionId: Int): Long { + val key = "${name.lowercase()}/$lang/$versionId" + val bytes = MessageDigest.getInstance("MD5").digest(key.toByteArray()) + return (0..7).map { bytes[it].toLong() and 0xff shl 8 * (7 - it) }.reduce(Long::or) and Long.MAX_VALUE + } + /** * Headers builder for requests. Implementations can override this method for custom headers. */ @@ -80,7 +105,7 @@ abstract class HttpSource : CatalogueSource { /** * Visible name of the source. */ - override fun toString() = "$name (${lang.uppercase(Locale.getDefault())})" + override fun toString() = "$name (${lang.uppercase()})" /** * Returns an observable containing a page with a list of manga. Normally it's not needed to @@ -124,9 +149,20 @@ abstract class HttpSource : CatalogueSource { * @param query the search query. * @param filters the list of filters to apply. */ - override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable { - return client.newCall(searchMangaRequest(page, query, filters)) - .asObservableSuccess() + override fun fetchSearchManga( + page: Int, + query: String, + filters: FilterList, + ): Observable { + return Observable.defer { + try { + client.newCall(searchMangaRequest(page, query, filters)).asObservableSuccess() + } catch (e: NoClassDefFoundError) { + // RxJava doesn't handle Errors, which tends to happen during global searches + // if an old extension using non-existent classes is still around + throw RuntimeException(e) + } + } .map { response -> searchMangaParse(response) } @@ -139,7 +175,11 @@ abstract class HttpSource : CatalogueSource { * @param query the search query. * @param filters the list of filters to apply. */ - protected abstract fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request + protected abstract fun searchMangaRequest( + page: Int, + query: String, + filters: FilterList, + ): Request /** * Parses the response from the site and returns a [MangasPage] object. @@ -176,11 +216,18 @@ abstract class HttpSource : CatalogueSource { protected abstract fun latestUpdatesParse(response: Response): MangasPage /** - * Returns an observable with the updated details for a manga. Normally it's not needed to - * override this method. + * Get the updated details for a manga. + * Normally it's not needed to override this method. * - * @param manga the manga to be updated. + * @param manga the manga to update. + * @return the updated manga. */ + @Suppress("DEPRECATION") + override suspend fun getMangaDetails(manga: SManga): SManga { + return fetchMangaDetails(manga).awaitSingle() + } + + @Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getMangaDetails")) override fun fetchMangaDetails(manga: SManga): Observable { return client.newCall(mangaDetailsRequest(manga)) .asObservableSuccess() @@ -207,11 +254,23 @@ abstract class HttpSource : CatalogueSource { protected abstract fun mangaDetailsParse(response: Response): SManga /** - * Returns an observable with the updated chapter list for a manga. Normally it's not needed to - * override this method. If a manga is licensed an empty chapter list observable is returned + * Get all the available chapters for a manga. + * Normally it's not needed to override this method. * - * @param manga the manga to look for chapters. + * @param manga the manga to update. + * @return the chapters for the manga. + * @throws LicensedMangaChaptersException if a manga is licensed and therefore no chapters are available. */ + @Suppress("DEPRECATION") + override suspend fun getChapterList(manga: SManga): List { + if (manga.status == SManga.LICENSED) { + throw LicensedMangaChaptersException() + } + + return fetchChapterList(manga).awaitSingle() + } + + @Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getChapterList")) override fun fetchChapterList(manga: SManga): Observable> { return if (manga.status != SManga.LICENSED) { client.newCall(chapterListRequest(manga)) @@ -220,7 +279,7 @@ abstract class HttpSource : CatalogueSource { chapterListParse(response) } } else { - Observable.error(Exception("Licensed - No chapters to show")) + Observable.error(LicensedMangaChaptersException()) } } @@ -242,10 +301,18 @@ abstract class HttpSource : CatalogueSource { protected abstract fun chapterListParse(response: Response): List /** - * Returns an observable with the page list for a chapter. + * Get the list of pages a chapter has. Pages should be returned + * in the expected order; the index is ignored. * - * @param chapter the chapter whose page list has to be fetched. + * @param chapter the chapter. + * @return the pages for the chapter. */ + @Suppress("DEPRECATION") + override suspend fun getPageList(chapter: SChapter): List { + return fetchPageList(chapter).awaitSingle() + } + + @Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getPageList")) override fun fetchPageList(chapter: SChapter): Observable> { return client.newCall(pageListRequest(chapter)) .asObservableSuccess() @@ -275,8 +342,15 @@ abstract class HttpSource : CatalogueSource { * Returns an observable with the page containing the source url of the image. If there's any * error, it will return null instead of throwing an exception. * + * @since extensions-lib 1.5 * @param page the page whose source image has to be fetched. */ + @Suppress("DEPRECATION") + open suspend fun getImageUrl(page: Page): String { + return fetchImageUrl(page).awaitSingle() + } + + @Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getImageUrl")) open fun fetchImageUrl(page: Page): Observable { return client.newCall(imageUrlRequest(page)) .asObservableSuccess() @@ -300,23 +374,14 @@ abstract class HttpSource : CatalogueSource { */ protected abstract fun imageUrlParse(response: Response): String - /** - * Returns an observable with the response of the source image. - * - * @param page the page whose source image has to be downloaded. - */ - fun fetchImage(page: Page): Observable { - return client.newCachelessCallWithProgress(imageRequest(page), page) - .asObservableSuccess() - } - /** * 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. */ - suspend fun getImage(page: Page): Response { - // images will be cached or saved manually, so don't take up network cache + open suspend fun getImage(page: Page): Response { return client.newCachelessCallWithProgress(imageRequest(page), page) .awaitSuccess() } @@ -438,11 +503,12 @@ abstract class HttpSource : CatalogueSource { * @param chapter the chapter to be added. * @param manga the manga of the chapter. */ - open fun prepareNewChapter(chapter: SChapter, manga: SManga) { - } + open fun prepareNewChapter(chapter: SChapter, manga: SManga) {} /** * Returns the list of filters for the source. */ override fun getFilterList() = FilterList() } + +class LicensedMangaChaptersException : Exception("Licensed - No chapters to show") diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/online/HttpSourceFetcher.kt b/app/src/main/java/eu/kanade/tachiyomi/source/online/HttpSourceFetcher.kt deleted file mode 100644 index 6b5e4a6381..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/source/online/HttpSourceFetcher.kt +++ /dev/null @@ -1,25 +0,0 @@ -package eu.kanade.tachiyomi.source.online - -import eu.kanade.tachiyomi.source.model.Page -import rx.Observable - -fun HttpSource.getImageUrl(page: Page): Observable { - page.status = Page.State.LOAD_PAGE - return fetchImageUrl(page) - .doOnError { page.status = Page.State.ERROR } - .onErrorReturn { null } - .doOnNext { page.imageUrl = it } - .map { page } -} - -fun HttpSource.fetchAllImageUrlsFromPageList(pages: List): Observable { - return Observable.from(pages) - .filter { !it.imageUrl.isNullOrEmpty() } - .mergeWith(fetchRemainingImageUrlsFromPageList(pages)) -} - -fun HttpSource.fetchRemainingImageUrlsFromPageList(pages: List): Observable { - return Observable.from(pages) - .filter { it.imageUrl.isNullOrEmpty() } - .concatMap { getImageUrl(it) } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/extension/details/ExtensionDetailsController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/extension/details/ExtensionDetailsController.kt index d84a966945..edd8d4f631 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/extension/details/ExtensionDetailsController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/extension/details/ExtensionDetailsController.kt @@ -34,8 +34,9 @@ import eu.kanade.tachiyomi.extension.model.Extension import eu.kanade.tachiyomi.network.NetworkHelper import eu.kanade.tachiyomi.source.ConfigurableSource import eu.kanade.tachiyomi.source.Source -import eu.kanade.tachiyomi.source.getPreferenceKey import eu.kanade.tachiyomi.source.online.HttpSource +import eu.kanade.tachiyomi.source.preferenceKey +import eu.kanade.tachiyomi.source.sourcePreferences import eu.kanade.tachiyomi.ui.base.controller.BaseCoroutineController import eu.kanade.tachiyomi.ui.setting.DSL import eu.kanade.tachiyomi.ui.setting.onChange @@ -231,7 +232,7 @@ class ExtensionDetailsController(bundle: Bundle? = null) : val prefs = mutableListOf() val block: (@DSL SwitchPreferenceCompat).() -> Unit = { - key = source.getPreferenceKey() + "_enabled" + key = source.preferenceKey() + "_enabled" title = when { isMultiSource && !isMultiLangSingleSource -> source.toString() else -> LocaleHelper.getSourceDisplayName(source.lang, context) @@ -278,9 +279,7 @@ class ExtensionDetailsController(bundle: Bundle? = null) : val newScreen = screen.preferenceManager.createPreferenceScreen(context) source.setupPreferenceScreen(newScreen) - val dataStore = SharedPreferencesDataStore( - context.getSharedPreferences(source.getPreferenceKey(), Context.MODE_PRIVATE), - ) + val dataStore = SharedPreferencesDataStore(source.sourcePreferences()) // Reparent the preferences while (newScreen.preferenceCount != 0) { val pref = newScreen.getPreference(0) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/HttpPageLoader.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/HttpPageLoader.kt index 0996e35274..4af8537c43 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/HttpPageLoader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/HttpPageLoader.kt @@ -6,7 +6,6 @@ import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter import eu.kanade.tachiyomi.ui.reader.model.ReaderPage -import eu.kanade.tachiyomi.util.system.awaitSingle import eu.kanade.tachiyomi.util.system.launchIO import eu.kanade.tachiyomi.util.system.withIOContext import kotlinx.coroutines.CancellationException @@ -195,7 +194,7 @@ class HttpPageLoader( try { if (page.imageUrl.isNullOrEmpty()) { page.status = Page.State.LOAD_PAGE - page.imageUrl = source.fetchImageUrl(page).awaitSingle() + page.imageUrl = source.getImageUrl(page) } val imageUrl = page.imageUrl!! diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourcePager.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourcePager.kt index 0513ada277..13ef6a7aa3 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourcePager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourcePager.kt @@ -2,21 +2,18 @@ package eu.kanade.tachiyomi.ui.source.browse import eu.kanade.tachiyomi.source.CatalogueSource import eu.kanade.tachiyomi.source.model.FilterList -import eu.kanade.tachiyomi.util.system.awaitSingle open class BrowseSourcePager(val source: CatalogueSource, val query: String, val filters: FilterList) : Pager() { override suspend fun requestNextPage() { val page = currentPage - val observable = if (query.isBlank() && filters.isEmpty()) { - source.fetchPopularManga(page) + val mangasPage = if (query.isBlank() && filters.isEmpty()) { + source.getPopularManga(page) } else { - source.fetchSearchManga(page, query, filters) + source.getSearchManga(page, query, filters) } - val mangasPage = observable.awaitSingle() - if (mangasPage.mangas.isNotEmpty()) { onPageReceived(mangasPage) } else { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/LatestUpdatesPager.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/LatestUpdatesPager.kt index 97d95c726e..9b7a9cf52a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/LatestUpdatesPager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/LatestUpdatesPager.kt @@ -1,7 +1,6 @@ package eu.kanade.tachiyomi.ui.source.browse import eu.kanade.tachiyomi.source.CatalogueSource -import eu.kanade.tachiyomi.util.system.awaitSingle /** * LatestUpdatesPager inherited from the general Pager. @@ -9,7 +8,7 @@ import eu.kanade.tachiyomi.util.system.awaitSingle class LatestUpdatesPager(val source: CatalogueSource) : Pager() { override suspend fun requestNextPage() { - val mangasPage = source.fetchLatestUpdates(currentPage).awaitSingle() + val mangasPage = source.getLatestUpdates(currentPage) onPageReceived(mangasPage) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/source/globalsearch/GlobalSearchPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/source/globalsearch/GlobalSearchPresenter.kt index 8e7d65d42c..7671ac0095 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/source/globalsearch/GlobalSearchPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/source/globalsearch/GlobalSearchPresenter.kt @@ -12,7 +12,6 @@ import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.model.MangasPage import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.ui.base.presenter.BaseCoroutinePresenter -import eu.kanade.tachiyomi.util.system.awaitSingle import eu.kanade.tachiyomi.util.system.launchIO import eu.kanade.tachiyomi.util.system.launchUI import eu.kanade.tachiyomi.util.system.withUIContext @@ -176,7 +175,7 @@ open class GlobalSearchPresenter( return@mainLaunch } val mangas = try { - source.fetchSearchManga(1, query, source.getFilterList()).awaitSingle() + source.getSearchManga(1, query, source.getFilterList()) } catch (error: Exception) { MangasPage(emptyList(), false) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/JsoupExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/util/JsoupExtensions.kt index 2dc39e1094..6c166448a5 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/JsoupExtensions.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/JsoupExtensions.kt @@ -22,5 +22,5 @@ fun Element.attrOrText(css: String): String { * @param html the body of the response. Use only if the body was read before calling this method. */ fun Response.asJsoup(html: String? = null): Document { - return Jsoup.parse(html ?: body!!.string(), request.url.toString()) + return Jsoup.parse(html ?: body.string(), request.url.toString()) }