feat(Anilist): Parse GraphQL error responses for downtime and token expiry (#1591)

* Create ALError.kt

* Update AnilistApi.kt

* Update AnilistApi.kt

* Update AnilistApi.kt

* Add error handling for AniList API responses

* Update AnilistApi.kt

* Update ALError.kt

* Update AnilistApi.kt

* fix: improve error handling and resource management in AniList API calls

* Fix interpolation sign & add comment

---------

Co-authored-by: Cuong-Tran <16017808+cuong-tran@users.noreply.github.com>
This commit is contained in:
Itsmechinmoy 2026-05-19 16:57:36 +05:30 committed by GitHub
parent 9a7d1b6143
commit ca26501aef
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 67 additions and 8 deletions

View file

@ -5,6 +5,7 @@ import androidx.core.net.toUri
import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.track.anilist.dto.ALAddMangaResult
import eu.kanade.tachiyomi.data.track.anilist.dto.ALCurrentUserResult
import eu.kanade.tachiyomi.data.track.anilist.dto.ALError
import eu.kanade.tachiyomi.data.track.anilist.dto.ALIdSearchResult
import eu.kanade.tachiyomi.data.track.anilist.dto.ALMangaMetadata
import eu.kanade.tachiyomi.data.track.anilist.dto.ALOAuth
@ -26,6 +27,7 @@ import kotlinx.serialization.json.put
import kotlinx.serialization.json.putJsonObject
import okhttp3.OkHttpClient
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.Response
import tachiyomi.core.common.util.lang.withIOContext
import uy.kohesive.injekt.injectLazy
import java.time.Instant
@ -43,6 +45,25 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
.rateLimit(permits = 85, period = 1.minutes)
.build()
// KMK -->
private fun Response.parseALError() {
val bodyString = peekBody(1024 * 1024).string()
val errorObj = try {
json.decodeFromString<ALError>(bodyString)
} catch (_: Exception) {
null
}
errorObj?.errors?.firstOrNull()?.let {
val msg = it.message
if (msg.contains("Invalid token") || it.status == 401) {
throw Exception("AniList token expired, please login again")
}
throw Exception(msg)
}
}
// KMK <--
suspend fun addLibManga(track: Track): Track {
return withIOContext {
val query = $$"""
@ -71,6 +92,9 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
),
)
.awaitSuccess()
// KMK -->
.also { it.parseALError() }
// KMK <--
.parseAs<ALAddMangaResult>()
.let {
track.library_id = it.data.entry.id
@ -112,6 +136,9 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
}
authClient.newCall(POST(API_URL, body = payload.toString().toRequestBody(jsonMime)))
.awaitSuccess()
// KMK -->
.use { it.parseALError() }
// KMK <--
track
}
}
@ -134,6 +161,9 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
}
authClient.newCall(POST(API_URL, body = payload.toString().toRequestBody(jsonMime)))
.awaitSuccess()
// KMK -->
.use { it.parseALError() }
// KMK <--
}
}
@ -192,6 +222,9 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
),
)
.awaitSuccess()
// KMK -->
.also { it.parseALError() }
// KMK <--
.parseAs<ALSearchResult>()
.data.page.media
.map { it.toALManga().toTrack() }
@ -271,6 +304,9 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
),
)
.awaitSuccess()
// KMK -->
.also { it.parseALError() }
// KMK <--
.parseAs<ALUserListMangaQueryResult>()
.data.page.mediaList
.map { it.toALUserManga() }
@ -312,6 +348,9 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
),
)
.awaitSuccess()
// KMK -->
.also { it.parseALError() }
// KMK <--
.parseAs<ALCurrentUserResult>()
.let {
val viewer = it.data.viewer
@ -323,9 +362,9 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
suspend fun getMangaMetadata(track: DomainTrack): TrackMangaMetadata {
return withIOContext {
val query = """
|query (${'$'}mangaId: Int!) {
|Media (id: ${'$'}mangaId) {
val query = $$"""
|query ($mangaId: Int!) {
|Media (id: $mangaId) {
|id
|title {
|userPreferred
@ -365,9 +404,12 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
),
)
.awaitSuccess()
// KMK -->
.also { it.parseALError() }
// KMK <--
.parseAs<ALMangaMetadata>()
.let {
val media = it.data.media
.let { metadata ->
val media = metadata.data.media
TrackMangaMetadata(
remoteId = media.id,
title = media.title.userPreferred,
@ -392,9 +434,9 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
// SY -->
suspend fun searchById(id: String): TrackSearch {
return withIOContext {
val query = """
|query (${'$'}mangaId: Int!) {
|Media (id: ${'$'}mangaId) {
val query = $$"""
|query ($mangaId: Int!) {
|Media (id: $mangaId) {
|id
|title {
|userPreferred
@ -430,6 +472,9 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
),
)
.awaitSuccess()
// KMK -->
.also { it.parseALError() }
// KMK <--
.parseAs<ALIdSearchResult>()
.data.media
.toALManga()

View file

@ -0,0 +1,14 @@
package eu.kanade.tachiyomi.data.track.anilist.dto
import kotlinx.serialization.Serializable
@Serializable
data class ALError(
val errors: List<ALErrorItem>? = null,
)
@Serializable
data class ALErrorItem(
val message: String,
val status: Int? = null,
)