Browse Source

Consume and extend 1.x Source API

TODO: make the rest of the app actually call the 1.x functions
arkon 4 years ago
parent
commit
2ab6af6471

+ 6 - 0
app/build.gradle

@@ -129,6 +129,9 @@ androidExtensions {
 
 dependencies {
 
+    // Source models and interfaces from Tachiyomi 1.x
+    implementation 'tachiyomi.sourceapi:source-api:1.1'
+
     // AndroidX libraries
     implementation 'androidx.annotation:annotation:1.2.0-alpha01'
     implementation 'androidx.appcompat:appcompat:1.3.0-alpha02'
@@ -297,6 +300,9 @@ buildscript {
 
 repositories {
     mavenCentral()
+    maven {
+        url "https://dl.bintray.com/tachiyomiorg/maven"
+    }
 }
 
 // See https://kotlinlang.org/docs/reference/experimental.html#experimental-status-of-experimental-api-markers

+ 14 - 0
app/src/main/java/eu/kanade/tachiyomi/data/database/models/Manga.kt

@@ -1,6 +1,7 @@
 package eu.kanade.tachiyomi.data.database.models
 
 import eu.kanade.tachiyomi.source.model.SManga
+import tachiyomi.source.model.MangaInfo
 
 interface Manga : SManga {
 
@@ -98,3 +99,16 @@ interface Manga : SManga {
         }
     }
 }
+
+fun Manga.toMangaInfo(): MangaInfo {
+    return MangaInfo(
+        artist = this.artist ?: "",
+        author = this.author ?: "",
+        cover = this.thumbnail_url ?: "",
+        description = this.description ?: "",
+        genres = this.getGenres() ?: emptyList(),
+        key = this.url,
+        status = this.status,
+        title = this.title
+    )
+}

+ 1 - 1
app/src/main/java/eu/kanade/tachiyomi/source/CatalogueSource.kt

@@ -9,7 +9,7 @@ interface CatalogueSource : Source {
     /**
      * An ISO 639-1 compliant language code (two letters in lower case).
      */
-    val lang: String
+    override val lang: String
 
     /**
      * Whether the source has support for latest updates.

+ 41 - 3
app/src/main/java/eu/kanade/tachiyomi/source/Source.kt

@@ -5,30 +5,42 @@ import eu.kanade.tachiyomi.extension.ExtensionManager
 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.source.model.toChapterInfo
+import eu.kanade.tachiyomi.source.model.toMangaInfo
+import eu.kanade.tachiyomi.source.model.toPageInfo
+import eu.kanade.tachiyomi.source.model.toSChapter
+import eu.kanade.tachiyomi.source.model.toSManga
+import eu.kanade.tachiyomi.util.lang.awaitSingle
 import rx.Observable
+import tachiyomi.source.model.ChapterInfo
+import tachiyomi.source.model.MangaInfo
 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...
  */
-interface Source {
+interface Source : tachiyomi.source.Source {
 
     /**
      * Id for the source. Must be unique.
      */
-    val id: Long
+    override val id: Long
 
     /**
      * Name of the source.
      */
-    val name: String
+    override val name: String
+
+    override val lang: String
+        get() = ""
 
     /**
      * Returns an observable with the updated details for a manga.
      *
      * @param manga the manga to update.
      */
+    @Deprecated("Use getMangaDetails instead")
     fun fetchMangaDetails(manga: SManga): Observable<SManga>
 
     /**
@@ -36,6 +48,7 @@ interface Source {
      *
      * @param manga the manga to update.
      */
+    @Deprecated("Use getChapterList instead")
     fun fetchChapterList(manga: SManga): Observable<List<SChapter>>
 
     /**
@@ -43,7 +56,32 @@ interface Source {
      *
      * @param chapter the chapter.
      */
+    @Deprecated("Use getPageList instead")
     fun fetchPageList(chapter: SChapter): Observable<List<Page>>
+
+    /**
+     * [1.x API] Get the updated details for a manga.
+     */
+    override suspend fun getMangaDetails(manga: MangaInfo): MangaInfo {
+        return fetchMangaDetails(manga.toSManga()).awaitSingle()
+            .toMangaInfo()
+    }
+
+    /**
+     * [1.x API] Get all the available chapters for a manga.
+     */
+    override suspend fun getChapterList(manga: MangaInfo): List<ChapterInfo> {
+        return fetchChapterList(manga.toSManga()).awaitSingle()
+            .map { it.toChapterInfo() }
+    }
+
+    /**
+     * [1.x API] Get the list of pages a chapter has.
+     */
+    override suspend fun getPageList(chapter: ChapterInfo): List<tachiyomi.source.model.Page> {
+        return fetchPageList(chapter.toSChapter()).awaitSingle()
+            .map { it.toPageInfo() }
+    }
 }
 
 fun Source.icon(): Drawable? = Injekt.get<ExtensionManager>().getAppIconForSource(this)

+ 7 - 0
app/src/main/java/eu/kanade/tachiyomi/source/model/Page.kt

@@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.source.model
 import android.net.Uri
 import eu.kanade.tachiyomi.network.ProgressListener
 import rx.subjects.Subject
+import tachiyomi.source.model.PageUrl
 
 open class Page(
     val index: Int,
@@ -61,3 +62,9 @@ open class Page(
         const val ERROR = 4
     }
 }
+
+fun Page.toPageInfo(): PageUrl {
+    return PageUrl(
+        url = this.imageUrl ?: this.url
+    )
+}

+ 22 - 0
app/src/main/java/eu/kanade/tachiyomi/source/model/SChapter.kt

@@ -1,5 +1,6 @@
 package eu.kanade.tachiyomi.source.model
 
+import tachiyomi.source.model.ChapterInfo
 import java.io.Serializable
 
 interface SChapter : Serializable {
@@ -28,3 +29,24 @@ interface SChapter : Serializable {
         }
     }
 }
+
+fun SChapter.toChapterInfo(): ChapterInfo {
+    return ChapterInfo(
+        dateUpload = this.date_upload,
+        key = this.url,
+        name = this.name,
+        number = this.chapter_number,
+        scanlator = this.scanlator ?: ""
+    )
+}
+
+fun ChapterInfo.toSChapter(): SChapter {
+    val chapter = this
+    return SChapter.create().apply {
+        url = chapter.key
+        name = chapter.name
+        date_upload = chapter.dateUpload
+        chapter_number = chapter.number
+        scanlator = chapter.scanlator
+    }
+}

+ 28 - 0
app/src/main/java/eu/kanade/tachiyomi/source/model/SManga.kt

@@ -1,5 +1,6 @@
 package eu.kanade.tachiyomi.source.model
 
+import tachiyomi.source.model.MangaInfo
 import java.io.Serializable
 
 interface SManga : Serializable {
@@ -61,3 +62,30 @@ interface SManga : Serializable {
         }
     }
 }
+
+fun SManga.toMangaInfo(): MangaInfo {
+    return MangaInfo(
+        key = this.url,
+        title = this.title,
+        artist = this.artist ?: "",
+        author = this.author ?: "",
+        description = this.description ?: "",
+        genres = this.genre?.split(", ") ?: emptyList(),
+        status = this.status,
+        cover = this.thumbnail_url ?: ""
+    )
+}
+
+fun MangaInfo.toSManga(): SManga {
+    val mangaInfo = this
+    return SManga.create().apply {
+        url = mangaInfo.key
+        title = mangaInfo.title
+        artist = mangaInfo.artist
+        author = mangaInfo.author
+        description = mangaInfo.description
+        genre = mangaInfo.genres.joinToString(", ")
+        status = mangaInfo.status
+        thumbnail_url = mangaInfo.cover
+    }
+}

+ 230 - 0
app/src/main/java/eu/kanade/tachiyomi/util/lang/RxCoroutineBridge.kt

@@ -0,0 +1,230 @@
+package eu.kanade.tachiyomi.util.lang
+
+import com.pushtorefresh.storio.operations.PreparedOperation
+import com.pushtorefresh.storio.sqlite.operations.get.PreparedGetObject
+import kotlinx.coroutines.CancellableContinuation
+import kotlinx.coroutines.CancellationException
+import kotlinx.coroutines.CoroutineStart
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.GlobalScope
+import kotlinx.coroutines.InternalCoroutinesApi
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.callbackFlow
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.suspendCancellableCoroutine
+import rx.Completable
+import rx.CompletableSubscriber
+import rx.Emitter
+import rx.Observable
+import rx.Observer
+import rx.Scheduler
+import rx.Single
+import rx.SingleSubscriber
+import rx.Subscriber
+import rx.Subscription
+import kotlin.coroutines.resume
+import kotlin.coroutines.resumeWithException
+
+/*
+ * Util functions for bridging RxJava and coroutines. Taken from TachiyomiEH/SY.
+ */
+
+@ExperimentalCoroutinesApi
+suspend fun <T> Single<T>.await(subscribeOn: Scheduler? = null): T {
+    return suspendCancellableCoroutine { continuation ->
+        val self = if (subscribeOn != null) subscribeOn(subscribeOn) else this
+        lateinit var sub: Subscription
+        sub = self.subscribe(
+            {
+                continuation.resume(it) {
+                    sub.unsubscribe()
+                }
+            },
+            {
+                if (!continuation.isCancelled) {
+                    continuation.resumeWithException(it)
+                }
+            }
+        )
+
+        continuation.invokeOnCancellation {
+            sub.unsubscribe()
+        }
+    }
+}
+
+suspend fun <T> PreparedOperation<T>.await(): T = asRxSingle().await()
+suspend fun <T> PreparedGetObject<T>.await(): T? = asRxSingle().await()
+
+@ExperimentalCoroutinesApi
+suspend fun Completable.awaitSuspending(subscribeOn: Scheduler? = null) {
+    return suspendCancellableCoroutine { continuation ->
+        val self = if (subscribeOn != null) subscribeOn(subscribeOn) else this
+        lateinit var sub: Subscription
+        sub = self.subscribe(
+            {
+                continuation.resume(Unit) {
+                    sub.unsubscribe()
+                }
+            },
+            {
+                if (!continuation.isCancelled) {
+                    continuation.resumeWithException(it)
+                }
+            }
+        )
+
+        continuation.invokeOnCancellation {
+            sub.unsubscribe()
+        }
+    }
+}
+
+suspend fun Completable.awaitCompleted(): Unit = suspendCancellableCoroutine { cont ->
+    subscribe(
+        object : CompletableSubscriber {
+            override fun onSubscribe(s: Subscription) {
+                cont.unsubscribeOnCancellation(s)
+            }
+
+            override fun onCompleted() {
+                cont.resume(Unit)
+            }
+
+            override fun onError(e: Throwable) {
+                cont.resumeWithException(e)
+            }
+        }
+    )
+}
+
+suspend fun <T> Single<T>.await(): T = suspendCancellableCoroutine { cont ->
+    cont.unsubscribeOnCancellation(
+        subscribe(
+            object : SingleSubscriber<T>() {
+                override fun onSuccess(t: T) {
+                    cont.resume(t)
+                }
+
+                override fun onError(error: Throwable) {
+                    cont.resumeWithException(error)
+                }
+            }
+        )
+    )
+}
+
+@OptIn(InternalCoroutinesApi::class, ExperimentalCoroutinesApi::class)
+suspend fun <T> Observable<T>.awaitFirst(): T = first().awaitOne()
+
+@OptIn(InternalCoroutinesApi::class, ExperimentalCoroutinesApi::class)
+suspend fun <T> Observable<T>.awaitFirstOrDefault(default: T): T = firstOrDefault(default).awaitOne()
+
+@OptIn(InternalCoroutinesApi::class, ExperimentalCoroutinesApi::class)
+suspend fun <T> Observable<T>.awaitFirstOrNull(): T? = firstOrDefault(null).awaitOne()
+
+@OptIn(InternalCoroutinesApi::class, ExperimentalCoroutinesApi::class)
+suspend fun <T> Observable<T>.awaitFirstOrElse(defaultValue: () -> T): T = switchIfEmpty(
+    Observable.fromCallable(
+        defaultValue
+    )
+).first().awaitOne()
+
+@OptIn(InternalCoroutinesApi::class, ExperimentalCoroutinesApi::class)
+suspend fun <T> Observable<T>.awaitLast(): T = last().awaitOne()
+
+@OptIn(InternalCoroutinesApi::class, ExperimentalCoroutinesApi::class)
+suspend fun <T> Observable<T>.awaitSingle(): T = single().awaitOne()
+
+suspend fun <T> Observable<T>.awaitSingleOrDefault(default: T): T = singleOrDefault(default).awaitOne()
+
+suspend fun <T> Observable<T>.awaitSingleOrNull(): T? = singleOrDefault(null).awaitOne()
+
+@OptIn(InternalCoroutinesApi::class, ExperimentalCoroutinesApi::class)
+private suspend fun <T> Observable<T>.awaitOne(): T = suspendCancellableCoroutine { cont ->
+    cont.unsubscribeOnCancellation(
+        subscribe(
+            object : Subscriber<T>() {
+                override fun onStart() {
+                    request(1)
+                }
+
+                override fun onNext(t: T) {
+                    cont.resume(t)
+                }
+
+                override fun onCompleted() {
+                    if (cont.isActive) cont.resumeWithException(
+                        IllegalStateException(
+                            "Should have invoked onNext"
+                        )
+                    )
+                }
+
+                override fun onError(e: Throwable) {
+                    /*
+                       * Rx1 observable throws NoSuchElementException if cancellation happened before
+                       * element emission. To mitigate this we try to atomically resume continuation with exception:
+                       * if resume failed, then we know that continuation successfully cancelled itself
+                       */
+                    val token = cont.tryResumeWithException(e)
+                    if (token != null) {
+                        cont.completeResume(token)
+                    }
+                }
+            }
+        )
+    )
+}
+
+internal fun <T> CancellableContinuation<T>.unsubscribeOnCancellation(sub: Subscription) =
+    invokeOnCancellation { sub.unsubscribe() }
+
+@ExperimentalCoroutinesApi
+fun <T : Any> Observable<T>.asFlow(): Flow<T> = callbackFlow {
+    val observer = object : Observer<T> {
+        override fun onNext(t: T) {
+            offer(t)
+        }
+
+        override fun onError(e: Throwable) {
+            close(e)
+        }
+
+        override fun onCompleted() {
+            close()
+        }
+    }
+    val subscription = subscribe(observer)
+    awaitClose { subscription.unsubscribe() }
+}
+
+@ExperimentalCoroutinesApi
+fun <T : Any> Flow<T>.asObservable(backpressureMode: Emitter.BackpressureMode = Emitter.BackpressureMode.NONE): Observable<T> {
+    return Observable.create(
+        { emitter ->
+            /*
+         * ATOMIC is used here to provide stable behaviour of subscribe+dispose pair even if
+         * asObservable is already invoked from unconfined
+         */
+            val job = GlobalScope.launch(Dispatchers.Unconfined, start = CoroutineStart.ATOMIC) {
+                try {
+                    collect { emitter.onNext(it) }
+                    emitter.onCompleted()
+                } catch (e: Throwable) {
+                    // Ignore `CancellationException` as error, since it indicates "normal cancellation"
+                    if (e !is CancellationException) {
+                        emitter.onError(e)
+                    } else {
+                        emitter.onCompleted()
+                    }
+                }
+            }
+            emitter.setCancellation { job.cancel() }
+        },
+        backpressureMode
+    )
+}