diff --git a/app/src/main/java/eu/kanade/domain/track/service/TrackPreferences.kt b/app/src/main/java/eu/kanade/domain/track/service/TrackPreferences.kt index 8ed234b90..3e99d5ba0 100644 --- a/app/src/main/java/eu/kanade/domain/track/service/TrackPreferences.kt +++ b/app/src/main/java/eu/kanade/domain/track/service/TrackPreferences.kt @@ -19,9 +19,15 @@ class TrackPreferences( "", ) + fun trackAuthExpired(tracker: Tracker) = preferenceStore.getBoolean( + Preference.privateKey("pref_tracker_auth_expired_${tracker.id}"), + false, + ) + fun setCredentials(tracker: Tracker, username: String, password: String) { trackUsername(tracker).set(username) trackPassword(tracker).set(password) + trackAuthExpired(tracker).set(false) } fun trackToken(tracker: Tracker) = preferenceStore.getString(Preference.privateKey("track_token_${tracker.id}"), "") diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeList.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeList.kt index ee377b9e5..33daee1a9 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeList.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeList.kt @@ -35,7 +35,7 @@ class MyAnimeList(id: Long) : BaseTracker(id, "MyAnimeList"), DeletableTracker { 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(id, client, interceptor) } override val supportsReadingDates: Boolean = true @@ -155,6 +155,14 @@ class MyAnimeList(id: Long) : BaseTracker(id, "MyAnimeList"), DeletableTracker { interceptor.setAuth(null) } + fun getIfAuthExpired(): Boolean { + return trackPreferences.trackAuthExpired(this).get() + } + + fun setAuthExpired() { + trackPreferences.trackAuthExpired(this).set(true) + } + fun saveOAuth(oAuth: OAuth?) { trackPreferences.trackToken(this).set(json.encodeToString(oAuth)) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeListApi.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeListApi.kt index dabb25aae..563100304 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeListApi.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeListApi.kt @@ -25,6 +25,7 @@ import kotlinx.serialization.json.jsonArray import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.jsonPrimitive import kotlinx.serialization.json.long +import logcat.logcat import okhttp3.FormBody import okhttp3.Headers import okhttp3.OkHttpClient @@ -49,13 +50,13 @@ class MyAnimeListApi( suspend fun getAccessToken(authCode: String): OAuth { return withIOContext { val formBody: RequestBody = FormBody.Builder() - .add("client_id", clientId) + .add("client_id", CLIENT_ID) .add("code", authCode) .add("code_verifier", codeVerifier) .add("grant_type", "authorization_code") .build() with(json) { - client.newCall(POST("$baseOAuthUrl/token", body = formBody)) + client.newCall(POST("$BASE_OAUTH_URL/token", body = formBody)) .awaitSuccess() .parseAs() } @@ -65,7 +66,7 @@ class MyAnimeListApi( suspend fun getCurrentUser(): String { return withIOContext { val request = Request.Builder() - .url("$baseApiUrl/users/@me") + .url("$BASE_API_URL/users/@me") .get() .build() with(json) { @@ -79,7 +80,7 @@ class MyAnimeListApi( suspend fun search(query: String): List { 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... .appendQueryParameter("q", query.take(64)) .appendQueryParameter("nsfw", "true") @@ -104,7 +105,7 @@ class MyAnimeListApi( suspend fun getMangaDetails(id: Int): TrackSearch { return withIOContext { - val url = "$baseApiUrl/manga".toUri().buildUpon() + val url = "$BASE_API_URL/manga".toUri().buildUpon() .appendPath(id.toString()) .appendQueryParameter( "fields", @@ -180,7 +181,7 @@ class MyAnimeListApi( suspend fun findListItem(track: Track): Track? { return withIOContext { - val uri = "$baseApiUrl/manga".toUri().buildUpon() + val uri = "$BASE_API_URL/manga".toUri().buildUpon() .appendPath(track.remote_id.toString()) .appendQueryParameter("fields", "num_chapters,my_list_status{start_date,finish_date}") .build() @@ -218,7 +219,7 @@ class MyAnimeListApi( // Check next page if there's more if (!obj["paging"]!!.jsonObject["next"]?.jsonPrimitive?.contentOrNull.isNullOrBlank()) { - matches + findListItems(query, offset + listPaginationAmount) + matches + findListItems(query, offset + LIST_PAGINATION_AMOUNT) } else { matches } @@ -227,9 +228,9 @@ class MyAnimeListApi( private suspend fun getListPage(offset: Int): JsonObject { 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("limit", listPaginationAmount.toString()) + .appendQueryParameter("limit", LIST_PAGINATION_AMOUNT.toString()) if (offset > 0) { urlBuilder.appendQueryParameter("offset", offset.toString()) } @@ -279,30 +280,29 @@ class MyAnimeListApi( } companion object { - // Registered under arkon's MAL account - private const val clientId = "f46004a9c16483b6d87b5bf10de56d97" + private const val CLIENT_ID = "c46c9e24640a64dad5be5ca7a1a53a0f" - private const val baseOAuthUrl = "https://myanimelist.net/v1/oauth2" - private const val baseApiUrl = "https://api.myanimelist.net/v2" + private const val BASE_OAUTH_URL = "https://myanimelist.net/v1/oauth2" + 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 = "" - fun authUrl(): Uri = "$baseOAuthUrl/authorize".toUri().buildUpon() - .appendQueryParameter("client_id", clientId) + fun authUrl(): Uri = "$BASE_OAUTH_URL/authorize".toUri().buildUpon() + .appendQueryParameter("client_id", CLIENT_ID) .appendQueryParameter("code_challenge", getPkceChallengeCode()) .appendQueryParameter("response_type", "code") .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("my_list_status") .build() fun refreshTokenRequest(oauth: OAuth): Request { val formBody: RequestBody = FormBody.Builder() - .add("client_id", clientId) + .add("client_id", CLIENT_ID) .add("refresh_token", oauth.refresh_token) .add("grant_type", "refresh_token") .build() @@ -314,7 +314,7 @@ class MyAnimeListApi( .add("Authorization", "Bearer ${oauth.access_token}") .build() - return POST("$baseOAuthUrl/token", body = formBody, headers = headers) + return POST("$BASE_OAUTH_URL/token", body = formBody, headers = headers) } private fun getPkceChallengeCode(): String { diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeListInterceptor.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeListInterceptor.kt index 4fa1ec6ac..b109bd763 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeListInterceptor.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeListInterceptor.kt @@ -8,28 +8,26 @@ import okhttp3.Response import uy.kohesive.injekt.injectLazy 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 var oauth: OAuth? = null + private var oauth: OAuth? = myanimelist.loadOAuth() + private val tokenExpired get() = myanimelist.getIfAuthExpired() override fun intercept(chain: Interceptor.Chain): Response { + if (tokenExpired) { + throw MALTokenExpired() + } val originalRequest = chain.request() - if (token.isNullOrEmpty()) { - throw IOException("Not authenticated with MyAnimeList") - } - if (oauth == null) { - oauth = myanimelist.loadOAuth() - } // Refresh access token if expired if (oauth != null && oauth!!.isExpired()) { setAuth(refreshToken(chain)) } if (oauth == null) { - throw IOException("No authentication token") + throw IOException("MAL: User is not authenticated") } // Add the authorization header to the original request @@ -66,15 +64,16 @@ class MyAnimeListInterceptor(private val myanimelist: MyAnimeList, private var t * and the oauth object. */ fun setAuth(oauth: OAuth?) { - token = oauth?.access_token this.oauth = oauth myanimelist.saveOAuth(oauth) } private fun refreshToken(chain: Interceptor.Chain): OAuth { - val newOauth = runCatching { + return runCatching { val oauthResponse = chain.proceed(MyAnimeListApi.refreshTokenRequest(oauth!!)) - + if (oauthResponse.code == 401) { + myanimelist.setAuthExpired() + } if (oauthResponse.isSuccessful) { with(json) { oauthResponse.parseAs() } } else { @@ -82,11 +81,9 @@ class MyAnimeListInterceptor(private val myanimelist: MyAnimeList, private var t null } } - - if (newOauth.getOrNull() == null) { - throw IOException("Failed to refresh the access token") - } - - return newOauth.getOrNull()!! + .getOrNull() + ?: throw MALTokenExpired() } } + +class MALTokenExpired: IOException("MAL: Login has expired")