Convert Bangumi to kotlinx.serialization

This commit is contained in:
Jays2Kings 2022-04-25 23:09:57 -04:00
parent 07f5056b45
commit d2c1582a48
9 changed files with 226 additions and 170 deletions

View file

@ -0,0 +1,10 @@
package eu.kanade.tachiyomi.data.track.bangumi
import kotlinx.serialization.Serializable
@Serializable
data class Avatar(
val large: String? = "",
val medium: String? = "",
val small: String? = "",
)

View file

@ -3,12 +3,14 @@ package eu.kanade.tachiyomi.data.track.bangumi
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.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
@ -17,9 +19,9 @@ class Bangumi(private val context: Context, id: Int) : TrackService(id) {
@StringRes @StringRes
override fun nameRes() = R.string.bangumi override fun nameRes() = R.string.bangumi
private val gson: Gson by injectLazy() private val json: Json by injectLazy()
private val interceptor by lazy { BangumiInterceptor(this, gson) } private val interceptor by lazy { BangumiInterceptor(this) }
private val api by lazy { BangumiApi(client, interceptor) } private val api by lazy { BangumiApi(client, interceptor) }
@ -39,7 +41,7 @@ class Bangumi(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)
api.addLibManga(track) api.addLibManga(track)
return update(track) return update(track)
} }
@ -78,22 +80,22 @@ class Bangumi(private val context: Context, id: Int) : TrackService(id) {
override fun getLogoColor() = Color.rgb(240, 145, 153) override fun getLogoColor() = Color.rgb(240, 145, 153)
override fun getStatusList(): List<Int> { override fun getStatusList(): List<Int> {
return listOf(READING, COMPLETED, ON_HOLD, DROPPED, PLANNING) return listOf(READING, COMPLETED, ON_HOLD, DROPPED, PLAN_TO_READ)
} }
override fun isCompletedStatus(index: Int) = getStatusList()[index] == COMPLETED override fun isCompletedStatus(index: Int) = getStatusList()[index] == COMPLETED
override fun completedStatus(): Int = 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) {
READING -> getString(R.string.reading) READING -> getString(R.string.reading)
PLAN_TO_READ -> getString(R.string.plan_to_read)
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)
else -> "" else -> ""
} }
} }
@ -101,7 +103,7 @@ class Bangumi(private val context: Context, id: Int) : TrackService(id) {
override fun getGlobalStatus(status: Int): String = with(context) { override fun getGlobalStatus(status: Int): String = with(context) {
when (status) { when (status) {
READING -> getString(R.string.reading) READING -> getString(R.string.reading)
PLANNING -> getString(R.string.plan_to_read) PLAN_TO_READ -> getString(R.string.plan_to_read)
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)
@ -109,7 +111,7 @@ class Bangumi(private val context: Context, id: Int) : TrackService(id) {
} }
} }
override suspend fun login(username: String, password: String): Boolean = login(password) override suspend fun login(username: String, password: String) = login(password)
suspend fun login(code: String): Boolean { suspend fun login(code: String): Boolean {
try { try {
@ -125,13 +127,12 @@ class Bangumi(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
} }
@ -140,10 +141,11 @@ class Bangumi(private val context: Context, id: Int) : TrackService(id) {
override fun logout() { override fun logout() {
super.logout() super.logout()
preferences.trackToken(this).delete() preferences.trackToken(this).delete()
interceptor.newAuth(null)
} }
companion object { companion object {
const val PLANNING = 1 const val PLAN_TO_READ = 1
const val COMPLETED = 2 const val COMPLETED = 2
const val READING = 3 const val READING = 3
const val ON_HOLD = 4 const val ON_HOLD = 4

View file

@ -1,152 +1,180 @@
package eu.kanade.tachiyomi.data.track.bangumi package eu.kanade.tachiyomi.data.track.bangumi
import android.net.Uri
import androidx.core.net.toUri import androidx.core.net.toUri
import com.github.salomonbrys.kotson.array
import com.github.salomonbrys.kotson.obj
import com.google.gson.Gson
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.GET
import eu.kanade.tachiyomi.network.POST import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.network.await import eu.kanade.tachiyomi.network.await
import kotlinx.coroutines.Dispatchers import eu.kanade.tachiyomi.network.parseAs
import kotlinx.coroutines.withContext import eu.kanade.tachiyomi.util.system.withIOContext
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject
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 okhttp3.CacheControl import okhttp3.CacheControl
import okhttp3.FormBody import okhttp3.FormBody
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.net.URLEncoder import java.net.URLEncoder
import java.nio.charset.StandardCharsets
class BangumiApi(private val client: OkHttpClient, interceptor: BangumiInterceptor) { class BangumiApi(private val client: OkHttpClient, interceptor: BangumiInterceptor) {
private val gson: Gson by injectLazy() private val json: Json by injectLazy()
private val authClient = client.newBuilder().addInterceptor(interceptor).build() private val authClient = client.newBuilder().addInterceptor(interceptor).build()
suspend fun addLibManga(track: Track): Track { suspend fun addLibManga(track: Track): Track {
val body = FormBody.Builder().add("rating", track.score.toInt().toString()) return withIOContext {
.add("status", track.toBangumiStatus()).build() val body = FormBody.Builder()
val request = .add("rating", track.score.toInt().toString())
Request.Builder().url("$apiUrl/collection/${track.media_id}/update").post(body).build() .add("status", track.toBangumiStatus())
val response = authClient.newCall(request).await() .build()
return track authClient.newCall(POST("$apiUrl/collection/${track.media_id}/update", body = body))
.await()
track
}
} }
suspend fun updateLibManga(track: Track): Track { suspend fun updateLibManga(track: Track): Track {
// chapter update return withIOContext {
return withContext(Dispatchers.IO) {
val body =
FormBody.Builder().add("watched_eps", track.last_chapter_read.toInt().toString()).build()
val request =
Request.Builder().url("$apiUrl/subject/${track.media_id}/update/watched_eps")
.post(body).build()
// read status update // read status update
val sbody = FormBody.Builder().add("status", track.toBangumiStatus()).build() val sbody = FormBody.Builder()
val srequest = .add("rating", track.score.toInt().toString())
Request.Builder().url("$apiUrl/collection/${track.media_id}/update").post(sbody) .add("status", track.toBangumiStatus())
.build() .build()
authClient.newCall(srequest).execute() authClient.newCall(POST("$apiUrl/collection/${track.media_id}/update", body = sbody))
authClient.newCall(request).execute() .await()
// chapter update
val body = FormBody.Builder()
.add("watched_eps", track.last_chapter_read.toInt().toString())
.build()
authClient.newCall(
POST(
"$apiUrl/subject/${track.media_id}/update/watched_eps",
body = body,
),
).await()
track track
} }
} }
suspend fun search(search: String): List<TrackSearch> { suspend fun search(search: String): List<TrackSearch> {
return withContext(Dispatchers.IO) { return withIOContext {
val url = "$apiUrl/search/subject/${URLEncoder.encode(search, Charsets.UTF_8.name())}" val url = "$apiUrl/search/subject/${URLEncoder.encode(search, StandardCharsets.UTF_8.name())}"
.toUri().buildUpon().appendQueryParameter("max_results", "20").build() .toUri()
val request = Request.Builder().url(url.toString()).get().build() .buildUpon()
.appendQueryParameter("max_results", "20")
val netResponse = authClient.newCall(request).await() .build()
var responseBody = netResponse.body?.string().orEmpty() authClient.newCall(GET(url.toString()))
if (responseBody.isEmpty()) { .await()
throw Exception("Null Response") .use {
} var responseBody = it.body?.string().orEmpty()
if (responseBody.contains("\"code\":404")) { if (responseBody.isEmpty()) {
responseBody = "{\"results\":0,\"list\":[]}" throw Exception("Null Response")
} }
val response = JsonParser.parseString(responseBody).obj["list"]?.array if (responseBody.contains("\"code\":404")) {
if (response != null) { responseBody = "{\"results\":0,\"list\":[]}"
response.filter { it.obj["type"].asInt == 1 }?.map { jsonToSearch(it.obj) } }
} else { val response = json.decodeFromString<JsonObject>(responseBody)["list"]?.jsonArray
listOf() response?.filter { it.jsonObject["type"]?.jsonPrimitive?.int == 1 }
} ?.map { jsonToSearch(it.jsonObject) }.orEmpty()
}
} }
} }
private fun jsonToSearch(obj: JsonObject): TrackSearch { private fun jsonToSearch(obj: JsonObject): TrackSearch {
return TrackSearch.create(TrackManager.BANGUMI).apply { val coverUrl = if (obj["images"] is JsonObject) {
media_id = obj["id"].asInt obj["images"]?.jsonObject?.get("common")?.jsonPrimitive?.contentOrNull ?: ""
title = obj["name_cn"].asString } else {
cover_url = obj["images"].obj["common"].asString // Sometimes JsonNull
summary = obj["name"].asString ""
tracking_url = obj["url"].asString
} }
} val totalChapters = if (obj["eps_count"] != null) {
obj["eps_count"]!!.jsonPrimitive.int
private fun jsonToTrack(mangas: JsonObject): Track { } else {
return Track.create(TrackManager.BANGUMI).apply { 0
title = mangas["name"].asString }
media_id = mangas["id"].asInt return TrackSearch.create(TrackManager.BANGUMI).apply {
score = media_id = obj["id"]!!.jsonPrimitive.int
if (mangas["rating"] != null) (if (mangas["rating"].isJsonObject) mangas["rating"].obj["score"].asFloat else 0f) title = obj["name_cn"]!!.jsonPrimitive.content
else 0f cover_url = coverUrl
status = Bangumi.DEFAULT_STATUS summary = obj["name"]!!.jsonPrimitive.content
tracking_url = mangas["url"].asString tracking_url = obj["url"]!!.jsonPrimitive.content
total_chapters = totalChapters
} }
} }
suspend fun findLibManga(track: Track): Track? { suspend fun findLibManga(track: Track): Track? {
return withContext(Dispatchers.IO) { return withIOContext {
val urlMangas = "$apiUrl/subject/${track.media_id}" authClient.newCall(GET("$apiUrl/subject/${track.media_id}"))
val requestMangas = Request.Builder().url(urlMangas).get().build() .await()
val netResponse = authClient.newCall(requestMangas).execute() .parseAs<JsonObject>()
val responseBody = netResponse.body?.string().orEmpty() .let { jsonToSearch(it) }
jsonToTrack(JsonParser.parseString(responseBody).obj)
} }
} }
suspend fun statusLibManga(track: Track): Track? { suspend fun statusLibManga(track: Track): Track? {
val urlUserRead = "$apiUrl/collection/${track.media_id}" return withIOContext {
val requestUserRead = val urlUserRead = "$apiUrl/collection/${track.media_id}"
Request.Builder().url(urlUserRead).cacheControl(CacheControl.FORCE_NETWORK).get() val requestUserRead = Request.Builder()
.url(urlUserRead)
.cacheControl(CacheControl.FORCE_NETWORK)
.get()
.build() .build()
// todo get user readed chapter here // TODO: get user readed chapter here
val response = authClient.newCall(requestUserRead).await() var response = authClient.newCall(requestUserRead).await()
val resp = response.body?.toString() var responseBody = response.body?.string().orEmpty()
val coll = gson.fromJson(resp, Collection::class.java)
track.status = coll.status?.id!!
track.last_chapter_read = coll.ep_status!!.toFloat()
return track
}
suspend fun accessToken(code: String): OAuth {
return withContext(Dispatchers.IO) {
val netResponse = client.newCall(accessTokenRequest(code)).execute()
val responseBody = netResponse.body?.string().orEmpty()
if (responseBody.isEmpty()) { if (responseBody.isEmpty()) {
throw Exception("Null Response") throw Exception("Null Response")
} }
gson.fromJson(responseBody, OAuth::class.java) if (responseBody.contains("\"code\":400")) {
null
} else {
json.decodeFromString<Collection>(responseBody).let {
track.status = it.status?.id!!
track.last_chapter_read = it.ep_status!!.toFloat()
track.score = it.rating!!
track
}
}
}
}
suspend fun accessToken(code: String): OAuth {
return withIOContext {
client.newCall(accessTokenRequest(code))
.await()
.parseAs()
} }
} }
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 = "bgm10555cda0762e80ca" private const val clientId = "bgm10555cda0762e80ca"
private const val clientSecret = "8fff394a8627b4c388cbf349ec865775" private const val clientSecret = "8fff394a8627b4c388cbf349ec865775"
private const val baseUrl = "https://bangumi.org"
private const val apiUrl = "https://api.bgm.tv" private const val apiUrl = "https://api.bgm.tv"
private const val oauthUrl = "https://bgm.tv/oauth/access_token" private const val oauthUrl = "https://bgm.tv/oauth/access_token"
private const val loginUrl = "https://bgm.tv/oauth/authorize" private const val loginUrl = "https://bgm.tv/oauth/authorize"
@ -158,15 +186,22 @@ class BangumiApi(private val client: OkHttpClient, interceptor: BangumiIntercept
return "$baseMangaUrl/$remoteId" return "$baseMangaUrl/$remoteId"
} }
fun authUrl() = loginUrl.toUri().buildUpon().appendQueryParameter("client_id", clientId) fun authUrl(): Uri =
.appendQueryParameter("response_type", "code") loginUrl.toUri().buildUpon()
.appendQueryParameter("redirect_uri", redirectUrl).build() .appendQueryParameter("client_id", clientId)
.appendQueryParameter("response_type", "code")
.appendQueryParameter("redirect_uri", redirectUrl)
.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) .add("grant_type", "refresh_token")
.add("redirect_uri", redirectUrl).build() .add("client_id", clientId)
.add("client_secret", clientSecret)
.add("refresh_token", token)
.add("redirect_uri", redirectUrl)
.build(),
) )
} }
} }

