浏览代码

Add more replacement suspend functions for source APIs

These are basically 1-to-1 replacements for the existing RxJava APIs.
This will make the initial migration off of RxJava simpler. We'll
revisit the actual call flows in followup versions of the API.
arkon 1 年之前
父节点
当前提交
26c5d761da

+ 1 - 2
app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt

@@ -43,7 +43,6 @@ import nl.adaptivity.xmlutil.serialization.XML
 import okhttp3.Response
 import tachiyomi.core.metadata.comicinfo.COMIC_INFO_FILE
 import tachiyomi.core.metadata.comicinfo.ComicInfo
-import tachiyomi.core.util.lang.awaitSingle
 import tachiyomi.core.util.lang.launchIO
 import tachiyomi.core.util.lang.launchNow
 import tachiyomi.core.util.lang.withIOContext
@@ -363,7 +362,7 @@ class Downloader(
                         if (page.imageUrl.isNullOrEmpty()) {
                             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
                             }

+ 1 - 2
app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/SearchScreenModel.kt

@@ -19,7 +19,6 @@ import kotlinx.coroutines.flow.update
 import kotlinx.coroutines.isActive
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.withContext
-import tachiyomi.core.util.lang.awaitSingle
 import tachiyomi.domain.manga.interactor.GetManga
 import tachiyomi.domain.manga.interactor.NetworkToLocalManga
 import tachiyomi.domain.manga.model.Manga
@@ -140,7 +139,7 @@ abstract class SearchScreenModel(
 
                     try {
                         val page = withContext(coroutineDispatcher) {
-                            source.fetchSearchManga(1, query, source.getFilterList()).awaitSingle()
+                            source.getSearchManga(1, query, source.getFilterList())
                         }
 
                         val titles = page.mangas.map {

+ 1 - 2
app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/HttpPageLoader.kt

@@ -15,7 +15,6 @@ import kotlinx.coroutines.flow.filter
 import kotlinx.coroutines.flow.flow
 import kotlinx.coroutines.runInterruptible
 import kotlinx.coroutines.suspendCancellableCoroutine
-import tachiyomi.core.util.lang.awaitSingle
 import tachiyomi.core.util.lang.launchIO
 import tachiyomi.core.util.lang.withIOContext
 import uy.kohesive.injekt.Injekt
@@ -170,7 +169,7 @@ internal 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!!
 

+ 12 - 9
core/src/main/java/eu/kanade/tachiyomi/network/OkHttpExtensions.kt

@@ -58,6 +58,15 @@ fun Call.asObservable(): Observable<Response> {
     }
 }
 
+fun Call.asObservableSuccess(): Observable<Response> {
+    return asObservable().doOnNext { response ->
+        if (!response.isSuccessful) {
+            response.close()
+            throw HttpException(response.code)
+        }
+    }
+}
+
 // Based on https://github.com/gildor/kotlin-coroutines-okhttp
 @OptIn(ExperimentalCoroutinesApi::class)
 private suspend fun Call.await(callStack: Array<StackTraceElement>): Response {
@@ -95,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)
@@ -105,15 +117,6 @@ suspend fun Call.awaitSuccess(): Response {
     return response
 }
 
-fun Call.asObservableSuccess(): Observable<Response> {
-    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)

+ 3 - 4
data/src/main/java/tachiyomi/data/source/SourcePagingSource.kt

@@ -5,7 +5,6 @@ import eu.kanade.tachiyomi.source.CatalogueSource
 import eu.kanade.tachiyomi.source.model.FilterList
 import eu.kanade.tachiyomi.source.model.MangasPage
 import eu.kanade.tachiyomi.source.model.SManga
-import tachiyomi.core.util.lang.awaitSingle
 import tachiyomi.core.util.lang.withIOContext
 import tachiyomi.domain.source.repository.SourcePagingSourceType
 
@@ -13,19 +12,19 @@ class SourceSearchPagingSource(source: CatalogueSource, val query: String, val f
     source,
 ) {
     override suspend fun requestNextPage(currentPage: Int): MangasPage {
-        return source.fetchSearchManga(currentPage, query, filters).awaitSingle()
+        return source.getSearchManga(currentPage, query, filters)
     }
 }
 
 class SourcePopularPagingSource(source: CatalogueSource) : SourcePagingSource(source) {
     override suspend fun requestNextPage(currentPage: Int): MangasPage {
-        return source.fetchPopularManga(currentPage).awaitSingle()
+        return source.getPopularManga(currentPage)
     }
 }
 
 class SourceLatestPagingSource(source: CatalogueSource) : SourcePagingSource(source) {
     override suspend fun requestNextPage(currentPage: Int): MangasPage {
-        return source.fetchLatestUpdates(currentPage).awaitSingle()
+        return source.getLatestUpdates(currentPage)
     }
 }
 

+ 5 - 26
domain/src/main/java/tachiyomi/domain/source/model/StubSource.kt

@@ -4,7 +4,6 @@ import eu.kanade.tachiyomi.source.Source
 import eu.kanade.tachiyomi.source.model.Page
 import eu.kanade.tachiyomi.source.model.SChapter
 import eu.kanade.tachiyomi.source.model.SManga
-import rx.Observable
 
 class StubSource(
     override val id: Long,
@@ -14,36 +13,16 @@ class StubSource(
 
     private val isInvalid: Boolean = name.isBlank() || lang.isBlank()
 
-    override suspend fun getMangaDetails(manga: SManga): SManga {
+    override suspend fun getMangaDetails(manga: SManga): SManga =
         throw SourceNotInstalledException()
-    }
 
-    @Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getMangaDetails"))
-    override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
-        return Observable.error(SourceNotInstalledException())
-    }
-
-    override suspend fun getChapterList(manga: SManga): List<SChapter> {
+    override suspend fun getChapterList(manga: SManga): List<SChapter> =
         throw SourceNotInstalledException()
-    }
-
-    @Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getChapterList"))
-    override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
-        return Observable.error(SourceNotInstalledException())
-    }
-
-    override suspend fun getPageList(chapter: SChapter): List<Page> {
+    override suspend fun getPageList(chapter: SChapter): List<Page> =
         throw SourceNotInstalledException()
-    }
-
-    @Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getPageList"))
-    override fun fetchPageList(chapter: SChapter): Observable<List<Page>> {
-        return Observable.error(SourceNotInstalledException())
-    }
 
-    override fun toString(): String {
-        return if (isInvalid.not()) "$name (${lang.uppercase()})" else id.toString()
-    }
+    override fun toString(): String =
+        if (isInvalid.not()) "$name (${lang.uppercase()})" else id.toString()
 }
 
 class SourceNotInstalledException : Exception()

+ 40 - 6
source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/CatalogueSource.kt

@@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.source
 import eu.kanade.tachiyomi.source.model.FilterList
 import eu.kanade.tachiyomi.source.model.MangasPage
 import rx.Observable
+import tachiyomi.core.util.lang.awaitSingle
 
 interface CatalogueSource : Source {
 
@@ -17,30 +18,63 @@ interface CatalogueSource : Source {
     val supportsLatest: Boolean
 
     /**
-     * Returns an observable containing a page with a list of manga.
+     * Get a page with a list of manga.
      *
+     * @since extensions-lib 1.5
      * @param page the page number to retrieve.
      */
-    fun fetchPopularManga(page: Int): Observable<MangasPage>
+    @Suppress("DEPRECATION")
+    suspend fun getPopularManga(page: Int): MangasPage {
+        return fetchPopularManga(page).awaitSingle()
+    }
 
     /**
-     * Returns an observable containing a page with a list of manga.
+     * Get a page with a list of manga.
      *
+     * @since extensions-lib 1.5
      * @param page the page number to retrieve.
      * @param query the search query.
      * @param filters the list of filters to apply.
      */
-    fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage>
+    @Suppress("DEPRECATION")
+    suspend fun getSearchManga(page: Int, query: String, filters: FilterList): MangasPage {
+        return fetchSearchManga(page, query, filters).awaitSingle()
+    }
 
     /**
-     * Returns an observable containing a page with a list of latest manga updates.
+     * Get a page with a list of latest manga updates.
      *
+     * @since extensions-lib 1.5
      * @param page the page number to retrieve.
      */
-    fun fetchLatestUpdates(page: Int): Observable<MangasPage>
+    @Suppress("DEPRECATION")
+    suspend fun getLatestUpdates(page: Int): MangasPage {
+        return fetchLatestUpdates(page).awaitSingle()
+    }
 
     /**
      * Returns the list of filters for the source.
      */
     fun getFilterList(): FilterList
+
+    @Deprecated(
+        "Use the non-RxJava API instead",
+        ReplaceWith("getPopularManga"),
+    )
+    fun fetchPopularManga(page: Int): Observable<MangasPage> =
+        throw IllegalStateException("Not used")
+
+    @Deprecated(
+        "Use the non-RxJava API instead",
+        ReplaceWith("getSearchManga"),
+    )
+    fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> =
+        throw IllegalStateException("Not used")
+
+    @Deprecated(
+        "Use the non-RxJava API instead",
+        ReplaceWith("getLatestUpdates"),
+    )
+    fun fetchLatestUpdates(page: Int): Observable<MangasPage> =
+        throw IllegalStateException("Not used")
 }

+ 13 - 0
source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/ConfigurableSource.kt

@@ -1,6 +1,19 @@
 package eu.kanade.tachiyomi.source
 
+import android.app.Application
+import android.content.SharedPreferences
+import uy.kohesive.injekt.Injekt
+import uy.kohesive.injekt.api.get
+
 interface ConfigurableSource : Source {
 
+    /**
+     * Gets instance of [SharedPreferences] scoped to the specific source.
+     *
+     * @since extensions-lib 1.5
+     */
+    fun getPreferences(): SharedPreferences =
+        Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
+
     fun setupPreferenceScreen(screen: PreferenceScreen)
 }

+ 9 - 26
source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/Source.kt

@@ -27,7 +27,7 @@ interface Source {
     /**
      * Get the updated details for a manga.
      *
-     * @since extensions-lib 1.4
+     * @since extensions-lib 1.5
      * @param manga the manga to update.
      * @return the updated manga.
      */
@@ -39,7 +39,7 @@ interface Source {
     /**
      * Get all the available chapters for a manga.
      *
-     * @since extensions-lib 1.4
+     * @since extensions-lib 1.5
      * @param manga the manga to update.
      * @return the chapters for the manga.
      */
@@ -52,7 +52,7 @@ interface Source {
      * Get the list of pages a chapter has. Pages should be returned
      * in the expected order; the index is ignored.
      *
-     * @since extensions-lib 1.4
+     * @since extensions-lib 1.5
      * @param chapter the chapter.
      * @return the pages for the chapter.
      */
@@ -61,41 +61,24 @@ interface Source {
         return fetchPageList(chapter).awaitSingle()
     }
 
-    /**
-     * Returns an observable with the updated details for a manga.
-     *
-     * @param manga the manga to update.
-     */
     @Deprecated(
         "Use the non-RxJava API instead",
         ReplaceWith("getMangaDetails"),
     )
-    fun fetchMangaDetails(manga: SManga): Observable<SManga> = throw IllegalStateException(
-        "Not used",
-    )
+    fun fetchMangaDetails(manga: SManga): Observable<SManga> =
+        throw IllegalStateException("Not used")
 
-    /**
-     * Returns an observable with all the available chapters for a manga.
-     *
-     * @param manga the manga to update.
-     */
     @Deprecated(
         "Use the non-RxJava API instead",
         ReplaceWith("getChapterList"),
     )
-    fun fetchChapterList(manga: SManga): Observable<List<SChapter>> = throw IllegalStateException(
-        "Not used",
-    )
+    fun fetchChapterList(manga: SManga): Observable<List<SChapter>> =
+        throw IllegalStateException("Not used")
 
-    /**
-     * Returns an observable with the list of pages a chapter has. Pages should be returned
-     * in the expected order; the index is ignored.
-     *
-     * @param chapter the chapter.
-     */
     @Deprecated(
         "Use the non-RxJava API instead",
         ReplaceWith("getPageList"),
     )
-    fun fetchPageList(chapter: SChapter): Observable<List<Page>> = Observable.empty()
+    fun fetchPageList(chapter: SChapter): Observable<List<Page>> =
+        throw IllegalStateException("Not used")
 }

+ 48 - 21
source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/online/HttpSource.kt

@@ -16,6 +16,7 @@ import okhttp3.OkHttpClient
 import okhttp3.Request
 import okhttp3.Response
 import rx.Observable
+import tachiyomi.core.util.lang.awaitSingle
 import uy.kohesive.injekt.injectLazy
 import java.net.URI
 import java.net.URISyntaxException
@@ -24,6 +25,7 @@ import java.security.MessageDigest
 /**
  * A simple implementation for sources from a website.
  */
+@Suppress("unused")
 abstract class HttpSource : CatalogueSource {
 
     /**
@@ -81,6 +83,7 @@ abstract class HttpSource : CatalogueSource {
      * @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())
@@ -202,11 +205,18 @@ abstract class HttpSource : CatalogueSource {
     protected abstract fun latestUpdatesParse(response: Response): MangasPage
 
     /**
-     * Returns an observable with the updated details for a manga. Normally it's not needed to
-     * override this method.
+     * Get the updated details for a manga.
+     * Normally it's not needed to override this method.
      *
-     * @param manga the manga to be updated.
+     * @param manga the manga to update.
+     * @return the updated manga.
      */
+    @Suppress("DEPRECATION")
+    override suspend fun getMangaDetails(manga: SManga): SManga {
+        return fetchMangaDetails(manga).awaitSingle()
+    }
+
+    @Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getMangaDetails"))
     override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
         return client.newCall(mangaDetailsRequest(manga))
             .asObservableSuccess()
@@ -233,11 +243,23 @@ abstract class HttpSource : CatalogueSource {
     protected abstract fun mangaDetailsParse(response: Response): SManga
 
     /**
-     * Returns an observable with the updated chapter list for a manga. Normally it's not needed to
-     * override this method.  If a manga is licensed an empty chapter list observable is returned
+     * Get all the available chapters for a manga.
+     * Normally it's not needed to override this method.
      *
-     * @param manga the manga to look for chapters.
+     * @param manga the manga to update.
+     * @return the chapters for the manga.
+     * @throws LicensedMangaChaptersException if a manga is licensed and therefore no chapters are available.
      */
+    @Suppress("DEPRECATION")
+    override suspend fun getChapterList(manga: SManga): List<SChapter> {
+        if (manga.status == SManga.LICENSED) {
+            throw LicensedMangaChaptersException()
+        }
+
+        return fetchChapterList(manga).awaitSingle()
+    }
+
+    @Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getChapterList"))
     override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
         return if (manga.status != SManga.LICENSED) {
             client.newCall(chapterListRequest(manga))
@@ -268,10 +290,18 @@ abstract class HttpSource : CatalogueSource {
     protected abstract fun chapterListParse(response: Response): List<SChapter>
 
     /**
-     * Returns an observable with the page list for a chapter.
+     * Get the list of pages a chapter has. Pages should be returned
+     * in the expected order; the index is ignored.
      *
-     * @param chapter the chapter whose page list has to be fetched.
+     * @param chapter the chapter.
+     * @return the pages for the chapter.
      */
+    @Suppress("DEPRECATION")
+    override suspend fun getPageList(chapter: SChapter): List<Page> {
+        return fetchPageList(chapter).awaitSingle()
+    }
+
+    @Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getPageList"))
     override fun fetchPageList(chapter: SChapter): Observable<List<Page>> {
         return client.newCall(pageListRequest(chapter))
             .asObservableSuccess()
@@ -301,8 +331,15 @@ abstract class HttpSource : CatalogueSource {
      * Returns an observable with the page containing the source url of the image. If there's any
      * error, it will return null instead of throwing an exception.
      *
+     * @since extensions-lib 1.5
      * @param page the page whose source image has to be fetched.
      */
+    @Suppress("DEPRECATION")
+    open suspend fun getImageUrl(page: Page): String {
+        return fetchImageUrl(page).awaitSingle()
+    }
+
+    @Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getImageUrl"))
     open fun fetchImageUrl(page: Page): Observable<String> {
         return client.newCall(imageUrlRequest(page))
             .asObservableSuccess()
@@ -326,24 +363,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<Response> {
-        // images will be cached or saved manually, so don't take up network cache
-        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()
     }

+ 0 - 25
source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/online/HttpSourceFetcher.kt

@@ -1,25 +0,0 @@
-package eu.kanade.tachiyomi.source.online
-
-import eu.kanade.tachiyomi.source.model.Page
-import rx.Observable
-
-fun HttpSource.fetchAllImageUrlsFromPageList(pages: List<Page>): Observable<Page> {
-    return Observable.from(pages)
-        .filter { !it.imageUrl.isNullOrEmpty() }
-        .mergeWith(fetchRemainingImageUrlsFromPageList(pages))
-}
-
-private fun HttpSource.fetchRemainingImageUrlsFromPageList(pages: List<Page>): Observable<Page> {
-    return Observable.from(pages)
-        .filter { it.imageUrl.isNullOrEmpty() }
-        .concatMap { getImageUrl(it) }
-}
-
-private fun HttpSource.getImageUrl(page: Page): Observable<Page> {
-    page.status = Page.State.LOAD_PAGE
-    return fetchImageUrl(page)
-        .doOnError { page.status = Page.State.ERROR }
-        .onErrorReturn { null }
-        .doOnNext { page.imageUrl = it }
-        .map { page }
-}

+ 4 - 5
source-local/src/androidMain/kotlin/tachiyomi/source/local/LocalSource.kt

@@ -16,7 +16,6 @@ import kotlinx.serialization.json.decodeFromStream
 import logcat.LogPriority
 import nl.adaptivity.xmlutil.AndroidXmlReader
 import nl.adaptivity.xmlutil.serialization.XML
-import rx.Observable
 import tachiyomi.core.metadata.comicinfo.COMIC_INFO_FILE
 import tachiyomi.core.metadata.comicinfo.ComicInfo
 import tachiyomi.core.metadata.comicinfo.copyFromComicInfo
@@ -66,11 +65,11 @@ actual class LocalSource(
     override val supportsLatest: Boolean = true
 
     // Browse related
-    override fun fetchPopularManga(page: Int) = fetchSearchManga(page, "", POPULAR_FILTERS)
+    override suspend fun getPopularManga(page: Int) = getSearchManga(page, "", POPULAR_FILTERS)
 
-    override fun fetchLatestUpdates(page: Int) = fetchSearchManga(page, "", LATEST_FILTERS)
+    override suspend fun getLatestUpdates(page: Int) = getSearchManga(page, "", LATEST_FILTERS)
 
-    override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
+    override suspend fun getSearchManga(page: Int, query: String, filters: FilterList): MangasPage {
         val baseDirsFiles = fileSystem.getFilesInBaseDirectories()
         val lastModifiedLimit by lazy { if (filters === LATEST_FILTERS) System.currentTimeMillis() - LATEST_THRESHOLD else 0L }
         var mangaDirs = baseDirsFiles
@@ -143,7 +142,7 @@ actual class LocalSource(
             }
         }
 
-        return Observable.just(MangasPage(mangas.toList(), false))
+        return MangasPage(mangas.toList(), false)
     }
 
     // Manga details related