From 17b70ab38c84b6aaa4694f4b8c30a29a17231130 Mon Sep 17 00:00:00 2001 From: arkon Date: Mon, 4 Jan 2021 11:29:24 -0500 Subject: [PATCH] Refactor Kitsu API to remove Retrofit usage --- app/build.gradle.kts | 5 - .../data/track/anilist/AnilistApi.kt | 7 +- .../tachiyomi/data/track/kitsu/KitsuApi.kt | 349 +++++++++--------- .../data/track/shikimori/ShikimoriApi.kt | 3 +- .../tachiyomi/network/OkHttpExtensions.kt | 3 + 5 files changed, 187 insertions(+), 180 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 04ad59b54..7b45e1a0e 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -170,11 +170,6 @@ dependencies { // TLS 1.3 support for Android < 10 implementation("org.conscrypt:conscrypt-android:2.5.1") - // REST - val retrofitVersion = "2.9.0" - implementation("com.squareup.retrofit2:retrofit:$retrofitVersion") - implementation("com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:0.8.0") - // JSON val kotlinSerializationVersion = "1.0.1" implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlinSerializationVersion") diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistApi.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistApi.kt index dc9bfc098..3f7ed02e9 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistApi.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistApi.kt @@ -6,10 +6,10 @@ import eu.kanade.tachiyomi.data.database.models.Track import eu.kanade.tachiyomi.data.track.model.TrackSearch import eu.kanade.tachiyomi.network.POST import eu.kanade.tachiyomi.network.await +import eu.kanade.tachiyomi.network.jsonMime import eu.kanade.tachiyomi.network.parseAs import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext -import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.json.contentOrNull @@ -21,17 +21,12 @@ import kotlinx.serialization.json.jsonPrimitive import kotlinx.serialization.json.long import kotlinx.serialization.json.put import kotlinx.serialization.json.putJsonObject -import okhttp3.MediaType.Companion.toMediaType import okhttp3.OkHttpClient import okhttp3.RequestBody.Companion.toRequestBody -import uy.kohesive.injekt.injectLazy import java.util.Calendar class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) { - private val json: Json by injectLazy() - - private val jsonMime = "application/json; charset=utf-8".toMediaType() private val authClient = client.newBuilder().addInterceptor(interceptor).build() suspend fun addLibManga(track: Track): Track { diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuApi.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuApi.kt index eafb6e5c1..1ffb41c7f 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuApi.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuApi.kt @@ -1,10 +1,15 @@ package eu.kanade.tachiyomi.data.track.kitsu -import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory +import androidx.core.net.toUri import eu.kanade.tachiyomi.data.database.models.Track import eu.kanade.tachiyomi.data.track.model.TrackSearch +import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.POST -import kotlinx.serialization.json.Json +import eu.kanade.tachiyomi.network.await +import eu.kanade.tachiyomi.network.jsonMime +import eu.kanade.tachiyomi.network.parseAs +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.json.int @@ -14,226 +19,236 @@ import kotlinx.serialization.json.jsonPrimitive import kotlinx.serialization.json.put import kotlinx.serialization.json.putJsonObject import okhttp3.FormBody +import okhttp3.Headers.Companion.headersOf import okhttp3.MediaType.Companion.toMediaType import okhttp3.OkHttpClient -import retrofit2.Retrofit -import retrofit2.http.Body -import retrofit2.http.Field -import retrofit2.http.FormUrlEncoded -import retrofit2.http.GET -import retrofit2.http.Header -import retrofit2.http.Headers -import retrofit2.http.PATCH -import retrofit2.http.POST -import retrofit2.http.Path -import retrofit2.http.Query +import okhttp3.Request +import okhttp3.RequestBody +import okhttp3.RequestBody.Companion.toRequestBody class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor) { private val authClient = client.newBuilder().addInterceptor(interceptor).build() - private val rest = Retrofit.Builder() - .baseUrl(baseUrl) - .client(authClient) - .addConverterFactory(jsonConverter) - .build() - .create(Rest::class.java) - - private val searchRest = Retrofit.Builder() - .baseUrl(algoliaKeyUrl) - .client(authClient) - .addConverterFactory(jsonConverter) - .build() - .create(SearchKeyRest::class.java) - - private val algoliaRest = Retrofit.Builder() - .baseUrl(algoliaUrl) - .client(client) - .addConverterFactory(jsonConverter) - .build() - .create(AgoliaSearchRest::class.java) - 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") - } + return withContext(Dispatchers.IO) { + val data = buildJsonObject { + putJsonObject("data") { + put("type", "libraryEntries") + putJsonObject("attributes") { + put("status", track.toKitsuStatus()) + put("progress", track.last_chapter_read) } - putJsonObject("media") { - putJsonObject("data") { - put("id", track.media_id) - put("type", "manga") + 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 + authClient.newCall( + POST( + "${baseUrl}library-entries", + headers = headersOf( + "Content-Type", + "application/vnd.api+json" + ), + body = data.toString().toRequestBody("application/vnd.api+json".toMediaType()) + ) + ) + .await() + .parseAs() + .let { + track.media_id = it["data"]!!.jsonObject["id"]!!.jsonPrimitive.int + 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()) + return withContext(Dispatchers.IO) { + 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 + authClient.newCall( + Request.Builder() + .url("${baseUrl}library-entries/${track.media_id}") + .headers( + headersOf( + "Content-Type", + "application/vnd.api+json" + ) + ) + .patch(data.toString().toRequestBody("application/vnd.api+json".toMediaType())) + .build() + ) + .await() + .parseAs() + .let { + track + } + } } suspend fun search(query: String): List { - val json = searchRest.getKey() - val key = json["media"]!!.jsonObject["key"]!!.jsonPrimitive.content - return algoliaSearch(key, query) + return withContext(Dispatchers.IO) { + authClient.newCall(GET(algoliaKeyUrl)) + .await() + .parseAs() + .let { + val key = it["media"]!!.jsonObject["key"]!!.jsonPrimitive.content + algoliaSearch(key, query) + } + } } private suspend fun algoliaSearch(key: String, query: String): List { - val jsonObject = buildJsonObject { - put("params", "query=$query$algoliaFilter") + return withContext(Dispatchers.IO) { + val jsonObject = buildJsonObject { + put("params", "query=$query$algoliaFilter") + } + + client.newCall( + POST( + algoliaUrl, + headers = headersOf( + "X-Algolia-Application-Id", + algoliaAppId, + "X-Algolia-API-Key", + key, + ), + body = jsonObject.toString().toRequestBody(jsonMime) + ) + ) + .await() + .parseAs() + .let { + it["hits"]!!.jsonArray + .map { KitsuSearchManga(it.jsonObject) } + .filter { it.subType != "novel" } + .map { it.toTrack() } + } } - 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 + return withContext(Dispatchers.IO) { + val url = "${baseUrl}library-entries".toUri().buildUpon() + .encodedQuery("filter[manga_id]=${track.media_id}&filter[user_id]=$userId") + .appendQueryParameter("include", "manga") + .build() + authClient.newCall(GET(url.toString())) + .await() + .parseAs() + .let { + val data = it["data"]!!.jsonArray + if (data.size > 0) { + val manga = it["included"]!!.jsonArray[0].jsonObject + KitsuLibManga(data[0].jsonObject, manga).toTrack() + } else { + null + } + } } } 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") + return withContext(Dispatchers.IO) { + val url = "${baseUrl}library-entries".toUri().buildUpon() + .encodedQuery("filter[id]=${track.media_id}") + .appendQueryParameter("include", "manga") + .build() + authClient.newCall(GET(url.toString())) + .await() + .parseAs() + .let { + val data = it["data"]!!.jsonArray + if (data.size > 0) { + val manga = it["included"]!!.jsonArray[0].jsonObject + KitsuLibManga(data[0].jsonObject, manga).toTrack() + } else { + throw Exception("Could not find manga") + } + } } } suspend fun login(username: String, password: String): OAuth { - return Retrofit.Builder() - .baseUrl(loginUrl) - .client(client) - .addConverterFactory(jsonConverter) - .build() - .create(LoginRest::class.java) - .requestAccessToken(username, password) + return withContext(Dispatchers.IO) { + val formBody: RequestBody = FormBody.Builder() + .add("username", username) + .add("password", password) + .add("grant_type", "password") + .add("client_id", clientId) + .add("client_secret", clientSecret) + .build() + client.newCall(POST(loginUrl, body = formBody)) + .await() + .parseAs() + } } 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") - suspend fun addLibManga( - @Body data: JsonObject - ): JsonObject - - @Headers("Content-Type: application/vnd.api+json") - @PATCH("library-entries/{id}") - suspend fun updateLibManga( - @Path("id") remoteId: Int, - @Body data: JsonObject - ): JsonObject - - @GET("library-entries") - suspend fun findLibManga( - @Query("filter[manga_id]", encoded = true) remoteId: Int, - @Query("filter[user_id]", encoded = true) userId: String, - @Query("include") includes: String = "manga" - ): JsonObject - - @GET("library-entries") - suspend fun getLibManga( - @Query("filter[id]", encoded = true) remoteId: Int, - @Query("include") includes: String = "manga" - ): JsonObject - - @GET("users") - suspend fun getCurrentUser( - @Query("filter[self]", encoded = true) self: Boolean = true - ): JsonObject - } - - private interface SearchKeyRest { - @GET("media/") - suspend fun getKey(): JsonObject - } - - private interface AgoliaSearchRest { - @POST("query/") - 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") - 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 - ): OAuth + return withContext(Dispatchers.IO) { + val url = "${baseUrl}users".toUri().buildUpon() + .encodedQuery("filter[self]=true") + .build() + authClient.newCall(GET(url.toString())) + .await() + .parseAs() + .let { + it["data"]!!.jsonArray[0].jsonObject["id"]!!.jsonPrimitive.content + } + } } companion object { - private const val clientId = "dd031b32d2f56c990b1425efe6c42ad847e7fe3ab46bf1299f05ecd856bdb7dd" - private const val clientSecret = "54d7307928f63414defd96399fc31ba847961ceaecef3a5fd93144e960c0e151" - private const val baseUrl = "https://kitsu.io/api/edge/" - private const val loginUrl = "https://kitsu.io/api/" - private const val baseMangaUrl = "https://kitsu.io/manga/" - private const val algoliaKeyUrl = "https://kitsu.io/api/edge/algolia-keys/" - private const val algoliaUrl = "https://AWQO5J657S-dsn.algolia.net/1/indexes/production_media/" - 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 const val clientId = + "dd031b32d2f56c990b1425efe6c42ad847e7fe3ab46bf1299f05ecd856bdb7dd" + private const val clientSecret = + "54d7307928f63414defd96399fc31ba847961ceaecef3a5fd93144e960c0e151" - private val jsonConverter = Json { ignoreUnknownKeys = true }.asConverterFactory("application/json".toMediaType()) + private const val baseUrl = "https://kitsu.io/api/edge/" + private const val loginUrl = "https://kitsu.io/api/oauth/token" + private const val baseMangaUrl = "https://kitsu.io/manga/" + private const val algoliaKeyUrl = "https://kitsu.io/api/edge/algolia-keys/media/" + + private const val algoliaUrl = + "https://AWQO5J657S-dsn.algolia.net/1/indexes/production_media/query/" + 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" fun mangaUrl(remoteId: Int): String { return baseMangaUrl + remoteId } fun refreshTokenRequest(token: String) = POST( - "${loginUrl}oauth/token", + loginUrl, body = FormBody.Builder() .add("grant_type", "refresh_token") + .add("refresh_token", token) .add("client_id", clientId) .add("client_secret", clientSecret) - .add("refresh_token", token) .build() ) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/ShikimoriApi.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/ShikimoriApi.kt index ae1e81d1f..a8c7dbb1d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/ShikimoriApi.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/ShikimoriApi.kt @@ -7,6 +7,7 @@ import eu.kanade.tachiyomi.data.track.model.TrackSearch import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.POST import eu.kanade.tachiyomi.network.await +import eu.kanade.tachiyomi.network.jsonMime import eu.kanade.tachiyomi.network.parseAs import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.runBlocking @@ -21,13 +22,11 @@ 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 okhttp3.RequestBody.Companion.toRequestBody class ShikimoriApi(private val client: OkHttpClient, interceptor: ShikimoriInterceptor) { - private val jsonMime = "application/json; charset=utf-8".toMediaType() private val authClient = client.newBuilder().addInterceptor(interceptor).build() suspend fun addLibManga(track: Track, user_id: String): Track { diff --git a/app/src/main/java/eu/kanade/tachiyomi/network/OkHttpExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/network/OkHttpExtensions.kt index 4a99bd2de..f50c71a93 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/network/OkHttpExtensions.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/network/OkHttpExtensions.kt @@ -5,6 +5,7 @@ import kotlinx.serialization.decodeFromString import kotlinx.serialization.json.Json import okhttp3.Call import okhttp3.Callback +import okhttp3.MediaType.Companion.toMediaType import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.Response @@ -19,6 +20,8 @@ import java.util.concurrent.atomic.AtomicBoolean import kotlin.coroutines.resume import kotlin.coroutines.resumeWithException +val jsonMime = "application/json; charset=utf-8".toMediaType() + fun Call.asObservable(): Observable { return Observable.unsafeCreate { subscriber -> // Since Call is a one-shot type, clone it for each new subscriber.