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.
pull/9918/head
arkon 1 year ago
parent 1668be8587
commit 26c5d761da

@ -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
}

@ -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 {

@ -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!!

@ -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)

@ -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)
}
}

@ -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()

@ -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")
}

@ -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)
}

@ -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")
}

@ -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()
}

@ -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 }
}

@ -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

Loading…
Cancel
Save