mirror of
https://github.com/mihonapp/mihon.git
synced 2024-11-21 20:47:03 -05:00
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:
parent
9493577de2
commit
2ab6af6471
8 changed files with 349 additions and 4 deletions
|
@ -129,6 +129,9 @@ androidExtensions {
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
|
||||||
|
// Source models and interfaces from Tachiyomi 1.x
|
||||||
|
implementation 'tachiyomi.sourceapi:source-api:1.1'
|
||||||
|
|
||||||
// AndroidX libraries
|
// AndroidX libraries
|
||||||
implementation 'androidx.annotation:annotation:1.2.0-alpha01'
|
implementation 'androidx.annotation:annotation:1.2.0-alpha01'
|
||||||
implementation 'androidx.appcompat:appcompat:1.3.0-alpha02'
|
implementation 'androidx.appcompat:appcompat:1.3.0-alpha02'
|
||||||
|
@ -297,6 +300,9 @@ buildscript {
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
|
maven {
|
||||||
|
url "https://dl.bintray.com/tachiyomiorg/maven"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// See https://kotlinlang.org/docs/reference/experimental.html#experimental-status-of-experimental-api-markers
|
// See https://kotlinlang.org/docs/reference/experimental.html#experimental-status-of-experimental-api-markers
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package eu.kanade.tachiyomi.data.database.models
|
package eu.kanade.tachiyomi.data.database.models
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
|
import tachiyomi.source.model.MangaInfo
|
||||||
|
|
||||||
interface Manga : SManga {
|
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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
|
@ -9,7 +9,7 @@ interface CatalogueSource : Source {
|
||||||
/**
|
/**
|
||||||
* An ISO 639-1 compliant language code (two letters in lower case).
|
* 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.
|
* Whether the source has support for latest updates.
|
||||||
|
|
|
@ -5,30 +5,42 @@ import eu.kanade.tachiyomi.extension.ExtensionManager
|
||||||
import eu.kanade.tachiyomi.source.model.Page
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
import eu.kanade.tachiyomi.source.model.SChapter
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
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 rx.Observable
|
||||||
|
import tachiyomi.source.model.ChapterInfo
|
||||||
|
import tachiyomi.source.model.MangaInfo
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
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 {
|
interface Source : tachiyomi.source.Source {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Id for the source. Must be unique.
|
* Id for the source. Must be unique.
|
||||||
*/
|
*/
|
||||||
val id: Long
|
override val id: Long
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Name of the source.
|
* 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.
|
* Returns an observable with the updated details for a manga.
|
||||||
*
|
*
|
||||||
* @param manga the manga to update.
|
* @param manga the manga to update.
|
||||||
*/
|
*/
|
||||||
|
@Deprecated("Use getMangaDetails instead")
|
||||||
fun fetchMangaDetails(manga: SManga): Observable<SManga>
|
fun fetchMangaDetails(manga: SManga): Observable<SManga>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -36,6 +48,7 @@ interface Source {
|
||||||
*
|
*
|
||||||
* @param manga the manga to update.
|
* @param manga the manga to update.
|
||||||
*/
|
*/
|
||||||
|
@Deprecated("Use getChapterList instead")
|
||||||
fun fetchChapterList(manga: SManga): Observable<List<SChapter>>
|
fun fetchChapterList(manga: SManga): Observable<List<SChapter>>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -43,7 +56,32 @@ interface Source {
|
||||||
*
|
*
|
||||||
* @param chapter the chapter.
|
* @param chapter the chapter.
|
||||||
*/
|
*/
|
||||||
|
@Deprecated("Use getPageList instead")
|
||||||
fun fetchPageList(chapter: SChapter): Observable<List<Page>>
|
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)
|
fun Source.icon(): Drawable? = Injekt.get<ExtensionManager>().getAppIconForSource(this)
|
||||||
|
|
|
@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.source.model
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import eu.kanade.tachiyomi.network.ProgressListener
|
import eu.kanade.tachiyomi.network.ProgressListener
|
||||||
import rx.subjects.Subject
|
import rx.subjects.Subject
|
||||||
|
import tachiyomi.source.model.PageUrl
|
||||||
|
|
||||||
open class Page(
|
open class Page(
|
||||||
val index: Int,
|
val index: Int,
|
||||||
|
@ -61,3 +62,9 @@ open class Page(
|
||||||
const val ERROR = 4
|
const val ERROR = 4
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun Page.toPageInfo(): PageUrl {
|
||||||
|
return PageUrl(
|
||||||
|
url = this.imageUrl ?: this.url
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
package eu.kanade.tachiyomi.source.model
|
package eu.kanade.tachiyomi.source.model
|
||||||
|
|
||||||
|
import tachiyomi.source.model.ChapterInfo
|
||||||
import java.io.Serializable
|
import java.io.Serializable
|
||||||
|
|
||||||
interface SChapter : 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
package eu.kanade.tachiyomi.source.model
|
package eu.kanade.tachiyomi.source.model
|
||||||
|
|
||||||
|
import tachiyomi.source.model.MangaInfo
|
||||||
import java.io.Serializable
|
import java.io.Serializable
|
||||||
|
|
||||||
interface SManga : 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
)
|
||||||
|
}
|
Loading…
Reference in a new issue