mirror of
				https://github.com/mihonapp/mihon.git
				synced 2025-10-31 06:17:57 +01: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:
		| @@ -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 | ||||
|     ) | ||||
| } | ||||
|   | ||||
| @@ -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. | ||||
|   | ||||
| @@ -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) | ||||
|   | ||||
| @@ -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 | ||||
|     ) | ||||
| } | ||||
|   | ||||
| @@ -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 | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -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 | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -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 | ||||
|     ) | ||||
| } | ||||
		Reference in New Issue
	
	Block a user