Convert Shikimori to kotlinx.serialization

with this, it is over. now for the final cleanup
This commit is contained in:
Jays2Kings 2022-04-25 23:47:27 -04:00
parent d2c1582a48
commit 33135f766d
6 changed files with 191 additions and 177 deletions

View file

@ -0,0 +1,16 @@
package eu.kanade.tachiyomi.data.track.shikimori
import kotlinx.serialization.Serializable
@Serializable
data class OAuth(
val access_token: String,
val token_type: String,
val created_at: Long,
val expires_in: Long,
val refresh_token: String?,
) {
// Access token lives 1 day
fun isExpired() = (System.currentTimeMillis() / 1000) > (created_at + expires_in - 3600)
}

View file

@ -3,23 +3,36 @@ package eu.kanade.tachiyomi.data.track.shikimori
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.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
class Shikimori(private val context: Context, id: Int) : TrackService(id) { class Shikimori(private val context: Context, id: Int) : TrackService(id) {
companion object {
const val READING = 1
const val COMPLETED = 2
const val ON_HOLD = 3
const val DROPPED = 4
const val PLAN_TO_READ = 5
const val REREADING = 6
const val DEFAULT_STATUS = READING
const val DEFAULT_SCORE = 0
}
@StringRes @StringRes
override fun nameRes() = R.string.shikimori override fun nameRes() = R.string.shikimori
private val gson: Gson by injectLazy() private val json: Json by injectLazy()
private val interceptor by lazy { ShikimoriInterceptor(this, gson) } private val interceptor by lazy { ShikimoriInterceptor(this) }
private val api by lazy { ShikimoriApi(client, interceptor) } private val api by lazy { ShikimoriApi(client, interceptor) }
@ -28,14 +41,14 @@ class Shikimori(private val context: Context, id: Int) : TrackService(id) {
override fun getLogoColor() = Color.rgb(40, 40, 40) override fun getLogoColor() = Color.rgb(40, 40, 40)
override fun getStatusList(): List<Int> { override fun getStatusList(): List<Int> {
return listOf(READING, COMPLETED, ON_HOLD, DROPPED, PLANNING, REPEATING) return listOf(READING, COMPLETED, ON_HOLD, DROPPED, PLAN_TO_READ, REREADING)
} }
override fun isCompletedStatus(index: Int) = getStatusList()[index] == COMPLETED override fun isCompletedStatus(index: Int) = getStatusList()[index] == COMPLETED
override fun completedStatus(): Int = MyAnimeList.COMPLETED override fun completedStatus(): Int = COMPLETED
override fun readingStatus() = READING override fun readingStatus() = READING
override fun planningStatus() = PLANNING override fun planningStatus() = PLAN_TO_READ
override fun getStatus(status: Int): String = with(context) { override fun getStatus(status: Int): String = with(context) {
when (status) { when (status) {
@ -43,23 +56,13 @@ class Shikimori(private val context: Context, id: Int) : TrackService(id) {
COMPLETED -> getString(R.string.completed) COMPLETED -> getString(R.string.completed)
ON_HOLD -> getString(R.string.on_hold) ON_HOLD -> getString(R.string.on_hold)
DROPPED -> getString(R.string.dropped) DROPPED -> getString(R.string.dropped)
PLANNING -> getString(R.string.plan_to_read) PLAN_TO_READ -> getString(R.string.plan_to_read)
REPEATING -> getString(R.string.rereading) REREADING -> getString(R.string.rereading)
else -> "" else -> ""
} }
} }
override fun getGlobalStatus(status: Int): String = with(context) { override fun getGlobalStatus(status: Int): String = getStatus(status)
when (status) {
READING -> getString(R.string.reading)
PLANNING -> getString(R.string.plan_to_read)
COMPLETED -> getString(R.string.completed)
ON_HOLD -> getString(R.string.on_hold)
DROPPED -> getString(R.string.dropped)
REPEATING -> getString(R.string.rereading)
else -> ""
}
}
override fun getScoreList(): List<String> { override fun getScoreList(): List<String> {
return IntRange(0, 10).map(Int::toString) return IntRange(0, 10).map(Int::toString)
@ -77,12 +80,11 @@ class Shikimori(private val context: Context, id: Int) : TrackService(id) {
override suspend fun add(track: Track): Track { override suspend fun add(track: Track): Track {
track.score = DEFAULT_SCORE.toFloat() track.score = DEFAULT_SCORE.toFloat()
track.status = DEFAULT_STATUS track.status = DEFAULT_STATUS
updateNewTrackInfo(track, PLANNING) updateNewTrackInfo(track, PLAN_TO_READ)
return api.addLibManga(track, getUsername()) return api.addLibManga(track, getUsername())
} }
override suspend fun bind(track: Track): Track { override suspend fun bind(track: Track): Track {
val remoteTrack = api.findLibManga(track, getUsername()) val remoteTrack = api.findLibManga(track, getUsername())
return if (remoteTrack != null) { return if (remoteTrack != null) {
track.copyPersonalFrom(remoteTrack) track.copyPersonalFrom(remoteTrack)
track.library_id = remoteTrack.library_id track.library_id = remoteTrack.library_id
@ -94,9 +96,7 @@ class Shikimori(private val context: Context, id: Int) : TrackService(id) {
override fun canRemoveFromService(): Boolean = true override fun canRemoveFromService(): Boolean = true
override suspend fun removeFromService(track: Track): Boolean { override suspend fun removeFromService(track: Track) = api.remove(track, getUsername())
return api.remove(track, getUsername())
}
override suspend fun search(query: String) = api.search(query) override suspend fun search(query: String) = api.search(query)
@ -115,7 +115,6 @@ class Shikimori(private val context: Context, id: Int) : TrackService(id) {
suspend fun login(code: String): Boolean { suspend fun login(code: String): Boolean {
return try { return try {
val oauth = api.accessToken(code) val oauth = api.accessToken(code)
interceptor.newAuth(oauth) interceptor.newAuth(oauth)
val user = api.getCurrentUser() val user = api.getCurrentUser()
saveCredentials(user.toString(), oauth.access_token) saveCredentials(user.toString(), oauth.access_token)
@ -128,13 +127,12 @@ class Shikimori(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
} }
@ -145,16 +143,4 @@ class Shikimori(private val context: Context, id: Int) : TrackService(id) {
preferences.trackToken(this).delete() preferences.trackToken(this).delete()
interceptor.newAuth(null) interceptor.newAuth(null)
} }
companion object {
const val READING = 1
const val COMPLETED = 2
const val ON_HOLD = 3
const val DROPPED = 4
const val PLANNING = 5
const val REPEATING = 6
const val DEFAULT_STATUS = READING
const val DEFAULT_SCORE = 0
}
} }

View file

@ -1,115 +1,115 @@
package eu.kanade.tachiyomi.data.track.shikimori package eu.kanade.tachiyomi.data.track.shikimori
import androidx.core.net.toUri import androidx.core.net.toUri
import com.github.salomonbrys.kotson.array
import com.github.salomonbrys.kotson.get
import com.github.salomonbrys.kotson.jsonObject
import com.github.salomonbrys.kotson.nullString
import com.github.salomonbrys.kotson.obj
import com.google.gson.Gson
import com.google.gson.JsonArray
import com.google.gson.JsonObject
import com.google.gson.JsonParser
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 eu.kanade.tachiyomi.network.DELETE
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST import eu.kanade.tachiyomi.network.POST
import kotlinx.coroutines.Dispatchers import eu.kanade.tachiyomi.network.await
import kotlinx.coroutines.withContext import eu.kanade.tachiyomi.network.jsonMime
import eu.kanade.tachiyomi.network.parseAs
import eu.kanade.tachiyomi.util.system.withIOContext
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.contentOrNull
import kotlinx.serialization.json.float
import kotlinx.serialization.json.int
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.MediaType.Companion.toMediaTypeOrNull
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.RequestBody.Companion.toRequestBody
import timber.log.Timber import timber.log.Timber
import uy.kohesive.injekt.injectLazy
class ShikimoriApi(private val client: OkHttpClient, interceptor: ShikimoriInterceptor) { class ShikimoriApi(private val client: OkHttpClient, interceptor: ShikimoriInterceptor) {
private val gson: Gson by injectLazy()
private val jsonime = "application/json; charset=utf-8".toMediaTypeOrNull()
private val authClient = client.newBuilder().addInterceptor(interceptor).build() private val authClient = client.newBuilder().addInterceptor(interceptor).build()
suspend fun updateLibManga(track: Track, user_id: String): Track = addLibManga(track, user_id)
suspend fun addLibManga(track: Track, user_id: String): Track { suspend fun addLibManga(track: Track, user_id: String): Track {
return withContext(Dispatchers.IO) { return withIOContext {
val payload = jsonObject( val payload = buildJsonObject {
"user_rate" to jsonObject( putJsonObject("user_rate") {
"user_id" to user_id, put("user_id", user_id)
"target_id" to track.media_id, put("target_id", track.media_id)
"target_type" to "Manga", put("target_type", "Manga")
"chapters" to track.last_chapter_read.toInt(), put("chapters", track.last_chapter_read.toInt())
"score" to track.score.toInt(), put("score", track.score.toInt())
"status" to track.toShikimoriStatus() put("status", track.toShikimoriStatus())
) }
) }
val body = payload.toString().toRequestBody(jsonime) authClient.newCall(
val request = Request.Builder().url("$apiUrl/v2/user_rates").post(body).build() POST(
authClient.newCall(request).execute() "$apiUrl/v2/user_rates",
body = payload.toString().toRequestBody(jsonMime),
),
).await()
track track
} }
} }
suspend fun updateLibManga(track: Track, user_id: String): Track = addLibManga(track, user_id)
suspend fun search(search: String): List<TrackSearch> { suspend fun search(search: String): List<TrackSearch> {
return withContext(Dispatchers.IO) { return withIOContext {
val url = val url = "$apiUrl/mangas".toUri().buildUpon()
"$apiUrl/mangas".toUri().buildUpon().appendQueryParameter("order", "popularity") .appendQueryParameter("order", "popularity")
.appendQueryParameter("search", search).appendQueryParameter("limit", "20") .appendQueryParameter("search", search)
.build() .appendQueryParameter("limit", "20")
val request = Request.Builder().url(url.toString()).get().build() .build()
val netResponse = authClient.newCall(request).execute() authClient.newCall(GET(url.toString()))
.await()
val responseBody = netResponse.body?.string().orEmpty() .parseAs<JsonArray>()
if (responseBody.isEmpty()) { .let { response ->
throw Exception("Null Response") response.map {
} jsonToSearch(it.jsonObject)
val response = JsonParser.parseString(responseBody).array }
}
response.map { jsonToSearch(it.obj) }
} }
} }
suspend fun remove(track: Track, user_id: String): Boolean { suspend fun remove(track: Track, user_id: String): Boolean {
return withContext(Dispatchers.IO) { return withIOContext {
try { try {
val rates = getUserRates(track, user_id) val rates = getUserRates(track, user_id)
val id = rates.last()["id"] val id = rates.last().jsonObject["id"]!!.jsonPrimitive.content
val url = "$apiUrl/v2/user_rates/$id" val url = "$apiUrl/v2/user_rates/$id"
val request = Request.Builder().url(url).delete().build() authClient.newCall(DELETE(url)).await()
authClient.newCall(request).execute() true
return@withContext true
} catch (e: Exception) { } catch (e: Exception) {
Timber.w(e) Timber.w(e)
false
} }
return@withContext false
} }
} }
private fun jsonToSearch(obj: JsonObject): TrackSearch { private fun jsonToSearch(obj: JsonObject): TrackSearch {
return TrackSearch.create(TrackManager.SHIKIMORI).apply { return TrackSearch.create(TrackManager.SHIKIMORI).apply {
media_id = obj["id"].asInt media_id = obj["id"]!!.jsonPrimitive.int
title = obj["name"].asString title = obj["name"]!!.jsonPrimitive.content
total_chapters = obj["chapters"].asInt total_chapters = obj["chapters"]!!.jsonPrimitive.int
cover_url = baseUrl + obj["image"].obj["preview"].asString cover_url = baseUrl + obj["image"]!!.jsonObject["preview"]!!.jsonPrimitive.content
summary = "" summary = ""
tracking_url = baseUrl + obj["url"].asString tracking_url = baseUrl + obj["url"]!!.jsonPrimitive.content
publishing_status = obj["status"].asString publishing_status = obj["status"]!!.jsonPrimitive.content
publishing_type = obj["kind"].asString publishing_type = obj["kind"]!!.jsonPrimitive.content
start_date = obj.get("aired_on").nullString.orEmpty() start_date = obj["aired_on"]?.jsonPrimitive?.contentOrNull ?: ""
} }
} }
private fun jsonToTrack(obj: JsonObject, mangas: JsonObject): Track { private fun jsonToTrack(obj: JsonObject, mangas: JsonObject): Track {
return Track.create(TrackManager.SHIKIMORI).apply { return Track.create(TrackManager.SHIKIMORI).apply {
title = mangas["name"].asString title = mangas["name"]!!.jsonPrimitive.content
media_id = obj["id"].asInt media_id = obj["id"]!!.jsonPrimitive.int
total_chapters = mangas["chapters"].asInt total_chapters = mangas["chapters"]!!.jsonPrimitive.int
last_chapter_read = obj["chapters"].asFloat last_chapter_read = obj["chapters"]!!.jsonPrimitive.float
score = (obj["score"].asInt).toFloat() score = (obj["score"]!!.jsonPrimitive.int).toFloat()
status = toTrackStatus(obj["status"].asString) status = toTrackStatus(obj["status"]!!.jsonPrimitive.content)
tracking_url = baseUrl + mangas["url"].asString tracking_url = baseUrl + mangas["url"]!!.jsonPrimitive.content
} }
} }
@ -117,70 +117,70 @@ class ShikimoriApi(private val client: OkHttpClient, interceptor: ShikimoriInter
val url = "$apiUrl/v2/user_rates".toUri().buildUpon() val url = "$apiUrl/v2/user_rates".toUri().buildUpon()
.appendQueryParameter("user_id", user_id) .appendQueryParameter("user_id", user_id)
.appendQueryParameter("target_id", track.media_id.toString()) .appendQueryParameter("target_id", track.media_id.toString())
.appendQueryParameter("target_type", "Manga").build() .appendQueryParameter("target_type", "Manga")
val request = Request.Builder().url(url.toString()).get().build() .build()
return authClient.newCall(GET(url.toString()))
val requestResponse = authClient.newCall(request).execute() .execute()
val requestResponseBody = requestResponse.body?.string().orEmpty() .parseAs()
if (requestResponseBody.isEmpty()) {
throw Exception("Null Response")
}
return JsonParser.parseString(requestResponseBody).array
} }
suspend fun findLibManga(track: Track, user_id: String): Track? { suspend fun findLibManga(track: Track, user_id: String): Track? {
return withContext(Dispatchers.IO) { return withIOContext {
val urlMangas = "$apiUrl/mangas".toUri().buildUpon().appendPath(track.media_id.toString()) val urlMangas = "$apiUrl/mangas".toUri().buildUpon()
.appendPath(track.media_id.toString())
.build() .build()
val requestMangas = Request.Builder().url(urlMangas.toString()).get().build() val mangas = authClient.newCall(GET(urlMangas.toString()))
.await()
val requestMangasResponse = authClient.newCall(requestMangas).execute() .parseAs<JsonObject>()
val requestMangasBody = requestMangasResponse.body?.string().orEmpty()
val mangas = JsonParser.parseString(requestMangasBody).obj
val entry = getUserRates(track, user_id) val entry = getUserRates(track, user_id)
return@withContext entry.map { if (entry.size > 1) {
jsonToTrack(it.obj, mangas) throw Exception("Too much mangas in response")
}
entry.map {
jsonToTrack(it.jsonObject, mangas)
}.firstOrNull() }.firstOrNull()
} }
} }
suspend fun getCurrentUser(): Int { suspend fun getCurrentUser(): Int {
return withContext(Dispatchers.IO) { return withIOContext {
val user = authClient.newCall(GET("$apiUrl/users/whoami")).execute().body?.string() authClient.newCall(GET("$apiUrl/users/whoami"))
JsonParser.parseString(user).obj["id"].asInt .await()
.parseAs<JsonObject>()
.let {
it["id"]!!.jsonPrimitive.int
}
} }
} }
suspend fun accessToken(code: String): OAuth { suspend fun accessToken(code: String): OAuth {
return withContext(Dispatchers.IO) { return withIOContext {
val netResponse = client.newCall(accessTokenRequest(code)).execute() client.newCall(accessTokenRequest(code))
val responseBody = netResponse.body?.string().orEmpty() .await()
if (responseBody.isEmpty()) { .parseAs()
throw Exception("Null Response")
}
gson.fromJson(responseBody, OAuth::class.java)
} }
} }
private fun accessTokenRequest(code: String) = POST( private fun accessTokenRequest(code: String) = POST(
oauthUrl, oauthUrl,
body = FormBody.Builder().add("grant_type", "authorization_code").add("client_id", clientId) body = FormBody.Builder()
.add("client_secret", clientSecret).add("code", code).add("redirect_uri", redirectUrl) .add("grant_type", "authorization_code")
.build() .add("client_id", clientId)
.add("client_secret", clientSecret)
.add("code", code)
.add("redirect_uri", redirectUrl)
.build(),
) )
companion object { companion object {
private const val clientId = private const val clientId = "1aaf4cf232372708e98b5abc813d795b539c5a916dbbfe9ac61bf02a360832cc"
"1aaf4cf232372708e98b5abc813d795b539c5a916dbbfe9ac61bf02a360832cc" private const val clientSecret = "229942c742dd4cde803125d17d64501d91c0b12e14cb1e5120184d77d67024c0"
private const val clientSecret =
"229942c742dd4cde803125d17d64501d91c0b12e14cb1e5120184d77d67024c0"
private const val baseUrl = "https://shikimori.one" private const val baseUrl = "https://shikimori.one"
private const val apiUrl = "https://shikimori.one/api" private const val apiUrl = "$baseUrl/api"
private const val oauthUrl = "https://shikimori.one/oauth/token" private const val oauthUrl = "$baseUrl/oauth/token"
private const val loginUrl = "https://shikimori.one/oauth/authorize" private const val loginUrl = "$baseUrl/oauth/authorize"
private const val redirectUrl = "tachiyomi://shikimori-auth" private const val redirectUrl = "tachiyomi://shikimori-auth"
private const val baseMangaUrl = "$apiUrl/mangas" private const val baseMangaUrl = "$apiUrl/mangas"
@ -189,14 +189,21 @@ class ShikimoriApi(private val client: OkHttpClient, interceptor: ShikimoriInter
return "$baseMangaUrl/$remoteId" return "$baseMangaUrl/$remoteId"
} }
fun authUrl() = loginUrl.toUri().buildUpon().appendQueryParameter("client_id", clientId) fun authUrl() =
.appendQueryParameter("redirect_uri", redirectUrl) loginUrl.toUri().buildUpon()
.appendQueryParameter("response_type", "code").build() .appendQueryParameter("client_id", clientId)
.appendQueryParameter("redirect_uri", redirectUrl)
.appendQueryParameter("response_type", "code")
.build()
fun refreshTokenRequest(token: String) = POST( fun refreshTokenRequest(token: String) = POST(
oauthUrl, oauthUrl,
body = FormBody.Builder().add("grant_type", "refresh_token").add("client_id", clientId) body = FormBody.Builder()
.add("client_secret", clientSecret).add("refresh_token", token).build() .add("grant_type", "refresh_token")
.add("client_id", clientId)
.add("client_secret", clientSecret)
.add("refresh_token", token)
.build(),
) )
} }
} }

View file

@ -1,10 +1,14 @@
package eu.kanade.tachiyomi.data.track.shikimori package eu.kanade.tachiyomi.data.track.shikimori
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 ShikimoriInterceptor(val shikimori: Shikimori, val gson: Gson) : Interceptor { class ShikimoriInterceptor(val shikimori: Shikimori) : Interceptor {
private val json: Json by injectLazy()
/** /**
* OAuth object used for authenticated requests. * OAuth object used for authenticated requests.
@ -22,7 +26,7 @@ class ShikimoriInterceptor(val shikimori: Shikimori, val gson: Gson) : Intercept
if (currAuth.isExpired()) { if (currAuth.isExpired()) {
val response = chain.proceed(ShikimoriApi.refreshTokenRequest(refreshToken)) val response = chain.proceed(ShikimoriApi.refreshTokenRequest(refreshToken))
if (response.isSuccessful) { if (response.isSuccessful) {
newAuth(gson.fromJson(response.body!!.string(), OAuth::class.java)) newAuth(json.decodeFromString<OAuth>(response.body!!.string()))
} else { } else {
response.close() response.close()
} }

View file

@ -7,9 +7,9 @@ fun Track.toShikimoriStatus() = when (status) {
Shikimori.COMPLETED -> "completed" Shikimori.COMPLETED -> "completed"
Shikimori.ON_HOLD -> "on_hold" Shikimori.ON_HOLD -> "on_hold"
Shikimori.DROPPED -> "dropped" Shikimori.DROPPED -> "dropped"
Shikimori.PLANNING -> "planned" Shikimori.PLAN_TO_READ -> "planned"
Shikimori.REPEATING -> "rewatching" Shikimori.REREADING -> "rewatching"
else -> throw NotImplementedError("Unknown status") else -> throw NotImplementedError("Unknown status: $status")
} }
fun toTrackStatus(status: String) = when (status) { fun toTrackStatus(status: String) = when (status) {
@ -17,20 +17,7 @@ fun toTrackStatus(status: String) = when (status) {
"completed" -> Shikimori.COMPLETED "completed" -> Shikimori.COMPLETED
"on_hold" -> Shikimori.ON_HOLD "on_hold" -> Shikimori.ON_HOLD
"dropped" -> Shikimori.DROPPED "dropped" -> Shikimori.DROPPED
"planned" -> Shikimori.PLANNING "planned" -> Shikimori.PLAN_TO_READ
"rewatching" -> Shikimori.REPEATING "rewatching" -> Shikimori.REREADING
else -> throw NotImplementedError("Unknown status: $status")
else -> throw Exception("Unknown status")
}
data class OAuth(
val access_token: String,
val token_type: String,
val created_at: Long,
val expires_in: Long,
val refresh_token: String?
) {
// Access token lives 1 day
fun isExpired() = (System.currentTimeMillis() / 1000) > (created_at + expires_in - 3600)
} }

View file

@ -14,7 +14,7 @@ private val DEFAULT_BODY: RequestBody = FormBody.Builder().build()
fun GET( fun GET(
url: String, url: String,
headers: Headers = DEFAULT_HEADERS, headers: Headers = DEFAULT_HEADERS,
cache: CacheControl = DEFAULT_CACHE_CONTROL cache: CacheControl = DEFAULT_CACHE_CONTROL,
): Request { ): Request {
return Request.Builder() return Request.Builder()
.url(url) .url(url)
@ -27,7 +27,7 @@ fun POST(
url: String, url: String,
headers: Headers = DEFAULT_HEADERS, headers: Headers = DEFAULT_HEADERS,
body: RequestBody = DEFAULT_BODY, body: RequestBody = DEFAULT_BODY,
cache: CacheControl = DEFAULT_CACHE_CONTROL cache: CacheControl = DEFAULT_CACHE_CONTROL,
): Request { ): Request {
return Request.Builder() return Request.Builder()
.url(url) .url(url)
@ -36,3 +36,17 @@ fun POST(
.cacheControl(cache) .cacheControl(cache)
.build() .build()
} }
fun DELETE(
url: String,
headers: Headers = DEFAULT_HEADERS,
body: RequestBody = DEFAULT_BODY,
cache: CacheControl = DEFAULT_CACHE_CONTROL,
): Request {
return Request.Builder()
.url(url)
.delete(body)
.headers(headers)
.cacheControl(cache)
.build()
}