fix: Refactor MAL code to not spam refresh token when it fails

This commit is contained in:
AntsyLich 2024-01-28 01:12:18 +07:00 committed by ziro
parent 27e7708803
commit c926467c96
Signed by: null2264
GPG key ID: BA64F8B60AF3EFB6
4 changed files with 67 additions and 54 deletions

View file

@ -12,9 +12,15 @@ class TrackPreferences(
fun trackPassword(sync: TrackService) = preferenceStore.getString(trackPassword(sync.id), "") fun trackPassword(sync: TrackService) = preferenceStore.getString(trackPassword(sync.id), "")
fun trackAuthExpired(sync: TrackService) = preferenceStore.getBoolean(
Preference.privateKey("pref_tracker_auth_expired_${sync.id}"),
false,
)
fun setCredentials(sync: TrackService, username: String, password: String) { fun setCredentials(sync: TrackService, username: String, password: String) {
trackUsername(sync).set(username) trackUsername(sync).set(username)
trackPassword(sync).set(password) trackPassword(sync).set(password)
trackAuthExpired(sync).set(false)
} }
fun trackToken(sync: TrackService) = preferenceStore.getString(trackToken(sync.id), "") fun trackToken(sync: TrackService) = preferenceStore.getString(trackToken(sync.id), "")

View file

@ -8,7 +8,6 @@ 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.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import timber.log.Timber import timber.log.Timber
@ -17,7 +16,7 @@ import uy.kohesive.injekt.injectLazy
class MyAnimeList(private val context: Context, id: Int) : TrackService(id) { class MyAnimeList(private val context: Context, id: Int) : TrackService(id) {
private val json: Json by injectLazy() private val json: Json by injectLazy()
private val interceptor by lazy { MyAnimeListInterceptor(this, getPassword()) } private val interceptor by lazy { MyAnimeListInterceptor(this) }
private val api by lazy { MyAnimeListApi(client, interceptor) } private val api by lazy { MyAnimeListApi(client, interceptor) }
@StringRes @StringRes
@ -142,6 +141,14 @@ class MyAnimeList(private val context: Context, id: Int) : TrackService(id) {
interceptor.setAuth(null) interceptor.setAuth(null)
} }
fun getIfAuthExpired(): Boolean {
return trackPreferences.trackAuthExpired(this).get()
}
fun setAuthExpired() {
trackPreferences.trackAuthExpired(this).set(true)
}
fun saveOAuth(oAuth: OAuth?) { fun saveOAuth(oAuth: OAuth?) {
trackPreferences.trackToken(this).set(json.encodeToString(oAuth)) trackPreferences.trackToken(this).set(json.encodeToString(oAuth))
} }
@ -153,6 +160,7 @@ class MyAnimeList(private val context: Context, id: Int) : TrackService(id) {
null null
} }
} }
companion object { companion object {
const val READING = 1 const val READING = 1
const val COMPLETED = 2 const val COMPLETED = 2

View file

@ -41,13 +41,13 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI
suspend fun getAccessToken(authCode: String): OAuth { suspend fun getAccessToken(authCode: String): OAuth {
return withIOContext { return withIOContext {
val formBody: RequestBody = FormBody.Builder() val formBody: RequestBody = FormBody.Builder()
.add("client_id", clientId) .add("client_id", CLIENT_ID)
.add("code", authCode) .add("code", authCode)
.add("code_verifier", codeVerifier) .add("code_verifier", codeVerifier)
.add("grant_type", "authorization_code") .add("grant_type", "authorization_code")
.build() .build()
with(json) { with(json) {
client.newCall(POST("$baseOAuthUrl/token", body = formBody)) client.newCall(POST("$BASE_OAUTH_URL/token", body = formBody))
.awaitSuccess() .awaitSuccess()
.parseAs() .parseAs()
} }
@ -57,7 +57,7 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI
suspend fun getCurrentUser(): String { suspend fun getCurrentUser(): String {
return withIOContext { return withIOContext {
val request = Request.Builder() val request = Request.Builder()
.url("$baseApiUrl/users/@me") .url("$BASE_API_URL/users/@me")
.get() .get()
.build() .build()
with(json) { with(json) {
@ -71,7 +71,7 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI
suspend fun search(query: String): List<TrackSearch> { suspend fun search(query: String): List<TrackSearch> {
return withIOContext { return withIOContext {
val url = "$baseApiUrl/manga".toUri().buildUpon() val url = "$BASE_API_URL/manga".toUri().buildUpon()
// MAL API throws a 400 when the query is over 64 characters... // MAL API throws a 400 when the query is over 64 characters...
.appendQueryParameter("q", query.take(64)) .appendQueryParameter("q", query.take(64))
.appendQueryParameter("nsfw", "true") .appendQueryParameter("nsfw", "true")
@ -96,7 +96,7 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI
suspend fun getMangaDetails(id: Int): TrackSearch { suspend fun getMangaDetails(id: Int): TrackSearch {
return withIOContext { return withIOContext {
val url = "$baseApiUrl/manga".toUri().buildUpon() val url = "$BASE_API_URL/manga".toUri().buildUpon()
.appendPath(id.toString()) .appendPath(id.toString())
.appendQueryParameter("fields", "id,title,synopsis,num_chapters,main_picture,status,media_type,start_date") .appendQueryParameter("fields", "id,title,synopsis,num_chapters,main_picture,status,media_type,start_date")
.build() .build()
@ -156,7 +156,7 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI
suspend fun findListItem(track: Track): Track? { suspend fun findListItem(track: Track): Track? {
return withIOContext { return withIOContext {
val uri = "$baseApiUrl/manga".toUri().buildUpon() val uri = "$BASE_API_URL/manga".toUri().buildUpon()
.appendPath(track.media_id.toString()) .appendPath(track.media_id.toString())
.appendQueryParameter("fields", "num_chapters,my_list_status{start_date,finish_date}") .appendQueryParameter("fields", "num_chapters,my_list_status{start_date,finish_date}")
.build() .build()
@ -194,7 +194,7 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI
// Check next page if there's more // Check next page if there's more
if (!obj["paging"]!!.jsonObject["next"]?.jsonPrimitive?.contentOrNull.isNullOrBlank()) { if (!obj["paging"]!!.jsonObject["next"]?.jsonPrimitive?.contentOrNull.isNullOrBlank()) {
matches + findListItems(query, offset + listPaginationAmount) matches + findListItems(query, offset + LIST_PAGINATION_AMOUNT)
} else { } else {
matches matches
} }
@ -203,9 +203,9 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI
private suspend fun getListPage(offset: Int): JsonObject { private suspend fun getListPage(offset: Int): JsonObject {
return withIOContext { return withIOContext {
val urlBuilder = "$baseApiUrl/users/@me/mangalist".toUri().buildUpon() val urlBuilder = "$BASE_API_URL/users/@me/mangalist".toUri().buildUpon()
.appendQueryParameter("fields", "list_status{start_date,finish_date}") .appendQueryParameter("fields", "list_status{start_date,finish_date}")
.appendQueryParameter("limit", listPaginationAmount.toString()) .appendQueryParameter("limit", LIST_PAGINATION_AMOUNT.toString())
if (offset > 0) { if (offset > 0) {
urlBuilder.appendQueryParameter("offset", offset.toString()) urlBuilder.appendQueryParameter("offset", offset.toString())
} }
@ -277,30 +277,29 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI
} }
companion object { companion object {
// Registered under jay's MAL account private const val CLIENT_ID = "9e6656c53d1910999cc3c537e0e6256a"
private const val clientId = "9e6656c53d1910999cc3c537e0e6256a"
private const val baseOAuthUrl = "https://myanimelist.net/v1/oauth2" private const val BASE_OAUTH_URL = "https://myanimelist.net/v1/oauth2"
private const val baseApiUrl = "https://api.myanimelist.net/v2" private const val BASE_API_URL = "https://api.myanimelist.net/v2"
private const val listPaginationAmount = 250 private const val LIST_PAGINATION_AMOUNT = 250
private var codeVerifier: String = "" private var codeVerifier: String = ""
fun authUrl(): Uri = "$baseOAuthUrl/authorize".toUri().buildUpon() fun authUrl(): Uri = "$BASE_OAUTH_URL/authorize".toUri().buildUpon()
.appendQueryParameter("client_id", clientId) .appendQueryParameter("client_id", CLIENT_ID)
.appendQueryParameter("code_challenge", getPkceChallengeCode()) .appendQueryParameter("code_challenge", getPkceChallengeCode())
.appendQueryParameter("response_type", "code") .appendQueryParameter("response_type", "code")
.build() .build()
fun mangaUrl(id: Long): Uri = "$baseApiUrl/manga".toUri().buildUpon() fun mangaUrl(id: Long): Uri = "$BASE_API_URL/manga".toUri().buildUpon()
.appendPath(id.toString()) .appendPath(id.toString())
.appendPath("my_list_status") .appendPath("my_list_status")
.build() .build()
fun refreshTokenRequest(oauth: OAuth): Request { fun refreshTokenRequest(oauth: OAuth): Request {
val formBody: RequestBody = FormBody.Builder() val formBody: RequestBody = FormBody.Builder()
.add("client_id", clientId) .add("client_id", CLIENT_ID)
.add("refresh_token", oauth.refresh_token) .add("refresh_token", oauth.refresh_token)
.add("grant_type", "refresh_token") .add("grant_type", "refresh_token")
.build() .build()
@ -312,7 +311,7 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI
.add("Authorization", "Bearer ${oauth.access_token}") .add("Authorization", "Bearer ${oauth.access_token}")
.build() .build()
return POST("$baseOAuthUrl/token", body = formBody, headers = headers) return POST("$BASE_OAUTH_URL/token", body = formBody, headers = headers)
} }
private fun getPkceChallengeCode(): String { private fun getPkceChallengeCode(): String {

View file

@ -1,58 +1,40 @@
package eu.kanade.tachiyomi.data.track.myanimelist package eu.kanade.tachiyomi.data.track.myanimelist
import eu.kanade.tachiyomi.BuildConfig
import eu.kanade.tachiyomi.network.parseAs import eu.kanade.tachiyomi.network.parseAs
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import okhttp3.Interceptor import okhttp3.Interceptor
import okhttp3.Response import okhttp3.Response
import okhttp3.internal.closeQuietly
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.io.IOException import java.io.IOException
class MyAnimeListInterceptor(private val myanimelist: MyAnimeList, private var token: String?) : Interceptor { class MyAnimeListInterceptor(private val myanimelist: MyAnimeList) : Interceptor {
private val json: Json by injectLazy() private val json: Json by injectLazy()
private var oauth: OAuth? = null private var oauth: OAuth? = myanimelist.loadOAuth()
private val tokenExpired get() = myanimelist.getIfAuthExpired()
override fun intercept(chain: Interceptor.Chain): Response { override fun intercept(chain: Interceptor.Chain): Response {
if (tokenExpired) {
throw MALTokenExpired()
}
val originalRequest = chain.request() val originalRequest = chain.request()
if (token.isNullOrEmpty()) { // Refresh access token if expired
throw IOException("Not authenticated with MyAnimeList") if (oauth != null && oauth!!.isExpired()) {
setAuth(refreshToken(chain))
} }
if (oauth == null) { if (oauth == null) {
oauth = myanimelist.loadOAuth() throw IOException("MAL: User is not authenticated")
}
// Refresh access token if expired or created_at is freshly set
if (oauth != null &&
(oauth!!.isExpired() || oauth!!.created_at == System.currentTimeMillis())
) {
val newOauth = with(json) {
runCatching {
val oauthResponse = chain.proceed(MyAnimeListApi.refreshTokenRequest(oauth!!))
if (oauthResponse.isSuccessful) {
oauthResponse.parseAs<OAuth>()
} else {
oauthResponse.closeQuietly()
null
}
}
}
if (newOauth.getOrNull() == null) {
throw IOException("Failed to refresh the access token")
}
setAuth(newOauth.getOrNull())
}
if (oauth == null) {
throw IOException("No authentication token")
} }
// Add the authorization header to the original request // Add the authorization header to the original request
val authRequest = originalRequest.newBuilder() val authRequest = originalRequest.newBuilder()
.addHeader("Authorization", "Bearer ${oauth!!.access_token}") .addHeader("Authorization", "Bearer ${oauth!!.access_token}")
.header("User-Agent", "null2264/yokai/${BuildConfig.VERSION_NAME} (${BuildConfig.APPLICATION_ID})")
.build() .build()
return chain.proceed(authRequest) return chain.proceed(authRequest)
@ -63,8 +45,26 @@ class MyAnimeListInterceptor(private val myanimelist: MyAnimeList, private var t
* and the oauth object. * and the oauth object.
*/ */
fun setAuth(oauth: OAuth?) { fun setAuth(oauth: OAuth?) {
token = oauth?.access_token
this.oauth = oauth this.oauth = oauth
myanimelist.saveOAuth(oauth) myanimelist.saveOAuth(oauth)
} }
private fun refreshToken(chain: Interceptor.Chain): OAuth {
return runCatching {
val oauthResponse = chain.proceed(MyAnimeListApi.refreshTokenRequest(oauth!!))
if (oauthResponse.code == 401) {
myanimelist.setAuthExpired()
} }
if (oauthResponse.isSuccessful) {
with(json) { oauthResponse.parseAs<OAuth>() }
} else {
oauthResponse.close()
null
}
}
.getOrNull()
?: throw MALTokenExpired()
}
}
class MALTokenExpired: IOException("MAL: Login has expired")