Add support for start/end fields for Kitsu (#5573)

Also shifting kitsu api to use kotlinx.serialization.json
This commit is contained in:
Andreas 2021-07-18 18:47:40 +02:00 committed by Jays2Kings
parent db82cdedcb
commit ba22641ff1
6 changed files with 315 additions and 235 deletions

View File

@ -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
} }

View File

@ -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()
) )
} }

View File

@ -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
}
}

View File

@ -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()
} }

View File

@ -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

View File

@ -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,