View file

@ -1,26 +1,21 @@
package eu.kanade.tachiyomi.data.track.bangumi package eu.kanade.tachiyomi.data.track.bangumi
import com.google.gson.Gson import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import okhttp3.FormBody import okhttp3.FormBody
import okhttp3.Interceptor import okhttp3.Interceptor
import okhttp3.Response import okhttp3.Response
import uy.kohesive.injekt.injectLazy
class BangumiInterceptor(val bangumi: Bangumi, val gson: Gson) : Interceptor { class BangumiInterceptor(val bangumi: Bangumi) : Interceptor {
private val json: Json by injectLazy()
/** /**
* OAuth object used for authenticated requests. * OAuth object used for authenticated requests.
*/ */
private var oauth: OAuth? = bangumi.restoreToken() private var oauth: OAuth? = bangumi.restoreToken()
fun addTocken(tocken: String, oidFormBody: FormBody): FormBody {
val newFormBody = FormBody.Builder()
for (i in 0 until oidFormBody.size) {
newFormBody.add(oidFormBody.name(i), oidFormBody.value(i))
}
newFormBody.add("access_token", tocken)
return newFormBody.build()
}
override fun intercept(chain: Interceptor.Chain): Response { override fun intercept(chain: Interceptor.Chain): Response {
val originalRequest = chain.request() val originalRequest = chain.request()
@ -29,7 +24,7 @@ class BangumiInterceptor(val bangumi: Bangumi, val gson: Gson) : Interceptor {
if (currAuth.isExpired()) { if (currAuth.isExpired()) {
val response = chain.proceed(BangumiApi.refreshTokenRequest(currAuth.refresh_token!!)) val response = chain.proceed(BangumiApi.refreshTokenRequest(currAuth.refresh_token!!))
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()
} }
@ -39,30 +34,35 @@ class BangumiInterceptor(val bangumi: Bangumi, val gson: Gson) : Interceptor {
.header("User-Agent", "Tachiyomi") .header("User-Agent", "Tachiyomi")
.url( .url(
originalRequest.url.newBuilder() originalRequest.url.newBuilder()
.addQueryParameter("access_token", currAuth.access_token).build() .addQueryParameter("access_token", currAuth.access_token).build(),
) )
.build() else originalRequest.newBuilder() .build() else originalRequest.newBuilder()
.post(addTocken(currAuth.access_token, originalRequest.body as FormBody)) .post(addToken(currAuth.access_token, originalRequest.body as FormBody))
.header("User-Agent", "Tachiyomi") .header("User-Agent", "Tachiyomi")
.build() .build()
return chain.proceed(authRequest) return chain.proceed(authRequest)
} }
fun newAuth(oauth: OAuth) { fun newAuth(oauth: OAuth?) {
this.oauth = OAuth( this.oauth = if (oauth == null) null else OAuth(
oauth.access_token, oauth.access_token,
oauth.token_type, oauth.token_type,
System.currentTimeMillis() / 1000, System.currentTimeMillis() / 1000,
oauth.expires_in, oauth.expires_in,
oauth.refresh_token, oauth.refresh_token,
this.oauth?.user_id this.oauth?.user_id,
) )
bangumi.saveToken(oauth) bangumi.saveToken(oauth)
} }
fun clearOauth() { private fun addToken(token: String, oidFormBody: FormBody): FormBody {
bangumi.saveToken(null) val newFormBody = FormBody.Builder()
for (i in 0 until oidFormBody.size) {
newFormBody.add(oidFormBody.name(i), oidFormBody.value(i))
}
newFormBody.add("access_token", token)
return newFormBody.build()
} }
} }

View file

@ -7,8 +7,8 @@ fun Track.toBangumiStatus() = when (status) {
Bangumi.COMPLETED -> "collect" Bangumi.COMPLETED -> "collect"
Bangumi.ON_HOLD -> "on_hold" Bangumi.ON_HOLD -> "on_hold"
Bangumi.DROPPED -> "dropped" Bangumi.DROPPED -> "dropped"
Bangumi.PLANNING -> "wish" Bangumi.PLAN_TO_READ -> "wish"
else -> throw NotImplementedError("Unknown status") else -> throw NotImplementedError("Unknown status: $status")
} }
fun toTrackStatus(status: String) = when (status) { fun toTrackStatus(status: String) = when (status) {
@ -16,7 +16,6 @@ fun toTrackStatus(status: String) = when (status) {
"collect" -> Bangumi.COMPLETED "collect" -> Bangumi.COMPLETED
"on_hold" -> Bangumi.ON_HOLD "on_hold" -> Bangumi.ON_HOLD
"dropped" -> Bangumi.DROPPED "dropped" -> Bangumi.DROPPED
"wish" -> Bangumi.PLANNING "wish" -> Bangumi.PLAN_TO_READ
else -> throw NotImplementedError("Unknown status: $status")
else -> throw Exception("Unknown status")
} }

View file

@ -1,47 +1,16 @@
package eu.kanade.tachiyomi.data.track.bangumi package eu.kanade.tachiyomi.data.track.bangumi
import kotlinx.serialization.Serializable
@Serializable
data class Collection( data class Collection(
val `private`: Int? = 0, val `private`: Int? = 0,
val comment: String? = "", val comment: String? = "",
val ep_status: Int? = 0, val ep_status: Int? = 0,
val lasttouch: Int? = 0, val lasttouch: Int? = 0,
val rating: Int? = 0, val rating: Float? = 0f,
val status: Status? = Status(), val status: Status? = Status(),
val tag: List<String?>? = listOf(), val tag: List<String?>? = listOf(),
val user: User? = User(), val user: User? = User(),
val vol_status: Int? = 0 val vol_status: Int? = 0,
)
data class OAuth(
val access_token: String,
val token_type: String,
val created_at: Long,
val expires_in: Long,
val refresh_token: String?,
val user_id: Long?
) {
// Access token refresh before expired
fun isExpired() = (System.currentTimeMillis() / 1000) > (created_at + expires_in - 3600)
}
data class Status(
val id: Int? = 0,
val name: String? = "",
val type: String? = ""
)
data class User(
val avatar: Avatar? = Avatar(),
val id: Int? = 0,
val nickname: String? = "",
val sign: String? = "",
val url: String? = "",
val usergroup: Int? = 0,
val username: String? = ""
)
data class Avatar(
val large: String? = "",
val medium: String? = "",
val small: String? = ""
) )

View file

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

View file

@ -0,0 +1,10 @@
package eu.kanade.tachiyomi.data.track.bangumi
import kotlinx.serialization.Serializable
@Serializable
data class Status(
val id: Int? = 0,
val name: String? = "",
val type: String? = "",
)

View file

@ -0,0 +1,14 @@
package eu.kanade.tachiyomi.data.track.bangumi
import kotlinx.serialization.Serializable
@Serializable
data class User(
val avatar: Avatar? = Avatar(),
val id: Int? = 0,
val nickname: String? = "",
val sign: String? = "",
val url: String? = "",
val usergroup: Int? = 0,
val username: String? = "",
)