Consume and extend 1.x Source API

TODO: make the rest of the app actually call the 1.x functions
This commit is contained in:
arkon 2020-10-26 10:52:28 -04:00
parent 9493577de2
commit 2ab6af6471
8 changed files with 349 additions and 4 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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