mirror of
				https://github.com/mihonapp/mihon.git
				synced 2025-11-04 08:08:55 +01:00 
			
		
		
		
	Migrate Kitsu API to coroutines and kotlinx.serialization
This commit is contained in:
		@@ -175,8 +175,6 @@ dependencies {
 | 
			
		||||
    final retrofit_version = '2.9.0'
 | 
			
		||||
    implementation "com.squareup.retrofit2:retrofit:$retrofit_version"
 | 
			
		||||
    implementation "com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:0.8.0"
 | 
			
		||||
    implementation "com.squareup.retrofit2:converter-gson:$retrofit_version"
 | 
			
		||||
    implementation "com.squareup.retrofit2:adapter-rxjava:$retrofit_version"
 | 
			
		||||
 | 
			
		||||
    // JSON
 | 
			
		||||
    final kotlin_serialization_version = '1.0.1'
 | 
			
		||||
 
 | 
			
		||||
@@ -7,6 +7,7 @@ import eu.kanade.tachiyomi.R
 | 
			
		||||
import eu.kanade.tachiyomi.data.database.models.Track
 | 
			
		||||
import eu.kanade.tachiyomi.data.track.TrackService
 | 
			
		||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
 | 
			
		||||
import eu.kanade.tachiyomi.util.lang.runAsObservable
 | 
			
		||||
import rx.Completable
 | 
			
		||||
import rx.Observable
 | 
			
		||||
import uy.kohesive.injekt.injectLazy
 | 
			
		||||
@@ -69,15 +70,15 @@ class Kitsu(private val context: Context, id: Int) : TrackService(id) {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun add(track: Track): Observable<Track> {
 | 
			
		||||
        return api.addLibManga(track, getUserId())
 | 
			
		||||
        return runAsObservable({ api.addLibManga(track, getUserId()) })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun update(track: Track): Observable<Track> {
 | 
			
		||||
        return api.updateLibManga(track)
 | 
			
		||||
        return runAsObservable({ api.updateLibManga(track) })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun bind(track: Track): Observable<Track> {
 | 
			
		||||
        return api.findLibManga(track, getUserId())
 | 
			
		||||
        return runAsObservable({ api.findLibManga(track, getUserId()) })
 | 
			
		||||
            .flatMap { remoteTrack ->
 | 
			
		||||
                if (remoteTrack != null) {
 | 
			
		||||
                    track.copyPersonalFrom(remoteTrack)
 | 
			
		||||
@@ -92,11 +93,11 @@ class Kitsu(private val context: Context, id: Int) : TrackService(id) {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun search(query: String): Observable<List<TrackSearch>> {
 | 
			
		||||
        return api.search(query)
 | 
			
		||||
        return runAsObservable({ api.search(query) })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun refresh(track: Track): Observable<Track> {
 | 
			
		||||
        return api.getLibManga(track)
 | 
			
		||||
        return runAsObservable({ api.getLibManga(track) })
 | 
			
		||||
            .map { remoteTrack ->
 | 
			
		||||
                track.copyPersonalFrom(remoteTrack)
 | 
			
		||||
                track.total_chapters = remoteTrack.total_chapters
 | 
			
		||||
@@ -105,9 +106,9 @@ class Kitsu(private val context: Context, id: Int) : TrackService(id) {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun login(username: String, password: String): Completable {
 | 
			
		||||
        return api.login(username, password)
 | 
			
		||||
        return runAsObservable({ api.login(username, password) })
 | 
			
		||||
            .doOnNext { interceptor.newAuth(it) }
 | 
			
		||||
            .flatMap { api.getCurrentUser() }
 | 
			
		||||
            .flatMap { runAsObservable({ api.getCurrentUser() }) }
 | 
			
		||||
            .doOnNext { userId -> saveCredentials(username, userId) }
 | 
			
		||||
            .doOnError { logout() }
 | 
			
		||||
            .toCompletable()
 | 
			
		||||
 
 | 
			
		||||
@@ -1,21 +1,22 @@
 | 
			
		||||
package eu.kanade.tachiyomi.data.track.kitsu
 | 
			
		||||
 | 
			
		||||
import com.github.salomonbrys.kotson.array
 | 
			
		||||
import com.github.salomonbrys.kotson.get
 | 
			
		||||
import com.github.salomonbrys.kotson.int
 | 
			
		||||
import com.github.salomonbrys.kotson.jsonObject
 | 
			
		||||
import com.github.salomonbrys.kotson.obj
 | 
			
		||||
import com.github.salomonbrys.kotson.string
 | 
			
		||||
import com.google.gson.GsonBuilder
 | 
			
		||||
import com.google.gson.JsonObject
 | 
			
		||||
import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory
 | 
			
		||||
import eu.kanade.tachiyomi.data.database.models.Track
 | 
			
		||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
 | 
			
		||||
import eu.kanade.tachiyomi.network.POST
 | 
			
		||||
import kotlinx.serialization.json.Json
 | 
			
		||||
import kotlinx.serialization.json.JsonObject
 | 
			
		||||
import kotlinx.serialization.json.buildJsonObject
 | 
			
		||||
import kotlinx.serialization.json.int
 | 
			
		||||
import kotlinx.serialization.json.jsonArray
 | 
			
		||||
import kotlinx.serialization.json.jsonObject
 | 
			
		||||
import kotlinx.serialization.json.jsonPrimitive
 | 
			
		||||
import kotlinx.serialization.json.put
 | 
			
		||||
import kotlinx.serialization.json.putJsonObject
 | 
			
		||||
import okhttp3.FormBody
 | 
			
		||||
import okhttp3.MediaType.Companion.toMediaType
 | 
			
		||||
import okhttp3.OkHttpClient
 | 
			
		||||
import retrofit2.Retrofit
 | 
			
		||||
import retrofit2.adapter.rxjava.RxJavaCallAdapterFactory
 | 
			
		||||
import retrofit2.converter.gson.GsonConverterFactory
 | 
			
		||||
import retrofit2.http.Body
 | 
			
		||||
import retrofit2.http.Field
 | 
			
		||||
import retrofit2.http.FormUrlEncoded
 | 
			
		||||
@@ -26,7 +27,6 @@ import retrofit2.http.PATCH
 | 
			
		||||
import retrofit2.http.POST
 | 
			
		||||
import retrofit2.http.Path
 | 
			
		||||
import retrofit2.http.Query
 | 
			
		||||
import rx.Observable
 | 
			
		||||
 | 
			
		||||
class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor) {
 | 
			
		||||
 | 
			
		||||
@@ -35,196 +35,179 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
 | 
			
		||||
    private val rest = Retrofit.Builder()
 | 
			
		||||
        .baseUrl(baseUrl)
 | 
			
		||||
        .client(authClient)
 | 
			
		||||
        .addConverterFactory(GsonConverterFactory.create(GsonBuilder().serializeNulls().create()))
 | 
			
		||||
        .addCallAdapterFactory(RxJavaCallAdapterFactory.create())
 | 
			
		||||
        .addConverterFactory(jsonConverter)
 | 
			
		||||
        .build()
 | 
			
		||||
        .create(Rest::class.java)
 | 
			
		||||
 | 
			
		||||
    private val searchRest = Retrofit.Builder()
 | 
			
		||||
        .baseUrl(algoliaKeyUrl)
 | 
			
		||||
        .client(authClient)
 | 
			
		||||
        .addConverterFactory(GsonConverterFactory.create())
 | 
			
		||||
        .addCallAdapterFactory(RxJavaCallAdapterFactory.create())
 | 
			
		||||
        .addConverterFactory(jsonConverter)
 | 
			
		||||
        .build()
 | 
			
		||||
        .create(SearchKeyRest::class.java)
 | 
			
		||||
 | 
			
		||||
    private val algoliaRest = Retrofit.Builder()
 | 
			
		||||
        .baseUrl(algoliaUrl)
 | 
			
		||||
        .client(client)
 | 
			
		||||
        .addConverterFactory(GsonConverterFactory.create())
 | 
			
		||||
        .addCallAdapterFactory(RxJavaCallAdapterFactory.create())
 | 
			
		||||
        .addConverterFactory(jsonConverter)
 | 
			
		||||
        .build()
 | 
			
		||||
        .create(AgoliaSearchRest::class.java)
 | 
			
		||||
 | 
			
		||||
    fun addLibManga(track: Track, userId: String): Observable<Track> {
 | 
			
		||||
        return Observable.defer {
 | 
			
		||||
            // @formatter:off
 | 
			
		||||
            val data = jsonObject(
 | 
			
		||||
                "type" to "libraryEntries",
 | 
			
		||||
                "attributes" to jsonObject(
 | 
			
		||||
                    "status" to track.toKitsuStatus(),
 | 
			
		||||
                    "progress" to track.last_chapter_read
 | 
			
		||||
                ),
 | 
			
		||||
                "relationships" to jsonObject(
 | 
			
		||||
                    "user" to jsonObject(
 | 
			
		||||
                        "data" to jsonObject(
 | 
			
		||||
                            "id" to userId,
 | 
			
		||||
                            "type" to "users"
 | 
			
		||||
                        )
 | 
			
		||||
                    ),
 | 
			
		||||
                    "media" to jsonObject(
 | 
			
		||||
                        "data" to jsonObject(
 | 
			
		||||
                            "id" to track.media_id,
 | 
			
		||||
                            "type" to "manga"
 | 
			
		||||
                        )
 | 
			
		||||
                    )
 | 
			
		||||
                )
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
            rest.addLibManga(jsonObject("data" to data))
 | 
			
		||||
                .map { json ->
 | 
			
		||||
                    track.media_id = json["data"]["id"].int
 | 
			
		||||
                    track
 | 
			
		||||
    suspend fun addLibManga(track: Track, userId: String): Track {
 | 
			
		||||
        val data = buildJsonObject {
 | 
			
		||||
            putJsonObject("data") {
 | 
			
		||||
                put("type", "libraryEntries")
 | 
			
		||||
                putJsonObject("attributes") {
 | 
			
		||||
                    put("status", track.toKitsuStatus())
 | 
			
		||||
                    put("progress", track.last_chapter_read)
 | 
			
		||||
                }
 | 
			
		||||
                putJsonObject("relationships") {
 | 
			
		||||
                    putJsonObject("user") {
 | 
			
		||||
                        putJsonObject("data") {
 | 
			
		||||
                            put("id", userId)
 | 
			
		||||
                            put("type", "users")
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                    putJsonObject("media") {
 | 
			
		||||
                        putJsonObject("data") {
 | 
			
		||||
                            put("id", track.media_id)
 | 
			
		||||
                            put("type", "manga")
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        val json = rest.addLibManga(data)
 | 
			
		||||
        track.media_id = json["data"]!!.jsonObject["id"]!!.jsonPrimitive.int
 | 
			
		||||
        return track
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    suspend fun updateLibManga(track: Track): Track {
 | 
			
		||||
        val data = buildJsonObject {
 | 
			
		||||
            putJsonObject("data") {
 | 
			
		||||
                put("type", "libraryEntries")
 | 
			
		||||
                put("id", track.media_id)
 | 
			
		||||
                putJsonObject("attributes") {
 | 
			
		||||
                    put("status", track.toKitsuStatus())
 | 
			
		||||
                    put("progress", track.last_chapter_read)
 | 
			
		||||
                    put("ratingTwenty", track.toKitsuScore())
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        rest.updateLibManga(track.media_id, data)
 | 
			
		||||
        return track
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    suspend fun search(query: String): List<TrackSearch> {
 | 
			
		||||
        val json = searchRest.getKey()
 | 
			
		||||
        val key = json["media"]!!.jsonObject["key"]!!.jsonPrimitive.content
 | 
			
		||||
        return algoliaSearch(key, query)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private suspend fun algoliaSearch(key: String, query: String): List<TrackSearch> {
 | 
			
		||||
        val jsonObject = buildJsonObject {
 | 
			
		||||
            put("params", "query=$query$algoliaFilter")
 | 
			
		||||
        }
 | 
			
		||||
        val json = algoliaRest.getSearchQuery(algoliaAppId, key, jsonObject)
 | 
			
		||||
        val data = json["hits"]!!.jsonArray
 | 
			
		||||
        return data.map { KitsuSearchManga(it.jsonObject) }
 | 
			
		||||
            .filter { it.subType != "novel" }
 | 
			
		||||
            .map { it.toTrack() }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    suspend fun findLibManga(track: Track, userId: String): Track? {
 | 
			
		||||
        val json = rest.findLibManga(track.media_id, userId)
 | 
			
		||||
        val data = json["data"]!!.jsonArray
 | 
			
		||||
        return if (data.size > 0) {
 | 
			
		||||
            val manga = json["included"]!!.jsonArray[0].jsonObject
 | 
			
		||||
            KitsuLibManga(data[0].jsonObject, manga).toTrack()
 | 
			
		||||
        } else {
 | 
			
		||||
            null
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun updateLibManga(track: Track): Observable<Track> {
 | 
			
		||||
        return Observable.defer {
 | 
			
		||||
            // @formatter:off
 | 
			
		||||
            val data = jsonObject(
 | 
			
		||||
                "type" to "libraryEntries",
 | 
			
		||||
                "id" to track.media_id,
 | 
			
		||||
                "attributes" to jsonObject(
 | 
			
		||||
                    "status" to track.toKitsuStatus(),
 | 
			
		||||
                    "progress" to track.last_chapter_read,
 | 
			
		||||
                    "ratingTwenty" to track.toKitsuScore()
 | 
			
		||||
                )
 | 
			
		||||
            )
 | 
			
		||||
            // @formatter:on
 | 
			
		||||
 | 
			
		||||
            rest.updateLibManga(track.media_id, jsonObject("data" to data))
 | 
			
		||||
                .map { track }
 | 
			
		||||
    suspend fun getLibManga(track: Track): Track {
 | 
			
		||||
        val json = rest.getLibManga(track.media_id)
 | 
			
		||||
        val data = json["data"]!!.jsonArray
 | 
			
		||||
        return if (data.size > 0) {
 | 
			
		||||
            val manga = json["included"]!!.jsonArray[0].jsonObject
 | 
			
		||||
            KitsuLibManga(data[0].jsonObject, manga).toTrack()
 | 
			
		||||
        } else {
 | 
			
		||||
            throw Exception("Could not find manga")
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun search(query: String): Observable<List<TrackSearch>> {
 | 
			
		||||
        return searchRest
 | 
			
		||||
            .getKey().map { json ->
 | 
			
		||||
                json["media"].asJsonObject["key"].string
 | 
			
		||||
            }.flatMap { key ->
 | 
			
		||||
                algoliaSearch(key, query)
 | 
			
		||||
            }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun algoliaSearch(key: String, query: String): Observable<List<TrackSearch>> {
 | 
			
		||||
        val jsonObject = jsonObject("params" to "query=$query$algoliaFilter")
 | 
			
		||||
        return algoliaRest
 | 
			
		||||
            .getSearchQuery(algoliaAppId, key, jsonObject)
 | 
			
		||||
            .map { json ->
 | 
			
		||||
                val data = json["hits"].array
 | 
			
		||||
                data.map { KitsuSearchManga(it.obj) }
 | 
			
		||||
                    .filter { it.subType != "novel" }
 | 
			
		||||
                    .map { it.toTrack() }
 | 
			
		||||
            }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun findLibManga(track: Track, userId: String): Observable<Track?> {
 | 
			
		||||
        return rest.findLibManga(track.media_id, userId)
 | 
			
		||||
            .map { json ->
 | 
			
		||||
                val data = json["data"].array
 | 
			
		||||
                if (data.size() > 0) {
 | 
			
		||||
                    val manga = json["included"].array[0].obj
 | 
			
		||||
                    KitsuLibManga(data[0].obj, manga).toTrack()
 | 
			
		||||
                } else {
 | 
			
		||||
                    null
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun getLibManga(track: Track): Observable<Track> {
 | 
			
		||||
        return rest.getLibManga(track.media_id)
 | 
			
		||||
            .map { json ->
 | 
			
		||||
                val data = json["data"].array
 | 
			
		||||
                if (data.size() > 0) {
 | 
			
		||||
                    val manga = json["included"].array[0].obj
 | 
			
		||||
                    KitsuLibManga(data[0].obj, manga).toTrack()
 | 
			
		||||
                } else {
 | 
			
		||||
                    throw Exception("Could not find manga")
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun login(username: String, password: String): Observable<OAuth> {
 | 
			
		||||
    suspend fun login(username: String, password: String): OAuth {
 | 
			
		||||
        return Retrofit.Builder()
 | 
			
		||||
            .baseUrl(loginUrl)
 | 
			
		||||
            .client(client)
 | 
			
		||||
            .addConverterFactory(GsonConverterFactory.create())
 | 
			
		||||
            .addCallAdapterFactory(RxJavaCallAdapterFactory.create())
 | 
			
		||||
            .addConverterFactory(jsonConverter)
 | 
			
		||||
            .build()
 | 
			
		||||
            .create(LoginRest::class.java)
 | 
			
		||||
            .requestAccessToken(username, password)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun getCurrentUser(): Observable<String> {
 | 
			
		||||
        return rest.getCurrentUser().map { it["data"].array[0]["id"].string }
 | 
			
		||||
    suspend fun getCurrentUser(): String {
 | 
			
		||||
        return rest.getCurrentUser()["data"]!!.jsonArray[0].jsonObject["id"]!!.jsonPrimitive.content
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private interface Rest {
 | 
			
		||||
 | 
			
		||||
        @Headers("Content-Type: application/vnd.api+json")
 | 
			
		||||
        @POST("library-entries")
 | 
			
		||||
        fun addLibManga(
 | 
			
		||||
        suspend fun addLibManga(
 | 
			
		||||
            @Body data: JsonObject
 | 
			
		||||
        ): Observable<JsonObject>
 | 
			
		||||
        ): JsonObject
 | 
			
		||||
 | 
			
		||||
        @Headers("Content-Type: application/vnd.api+json")
 | 
			
		||||
        @PATCH("library-entries/{id}")
 | 
			
		||||
        fun updateLibManga(
 | 
			
		||||
        suspend fun updateLibManga(
 | 
			
		||||
            @Path("id") remoteId: Int,
 | 
			
		||||
            @Body data: JsonObject
 | 
			
		||||
        ): Observable<JsonObject>
 | 
			
		||||
        ): JsonObject
 | 
			
		||||
 | 
			
		||||
        @GET("library-entries")
 | 
			
		||||
        fun findLibManga(
 | 
			
		||||
        suspend fun findLibManga(
 | 
			
		||||
            @Query("filter[manga_id]", encoded = true) remoteId: Int,
 | 
			
		||||
            @Query("filter[user_id]", encoded = true) userId: String,
 | 
			
		||||
            @Query("include") includes: String = "manga"
 | 
			
		||||
        ): Observable<JsonObject>
 | 
			
		||||
        ): JsonObject
 | 
			
		||||
 | 
			
		||||
        @GET("library-entries")
 | 
			
		||||
        fun getLibManga(
 | 
			
		||||
        suspend fun getLibManga(
 | 
			
		||||
            @Query("filter[id]", encoded = true) remoteId: Int,
 | 
			
		||||
            @Query("include") includes: String = "manga"
 | 
			
		||||
        ): Observable<JsonObject>
 | 
			
		||||
        ): JsonObject
 | 
			
		||||
 | 
			
		||||
        @GET("users")
 | 
			
		||||
        fun getCurrentUser(
 | 
			
		||||
        suspend fun getCurrentUser(
 | 
			
		||||
            @Query("filter[self]", encoded = true) self: Boolean = true
 | 
			
		||||
        ): Observable<JsonObject>
 | 
			
		||||
        ): JsonObject
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private interface SearchKeyRest {
 | 
			
		||||
        @GET("media/")
 | 
			
		||||
        fun getKey(): Observable<JsonObject>
 | 
			
		||||
        suspend fun getKey(): JsonObject
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private interface AgoliaSearchRest {
 | 
			
		||||
        @POST("query/")
 | 
			
		||||
        fun getSearchQuery(@Header("X-Algolia-Application-Id") appid: String, @Header("X-Algolia-API-Key") key: String, @Body json: JsonObject): Observable<JsonObject>
 | 
			
		||||
        suspend fun getSearchQuery(@Header("X-Algolia-Application-Id") appid: String, @Header("X-Algolia-API-Key") key: String, @Body json: JsonObject): JsonObject
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private interface LoginRest {
 | 
			
		||||
 | 
			
		||||
        @FormUrlEncoded
 | 
			
		||||
        @POST("oauth/token")
 | 
			
		||||
        fun requestAccessToken(
 | 
			
		||||
        suspend fun requestAccessToken(
 | 
			
		||||
            @Field("username") username: String,
 | 
			
		||||
            @Field("password") password: String,
 | 
			
		||||
            @Field("grant_type") grantType: String = "password",
 | 
			
		||||
            @Field("client_id") client_id: String = clientId,
 | 
			
		||||
            @Field("client_secret") client_secret: String = clientSecret
 | 
			
		||||
        ): Observable<OAuth>
 | 
			
		||||
        ): OAuth
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    companion object {
 | 
			
		||||
@@ -238,6 +221,8 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
 | 
			
		||||
        private const val algoliaAppId = "AWQO5J657S"
 | 
			
		||||
        private const val algoliaFilter = "&facetFilters=%5B%22kind%3Amanga%22%5D&attributesToRetrieve=%5B%22synopsis%22%2C%22canonicalTitle%22%2C%22chapterCount%22%2C%22posterImage%22%2C%22startDate%22%2C%22subtype%22%2C%22endDate%22%2C%20%22id%22%5D"
 | 
			
		||||
 | 
			
		||||
        private val jsonConverter = Json { ignoreUnknownKeys = true }.asConverterFactory("application/json".toMediaType())
 | 
			
		||||
 | 
			
		||||
        fun mangaUrl(remoteId: Int): String {
 | 
			
		||||
            return baseMangaUrl + remoteId
 | 
			
		||||
        }
 | 
			
		||||
 
 | 
			
		||||
@@ -1,32 +1,31 @@
 | 
			
		||||
package eu.kanade.tachiyomi.data.track.kitsu
 | 
			
		||||
 | 
			
		||||
import androidx.annotation.CallSuper
 | 
			
		||||
import com.github.salomonbrys.kotson.byInt
 | 
			
		||||
import com.github.salomonbrys.kotson.byString
 | 
			
		||||
import com.github.salomonbrys.kotson.nullInt
 | 
			
		||||
import com.github.salomonbrys.kotson.nullObj
 | 
			
		||||
import com.github.salomonbrys.kotson.nullString
 | 
			
		||||
import com.github.salomonbrys.kotson.obj
 | 
			
		||||
import com.google.gson.JsonObject
 | 
			
		||||
import eu.kanade.tachiyomi.data.database.models.Track
 | 
			
		||||
import eu.kanade.tachiyomi.data.track.TrackManager
 | 
			
		||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
 | 
			
		||||
import kotlinx.serialization.json.JsonObject
 | 
			
		||||
import kotlinx.serialization.json.contentOrNull
 | 
			
		||||
import kotlinx.serialization.json.int
 | 
			
		||||
import kotlinx.serialization.json.intOrNull
 | 
			
		||||
import kotlinx.serialization.json.jsonObject
 | 
			
		||||
import kotlinx.serialization.json.jsonPrimitive
 | 
			
		||||
import java.text.SimpleDateFormat
 | 
			
		||||
import java.util.Date
 | 
			
		||||
import java.util.Locale
 | 
			
		||||
 | 
			
		||||
class KitsuSearchManga(obj: JsonObject) {
 | 
			
		||||
    val id by obj.byInt
 | 
			
		||||
    private val canonicalTitle by obj.byString
 | 
			
		||||
    private val chapterCount = obj.get("chapterCount").nullInt
 | 
			
		||||
    val subType = obj.get("subtype").nullString
 | 
			
		||||
    val original = obj.get("posterImage").nullObj?.get("original")?.asString
 | 
			
		||||
    private val synopsis by obj.byString
 | 
			
		||||
    private var startDate = obj.get("startDate").nullString?.let {
 | 
			
		||||
    val id = obj["id"]!!.jsonPrimitive.int
 | 
			
		||||
    private val canonicalTitle = obj["canonicalTitle"]!!.jsonPrimitive.content
 | 
			
		||||
    private val chapterCount = obj["chapterCount"]?.jsonPrimitive?.intOrNull
 | 
			
		||||
    val subType = obj["subtype"]?.jsonPrimitive?.contentOrNull
 | 
			
		||||
    val original = obj["posterImage"]?.jsonObject?.get("original")?.jsonPrimitive?.content
 | 
			
		||||
    private val synopsis = obj["synopsis"]!!.jsonPrimitive.content
 | 
			
		||||
    private var startDate = obj["startDate"]?.jsonPrimitive?.contentOrNull?.let {
 | 
			
		||||
        val outputDf = SimpleDateFormat("yyyy-MM-dd", Locale.US)
 | 
			
		||||
        outputDf.format(Date(it.toLong() * 1000))
 | 
			
		||||
    }
 | 
			
		||||
    private val endDate = obj.get("endDate").nullString
 | 
			
		||||
    private val endDate = obj["endDate"]?.jsonPrimitive?.contentOrNull
 | 
			
		||||
 | 
			
		||||
    @CallSuper
 | 
			
		||||
    fun toTrack() = TrackSearch.create(TrackManager.KITSU).apply {
 | 
			
		||||
@@ -47,17 +46,17 @@ class KitsuSearchManga(obj: JsonObject) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class KitsuLibManga(obj: JsonObject, manga: JsonObject) {
 | 
			
		||||
    val id by manga.byInt
 | 
			
		||||
    private val canonicalTitle by manga["attributes"].byString
 | 
			
		||||
    private val chapterCount = manga["attributes"].obj.get("chapterCount").nullInt
 | 
			
		||||
    val type = manga["attributes"].obj.get("mangaType").nullString.orEmpty()
 | 
			
		||||
    val original by manga["attributes"].obj["posterImage"].byString
 | 
			
		||||
    private val synopsis by manga["attributes"].byString
 | 
			
		||||
    private val startDate = manga["attributes"].obj.get("startDate").nullString.orEmpty()
 | 
			
		||||
    private val libraryId by obj.byInt("id")
 | 
			
		||||
    val status by obj["attributes"].byString
 | 
			
		||||
    private val ratingTwenty = obj["attributes"].obj.get("ratingTwenty").nullString
 | 
			
		||||
    val progress by obj["attributes"].byInt
 | 
			
		||||
    val id = manga["id"]!!.jsonPrimitive.int
 | 
			
		||||
    private val canonicalTitle = manga["attributes"]!!.jsonObject["canonicalTitle"]!!.jsonPrimitive.content
 | 
			
		||||
    private val chapterCount = manga["attributes"]!!.jsonObject["chapterCount"]?.jsonPrimitive?.intOrNull
 | 
			
		||||
    val type = manga["attributes"]!!.jsonObject["mangaType"]?.jsonPrimitive?.contentOrNull.orEmpty()
 | 
			
		||||
    val original = manga["attributes"]!!.jsonObject["original"]!!.jsonObject["posterImage"]!!.jsonPrimitive.content
 | 
			
		||||
    private val synopsis = manga["attributes"]!!.jsonObject["synopsis"]!!.jsonPrimitive.content
 | 
			
		||||
    private val startDate = manga["attributes"]!!.jsonObject["startDate"]?.jsonPrimitive?.contentOrNull.orEmpty()
 | 
			
		||||
    private val libraryId = obj["id"]!!.jsonPrimitive.int
 | 
			
		||||
    val status = obj["attributes"]!!.jsonObject["status"]!!.jsonPrimitive.content
 | 
			
		||||
    private val ratingTwenty = obj["attributes"]!!.jsonObject["ratingTwenty"]?.jsonPrimitive?.contentOrNull
 | 
			
		||||
    val progress = obj["attributes"]!!.jsonObject["progress"]!!.jsonPrimitive.int
 | 
			
		||||
 | 
			
		||||
    fun toTrack() = TrackSearch.create(TrackManager.KITSU).apply {
 | 
			
		||||
        media_id = libraryId
 | 
			
		||||
 
 | 
			
		||||
@@ -6,7 +6,7 @@ import eu.kanade.tachiyomi.R
 | 
			
		||||
import eu.kanade.tachiyomi.data.database.models.Track
 | 
			
		||||
import eu.kanade.tachiyomi.data.track.TrackService
 | 
			
		||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
 | 
			
		||||
import kotlinx.coroutines.Dispatchers
 | 
			
		||||
import eu.kanade.tachiyomi.util.lang.runAsObservable
 | 
			
		||||
import kotlinx.coroutines.runBlocking
 | 
			
		||||
import kotlinx.serialization.decodeFromString
 | 
			
		||||
import kotlinx.serialization.encodeToString
 | 
			
		||||
@@ -68,24 +68,24 @@ class MyAnimeList(private val context: Context, id: Int) : TrackService(id) {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun add(track: Track): Observable<Track> {
 | 
			
		||||
        return runAsObservable { api.addItemToList(track) }
 | 
			
		||||
        return runAsObservable({ api.addItemToList(track) })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun update(track: Track): Observable<Track> {
 | 
			
		||||
        return runAsObservable { api.updateItem(track) }
 | 
			
		||||
        return runAsObservable({ api.updateItem(track) })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun bind(track: Track): Observable<Track> {
 | 
			
		||||
        // TODO: change this to call add and update like the other trackers?
 | 
			
		||||
        return runAsObservable { api.getListItem(track) }
 | 
			
		||||
        return runAsObservable({ api.getListItem(track) })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun search(query: String): Observable<List<TrackSearch>> {
 | 
			
		||||
        return runAsObservable { api.search(query) }
 | 
			
		||||
        return runAsObservable({ api.search(query) })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun refresh(track: Track): Observable<Track> {
 | 
			
		||||
        return runAsObservable { api.getListItem(track) }
 | 
			
		||||
        return runAsObservable({ api.getListItem(track) })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun login(username: String, password: String) = login(password)
 | 
			
		||||
@@ -122,11 +122,4 @@ class MyAnimeList(private val context: Context, id: Int) : TrackService(id) {
 | 
			
		||||
            null
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun <T> runAsObservable(block: suspend () -> T): Observable<T> {
 | 
			
		||||
        return Observable.fromCallable { runBlocking(Dispatchers.IO) { block() } }
 | 
			
		||||
            .subscribeOn(Schedulers.io())
 | 
			
		||||
            .observeOn(AndroidSchedulers.mainThread())
 | 
			
		||||
            .map { it }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -119,7 +119,8 @@ suspend fun <T> Single<T>.await(): T = suspendCancellableCoroutine { cont ->
 | 
			
		||||
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()
 | 
			
		||||
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()
 | 
			
		||||
@@ -137,7 +138,8 @@ 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>.awaitSingleOrDefault(default: T): T =
 | 
			
		||||
    singleOrDefault(default).awaitOne()
 | 
			
		||||
 | 
			
		||||
suspend fun <T> Observable<T>.awaitSingleOrNull(): T? = singleOrDefault(null).awaitOne()
 | 
			
		||||
 | 
			
		||||
@@ -203,9 +205,9 @@ fun <T : Any> Flow<T>.asObservable(backpressureMode: Emitter.BackpressureMode =
 | 
			
		||||
    return Observable.create(
 | 
			
		||||
        { emitter ->
 | 
			
		||||
            /*
 | 
			
		||||
         * ATOMIC is used here to provide stable behaviour of subscribe+dispose pair even if
 | 
			
		||||
         * asObservable is already invoked from unconfined
 | 
			
		||||
         */
 | 
			
		||||
             * 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) }
 | 
			
		||||
@@ -224,3 +226,28 @@ fun <T : Any> Flow<T>.asObservable(backpressureMode: Emitter.BackpressureMode =
 | 
			
		||||
        backpressureMode
 | 
			
		||||
    )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fun <T> runAsObservable(
 | 
			
		||||
    block: suspend () -> T,
 | 
			
		||||
    backpressureMode: Emitter.BackpressureMode = Emitter.BackpressureMode.NONE
 | 
			
		||||
): Observable<T> {
 | 
			
		||||
    return Observable.create(
 | 
			
		||||
        { emitter ->
 | 
			
		||||
            val job = GlobalScope.launch(Dispatchers.Unconfined, start = CoroutineStart.ATOMIC) {
 | 
			
		||||
                try {
 | 
			
		||||
                    emitter.onNext(block())
 | 
			
		||||
                    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