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