mirror of
https://github.com/tachiyomiorg/tachiyomi.git
synced 2024-11-20 03:59:16 +01:00
Add support for start/end fields for Kitsu (#5573)
Also shifting kitsu api to use kotlinx.serialization.json
This commit is contained in:
parent
db82cdedcb
commit
ba22641ff1
@ -3,13 +3,15 @@ package eu.kanade.tachiyomi.data.track.kitsu
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import com.google.gson.Gson
|
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.database.models.Track
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
import eu.kanade.tachiyomi.data.track.TrackService
|
import eu.kanade.tachiyomi.data.track.TrackService
|
||||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||||
import eu.kanade.tachiyomi.data.track.myanimelist.MyAnimeList
|
import eu.kanade.tachiyomi.data.track.myanimelist.MyAnimeList
|
||||||
import eu.kanade.tachiyomi.data.track.updateNewTrackInfo
|
import eu.kanade.tachiyomi.data.track.updateNewTrackInfo
|
||||||
|
import kotlinx.serialization.decodeFromString
|
||||||
|
import kotlinx.serialization.encodeToString
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.text.DecimalFormat
|
import java.text.DecimalFormat
|
||||||
@ -30,19 +32,17 @@ class Kitsu(private val context: Context, id: Int) : TrackService(id) {
|
|||||||
@StringRes
|
@StringRes
|
||||||
override fun nameRes() = R.string.kitsu
|
override fun nameRes() = R.string.kitsu
|
||||||
|
|
||||||
private val gson: Gson by injectLazy()
|
override val supportsReadingDates: Boolean = true
|
||||||
|
|
||||||
private val interceptor by lazy { KitsuInterceptor(this, gson) }
|
private val json: Json by injectLazy()
|
||||||
|
|
||||||
|
private val interceptor by lazy { KitsuInterceptor(this) }
|
||||||
|
|
||||||
private val api by lazy { KitsuApi(client, interceptor) }
|
private val api by lazy { KitsuApi(client, interceptor) }
|
||||||
|
|
||||||
override fun getLogo(): Int {
|
override fun getLogo() = R.drawable.ic_tracker_kitsu
|
||||||
return R.drawable.ic_tracker_kitsu
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getLogoColor(): Int {
|
override fun getLogoColor() = Color.rgb(51, 37, 50)
|
||||||
return Color.rgb(51, 37, 50)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getStatusList(): List<Int> {
|
override fun getStatusList(): List<Int> {
|
||||||
return listOf(READING, PLAN_TO_READ, COMPLETED, ON_HOLD, DROPPED)
|
return listOf(READING, PLAN_TO_READ, COMPLETED, ON_HOLD, DROPPED)
|
||||||
@ -135,15 +135,15 @@ class Kitsu(private val context: Context, id: Int) : TrackService(id) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun login(username: String, password: String): Boolean {
|
override suspend fun login(username: String, password: String): Boolean {
|
||||||
try {
|
return try {
|
||||||
val oauth = api.login(username, password)
|
val oauth = api.login(username, password)
|
||||||
interceptor.newAuth(oauth)
|
interceptor.newAuth(oauth)
|
||||||
val userId = api.getCurrentUser()
|
val userId = api.getCurrentUser()
|
||||||
saveCredentials(username, userId)
|
saveCredentials(username, userId)
|
||||||
return true
|
true
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.e(e)
|
Timber.e(e)
|
||||||
return false
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -157,13 +157,12 @@ class Kitsu(private val context: Context, id: Int) : TrackService(id) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun saveToken(oauth: OAuth?) {
|
fun saveToken(oauth: OAuth?) {
|
||||||
val json = gson.toJson(oauth)
|
preferences.trackToken(this).set(json.encodeToString(oauth))
|
||||||
preferences.trackToken(this).set(json)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun restoreToken(): OAuth? {
|
fun restoreToken(): OAuth? {
|
||||||
return try {
|
return try {
|
||||||
gson.fromJson(preferences.trackToken(this).get(), OAuth::class.java)
|
json.decodeFromString<OAuth>(preferences.trackToken(this).get())
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
@ -1,230 +1,269 @@
|
|||||||
package eu.kanade.tachiyomi.data.track.kitsu
|
package eu.kanade.tachiyomi.data.track.kitsu
|
||||||
|
|
||||||
import com.github.salomonbrys.kotson.array
|
import androidx.core.net.toUri
|
||||||
import com.github.salomonbrys.kotson.get
|
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 eu.kanade.tachiyomi.data.database.models.Track
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
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.POST
|
||||||
|
import eu.kanade.tachiyomi.network.await
|
||||||
|
import eu.kanade.tachiyomi.network.jsonMime
|
||||||
|
import eu.kanade.tachiyomi.network.parseAs
|
||||||
|
import eu.kanade.tachiyomi.util.system.withIOContext
|
||||||
|
import kotlinx.serialization.json.JsonObject
|
||||||
|
import kotlinx.serialization.json.buildJsonObject
|
||||||
|
import kotlinx.serialization.json.contentOrNull
|
||||||
|
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.FormBody
|
||||||
|
import okhttp3.Headers.Companion.headersOf
|
||||||
|
import okhttp3.MediaType.Companion.toMediaType
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import retrofit2.Retrofit
|
import okhttp3.Request
|
||||||
import retrofit2.converter.gson.GsonConverterFactory
|
import okhttp3.RequestBody
|
||||||
import retrofit2.http.Body
|
import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
import retrofit2.http.DELETE
|
|
||||||
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 timber.log.Timber
|
|
||||||
|
|
||||||
class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor) {
|
class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor) {
|
||||||
|
|
||||||
private val authClient = client.newBuilder().addInterceptor(interceptor).build()
|
private val authClient = client.newBuilder().addInterceptor(interceptor).build()
|
||||||
|
|
||||||
private val rest = Retrofit.Builder()
|
|
||||||
.baseUrl(baseUrl)
|
|
||||||
.client(authClient)
|
|
||||||
.addConverterFactory(GsonConverterFactory.create(GsonBuilder().serializeNulls().create()))
|
|
||||||
.build()
|
|
||||||
.create(KitsuApi.Rest::class.java)
|
|
||||||
|
|
||||||
private val searchRest = Retrofit.Builder()
|
|
||||||
.baseUrl(algoliaKeyUrl)
|
|
||||||
.client(authClient)
|
|
||||||
.addConverterFactory(GsonConverterFactory.create())
|
|
||||||
.build()
|
|
||||||
.create(KitsuApi.SearchKeyRest::class.java)
|
|
||||||
|
|
||||||
private val algoliaRest = Retrofit.Builder()
|
|
||||||
.baseUrl(algoliaUrl)
|
|
||||||
.client(client)
|
|
||||||
.addConverterFactory(GsonConverterFactory.create())
|
|
||||||
.build()
|
|
||||||
.create(KitsuApi.AgoliaSearchRest::class.java)
|
|
||||||
|
|
||||||
suspend fun addLibManga(track: Track, userId: String): Track {
|
suspend fun addLibManga(track: Track, userId: String): Track {
|
||||||
// @formatter:off
|
return withIOContext {
|
||||||
val data = jsonObject(
|
val data = buildJsonObject {
|
||||||
"type" to "libraryEntries",
|
putJsonObject("data") {
|
||||||
"attributes" to jsonObject(
|
put("type", "libraryEntries")
|
||||||
"status" to track.toKitsuStatus(),
|
putJsonObject("attributes") {
|
||||||
"progress" to track.last_chapter_read
|
put("status", track.toKitsuStatus())
|
||||||
),
|
put("progress", track.last_chapter_read)
|
||||||
"relationships" to jsonObject(
|
}
|
||||||
"user" to jsonObject(
|
putJsonObject("relationships") {
|
||||||
"data" to jsonObject(
|
putJsonObject("user") {
|
||||||
"id" to userId,
|
putJsonObject("data") {
|
||||||
"type" to "users"
|
put("id", userId)
|
||||||
)
|
put("type", "users")
|
||||||
),
|
}
|
||||||
"media" to jsonObject(
|
}
|
||||||
"data" to jsonObject(
|
putJsonObject("media") {
|
||||||
"id" to track.media_id,
|
putJsonObject("data") {
|
||||||
"type" to "manga"
|
put("id", track.media_id)
|
||||||
)
|
put("type", "manga")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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<JsonObject>()
|
||||||
val json = rest.addLibManga(jsonObject("data" to data))
|
.let {
|
||||||
track.media_id = json["data"]["id"].int
|
track.media_id = it["data"]!!.jsonObject["id"]!!.jsonPrimitive.int
|
||||||
return track
|
track
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun updateLibManga(track: Track): Track {
|
suspend fun updateLibManga(track: Track): Track {
|
||||||
// @formatter:off
|
return withIOContext {
|
||||||
val data = jsonObject(
|
val data = buildJsonObject {
|
||||||
"type" to "libraryEntries",
|
putJsonObject("data") {
|
||||||
"id" to track.media_id,
|
put("type", "libraryEntries")
|
||||||
"attributes" to jsonObject(
|
put("id", track.media_id)
|
||||||
"status" to track.toKitsuStatus(),
|
putJsonObject("attributes") {
|
||||||
"progress" to track.last_chapter_read,
|
put("status", track.toKitsuStatus())
|
||||||
"ratingTwenty" to track.toKitsuScore()
|
put("progress", track.last_chapter_read)
|
||||||
)
|
put("ratingTwenty", track.toKitsuScore())
|
||||||
)
|
put("startedAt", KitsuDateHelper.convert(track.started_reading_date))
|
||||||
// @formatter:on
|
put("finishedAt", KitsuDateHelper.convert(track.finished_reading_date))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
rest.updateLibManga(track.media_id, jsonObject("data" to data))
|
authClient.newCall(
|
||||||
return track
|
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<JsonObject>()
|
||||||
|
.let {
|
||||||
|
val manga = it["data"]?.jsonObject
|
||||||
|
if (manga != null) {
|
||||||
|
val startedAt = manga["attributes"]!!.jsonObject["startedAt"]?.jsonPrimitive?.contentOrNull
|
||||||
|
val finishedAt = manga["attributes"]!!.jsonObject["finishedAt"]?.jsonPrimitive?.contentOrNull
|
||||||
|
val startedDate = KitsuDateHelper.parse(startedAt)
|
||||||
|
if (track.started_reading_date <= 0L || startedDate > 0) {
|
||||||
|
track.started_reading_date = startedDate
|
||||||
|
}
|
||||||
|
val finishedDate = KitsuDateHelper.parse(finishedAt)
|
||||||
|
if (track.finished_reading_date <= 0L || finishedDate > 0) {
|
||||||
|
track.finished_reading_date = finishedDate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
track.apply {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun remove(track: Track): Boolean {
|
suspend fun remove(track: Track): Boolean {
|
||||||
try {
|
return withIOContext {
|
||||||
rest.deleteLibManga(track.media_id)
|
val data = buildJsonObject {
|
||||||
return true
|
putJsonObject("data") {
|
||||||
} catch (e: Exception) {
|
put("type", "libraryEntries")
|
||||||
Timber.w(e)
|
put("id", track.media_id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
authClient.newCall(
|
||||||
|
Request.Builder()
|
||||||
|
.url("${baseUrl}library-entries/${track.media_id}")
|
||||||
|
.headers(
|
||||||
|
headersOf(
|
||||||
|
"Content-Type",
|
||||||
|
"application/vnd.api+json"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.delete(data.toString().toRequestBody("application/vnd.api+json".toMediaType()))
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
.await()
|
||||||
|
.let {
|
||||||
|
true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun search(query: String): List<TrackSearch> {
|
suspend fun search(query: String): List<TrackSearch> {
|
||||||
val key = searchRest.getKey()["media"].asJsonObject["key"].string
|
return withIOContext {
|
||||||
return algoliaSearch(key, query)
|
authClient.newCall(GET(algoliaKeyUrl))
|
||||||
|
.await()
|
||||||
|
.parseAs<JsonObject>()
|
||||||
|
.let {
|
||||||
|
val key = it["media"]!!.jsonObject["key"]!!.jsonPrimitive.content
|
||||||
|
algoliaSearch(key, query)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun algoliaSearch(key: String, query: String): List<TrackSearch> {
|
private suspend fun algoliaSearch(key: String, query: String): List<TrackSearch> {
|
||||||
val jsonObject = jsonObject("params" to "query=$query$algoliaFilter")
|
return withIOContext {
|
||||||
val json = algoliaRest.getSearchQuery(algoliaAppId, key, jsonObject)
|
val jsonObject = buildJsonObject {
|
||||||
val data = json["hits"].array
|
put("params", "query=$query$algoliaFilter")
|
||||||
return data.map { KitsuSearchManga(it.obj) }
|
}
|
||||||
.filter { it.subType != "novel" }
|
|
||||||
.map { it.toTrack() }
|
client.newCall(
|
||||||
|
POST(
|
||||||
|
algoliaUrl,
|
||||||
|
headers = headersOf(
|
||||||
|
"X-Algolia-Application-Id",
|
||||||
|
algoliaAppId,
|
||||||
|
"X-Algolia-API-Key",
|
||||||
|
key,
|
||||||
|
),
|
||||||
|
body = jsonObject.toString().toRequestBody(jsonMime)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.await()
|
||||||
|
.parseAs<JsonObject>()
|
||||||
|
.let {
|
||||||
|
it["hits"]!!.jsonArray
|
||||||
|
.map { KitsuSearchManga(it.jsonObject) }
|
||||||
|
.filter { it.subType != "novel" }
|
||||||
|
.map { it.toTrack() }
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun findLibManga(track: Track, userId: String): Track? {
|
suspend fun findLibManga(track: Track, userId: String): Track? {
|
||||||
val json = rest.findLibManga(track.media_id, userId)
|
return withIOContext {
|
||||||
val data = json["data"].array
|
val url = "${baseUrl}library-entries".toUri().buildUpon()
|
||||||
return if (data.size() > 0) {
|
.encodedQuery("filter[manga_id]=${track.media_id}&filter[user_id]=$userId")
|
||||||
val manga = json["included"].array[0].obj
|
.appendQueryParameter("include", "manga")
|
||||||
KitsuLibManga(data[0].obj, manga).toTrack()
|
.build()
|
||||||
} else {
|
authClient.newCall(GET(url.toString()))
|
||||||
null
|
.await()
|
||||||
|
.parseAs<JsonObject>()
|
||||||
|
.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 {
|
suspend fun getLibManga(track: Track): Track {
|
||||||
val json = rest.getLibManga(track.media_id)
|
return withIOContext {
|
||||||
val data = json["data"].array
|
val url = "${baseUrl}library-entries".toUri().buildUpon()
|
||||||
if (data.size() > 0) {
|
.encodedQuery("filter[id]=${track.media_id}")
|
||||||
val manga = json["included"].array[0].obj
|
.appendQueryParameter("include", "manga")
|
||||||
return KitsuLibManga(data[0].obj, manga).toTrack()
|
.build()
|
||||||
} else {
|
authClient.newCall(GET(url.toString()))
|
||||||
throw Exception("Could not find manga")
|
.await()
|
||||||
|
.parseAs<JsonObject>()
|
||||||
|
.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 {
|
suspend fun login(username: String, password: String): OAuth {
|
||||||
return Retrofit.Builder()
|
return withIOContext {
|
||||||
.baseUrl(loginUrl)
|
val formBody: RequestBody = FormBody.Builder()
|
||||||
.client(client)
|
.add("username", username)
|
||||||
.addConverterFactory(GsonConverterFactory.create())
|
.add("password", password)
|
||||||
.build()
|
.add("grant_type", "password")
|
||||||
.create(KitsuApi.LoginRest::class.java)
|
.add("client_id", clientId)
|
||||||
.requestAccessToken(username, password)
|
.add("client_secret", clientSecret)
|
||||||
|
.build()
|
||||||
|
client.newCall(POST(loginUrl, body = formBody))
|
||||||
|
.await()
|
||||||
|
.parseAs()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getCurrentUser(): String {
|
suspend fun getCurrentUser(): String {
|
||||||
val currentUser = rest.getCurrentUser()
|
return withIOContext {
|
||||||
return currentUser["data"].array[0]["id"].string
|
val url = "${baseUrl}users".toUri().buildUpon()
|
||||||
}
|
.encodedQuery("filter[self]=true")
|
||||||
|
.build()
|
||||||
private interface Rest {
|
authClient.newCall(GET(url.toString()))
|
||||||
|
.await()
|
||||||
@Headers("Content-Type: application/vnd.api+json")
|
.parseAs<JsonObject>()
|
||||||
@POST("library-entries")
|
.let {
|
||||||
suspend fun addLibManga(
|
it["data"]!!.jsonArray[0].jsonObject["id"]!!.jsonPrimitive.content
|
||||||
@Body data: JsonObject
|
}
|
||||||
): JsonObject
|
}
|
||||||
|
|
||||||
@Headers("Content-Type: application/vnd.api+json")
|
|
||||||
@DELETE("library-entries/{id}")
|
|
||||||
suspend fun deleteLibManga(
|
|
||||||
@Path("id") remoteId: Int
|
|
||||||
): 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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@ -232,12 +271,14 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
|
|||||||
"dd031b32d2f56c990b1425efe6c42ad847e7fe3ab46bf1299f05ecd856bdb7dd"
|
"dd031b32d2f56c990b1425efe6c42ad847e7fe3ab46bf1299f05ecd856bdb7dd"
|
||||||
private const val clientSecret =
|
private const val clientSecret =
|
||||||
"54d7307928f63414defd96399fc31ba847961ceaecef3a5fd93144e960c0e151"
|
"54d7307928f63414defd96399fc31ba847961ceaecef3a5fd93144e960c0e151"
|
||||||
|
|
||||||
private const val baseUrl = "https://kitsu.io/api/edge/"
|
private const val baseUrl = "https://kitsu.io/api/edge/"
|
||||||
private const val loginUrl = "https://kitsu.io/api/"
|
private const val loginUrl = "https://kitsu.io/api/oauth/token"
|
||||||
private const val baseMangaUrl = "https://kitsu.io/manga/"
|
private const val baseMangaUrl = "https://kitsu.io/manga/"
|
||||||
private const val algoliaKeyUrl = "https://kitsu.io/api/edge/algolia-keys/"
|
private const val algoliaKeyUrl = "https://kitsu.io/api/edge/algolia-keys/media/"
|
||||||
|
|
||||||
private const val algoliaUrl =
|
private const val algoliaUrl =
|
||||||
"https://AWQO5J657S-dsn.algolia.net/1/indexes/production_media/"
|
"https://AWQO5J657S-dsn.algolia.net/1/indexes/production_media/query/"
|
||||||
private const val algoliaAppId = "AWQO5J657S"
|
private const val algoliaAppId = "AWQO5J657S"
|
||||||
private const val algoliaFilter =
|
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"
|
"&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"
|
||||||
@ -247,12 +288,12 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun refreshTokenRequest(token: String) = POST(
|
fun refreshTokenRequest(token: String) = POST(
|
||||||
"${loginUrl}oauth/token",
|
loginUrl,
|
||||||
body = FormBody.Builder()
|
body = FormBody.Builder()
|
||||||
.add("grant_type", "refresh_token")
|
.add("grant_type", "refresh_token")
|
||||||
|
.add("refresh_token", token)
|
||||||
.add("client_id", clientId)
|
.add("client_id", clientId)
|
||||||
.add("client_secret", clientSecret)
|
.add("client_secret", clientSecret)
|
||||||
.add("refresh_token", token)
|
|
||||||
.build()
|
.build()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,25 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.track.kitsu
|
||||||
|
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Date
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
object KitsuDateHelper {
|
||||||
|
|
||||||
|
private const val pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"
|
||||||
|
private val formatter = SimpleDateFormat(pattern, Locale.ENGLISH)
|
||||||
|
|
||||||
|
fun convert(dateValue: Long): String? {
|
||||||
|
if (dateValue <= 0L) return null
|
||||||
|
|
||||||
|
return formatter.format(Date(dateValue))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun parse(dateString: String?): Long {
|
||||||
|
if (dateString == null) return 0L
|
||||||
|
|
||||||
|
val dateValue = formatter.parse(dateString)
|
||||||
|
|
||||||
|
return dateValue?.time ?: return 0
|
||||||
|
}
|
||||||
|
}
|
@ -1,10 +1,14 @@
|
|||||||
package eu.kanade.tachiyomi.data.track.kitsu
|
package eu.kanade.tachiyomi.data.track.kitsu
|
||||||
|
|
||||||
import com.google.gson.Gson
|
import kotlinx.serialization.decodeFromString
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
import okhttp3.Interceptor
|
import okhttp3.Interceptor
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
class KitsuInterceptor(val kitsu: Kitsu, val gson: Gson) : Interceptor {
|
class KitsuInterceptor(val kitsu: Kitsu) : Interceptor {
|
||||||
|
|
||||||
|
private val json: Json by injectLazy()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* OAuth object used for authenticated requests.
|
* OAuth object used for authenticated requests.
|
||||||
@ -22,7 +26,7 @@ class KitsuInterceptor(val kitsu: Kitsu, val gson: Gson) : Interceptor {
|
|||||||
if (currAuth.isExpired()) {
|
if (currAuth.isExpired()) {
|
||||||
val response = chain.proceed(KitsuApi.refreshTokenRequest(refreshToken))
|
val response = chain.proceed(KitsuApi.refreshTokenRequest(refreshToken))
|
||||||
if (response.isSuccessful) {
|
if (response.isSuccessful) {
|
||||||
newAuth(gson.fromJson(response.body!!.string(), OAuth::class.java))
|
newAuth(json.decodeFromString(response.body!!.string()))
|
||||||
} else {
|
} else {
|
||||||
response.close()
|
response.close()
|
||||||
}
|
}
|
||||||
|
@ -1,32 +1,36 @@
|
|||||||
package eu.kanade.tachiyomi.data.track.kitsu
|
package eu.kanade.tachiyomi.data.track.kitsu
|
||||||
|
|
||||||
import androidx.annotation.CallSuper
|
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.database.models.Track
|
||||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
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.text.SimpleDateFormat
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
|
||||||
class KitsuSearchManga(obj: JsonObject) {
|
class KitsuSearchManga(obj: JsonObject) {
|
||||||
val id by obj.byInt
|
val id = obj["id"]!!.jsonPrimitive.int
|
||||||
private val canonicalTitle by obj.byString
|
private val canonicalTitle = obj["canonicalTitle"]!!.jsonPrimitive.content
|
||||||
private val chapterCount = obj.get("chapterCount").nullInt
|
private val chapterCount = obj["chapterCount"]?.jsonPrimitive?.intOrNull
|
||||||
val subType = obj.get("subtype").nullString
|
val subType = obj["subtype"]?.jsonPrimitive?.contentOrNull
|
||||||
val original = obj.get("posterImage").nullObj?.get("original")?.asString
|
val original = try {
|
||||||
private val synopsis by obj.byString
|
obj["posterImage"]?.jsonObject?.get("original")?.jsonPrimitive?.content
|
||||||
private var startDate = obj.get("startDate").nullString?.let {
|
} catch (e: IllegalArgumentException) {
|
||||||
|
// posterImage is sometimes a jsonNull object instead
|
||||||
|
null
|
||||||
|
}
|
||||||
|
private val synopsis = obj["synopsis"]!!.jsonPrimitive.content
|
||||||
|
private var startDate = obj["startDate"]?.jsonPrimitive?.contentOrNull?.let {
|
||||||
val outputDf = SimpleDateFormat("yyyy-MM-dd", Locale.US)
|
val outputDf = SimpleDateFormat("yyyy-MM-dd", Locale.US)
|
||||||
outputDf.format(Date(it.toLong() * 1000))
|
outputDf.format(Date(it.toLong() * 1000))
|
||||||
}
|
}
|
||||||
private val endDate = obj.get("endDate").nullString
|
private val endDate = obj["endDate"]?.jsonPrimitive?.contentOrNull
|
||||||
|
|
||||||
@CallSuper
|
@CallSuper
|
||||||
fun toTrack() = TrackSearch.create(TrackManager.KITSU).apply {
|
fun toTrack() = TrackSearch.create(TrackManager.KITSU).apply {
|
||||||
@ -36,10 +40,10 @@ class KitsuSearchManga(obj: JsonObject) {
|
|||||||
cover_url = original ?: ""
|
cover_url = original ?: ""
|
||||||
summary = synopsis
|
summary = synopsis
|
||||||
tracking_url = KitsuApi.mangaUrl(media_id)
|
tracking_url = KitsuApi.mangaUrl(media_id)
|
||||||
if (endDate == null) {
|
publishing_status = if (endDate == null) {
|
||||||
publishing_status = "Publishing"
|
"Publishing"
|
||||||
} else {
|
} else {
|
||||||
publishing_status = "Finished"
|
"Finished"
|
||||||
}
|
}
|
||||||
publishing_type = subType ?: ""
|
publishing_type = subType ?: ""
|
||||||
start_date = startDate ?: ""
|
start_date = startDate ?: ""
|
||||||
@ -47,17 +51,19 @@ class KitsuSearchManga(obj: JsonObject) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class KitsuLibManga(obj: JsonObject, manga: JsonObject) {
|
class KitsuLibManga(obj: JsonObject, manga: JsonObject) {
|
||||||
val id by manga.byInt
|
val id = manga["id"]!!.jsonPrimitive.int
|
||||||
private val canonicalTitle by manga["attributes"].byString
|
private val canonicalTitle = manga["attributes"]!!.jsonObject["canonicalTitle"]!!.jsonPrimitive.content
|
||||||
private val chapterCount = manga["attributes"].obj.get("chapterCount").nullInt
|
private val chapterCount = manga["attributes"]!!.jsonObject["chapterCount"]?.jsonPrimitive?.intOrNull
|
||||||
val type = manga["attributes"].obj.get("mangaType").nullString.orEmpty()
|
val type = manga["attributes"]!!.jsonObject["mangaType"]?.jsonPrimitive?.contentOrNull.orEmpty()
|
||||||
val original by manga["attributes"].obj["posterImage"].byString
|
val original = manga["attributes"]!!.jsonObject["posterImage"]!!.jsonObject["original"]!!.jsonPrimitive.content
|
||||||
private val synopsis by manga["attributes"].byString
|
private val synopsis = manga["attributes"]!!.jsonObject["synopsis"]!!.jsonPrimitive.content
|
||||||
private val startDate = manga["attributes"].obj.get("startDate").nullString.orEmpty()
|
private val startDate = manga["attributes"]!!.jsonObject["startDate"]?.jsonPrimitive?.contentOrNull.orEmpty()
|
||||||
private val libraryId by obj.byInt("id")
|
private val startedAt = obj["attributes"]!!.jsonObject["startedAt"]?.jsonPrimitive?.contentOrNull
|
||||||
val status by obj["attributes"].byString
|
private val finishedAt = obj["attributes"]!!.jsonObject["finishedAt"]?.jsonPrimitive?.contentOrNull
|
||||||
private val ratingTwenty = obj["attributes"].obj.get("ratingTwenty").nullString
|
private val libraryId = obj["id"]!!.jsonPrimitive.int
|
||||||
val progress by obj["attributes"].byInt
|
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 {
|
fun toTrack() = TrackSearch.create(TrackManager.KITSU).apply {
|
||||||
media_id = libraryId
|
media_id = libraryId
|
||||||
@ -69,6 +75,8 @@ class KitsuLibManga(obj: JsonObject, manga: JsonObject) {
|
|||||||
publishing_status = this@KitsuLibManga.status
|
publishing_status = this@KitsuLibManga.status
|
||||||
publishing_type = type
|
publishing_type = type
|
||||||
start_date = startDate
|
start_date = startDate
|
||||||
|
started_reading_date = KitsuDateHelper.parse(startedAt)
|
||||||
|
finished_reading_date = KitsuDateHelper.parse(finishedAt)
|
||||||
status = toTrackStatus()
|
status = toTrackStatus()
|
||||||
score = ratingTwenty?.let { it.toInt() / 2f } ?: 0f
|
score = ratingTwenty?.let { it.toInt() / 2f } ?: 0f
|
||||||
last_chapter_read = progress
|
last_chapter_read = progress
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
package eu.kanade.tachiyomi.data.track.kitsu
|
package eu.kanade.tachiyomi.data.track.kitsu
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
data class OAuth(
|
data class OAuth(
|
||||||
val access_token: String,
|
val access_token: String,
|
||||||
val token_type: String,
|
val token_type: String,
|
||||||
|
Loading…
Reference in New Issue
Block a user