From 51144aa45e11499f059749c1ea02eb0bf6251cff Mon Sep 17 00:00:00 2001 From: Ken Swenson <2048861+Flat@users.noreply.github.com> Date: Sat, 5 May 2018 08:05:02 -0400 Subject: [PATCH] Implement Anilist API v2 (closes #1159) (#1383) * Implement Anilist API v2 (closes #1159) Switches to using the Anilist v2 API. Login is now done by implicit grant and tokens are good for one year. Users will need to login again after token expiration. "clientId" on line 289 of AnilistApi.kt should be changed to Tachiyomi's own client ID number. * Code style formatting Revert to kotlin 1.2.30 Use correct client ID Rename AnilistApi.login to AnilistApi.createOAuth to reflect changed implementation Rename json mimetype variable from json to jsonMime for clarity Don't read response if it's ignored Remove unused parameters from api requests * Close netResponse after read * Refactor remote_id into media_id and library_id * DB: Refactor RemoteId Refactor RemoteId into library_id and media_id Implement function to fetch library_id if user is migrating rom APIv1 * Remove logging interceptor * Compatability and sql simplification * Fix score and minor improvements * Revert changes to Kitsu API --- .../tachiyomi/data/backup/BackupManager.kt | 7 +- .../backup/serializer/TrackTypeAdapter.kt | 13 +- .../tachiyomi/data/database/DbOpenHelper.kt | 5 +- .../data/database/mappers/TrackTypeMapping.kt | 9 +- .../tachiyomi/data/database/models/Track.kt | 4 +- .../data/database/models/TrackImpl.kt | 8 +- .../data/database/tables/TrackTable.kt | 10 +- .../data/preference/PreferencesHelper.kt | 2 +- .../tachiyomi/data/track/anilist/Anilist.kt | 114 ++++-- .../data/track/anilist/AnilistApi.kt | 342 ++++++++++++------ .../data/track/anilist/AnilistInterceptor.kt | 26 +- .../data/track/anilist/AnilistModels.kt | 49 ++- .../tachiyomi/data/track/anilist/OAuth.kt | 3 +- .../tachiyomi/data/track/kitsu/Kitsu.kt | 4 +- .../tachiyomi/data/track/kitsu/KitsuApi.kt | 14 +- .../tachiyomi/data/track/kitsu/KitsuModels.kt | 8 +- .../tachiyomi/data/track/model/TrackSearch.kt | 8 +- .../data/track/myanimelist/MyanimelistApi.kt | 14 +- .../ui/setting/AnilistLoginActivity.kt | 7 +- app/src/main/res/values/strings.xml | 1 + 20 files changed, 411 insertions(+), 237 deletions(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupManager.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupManager.kt index 4fcbafc2f7..c58b3e6033 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupManager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupManager.kt @@ -402,8 +402,11 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) { for (dbTrack in dbTracks) { if (track.sync_id == dbTrack.sync_id) { // The sync is already in the db, only update its fields - if (track.remote_id != dbTrack.remote_id) { - dbTrack.remote_id = track.remote_id + if (track.media_id != dbTrack.media_id) { + dbTrack.media_id = track.media_id + } + if (track.library_id != dbTrack.library_id) { + dbTrack.library_id = track.library_id } dbTrack.last_chapter_read = Math.max(dbTrack.last_chapter_read, track.last_chapter_read) isInDatabase = true diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/serializer/TrackTypeAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/serializer/TrackTypeAdapter.kt index 6f01fa0233..d9f3b8cac3 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/serializer/TrackTypeAdapter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/serializer/TrackTypeAdapter.kt @@ -1,5 +1,6 @@ package eu.kanade.tachiyomi.data.backup.serializer +import android.telecom.DisconnectCause.REMOTE import com.github.salomonbrys.kotson.typeAdapter import com.google.gson.TypeAdapter import com.google.gson.stream.JsonToken @@ -11,7 +12,8 @@ import eu.kanade.tachiyomi.data.database.models.TrackImpl object TrackTypeAdapter { private const val SYNC = "s" - private const val REMOTE = "r" + private const val MEDIA = "r" + private const val LIBRARY = "ml" private const val TITLE = "t" private const val LAST_READ = "l" private const val TRACKING_URL = "u" @@ -24,8 +26,10 @@ object TrackTypeAdapter { value(it.title) name(SYNC) value(it.sync_id) - name(REMOTE) - value(it.remote_id) + name(MEDIA) + value(it.media_id) + name(LIBRARY) + value(it.library_id) name(LAST_READ) value(it.last_chapter_read) name(TRACKING_URL) @@ -43,7 +47,8 @@ object TrackTypeAdapter { when (name) { TITLE -> track.title = nextString() SYNC -> track.sync_id = nextInt() - REMOTE -> track.remote_id = nextInt() + MEDIA -> track.media_id = nextInt() + LIBRARY -> track.library_id = nextLong() LAST_READ -> track.last_chapter_read = nextInt() TRACKING_URL -> track.tracking_url = nextString() } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/DbOpenHelper.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/DbOpenHelper.kt index 5330e1ab52..998a5a1a7b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/DbOpenHelper.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/DbOpenHelper.kt @@ -17,7 +17,7 @@ class DbOpenHelper(context: Context) /** * Version of the database. */ - const val DATABASE_VERSION = 6 + const val DATABASE_VERSION = 7 } override fun onCreate(db: SQLiteDatabase) = with(db) { @@ -57,6 +57,9 @@ class DbOpenHelper(context: Context) if (oldVersion < 6) { db.execSQL(TrackTable.addTrackingUrl) } + if (oldVersion < 7) { + db.execSQL(TrackTable.addLibraryId) + } } override fun onConfigure(db: SQLiteDatabase) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/mappers/TrackTypeMapping.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/mappers/TrackTypeMapping.kt index aaf64f23a1..6759316de9 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/mappers/TrackTypeMapping.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/mappers/TrackTypeMapping.kt @@ -13,8 +13,9 @@ import eu.kanade.tachiyomi.data.database.models.Track import eu.kanade.tachiyomi.data.database.models.TrackImpl import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_ID import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_LAST_CHAPTER_READ +import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_LIBRARY_ID import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_MANGA_ID -import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_REMOTE_ID +import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_MEDIA_ID import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_SCORE import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_STATUS import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_SYNC_ID @@ -45,7 +46,8 @@ class TrackPutResolver : DefaultPutResolver() { put(COL_ID, obj.id) put(COL_MANGA_ID, obj.manga_id) put(COL_SYNC_ID, obj.sync_id) - put(COL_REMOTE_ID, obj.remote_id) + put(COL_MEDIA_ID, obj.media_id) + put(COL_LIBRARY_ID, obj.library_id) put(COL_TITLE, obj.title) put(COL_LAST_CHAPTER_READ, obj.last_chapter_read) put(COL_TOTAL_CHAPTERS, obj.total_chapters) @@ -62,7 +64,8 @@ class TrackGetResolver : DefaultGetResolver() { id = cursor.getLong(cursor.getColumnIndex(COL_ID)) manga_id = cursor.getLong(cursor.getColumnIndex(COL_MANGA_ID)) sync_id = cursor.getInt(cursor.getColumnIndex(COL_SYNC_ID)) - remote_id = cursor.getInt(cursor.getColumnIndex(COL_REMOTE_ID)) + media_id = cursor.getInt(cursor.getColumnIndex(COL_MEDIA_ID)) + library_id = cursor.getLong(cursor.getColumnIndex(COL_LIBRARY_ID)) title = cursor.getString(cursor.getColumnIndex(COL_TITLE)) last_chapter_read = cursor.getInt(cursor.getColumnIndex(COL_LAST_CHAPTER_READ)) total_chapters = cursor.getInt(cursor.getColumnIndex(COL_TOTAL_CHAPTERS)) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/Track.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/Track.kt index 3b883a8747..19133e0371 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/Track.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/Track.kt @@ -10,7 +10,9 @@ interface Track : Serializable { var sync_id: Int - var remote_id: Int + var media_id: Int + + var library_id: Long? var title: String diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/TrackImpl.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/TrackImpl.kt index b7c445168f..65f6ec7ab1 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/TrackImpl.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/TrackImpl.kt @@ -8,7 +8,9 @@ class TrackImpl : Track { override var sync_id: Int = 0 - override var remote_id: Int = 0 + override var media_id: Int = 0 + + override var library_id: Long? = null override lateinit var title: String @@ -30,13 +32,13 @@ class TrackImpl : Track { if (manga_id != other.manga_id) return false if (sync_id != other.sync_id) return false - return remote_id == other.remote_id + return media_id == other.media_id } override fun hashCode(): Int { var result = (manga_id xor manga_id.ushr(32)).toInt() result = 31 * result + sync_id - result = 31 * result + remote_id + result = 31 * result + media_id return result } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/tables/TrackTable.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/tables/TrackTable.kt index 79aba55232..82c863fb90 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/tables/TrackTable.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/tables/TrackTable.kt @@ -10,7 +10,9 @@ object TrackTable { const val COL_SYNC_ID = "sync_id" - const val COL_REMOTE_ID = "remote_id" + const val COL_MEDIA_ID = "remote_id" + + const val COL_LIBRARY_ID = "library_id" const val COL_TITLE = "title" @@ -29,7 +31,8 @@ object TrackTable { $COL_ID INTEGER NOT NULL PRIMARY KEY, $COL_MANGA_ID INTEGER NOT NULL, $COL_SYNC_ID INTEGER NOT NULL, - $COL_REMOTE_ID INTEGER NOT NULL, + $COL_MEDIA_ID INTEGER NOT NULL, + $COL_LIBRARY_ID INTEGER, $COL_TITLE TEXT NOT NULL, $COL_LAST_CHAPTER_READ INTEGER NOT NULL, $COL_TOTAL_CHAPTERS INTEGER NOT NULL, @@ -43,4 +46,7 @@ object TrackTable { val addTrackingUrl: String get() = "ALTER TABLE $TABLE ADD COLUMN $COL_TRACKING_URL TEXT DEFAULT ''" + + val addLibraryId: String + get() = "ALTER TABLE $TABLE ADD COLUMN $COL_LIBRARY_ID INTEGER NULL" } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt index cf594838e7..f329bd904e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt @@ -119,7 +119,7 @@ class PreferencesHelper(val context: Context) { fun trackToken(sync: TrackService) = rxPrefs.getString(Keys.trackToken(sync.id), "") - fun anilistScoreType() = rxPrefs.getInteger("anilist_score_type", 0) + fun anilistScoreType() = rxPrefs.getString("anilist_score_type", "POINT_10") fun backupsDirectory() = rxPrefs.getString(Keys.backupDirectory, defaultBackupDir.toString()) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/Anilist.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/Anilist.kt index 0b71e3e853..eea78051f8 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/Anilist.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/Anilist.kt @@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.data.track.anilist import android.content.Context import android.graphics.Color +import com.google.gson.Gson import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.database.models.Track import eu.kanade.tachiyomi.data.preference.getOrDefault @@ -9,6 +10,7 @@ import eu.kanade.tachiyomi.data.track.TrackService import eu.kanade.tachiyomi.data.track.model.TrackSearch import rx.Completable import rx.Observable +import uy.kohesive.injekt.injectLazy class Anilist(private val context: Context, id: Int) : TrackService(id) { @@ -17,24 +19,45 @@ class Anilist(private val context: Context, id: Int) : TrackService(id) { const val COMPLETED = 2 const val ON_HOLD = 3 const val DROPPED = 4 - const val PLAN_TO_READ = 5 + const val PLANNING = 5 + const val REPEATING = 6 const val DEFAULT_STATUS = READING const val DEFAULT_SCORE = 0 + + const val POINT_100 = "POINT_100" + const val POINT_10 = "POINT_10" + const val POINT_10_DECIMAL = "POINT_10_DECIMAL" + const val POINT_5 = "POINT_5" + const val POINT_3 = "POINT_3" } override val name = "AniList" - private val interceptor by lazy { AnilistInterceptor(getPassword()) } + private val gson: Gson by injectLazy() + + private val interceptor by lazy { AnilistInterceptor(this, getPassword()) } private val api by lazy { AnilistApi(client, interceptor) } + private val scorePreference = preferences.anilistScoreType() + + init { + // If the preference is an int from APIv1, logout user to force using APIv2 + try { + scorePreference.get() + } catch (e: ClassCastException) { + logout() + scorePreference.delete() + } + } + override fun getLogo() = R.drawable.al override fun getLogoColor() = Color.rgb(18, 25, 35) override fun getStatusList(): List { - return listOf(READING, COMPLETED, ON_HOLD, DROPPED, PLAN_TO_READ) + return listOf(READING, COMPLETED, ON_HOLD, DROPPED, PLANNING, REPEATING) } override fun getStatus(status: Int): String = with(context) { @@ -43,48 +66,50 @@ class Anilist(private val context: Context, id: Int) : TrackService(id) { COMPLETED -> getString(R.string.completed) ON_HOLD -> getString(R.string.on_hold) DROPPED -> getString(R.string.dropped) - PLAN_TO_READ -> getString(R.string.plan_to_read) + PLANNING -> getString(R.string.plan_to_read) + REPEATING -> getString(R.string.repeating) else -> "" } } override fun getScoreList(): List { - return when (preferences.anilistScoreType().getOrDefault()) { + return when (scorePreference.getOrDefault()) { // 10 point - 0 -> IntRange(0, 10).map(Int::toString) + POINT_10 -> IntRange(0, 10).map(Int::toString) // 100 point - 1 -> IntRange(0, 100).map(Int::toString) + POINT_100 -> IntRange(0, 100).map(Int::toString) // 5 stars - 2 -> IntRange(0, 5).map { "$it ★" } + POINT_5 -> IntRange(0, 5).map { "$it ★" } // Smiley - 3 -> listOf("-", "😦", "😐", "😊") + POINT_3 -> listOf("-", "😦", "😐", "😊") // 10 point decimal - 4 -> IntRange(0, 100).map { (it / 10f).toString() } + POINT_10_DECIMAL -> IntRange(0, 100).map { (it / 10f).toString() } else -> throw Exception("Unknown score type") } } override fun indexToScore(index: Int): Float { - return when (preferences.anilistScoreType().getOrDefault()) { + return when (scorePreference.getOrDefault()) { // 10 point - 0 -> index * 10f + POINT_10 -> index * 10f // 100 point - 1 -> index.toFloat() + POINT_100 -> index.toFloat() // 5 stars - 2 -> index * 20f + POINT_5 -> index * 20f // Smiley - 3 -> index * 30f + POINT_3 -> index * 30f // 10 point decimal - 4 -> index.toFloat() + POINT_10_DECIMAL -> index.toFloat() else -> throw Exception("Unknown score type") } } override fun displayScore(track: Track): String { val score = track.score - return when (preferences.anilistScoreType().getOrDefault()) { - 2 -> "${(score / 20).toInt()} ★" - 3 -> when { + + return when (scorePreference.getOrDefault()) { + POINT_5 -> "${(score / 20).toInt()} ★" + POINT_3 -> when { score == 0f -> "0" score <= 30 -> "😦" score <= 60 -> "😐" @@ -102,15 +127,26 @@ class Anilist(private val context: Context, id: Int) : TrackService(id) { if (track.total_chapters != 0 && track.last_chapter_read == track.total_chapters) { track.status = COMPLETED } + // If user was using API v1 fetch library_id + if (track.library_id == null || track.library_id!! == 0L){ + return api.findLibManga(track, getUsername().toInt()).flatMap { + if (it == null) { + throw Exception("$track not found on user library") + } + track.library_id = it.library_id + api.updateLibManga(track) + } + } return api.updateLibManga(track) } override fun bind(track: Track): Observable { - return api.findLibManga(track, getUsername()) + return api.findLibManga(track, getUsername().toInt()) .flatMap { remoteTrack -> if (remoteTrack != null) { track.copyPersonalFrom(remoteTrack) + track.library_id = remoteTrack.library_id update(track) } else { // Set default fields if it's not found in the list @@ -126,7 +162,7 @@ class Anilist(private val context: Context, id: Int) : TrackService(id) { } override fun refresh(track: Track): Observable { - return api.getLibManga(track, getUsername()) + return api.getLibManga(track, getUsername().toInt()) .map { remoteTrack -> track.copyPersonalFrom(remoteTrack) track.total_chapters = remoteTrack.total_chapters @@ -136,26 +172,34 @@ class Anilist(private val context: Context, id: Int) : TrackService(id) { override fun login(username: String, password: String) = login(password) - fun login(authCode: String): Completable { - return api.login(authCode) - // Save the token in the interceptor. - .doOnNext { interceptor.setAuth(it) } - // Obtain the authenticated user from the API. - .zipWith(api.getCurrentUser().map { pair -> - preferences.anilistScoreType().set(pair.second) - pair.first - }, { oauth, user -> Pair(user, oauth.refresh_token!!) }) - // Save service credentials (username and refresh token). - .doOnNext { saveCredentials(it.first, it.second) } - // Logout on any error. - .doOnError { logout() } - .toCompletable() + fun login(token: String): Completable { + val oauth = api.createOAuth(token) + interceptor.setAuth(oauth) + return api.getCurrentUser().map { (username, scoreType) -> + scorePreference.set(scoreType) + saveCredentials(username.toString(), oauth.access_token) + }.doOnError{ + logout() + }.toCompletable() } override fun logout() { super.logout() + preferences.trackToken(this).set(null) interceptor.setAuth(null) } + fun saveOAuth(oAuth: OAuth?) { + preferences.trackToken(this).set(gson.toJson(oAuth)) + } + + fun loadOAuth(): OAuth? { + return try { + gson.fromJson(preferences.trackToken(this).get(), OAuth::class.java) + } catch (e: Exception) { + null + } + } + } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistApi.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistApi.kt index ee28643526..37d1eb14ac 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistApi.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistApi.kt @@ -1,167 +1,275 @@ package eu.kanade.tachiyomi.data.track.anilist import android.net.Uri -import com.github.salomonbrys.kotson.int -import com.github.salomonbrys.kotson.string +import com.github.salomonbrys.kotson.* import com.google.gson.JsonObject +import com.google.gson.JsonParser import eu.kanade.tachiyomi.data.database.models.Track import eu.kanade.tachiyomi.data.track.model.TrackSearch -import eu.kanade.tachiyomi.network.POST -import okhttp3.FormBody +import eu.kanade.tachiyomi.network.asObservableSuccess +import okhttp3.MediaType import okhttp3.OkHttpClient -import okhttp3.ResponseBody -import retrofit2.Response -import retrofit2.Retrofit -import retrofit2.adapter.rxjava.RxJavaCallAdapterFactory -import retrofit2.converter.gson.GsonConverterFactory -import retrofit2.http.* +import okhttp3.Request +import okhttp3.RequestBody import rx.Observable + class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) { - private val rest = restBuilder() - .client(client.newBuilder().addInterceptor(interceptor).build()) - .build() - .create(Rest::class.java) + private val parser = JsonParser() + private val jsonMime = MediaType.parse("application/json; charset=utf-8") + private val authClient = client.newBuilder().addInterceptor(interceptor).build() + fun addLibManga(track: Track): Observable { - return rest.addLibManga(track.remote_id, track.last_chapter_read, track.toAnilistStatus()) - .map { response -> - response.body()?.close() - if (!response.isSuccessful) { - throw Exception("Could not add manga") + val query = """ + mutation AddManga(${'$'}mangaId: Int, ${'$'}progress: Int, ${'$'}status: MediaListStatus) { + SaveMediaListEntry (mediaId: ${'$'}mangaId, progress: ${'$'}progress, status: ${'$'}status) + { id status } } + """ + val variables = jsonObject( + "mangaId" to track.media_id, + "progress" to track.last_chapter_read, + "status" to track.toAnilistStatus() + ) + val payload = jsonObject( + "query" to query, + "variables" to variables + ) + val body = RequestBody.create(jsonMime, payload.toString()) + val request = Request.Builder() + .url(apiUrl) + .post(body) + .build() + return authClient.newCall(request) + .asObservableSuccess() + .map { netResponse -> + val responseBody = netResponse.body()?.string().orEmpty() + netResponse.close() + if (responseBody.isEmpty()) { + throw Exception("Null Response") } + val response = parser.parse(responseBody).obj + track.library_id = response["data"]["SaveMediaListEntry"]["id"].asLong track } } fun updateLibManga(track: Track): Observable { - return rest.updateLibManga(track.remote_id, track.last_chapter_read, track.toAnilistStatus(), - track.toAnilistScore()) - .map { response -> - response.body()?.close() - if (!response.isSuccessful) { - throw Exception("Could not update manga") + val query = """ + mutation UpdateManga(${'$'}listId: Int, ${'$'}progress: Int, ${'$'}status: MediaListStatus, ${'$'}score: Int) { + SaveMediaListEntry (id: ${'$'}listId, progress: ${'$'}progress, status: ${'$'}status, scoreRaw: ${'$'}score) { + id + status + progress + } } + """ + val variables = jsonObject( + "listId" to track.library_id, + "progress" to track.last_chapter_read, + "status" to track.toAnilistStatus(), + "score" to track.score.toInt() + ) + val payload = jsonObject( + "query" to query, + "variables" to variables + ) + val body = RequestBody.create(jsonMime, payload.toString()) + val request = Request.Builder() + .url(apiUrl) + .post(body) + .build() + return authClient.newCall(request) + .asObservableSuccess() + .map { track } } - fun search(query: String): Observable> { - return rest.search(query, 1) - .map { list -> - list.filter { it.type != "Novel" }.map { it.toTrack() } + fun search(search: String): Observable> { + val query = """ + query Search(${'$'}query: String) { + Page (perPage: 25) { + media(search: ${'$'}query, type: MANGA, format: MANGA) { + id + title { + romaji + } + coverImage { + large + } + type + status + chapters + startDate { + year + month + day + } + } + } } - .onErrorReturn { emptyList() } - } - - fun getList(username: String): Observable> { - return rest.getLib(username) - .map { lib -> - lib.flatten().map { it.toTrack() } + """ + val variables = jsonObject( + "query" to search + ) + val payload = jsonObject( + "query" to query, + "variables" to variables + ) + val body = RequestBody.create(jsonMime, payload.toString()) + val request = Request.Builder() + .url(apiUrl) + .post(body) + .build() + return authClient.newCall(request) + .asObservableSuccess() + .map { netResponse -> + val responseBody = netResponse.body()?.string().orEmpty() + if (responseBody.isEmpty()) { + throw Exception("Null Response") + } + val response = parser.parse(responseBody).obj + val data = response["data"]!!.obj + val page = data["Page"].obj + val media = page["media"].array + val entries = media.map { jsonToALManga(it.obj) } + entries.map { it.toTrack() } } } - fun findLibManga(track: Track, username: String) : Observable { - // TODO avoid getting the entire list - return getList(username) - .map { list -> list.find { it.remote_id == track.remote_id } } + + fun findLibManga(track: Track, userid: Int) : Observable { + val query = """ + query (${'$'}id: Int!, ${'$'}manga_id: Int!) { + Page { + mediaList(userId: ${'$'}id, type: MANGA, mediaId: ${'$'}manga_id) { + id + status + scoreRaw: score(format: POINT_100) + progress + media{ + id + title { + romaji + } + coverImage { + large + } + type + status + chapters + startDate { + year + month + day + } + } + } + } + } + """ + val variables = jsonObject( + "id" to userid, + "manga_id" to track.media_id + ) + val payload = jsonObject( + "query" to query, + "variables" to variables + ) + val body = RequestBody.create(jsonMime, payload.toString()) + val request = Request.Builder() + .url(apiUrl) + .post(body) + .build() + return authClient.newCall(request) + .asObservableSuccess() + .map { netResponse -> + val responseBody = netResponse.body()?.string().orEmpty() + if (responseBody.isEmpty()) { + throw Exception("Null Response") + } + val response = parser.parse(responseBody).obj + val data = response["data"]!!.obj + val page = data["Page"].obj + val media = page["mediaList"].array + val entries = media.map { jsonToALUserManga(it.obj) } + entries.firstOrNull()?.toTrack() + + } } - fun getLibManga(track: Track, username: String): Observable { - return findLibManga(track, username) + fun getLibManga(track: Track, userid: Int): Observable { + return findLibManga(track, userid) .map { it ?: throw Exception("Could not find manga") } } - fun login(authCode: String): Observable { - return restBuilder() - .client(client) + fun createOAuth(token: String): OAuth { + return OAuth(token, "Bearer", System.currentTimeMillis() + 31536000000, 31536000000) + } + + fun getCurrentUser(): Observable> { + val query = """ + query User + { + Viewer { + id + mediaListOptions { + scoreFormat + } + } + } + """ + val payload = jsonObject( + "query" to query + ) + val body = RequestBody.create(jsonMime, payload.toString()) + val request = Request.Builder() + .url(apiUrl) + .post(body) .build() - .create(Rest::class.java) - .requestAccessToken(authCode) + return authClient.newCall(request) + .asObservableSuccess() + .map { netResponse -> + val responseBody = netResponse.body()?.string().orEmpty() + if (responseBody.isEmpty()) { + throw Exception("Null Response") + } + val response = parser.parse(responseBody).obj + val data = response["data"]!!.obj + val viewer = data["Viewer"].obj + Pair(viewer["id"].asInt, viewer["mediaListOptions"]["scoreFormat"].asString) + } } - fun getCurrentUser(): Observable> { - return rest.getCurrentUser() - .map { it["id"].string to it["score_type"].int } + fun jsonToALManga(struct: JsonObject): ALManga{ + return ALManga(struct["id"].asInt, struct["title"]["romaji"].asString, struct["coverImage"]["large"].asString, + null, struct["type"].asString, struct["status"].asString, + struct["startDate"]["year"].nullString.orEmpty() + struct["startDate"]["month"].nullString.orEmpty() + + struct["startDate"]["day"].nullString.orEmpty(), struct["chapters"].nullInt ?: 0) } - private fun restBuilder() = Retrofit.Builder() - .baseUrl(baseUrl) - .addConverterFactory(GsonConverterFactory.create()) - .addCallAdapterFactory(RxJavaCallAdapterFactory.create()) - - private interface Rest { - - @FormUrlEncoded - @POST("auth/access_token") - fun requestAccessToken( - @Field("code") code: String, - @Field("grant_type") grant_type: String = "authorization_code", - @Field("client_id") client_id: String = clientId, - @Field("client_secret") client_secret: String = clientSecret, - @Field("redirect_uri") redirect_uri: String = clientUrl - ) : Observable - - @GET("user") - fun getCurrentUser(): Observable - - @GET("manga/search/{query}") - fun search( - @Path("query") query: String, - @Query("page") page: Int - ): Observable> - - @GET("user/{username}/mangalist") - fun getLib( - @Path("username") username: String - ): Observable - - @FormUrlEncoded - @PUT("mangalist") - fun addLibManga( - @Field("id") id: Int, - @Field("chapters_read") chapters_read: Int, - @Field("list_status") list_status: String - ) : Observable> - - @FormUrlEncoded - @PUT("mangalist") - fun updateLibManga( - @Field("id") id: Int, - @Field("chapters_read") chapters_read: Int, - @Field("list_status") list_status: String, - @Field("score") score_raw: String - ) : Observable> - + fun jsonToALUserManga(struct: JsonObject): ALUserManga{ + return ALUserManga(struct["id"].asLong, struct["status"].asString, struct["scoreRaw"].asInt, struct["progress"].asInt, jsonToALManga(struct["media"].obj) ) } + companion object { - private const val clientId = "tachiyomi-hrtje" - private const val clientSecret = "nlGB5OmgE9YWq5dr3gIDbTQV0C" + private const val clientId = "385" private const val clientUrl = "tachiyomi://anilist-auth" - private const val baseUrl = "https://anilist.co/api/" + private const val apiUrl = "https://graphql.anilist.co/" + private const val baseUrl = "https://anilist.co/api/v2/" private const val baseMangaUrl = "https://anilist.co/manga/" - fun mangaUrl(remoteId: Int): String { - return baseMangaUrl + remoteId + fun mangaUrl(mediaId: Int): String { + return baseMangaUrl + mediaId } - fun authUrl() = Uri.parse("${baseUrl}auth/authorize").buildUpon() - .appendQueryParameter("grant_type", "authorization_code") + fun authUrl() = Uri.parse("${baseUrl}oauth/authorize").buildUpon() .appendQueryParameter("client_id", clientId) - .appendQueryParameter("redirect_uri", clientUrl) - .appendQueryParameter("response_type", "code") + .appendQueryParameter("response_type", "token") .build() - - fun refreshTokenRequest(token: String) = POST("${baseUrl}auth/access_token", - body = FormBody.Builder() - .add("grant_type", "refresh_token") - .add("client_id", clientId) - .add("client_secret", clientSecret) - .add("refresh_token", token) - .build()) - } -} \ No newline at end of file +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistInterceptor.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistInterceptor.kt index 2bb8525d3b..427b0acfed 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistInterceptor.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistInterceptor.kt @@ -1,10 +1,10 @@ package eu.kanade.tachiyomi.data.track.anilist -import com.google.gson.Gson import okhttp3.Interceptor import okhttp3.Response -class AnilistInterceptor(private var refreshToken: String?) : Interceptor { + +class AnilistInterceptor(val anilist: Anilist, private var token: String?) : Interceptor { /** * OAuth object used for authenticated requests. @@ -20,24 +20,21 @@ class AnilistInterceptor(private var refreshToken: String?) : Interceptor { override fun intercept(chain: Interceptor.Chain): Response { val originalRequest = chain.request() - if (refreshToken.isNullOrEmpty()) { + if (token.isNullOrEmpty()) { throw Exception("Not authenticated with Anilist") } - + if (oauth == null){ + oauth = anilist.loadOAuth() + } // Refresh access token if null or expired. - if (oauth == null || oauth!!.isExpired()) { - val response = chain.proceed(AnilistApi.refreshTokenRequest(refreshToken!!)) - oauth = if (response.isSuccessful) { - Gson().fromJson(response.body()!!.string(), OAuth::class.java) - } else { - response.close() - null - } + if (oauth!!.isExpired()) { + anilist.logout() + throw Exception("Token expired") } // Throw on null auth. if (oauth == null) { - throw Exception("Access token wasn't refreshed") + throw Exception("No authentication token") } // Add the authorization header to the original request. @@ -53,8 +50,9 @@ class AnilistInterceptor(private var refreshToken: String?) : Interceptor { * and the oauth object. */ fun setAuth(oauth: OAuth?) { - refreshToken = oauth?.refresh_token + token = oauth?.access_token this.oauth = oauth + anilist.saveOAuth(oauth) } } \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistModels.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistModels.kt index 8398477dc6..3083f3b696 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistModels.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistModels.kt @@ -11,7 +11,7 @@ import java.text.SimpleDateFormat import java.util.* data class ALManga( - val id: Int, + val media_id: Int, val title_romaji: String, val image_url_lge: String, val description: String?, @@ -21,12 +21,12 @@ data class ALManga( val total_chapters: Int) { fun toTrack() = TrackSearch.create(TrackManager.ANILIST).apply { - remote_id = this@ALManga.id + media_id = this@ALManga.media_id title = title_romaji total_chapters = this@ALManga.total_chapters cover_url = image_url_lge summary = description ?: "" - tracking_url = AnilistApi.mangaUrl(remote_id) + tracking_url = AnilistApi.mangaUrl(media_id) publishing_status = this@ALManga.publishing_status publishing_type = type if (!start_date_fuzzy.isNullOrBlank()) { @@ -43,40 +43,37 @@ data class ALManga( } data class ALUserManga( - val id: Int, + val library_id: Long, val list_status: String, val score_raw: Int, val chapters_read: Int, val manga: ALManga) { fun toTrack() = Track.create(TrackManager.ANILIST).apply { - remote_id = manga.id + media_id = manga.media_id status = toTrackStatus() score = score_raw.toFloat() last_chapter_read = chapters_read + library_id = this@ALUserManga.library_id } fun toTrackStatus() = when (list_status) { - "reading" -> Anilist.READING - "completed" -> Anilist.COMPLETED - "on-hold" -> Anilist.ON_HOLD - "dropped" -> Anilist.DROPPED - "plan to read" -> Anilist.PLAN_TO_READ + "CURRENT" -> Anilist.READING + "COMPLETED" -> Anilist.COMPLETED + "PAUSED" -> Anilist.ON_HOLD + "DROPPED" -> Anilist.DROPPED + "PLANNING" -> Anilist.PLANNING else -> throw NotImplementedError("Unknown status") } } -data class ALUserLists(val lists: Map>) { - - fun flatten() = lists.values.flatten() -} - fun Track.toAnilistStatus() = when (status) { - Anilist.READING -> "reading" - Anilist.COMPLETED -> "completed" - Anilist.ON_HOLD -> "on-hold" - Anilist.DROPPED -> "dropped" - Anilist.PLAN_TO_READ -> "plan to read" + Anilist.READING -> "CURRENT" + Anilist.COMPLETED -> "COMPLETED" + Anilist.ON_HOLD -> "PAUSED" + Anilist.DROPPED -> "DROPPED" + Anilist.PLANNING -> "PLANNING" + Anilist.REPEATING -> "REPEATING" else -> throw NotImplementedError("Unknown status") } @@ -84,11 +81,11 @@ private val preferences: PreferencesHelper by injectLazy() fun Track.toAnilistScore(): String = when (preferences.anilistScoreType().getOrDefault()) { // 10 point - 0 -> (score.toInt() / 10).toString() + "POINT_10" -> (score.toInt() / 10).toString() // 100 point - 1 -> score.toInt().toString() + "POINT_100" -> score.toInt().toString() // 5 stars - 2 -> when { + "POINT_5" -> when { score == 0f -> "0" score < 30 -> "1" score < 50 -> "2" @@ -97,13 +94,13 @@ fun Track.toAnilistScore(): String = when (preferences.anilistScoreType().getOrD else -> "5" } // Smiley - 3 -> when { + "POINT_3" -> when { score == 0f -> "0" score <= 30 -> ":(" score <= 60 -> ":|" else -> ":)" } // 10 point decimal - 4 -> (score / 10).toString() + "POINT_10_DECIMAL" -> (score / 10).toString() else -> throw Exception("Unknown score type") -} \ No newline at end of file +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/OAuth.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/OAuth.kt index 6f5238b377..1d7a31ac52 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/OAuth.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/OAuth.kt @@ -4,8 +4,7 @@ data class OAuth( val access_token: String, val token_type: String, val expires: Long, - val expires_in: Long, - val refresh_token: String?) { + val expires_in: Long) { fun isExpired() = System.currentTimeMillis() > expires } \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/Kitsu.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/Kitsu.kt index c7f4f94dd9..14be0ddb7a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/Kitsu.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/Kitsu.kt @@ -87,7 +87,7 @@ class Kitsu(private val context: Context, id: Int) : TrackService(id) { .flatMap { remoteTrack -> if (remoteTrack != null) { track.copyPersonalFrom(remoteTrack) - track.remote_id = remoteTrack.remote_id + track.media_id = remoteTrack.media_id update(track) } else { track.score = DEFAULT_SCORE @@ -141,4 +141,4 @@ class Kitsu(private val context: Context, id: Int) : TrackService(id) { } } -} \ No newline at end of file +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuApi.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuApi.kt index 03226896e5..9d16ddb5bc 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuApi.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuApi.kt @@ -42,7 +42,7 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor) ), "media" to jsonObject( "data" to jsonObject( - "id" to track.remote_id, + "id" to track.media_id, "type" to "manga" ) ) @@ -52,7 +52,7 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor) rest.addLibManga(jsonObject("data" to data)) .map { json -> - track.remote_id = json["data"]["id"].int + track.media_id = json["data"]["id"].int track } } @@ -63,7 +63,7 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor) // @formatter:off val data = jsonObject( "type" to "libraryEntries", - "id" to track.remote_id, + "id" to track.media_id, "attributes" to jsonObject( "status" to track.toKitsuStatus(), "progress" to track.last_chapter_read, @@ -72,7 +72,7 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor) ) // @formatter:on - rest.updateLibManga(track.remote_id, jsonObject("data" to data)) + rest.updateLibManga(track.media_id, jsonObject("data" to data)) .map { track } } } @@ -88,7 +88,7 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor) } fun findLibManga(track: Track, userId: String): Observable { - return rest.findLibManga(track.remote_id, userId) + return rest.findLibManga(track.media_id, userId) .map { json -> val data = json["data"].array if (data.size() > 0) { @@ -101,7 +101,7 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor) } fun getLibManga(track: Track): Observable { - return rest.getLibManga(track.remote_id) + return rest.getLibManga(track.media_id) .map { json -> val data = json["data"].array if (data.size() > 0) { @@ -204,4 +204,4 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor) } -} \ No newline at end of file +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuModels.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuModels.kt index be4ca50346..70fdef6d92 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuModels.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuModels.kt @@ -19,12 +19,12 @@ open class KitsuManga(obj: JsonObject) { @CallSuper open fun toTrack() = TrackSearch.create(TrackManager.KITSU).apply { - remote_id = this@KitsuManga.id + media_id = this@KitsuManga.id title = canonicalTitle total_chapters = chapterCount ?: 0 cover_url = original summary = synopsis - tracking_url = KitsuApi.mangaUrl(remote_id) + tracking_url = KitsuApi.mangaUrl(media_id) publishing_status = this@KitsuManga.status publishing_type = type start_date = startDate.orEmpty() @@ -32,13 +32,13 @@ open class KitsuManga(obj: JsonObject) { } class KitsuLibManga(obj: JsonObject, manga: JsonObject) : KitsuManga(manga) { - val remoteId by obj.byInt("id") + val libraryId by obj.byInt("id") override val status by obj["attributes"].byString val ratingTwenty = obj["attributes"].obj.get("ratingTwenty").nullString val progress by obj["attributes"].byInt override fun toTrack() = super.toTrack().apply { - remote_id = remoteId + media_id = libraryId // TODO migrate media ids to library ids status = toTrackStatus() score = ratingTwenty?.let { it.toInt() / 2f } ?: 0f last_chapter_read = progress diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/model/TrackSearch.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/model/TrackSearch.kt index 0e701730f4..a7fb8b80d2 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/model/TrackSearch.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/model/TrackSearch.kt @@ -10,7 +10,9 @@ class TrackSearch : Track { override var sync_id: Int = 0 - override var remote_id: Int = 0 + override var media_id: Int = 0 + + override var library_id: Long? = null override lateinit var title: String @@ -42,13 +44,13 @@ class TrackSearch : Track { if (manga_id != other.manga_id) return false if (sync_id != other.sync_id) return false - return remote_id == other.remote_id + return media_id == other.media_id } override fun hashCode(): Int { var result = (manga_id xor manga_id.ushr(32)).toInt() result = 31 * result + sync_id - result = 31 * result + remote_id + result = 31 * result + media_id return result } companion object { 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 392ff220b7..16c9269aa9 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 @@ -54,11 +54,11 @@ class MyanimelistApi(private val client: OkHttpClient, username: String, passwor .map { TrackSearch.create(TrackManager.MYANIMELIST).apply { title = it.selectText("title")!! - remote_id = it.selectInt("id") + media_id = it.selectInt("id") total_chapters = it.selectInt("chapters") summary = it.selectText("synopsis")!! cover_url = it.selectText("image")!! - tracking_url = MyanimelistApi.mangaUrl(remote_id) + tracking_url = MyanimelistApi.mangaUrl(media_id) publishing_status = it.selectText("status")!! publishing_type = it.selectText("type")!! start_date = it.selectText("start_date")!! @@ -77,13 +77,13 @@ class MyanimelistApi(private val client: OkHttpClient, username: String, passwor .map { TrackSearch.create(TrackManager.MYANIMELIST).apply { title = it.selectText("series_title")!! - remote_id = it.selectInt("series_mangadb_id") + media_id = it.selectInt("series_mangadb_id") last_chapter_read = it.selectInt("my_read_chapters") status = it.selectInt("my_status") score = it.selectInt("my_score").toFloat() total_chapters = it.selectInt("series_chapters") cover_url = it.selectText("series_image")!! - tracking_url = MyanimelistApi.mangaUrl(remote_id) + tracking_url = MyanimelistApi.mangaUrl(media_id) } } .toList() @@ -91,7 +91,7 @@ class MyanimelistApi(private val client: OkHttpClient, username: String, passwor fun findLibManga(track: Track, username: String): Observable { return getList(username) - .map { list -> list.find { it.remote_id == track.remote_id } } + .map { list -> list.find { it.media_id == track.media_id } } } fun getLibManga(track: Track, username: String): Observable { @@ -169,12 +169,12 @@ class MyanimelistApi(private val client: OkHttpClient, username: String, passwor fun getUpdateUrl(track: Track) = Uri.parse(baseUrl).buildUpon() .appendEncodedPath("api/mangalist/update") - .appendPath("${track.remote_id}.xml") + .appendPath("${track.media_id}.xml") .toString() fun getAddUrl(track: Track) = Uri.parse(baseUrl).buildUpon() .appendEncodedPath("api/mangalist/add") - .appendPath("${track.remote_id}.xml") + .appendPath("${track.media_id}.xml") .toString() fun createHeaders(username: String, password: String): Headers { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/AnilistLoginActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/AnilistLoginActivity.kt index 982e74fe1d..6b5da186ea 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/AnilistLoginActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/AnilistLoginActivity.kt @@ -23,9 +23,10 @@ class AnilistLoginActivity : AppCompatActivity() { val view = ProgressBar(this) setContentView(view, FrameLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT, CENTER)) - val code = intent.data?.getQueryParameter("code") - if (code != null) { - trackManager.aniList.login(code) + val regex = "(?:access_token=)(.*?)(?:&)".toRegex() + val matchResult = regex.find(intent.data?.fragment.toString()) + if (matchResult?.groups?.get(1) != null) { + trackManager.aniList.login(matchResult.groups[1]!!.value) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe({ diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c4ef4bfb4f..5ae68a606b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -385,6 +385,7 @@ Dropped On hold Plan to read + Re-reading Score Title Status