* 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
This commit is contained in:
parent
86a599d13f
commit
51144aa45e
@ -402,8 +402,11 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
|
|||||||
for (dbTrack in dbTracks) {
|
for (dbTrack in dbTracks) {
|
||||||
if (track.sync_id == dbTrack.sync_id) {
|
if (track.sync_id == dbTrack.sync_id) {
|
||||||
// The sync is already in the db, only update its fields
|
// The sync is already in the db, only update its fields
|
||||||
if (track.remote_id != dbTrack.remote_id) {
|
if (track.media_id != dbTrack.media_id) {
|
||||||
dbTrack.remote_id = track.remote_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)
|
dbTrack.last_chapter_read = Math.max(dbTrack.last_chapter_read, track.last_chapter_read)
|
||||||
isInDatabase = true
|
isInDatabase = true
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
package eu.kanade.tachiyomi.data.backup.serializer
|
package eu.kanade.tachiyomi.data.backup.serializer
|
||||||
|
|
||||||
|
import android.telecom.DisconnectCause.REMOTE
|
||||||
import com.github.salomonbrys.kotson.typeAdapter
|
import com.github.salomonbrys.kotson.typeAdapter
|
||||||
import com.google.gson.TypeAdapter
|
import com.google.gson.TypeAdapter
|
||||||
import com.google.gson.stream.JsonToken
|
import com.google.gson.stream.JsonToken
|
||||||
@ -11,7 +12,8 @@ import eu.kanade.tachiyomi.data.database.models.TrackImpl
|
|||||||
object TrackTypeAdapter {
|
object TrackTypeAdapter {
|
||||||
|
|
||||||
private const val SYNC = "s"
|
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 TITLE = "t"
|
||||||
private const val LAST_READ = "l"
|
private const val LAST_READ = "l"
|
||||||
private const val TRACKING_URL = "u"
|
private const val TRACKING_URL = "u"
|
||||||
@ -24,8 +26,10 @@ object TrackTypeAdapter {
|
|||||||
value(it.title)
|
value(it.title)
|
||||||
name(SYNC)
|
name(SYNC)
|
||||||
value(it.sync_id)
|
value(it.sync_id)
|
||||||
name(REMOTE)
|
name(MEDIA)
|
||||||
value(it.remote_id)
|
value(it.media_id)
|
||||||
|
name(LIBRARY)
|
||||||
|
value(it.library_id)
|
||||||
name(LAST_READ)
|
name(LAST_READ)
|
||||||
value(it.last_chapter_read)
|
value(it.last_chapter_read)
|
||||||
name(TRACKING_URL)
|
name(TRACKING_URL)
|
||||||
@ -43,7 +47,8 @@ object TrackTypeAdapter {
|
|||||||
when (name) {
|
when (name) {
|
||||||
TITLE -> track.title = nextString()
|
TITLE -> track.title = nextString()
|
||||||
SYNC -> track.sync_id = nextInt()
|
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()
|
LAST_READ -> track.last_chapter_read = nextInt()
|
||||||
TRACKING_URL -> track.tracking_url = nextString()
|
TRACKING_URL -> track.tracking_url = nextString()
|
||||||
}
|
}
|
||||||
|
@ -17,7 +17,7 @@ class DbOpenHelper(context: Context)
|
|||||||
/**
|
/**
|
||||||
* Version of the database.
|
* Version of the database.
|
||||||
*/
|
*/
|
||||||
const val DATABASE_VERSION = 6
|
const val DATABASE_VERSION = 7
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreate(db: SQLiteDatabase) = with(db) {
|
override fun onCreate(db: SQLiteDatabase) = with(db) {
|
||||||
@ -57,6 +57,9 @@ class DbOpenHelper(context: Context)
|
|||||||
if (oldVersion < 6) {
|
if (oldVersion < 6) {
|
||||||
db.execSQL(TrackTable.addTrackingUrl)
|
db.execSQL(TrackTable.addTrackingUrl)
|
||||||
}
|
}
|
||||||
|
if (oldVersion < 7) {
|
||||||
|
db.execSQL(TrackTable.addLibraryId)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onConfigure(db: SQLiteDatabase) {
|
override fun onConfigure(db: SQLiteDatabase) {
|
||||||
|
@ -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.models.TrackImpl
|
||||||
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_ID
|
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_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_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_SCORE
|
||||||
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_STATUS
|
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_STATUS
|
||||||
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_SYNC_ID
|
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_SYNC_ID
|
||||||
@ -45,7 +46,8 @@ class TrackPutResolver : DefaultPutResolver<Track>() {
|
|||||||
put(COL_ID, obj.id)
|
put(COL_ID, obj.id)
|
||||||
put(COL_MANGA_ID, obj.manga_id)
|
put(COL_MANGA_ID, obj.manga_id)
|
||||||
put(COL_SYNC_ID, obj.sync_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_TITLE, obj.title)
|
||||||
put(COL_LAST_CHAPTER_READ, obj.last_chapter_read)
|
put(COL_LAST_CHAPTER_READ, obj.last_chapter_read)
|
||||||
put(COL_TOTAL_CHAPTERS, obj.total_chapters)
|
put(COL_TOTAL_CHAPTERS, obj.total_chapters)
|
||||||
@ -62,7 +64,8 @@ class TrackGetResolver : DefaultGetResolver<Track>() {
|
|||||||
id = cursor.getLong(cursor.getColumnIndex(COL_ID))
|
id = cursor.getLong(cursor.getColumnIndex(COL_ID))
|
||||||
manga_id = cursor.getLong(cursor.getColumnIndex(COL_MANGA_ID))
|
manga_id = cursor.getLong(cursor.getColumnIndex(COL_MANGA_ID))
|
||||||
sync_id = cursor.getInt(cursor.getColumnIndex(COL_SYNC_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))
|
title = cursor.getString(cursor.getColumnIndex(COL_TITLE))
|
||||||
last_chapter_read = cursor.getInt(cursor.getColumnIndex(COL_LAST_CHAPTER_READ))
|
last_chapter_read = cursor.getInt(cursor.getColumnIndex(COL_LAST_CHAPTER_READ))
|
||||||
total_chapters = cursor.getInt(cursor.getColumnIndex(COL_TOTAL_CHAPTERS))
|
total_chapters = cursor.getInt(cursor.getColumnIndex(COL_TOTAL_CHAPTERS))
|
||||||
|
@ -10,7 +10,9 @@ interface Track : Serializable {
|
|||||||
|
|
||||||
var sync_id: Int
|
var sync_id: Int
|
||||||
|
|
||||||
var remote_id: Int
|
var media_id: Int
|
||||||
|
|
||||||
|
var library_id: Long?
|
||||||
|
|
||||||
var title: String
|
var title: String
|
||||||
|
|
||||||
|
@ -8,7 +8,9 @@ class TrackImpl : Track {
|
|||||||
|
|
||||||
override var sync_id: Int = 0
|
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
|
override lateinit var title: String
|
||||||
|
|
||||||
@ -30,13 +32,13 @@ class TrackImpl : Track {
|
|||||||
|
|
||||||
if (manga_id != other.manga_id) return false
|
if (manga_id != other.manga_id) return false
|
||||||
if (sync_id != other.sync_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 {
|
override fun hashCode(): Int {
|
||||||
var result = (manga_id xor manga_id.ushr(32)).toInt()
|
var result = (manga_id xor manga_id.ushr(32)).toInt()
|
||||||
result = 31 * result + sync_id
|
result = 31 * result + sync_id
|
||||||
result = 31 * result + remote_id
|
result = 31 * result + media_id
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -10,7 +10,9 @@ object TrackTable {
|
|||||||
|
|
||||||
const val COL_SYNC_ID = "sync_id"
|
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"
|
const val COL_TITLE = "title"
|
||||||
|
|
||||||
@ -29,7 +31,8 @@ object TrackTable {
|
|||||||
$COL_ID INTEGER NOT NULL PRIMARY KEY,
|
$COL_ID INTEGER NOT NULL PRIMARY KEY,
|
||||||
$COL_MANGA_ID INTEGER NOT NULL,
|
$COL_MANGA_ID INTEGER NOT NULL,
|
||||||
$COL_SYNC_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_TITLE TEXT NOT NULL,
|
||||||
$COL_LAST_CHAPTER_READ INTEGER NOT NULL,
|
$COL_LAST_CHAPTER_READ INTEGER NOT NULL,
|
||||||
$COL_TOTAL_CHAPTERS INTEGER NOT NULL,
|
$COL_TOTAL_CHAPTERS INTEGER NOT NULL,
|
||||||
@ -43,4 +46,7 @@ object TrackTable {
|
|||||||
|
|
||||||
val addTrackingUrl: String
|
val addTrackingUrl: String
|
||||||
get() = "ALTER TABLE $TABLE ADD COLUMN $COL_TRACKING_URL TEXT DEFAULT ''"
|
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"
|
||||||
}
|
}
|
||||||
|
@ -119,7 +119,7 @@ class PreferencesHelper(val context: Context) {
|
|||||||
|
|
||||||
fun trackToken(sync: TrackService) = rxPrefs.getString(Keys.trackToken(sync.id), "")
|
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())
|
fun backupsDirectory() = rxPrefs.getString(Keys.backupDirectory, defaultBackupDir.toString())
|
||||||
|
|
||||||
|
@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.data.track.anilist
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
|
import com.google.gson.Gson
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.database.models.Track
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
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 eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||||
import rx.Completable
|
import rx.Completable
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
class Anilist(private val context: Context, id: Int) : TrackService(id) {
|
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 COMPLETED = 2
|
||||||
const val ON_HOLD = 3
|
const val ON_HOLD = 3
|
||||||
const val DROPPED = 4
|
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_STATUS = READING
|
||||||
const val DEFAULT_SCORE = 0
|
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"
|
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 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 getLogo() = R.drawable.al
|
||||||
|
|
||||||
override fun getLogoColor() = Color.rgb(18, 25, 35)
|
override fun getLogoColor() = Color.rgb(18, 25, 35)
|
||||||
|
|
||||||
override fun getStatusList(): List<Int> {
|
override fun getStatusList(): List<Int> {
|
||||||
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) {
|
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)
|
COMPLETED -> getString(R.string.completed)
|
||||||
ON_HOLD -> getString(R.string.on_hold)
|
ON_HOLD -> getString(R.string.on_hold)
|
||||||
DROPPED -> getString(R.string.dropped)
|
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 -> ""
|
else -> ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getScoreList(): List<String> {
|
override fun getScoreList(): List<String> {
|
||||||
return when (preferences.anilistScoreType().getOrDefault()) {
|
return when (scorePreference.getOrDefault()) {
|
||||||
// 10 point
|
// 10 point
|
||||||
0 -> IntRange(0, 10).map(Int::toString)
|
POINT_10 -> IntRange(0, 10).map(Int::toString)
|
||||||
// 100 point
|
// 100 point
|
||||||
1 -> IntRange(0, 100).map(Int::toString)
|
POINT_100 -> IntRange(0, 100).map(Int::toString)
|
||||||
// 5 stars
|
// 5 stars
|
||||||
2 -> IntRange(0, 5).map { "$it ★" }
|
POINT_5 -> IntRange(0, 5).map { "$it ★" }
|
||||||
// Smiley
|
// Smiley
|
||||||
3 -> listOf("-", "😦", "😐", "😊")
|
POINT_3 -> listOf("-", "😦", "😐", "😊")
|
||||||
// 10 point decimal
|
// 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")
|
else -> throw Exception("Unknown score type")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun indexToScore(index: Int): Float {
|
override fun indexToScore(index: Int): Float {
|
||||||
return when (preferences.anilistScoreType().getOrDefault()) {
|
return when (scorePreference.getOrDefault()) {
|
||||||
// 10 point
|
// 10 point
|
||||||
0 -> index * 10f
|
POINT_10 -> index * 10f
|
||||||
// 100 point
|
// 100 point
|
||||||
1 -> index.toFloat()
|
POINT_100 -> index.toFloat()
|
||||||
// 5 stars
|
// 5 stars
|
||||||
2 -> index * 20f
|
POINT_5 -> index * 20f
|
||||||
// Smiley
|
// Smiley
|
||||||
3 -> index * 30f
|
POINT_3 -> index * 30f
|
||||||
// 10 point decimal
|
// 10 point decimal
|
||||||
4 -> index.toFloat()
|
POINT_10_DECIMAL -> index.toFloat()
|
||||||
else -> throw Exception("Unknown score type")
|
else -> throw Exception("Unknown score type")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun displayScore(track: Track): String {
|
override fun displayScore(track: Track): String {
|
||||||
val score = track.score
|
val score = track.score
|
||||||
return when (preferences.anilistScoreType().getOrDefault()) {
|
|
||||||
2 -> "${(score / 20).toInt()} ★"
|
return when (scorePreference.getOrDefault()) {
|
||||||
3 -> when {
|
POINT_5 -> "${(score / 20).toInt()} ★"
|
||||||
|
POINT_3 -> when {
|
||||||
score == 0f -> "0"
|
score == 0f -> "0"
|
||||||
score <= 30 -> "😦"
|
score <= 30 -> "😦"
|
||||||
score <= 60 -> "😐"
|
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) {
|
if (track.total_chapters != 0 && track.last_chapter_read == track.total_chapters) {
|
||||||
track.status = COMPLETED
|
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)
|
return api.updateLibManga(track)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun bind(track: Track): Observable<Track> {
|
override fun bind(track: Track): Observable<Track> {
|
||||||
return api.findLibManga(track, getUsername())
|
return api.findLibManga(track, getUsername().toInt())
|
||||||
.flatMap { remoteTrack ->
|
.flatMap { remoteTrack ->
|
||||||
if (remoteTrack != null) {
|
if (remoteTrack != null) {
|
||||||
track.copyPersonalFrom(remoteTrack)
|
track.copyPersonalFrom(remoteTrack)
|
||||||
|
track.library_id = remoteTrack.library_id
|
||||||
update(track)
|
update(track)
|
||||||
} else {
|
} else {
|
||||||
// Set default fields if it's not found in the list
|
// 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<Track> {
|
override fun refresh(track: Track): Observable<Track> {
|
||||||
return api.getLibManga(track, getUsername())
|
return api.getLibManga(track, getUsername().toInt())
|
||||||
.map { remoteTrack ->
|
.map { remoteTrack ->
|
||||||
track.copyPersonalFrom(remoteTrack)
|
track.copyPersonalFrom(remoteTrack)
|
||||||
track.total_chapters = remoteTrack.total_chapters
|
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)
|
override fun login(username: String, password: String) = login(password)
|
||||||
|
|
||||||
fun login(authCode: String): Completable {
|
fun login(token: String): Completable {
|
||||||
return api.login(authCode)
|
val oauth = api.createOAuth(token)
|
||||||
// Save the token in the interceptor.
|
interceptor.setAuth(oauth)
|
||||||
.doOnNext { interceptor.setAuth(it) }
|
return api.getCurrentUser().map { (username, scoreType) ->
|
||||||
// Obtain the authenticated user from the API.
|
scorePreference.set(scoreType)
|
||||||
.zipWith(api.getCurrentUser().map { pair ->
|
saveCredentials(username.toString(), oauth.access_token)
|
||||||
preferences.anilistScoreType().set(pair.second)
|
}.doOnError{
|
||||||
pair.first
|
logout()
|
||||||
}, { oauth, user -> Pair(user, oauth.refresh_token!!) })
|
}.toCompletable()
|
||||||
// Save service credentials (username and refresh token).
|
|
||||||
.doOnNext { saveCredentials(it.first, it.second) }
|
|
||||||
// Logout on any error.
|
|
||||||
.doOnError { logout() }
|
|
||||||
.toCompletable()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun logout() {
|
override fun logout() {
|
||||||
super.logout()
|
super.logout()
|
||||||
|
preferences.trackToken(this).set(null)
|
||||||
interceptor.setAuth(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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,167 +1,275 @@
|
|||||||
package eu.kanade.tachiyomi.data.track.anilist
|
package eu.kanade.tachiyomi.data.track.anilist
|
||||||
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import com.github.salomonbrys.kotson.int
|
import com.github.salomonbrys.kotson.*
|
||||||
import com.github.salomonbrys.kotson.string
|
|
||||||
import com.google.gson.JsonObject
|
import com.google.gson.JsonObject
|
||||||
|
import com.google.gson.JsonParser
|
||||||
import eu.kanade.tachiyomi.data.database.models.Track
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||||
import eu.kanade.tachiyomi.network.POST
|
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||||
import okhttp3.FormBody
|
import okhttp3.MediaType
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.ResponseBody
|
import okhttp3.Request
|
||||||
import retrofit2.Response
|
import okhttp3.RequestBody
|
||||||
import retrofit2.Retrofit
|
|
||||||
import retrofit2.adapter.rxjava.RxJavaCallAdapterFactory
|
|
||||||
import retrofit2.converter.gson.GsonConverterFactory
|
|
||||||
import retrofit2.http.*
|
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
|
|
||||||
|
|
||||||
class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
||||||
|
|
||||||
private val rest = restBuilder()
|
private val parser = JsonParser()
|
||||||
.client(client.newBuilder().addInterceptor(interceptor).build())
|
private val jsonMime = MediaType.parse("application/json; charset=utf-8")
|
||||||
.build()
|
private val authClient = client.newBuilder().addInterceptor(interceptor).build()
|
||||||
.create(Rest::class.java)
|
|
||||||
|
|
||||||
fun addLibManga(track: Track): Observable<Track> {
|
fun addLibManga(track: Track): Observable<Track> {
|
||||||
return rest.addLibManga(track.remote_id, track.last_chapter_read, track.toAnilistStatus())
|
val query = """
|
||||||
.map { response ->
|
mutation AddManga(${'$'}mangaId: Int, ${'$'}progress: Int, ${'$'}status: MediaListStatus) {
|
||||||
response.body()?.close()
|
SaveMediaListEntry (mediaId: ${'$'}mangaId, progress: ${'$'}progress, status: ${'$'}status)
|
||||||
if (!response.isSuccessful) {
|
{ id status } }
|
||||||
throw Exception("Could not add manga")
|
"""
|
||||||
|
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
|
track
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateLibManga(track: Track): Observable<Track> {
|
fun updateLibManga(track: Track): Observable<Track> {
|
||||||
return rest.updateLibManga(track.remote_id, track.last_chapter_read, track.toAnilistStatus(),
|
val query = """
|
||||||
track.toAnilistScore())
|
mutation UpdateManga(${'$'}listId: Int, ${'$'}progress: Int, ${'$'}status: MediaListStatus, ${'$'}score: Int) {
|
||||||
.map { response ->
|
SaveMediaListEntry (id: ${'$'}listId, progress: ${'$'}progress, status: ${'$'}status, scoreRaw: ${'$'}score) {
|
||||||
response.body()?.close()
|
id
|
||||||
if (!response.isSuccessful) {
|
status
|
||||||
throw Exception("Could not update manga")
|
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
|
track
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun search(query: String): Observable<List<TrackSearch>> {
|
fun search(search: String): Observable<List<TrackSearch>> {
|
||||||
return rest.search(query, 1)
|
val query = """
|
||||||
.map { list ->
|
query Search(${'$'}query: String) {
|
||||||
list.filter { it.type != "Novel" }.map { it.toTrack() }
|
Page (perPage: 25) {
|
||||||
|
media(search: ${'$'}query, type: MANGA, format: MANGA) {
|
||||||
|
id
|
||||||
|
title {
|
||||||
|
romaji
|
||||||
}
|
}
|
||||||
.onErrorReturn { emptyList() }
|
coverImage {
|
||||||
|
large
|
||||||
}
|
}
|
||||||
|
type
|
||||||
fun getList(username: String): Observable<List<Track>> {
|
status
|
||||||
return rest.getLib(username)
|
chapters
|
||||||
.map { lib ->
|
startDate {
|
||||||
lib.flatten().map { it.toTrack() }
|
year
|
||||||
|
month
|
||||||
|
day
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
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<Track?> {
|
|
||||||
// TODO avoid getting the entire list
|
fun findLibManga(track: Track, userid: Int) : Observable<Track?> {
|
||||||
return getList(username)
|
val query = """
|
||||||
.map { list -> list.find { it.remote_id == track.remote_id } }
|
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<Track> {
|
fun getLibManga(track: Track, userid: Int): Observable<Track> {
|
||||||
return findLibManga(track, username)
|
return findLibManga(track, userid)
|
||||||
.map { it ?: throw Exception("Could not find manga") }
|
.map { it ?: throw Exception("Could not find manga") }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun login(authCode: String): Observable<OAuth> {
|
fun createOAuth(token: String): OAuth {
|
||||||
return restBuilder()
|
return OAuth(token, "Bearer", System.currentTimeMillis() + 31536000000, 31536000000)
|
||||||
.client(client)
|
}
|
||||||
|
|
||||||
|
fun getCurrentUser(): Observable<Pair<Int, String>> {
|
||||||
|
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()
|
.build()
|
||||||
.create(Rest::class.java)
|
return authClient.newCall(request)
|
||||||
.requestAccessToken(authCode)
|
.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<Pair<String, Int>> {
|
fun jsonToALManga(struct: JsonObject): ALManga{
|
||||||
return rest.getCurrentUser()
|
return ALManga(struct["id"].asInt, struct["title"]["romaji"].asString, struct["coverImage"]["large"].asString,
|
||||||
.map { it["id"].string to it["score_type"].int }
|
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()
|
fun jsonToALUserManga(struct: JsonObject): ALUserManga{
|
||||||
.baseUrl(baseUrl)
|
return ALUserManga(struct["id"].asLong, struct["status"].asString, struct["scoreRaw"].asInt, struct["progress"].asInt, jsonToALManga(struct["media"].obj) )
|
||||||
.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<OAuth>
|
|
||||||
|
|
||||||
@GET("user")
|
|
||||||
fun getCurrentUser(): Observable<JsonObject>
|
|
||||||
|
|
||||||
@GET("manga/search/{query}")
|
|
||||||
fun search(
|
|
||||||
@Path("query") query: String,
|
|
||||||
@Query("page") page: Int
|
|
||||||
): Observable<List<ALManga>>
|
|
||||||
|
|
||||||
@GET("user/{username}/mangalist")
|
|
||||||
fun getLib(
|
|
||||||
@Path("username") username: String
|
|
||||||
): Observable<ALUserLists>
|
|
||||||
|
|
||||||
@FormUrlEncoded
|
|
||||||
@PUT("mangalist")
|
|
||||||
fun addLibManga(
|
|
||||||
@Field("id") id: Int,
|
|
||||||
@Field("chapters_read") chapters_read: Int,
|
|
||||||
@Field("list_status") list_status: String
|
|
||||||
) : Observable<Response<ResponseBody>>
|
|
||||||
|
|
||||||
@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<Response<ResponseBody>>
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val clientId = "tachiyomi-hrtje"
|
private const val clientId = "385"
|
||||||
private const val clientSecret = "nlGB5OmgE9YWq5dr3gIDbTQV0C"
|
|
||||||
private const val clientUrl = "tachiyomi://anilist-auth"
|
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/"
|
private const val baseMangaUrl = "https://anilist.co/manga/"
|
||||||
|
|
||||||
fun mangaUrl(remoteId: Int): String {
|
fun mangaUrl(mediaId: Int): String {
|
||||||
return baseMangaUrl + remoteId
|
return baseMangaUrl + mediaId
|
||||||
}
|
}
|
||||||
|
|
||||||
fun authUrl() = Uri.parse("${baseUrl}auth/authorize").buildUpon()
|
fun authUrl() = Uri.parse("${baseUrl}oauth/authorize").buildUpon()
|
||||||
.appendQueryParameter("grant_type", "authorization_code")
|
|
||||||
.appendQueryParameter("client_id", clientId)
|
.appendQueryParameter("client_id", clientId)
|
||||||
.appendQueryParameter("redirect_uri", clientUrl)
|
.appendQueryParameter("response_type", "token")
|
||||||
.appendQueryParameter("response_type", "code")
|
|
||||||
.build()
|
.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())
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -1,10 +1,10 @@
|
|||||||
package eu.kanade.tachiyomi.data.track.anilist
|
package eu.kanade.tachiyomi.data.track.anilist
|
||||||
|
|
||||||
import com.google.gson.Gson
|
|
||||||
import okhttp3.Interceptor
|
import okhttp3.Interceptor
|
||||||
import okhttp3.Response
|
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.
|
* OAuth object used for authenticated requests.
|
||||||
@ -20,24 +20,21 @@ class AnilistInterceptor(private var refreshToken: String?) : Interceptor {
|
|||||||
override fun intercept(chain: Interceptor.Chain): Response {
|
override fun intercept(chain: Interceptor.Chain): Response {
|
||||||
val originalRequest = chain.request()
|
val originalRequest = chain.request()
|
||||||
|
|
||||||
if (refreshToken.isNullOrEmpty()) {
|
if (token.isNullOrEmpty()) {
|
||||||
throw Exception("Not authenticated with Anilist")
|
throw Exception("Not authenticated with Anilist")
|
||||||
}
|
}
|
||||||
|
if (oauth == null){
|
||||||
// Refresh access token if null or expired.
|
oauth = anilist.loadOAuth()
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
// Refresh access token if null or expired.
|
||||||
|
if (oauth!!.isExpired()) {
|
||||||
|
anilist.logout()
|
||||||
|
throw Exception("Token expired")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Throw on null auth.
|
// Throw on null auth.
|
||||||
if (oauth == null) {
|
if (oauth == null) {
|
||||||
throw Exception("Access token wasn't refreshed")
|
throw Exception("No authentication token")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add the authorization header to the original request.
|
// Add the authorization header to the original request.
|
||||||
@ -53,8 +50,9 @@ class AnilistInterceptor(private var refreshToken: String?) : Interceptor {
|
|||||||
* and the oauth object.
|
* and the oauth object.
|
||||||
*/
|
*/
|
||||||
fun setAuth(oauth: OAuth?) {
|
fun setAuth(oauth: OAuth?) {
|
||||||
refreshToken = oauth?.refresh_token
|
token = oauth?.access_token
|
||||||
this.oauth = oauth
|
this.oauth = oauth
|
||||||
|
anilist.saveOAuth(oauth)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -11,7 +11,7 @@ import java.text.SimpleDateFormat
|
|||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
data class ALManga(
|
data class ALManga(
|
||||||
val id: Int,
|
val media_id: Int,
|
||||||
val title_romaji: String,
|
val title_romaji: String,
|
||||||
val image_url_lge: String,
|
val image_url_lge: String,
|
||||||
val description: String?,
|
val description: String?,
|
||||||
@ -21,12 +21,12 @@ data class ALManga(
|
|||||||
val total_chapters: Int) {
|
val total_chapters: Int) {
|
||||||
|
|
||||||
fun toTrack() = TrackSearch.create(TrackManager.ANILIST).apply {
|
fun toTrack() = TrackSearch.create(TrackManager.ANILIST).apply {
|
||||||
remote_id = this@ALManga.id
|
media_id = this@ALManga.media_id
|
||||||
title = title_romaji
|
title = title_romaji
|
||||||
total_chapters = this@ALManga.total_chapters
|
total_chapters = this@ALManga.total_chapters
|
||||||
cover_url = image_url_lge
|
cover_url = image_url_lge
|
||||||
summary = description ?: ""
|
summary = description ?: ""
|
||||||
tracking_url = AnilistApi.mangaUrl(remote_id)
|
tracking_url = AnilistApi.mangaUrl(media_id)
|
||||||
publishing_status = this@ALManga.publishing_status
|
publishing_status = this@ALManga.publishing_status
|
||||||
publishing_type = type
|
publishing_type = type
|
||||||
if (!start_date_fuzzy.isNullOrBlank()) {
|
if (!start_date_fuzzy.isNullOrBlank()) {
|
||||||
@ -43,40 +43,37 @@ data class ALManga(
|
|||||||
}
|
}
|
||||||
|
|
||||||
data class ALUserManga(
|
data class ALUserManga(
|
||||||
val id: Int,
|
val library_id: Long,
|
||||||
val list_status: String,
|
val list_status: String,
|
||||||
val score_raw: Int,
|
val score_raw: Int,
|
||||||
val chapters_read: Int,
|
val chapters_read: Int,
|
||||||
val manga: ALManga) {
|
val manga: ALManga) {
|
||||||
|
|
||||||
fun toTrack() = Track.create(TrackManager.ANILIST).apply {
|
fun toTrack() = Track.create(TrackManager.ANILIST).apply {
|
||||||
remote_id = manga.id
|
media_id = manga.media_id
|
||||||
status = toTrackStatus()
|
status = toTrackStatus()
|
||||||
score = score_raw.toFloat()
|
score = score_raw.toFloat()
|
||||||
last_chapter_read = chapters_read
|
last_chapter_read = chapters_read
|
||||||
|
library_id = this@ALUserManga.library_id
|
||||||
}
|
}
|
||||||
|
|
||||||
fun toTrackStatus() = when (list_status) {
|
fun toTrackStatus() = when (list_status) {
|
||||||
"reading" -> Anilist.READING
|
"CURRENT" -> Anilist.READING
|
||||||
"completed" -> Anilist.COMPLETED
|
"COMPLETED" -> Anilist.COMPLETED
|
||||||
"on-hold" -> Anilist.ON_HOLD
|
"PAUSED" -> Anilist.ON_HOLD
|
||||||
"dropped" -> Anilist.DROPPED
|
"DROPPED" -> Anilist.DROPPED
|
||||||
"plan to read" -> Anilist.PLAN_TO_READ
|
"PLANNING" -> Anilist.PLANNING
|
||||||
else -> throw NotImplementedError("Unknown status")
|
else -> throw NotImplementedError("Unknown status")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
data class ALUserLists(val lists: Map<String, List<ALUserManga>>) {
|
|
||||||
|
|
||||||
fun flatten() = lists.values.flatten()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Track.toAnilistStatus() = when (status) {
|
fun Track.toAnilistStatus() = when (status) {
|
||||||
Anilist.READING -> "reading"
|
Anilist.READING -> "CURRENT"
|
||||||
Anilist.COMPLETED -> "completed"
|
Anilist.COMPLETED -> "COMPLETED"
|
||||||
Anilist.ON_HOLD -> "on-hold"
|
Anilist.ON_HOLD -> "PAUSED"
|
||||||
Anilist.DROPPED -> "dropped"
|
Anilist.DROPPED -> "DROPPED"
|
||||||
Anilist.PLAN_TO_READ -> "plan to read"
|
Anilist.PLANNING -> "PLANNING"
|
||||||
|
Anilist.REPEATING -> "REPEATING"
|
||||||
else -> throw NotImplementedError("Unknown status")
|
else -> throw NotImplementedError("Unknown status")
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -84,11 +81,11 @@ private val preferences: PreferencesHelper by injectLazy()
|
|||||||
|
|
||||||
fun Track.toAnilistScore(): String = when (preferences.anilistScoreType().getOrDefault()) {
|
fun Track.toAnilistScore(): String = when (preferences.anilistScoreType().getOrDefault()) {
|
||||||
// 10 point
|
// 10 point
|
||||||
0 -> (score.toInt() / 10).toString()
|
"POINT_10" -> (score.toInt() / 10).toString()
|
||||||
// 100 point
|
// 100 point
|
||||||
1 -> score.toInt().toString()
|
"POINT_100" -> score.toInt().toString()
|
||||||
// 5 stars
|
// 5 stars
|
||||||
2 -> when {
|
"POINT_5" -> when {
|
||||||
score == 0f -> "0"
|
score == 0f -> "0"
|
||||||
score < 30 -> "1"
|
score < 30 -> "1"
|
||||||
score < 50 -> "2"
|
score < 50 -> "2"
|
||||||
@ -97,13 +94,13 @@ fun Track.toAnilistScore(): String = when (preferences.anilistScoreType().getOrD
|
|||||||
else -> "5"
|
else -> "5"
|
||||||
}
|
}
|
||||||
// Smiley
|
// Smiley
|
||||||
3 -> when {
|
"POINT_3" -> when {
|
||||||
score == 0f -> "0"
|
score == 0f -> "0"
|
||||||
score <= 30 -> ":("
|
score <= 30 -> ":("
|
||||||
score <= 60 -> ":|"
|
score <= 60 -> ":|"
|
||||||
else -> ":)"
|
else -> ":)"
|
||||||
}
|
}
|
||||||
// 10 point decimal
|
// 10 point decimal
|
||||||
4 -> (score / 10).toString()
|
"POINT_10_DECIMAL" -> (score / 10).toString()
|
||||||
else -> throw Exception("Unknown score type")
|
else -> throw Exception("Unknown score type")
|
||||||
}
|
}
|
@ -4,8 +4,7 @@ data class OAuth(
|
|||||||
val access_token: String,
|
val access_token: String,
|
||||||
val token_type: String,
|
val token_type: String,
|
||||||
val expires: Long,
|
val expires: Long,
|
||||||
val expires_in: Long,
|
val expires_in: Long) {
|
||||||
val refresh_token: String?) {
|
|
||||||
|
|
||||||
fun isExpired() = System.currentTimeMillis() > expires
|
fun isExpired() = System.currentTimeMillis() > expires
|
||||||
}
|
}
|
@ -87,7 +87,7 @@ class Kitsu(private val context: Context, id: Int) : TrackService(id) {
|
|||||||
.flatMap { remoteTrack ->
|
.flatMap { remoteTrack ->
|
||||||
if (remoteTrack != null) {
|
if (remoteTrack != null) {
|
||||||
track.copyPersonalFrom(remoteTrack)
|
track.copyPersonalFrom(remoteTrack)
|
||||||
track.remote_id = remoteTrack.remote_id
|
track.media_id = remoteTrack.media_id
|
||||||
update(track)
|
update(track)
|
||||||
} else {
|
} else {
|
||||||
track.score = DEFAULT_SCORE
|
track.score = DEFAULT_SCORE
|
||||||
|
@ -42,7 +42,7 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
|
|||||||
),
|
),
|
||||||
"media" to jsonObject(
|
"media" to jsonObject(
|
||||||
"data" to jsonObject(
|
"data" to jsonObject(
|
||||||
"id" to track.remote_id,
|
"id" to track.media_id,
|
||||||
"type" to "manga"
|
"type" to "manga"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@ -52,7 +52,7 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
|
|||||||
|
|
||||||
rest.addLibManga(jsonObject("data" to data))
|
rest.addLibManga(jsonObject("data" to data))
|
||||||
.map { json ->
|
.map { json ->
|
||||||
track.remote_id = json["data"]["id"].int
|
track.media_id = json["data"]["id"].int
|
||||||
track
|
track
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -63,7 +63,7 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
|
|||||||
// @formatter:off
|
// @formatter:off
|
||||||
val data = jsonObject(
|
val data = jsonObject(
|
||||||
"type" to "libraryEntries",
|
"type" to "libraryEntries",
|
||||||
"id" to track.remote_id,
|
"id" to track.media_id,
|
||||||
"attributes" to jsonObject(
|
"attributes" to jsonObject(
|
||||||
"status" to track.toKitsuStatus(),
|
"status" to track.toKitsuStatus(),
|
||||||
"progress" to track.last_chapter_read,
|
"progress" to track.last_chapter_read,
|
||||||
@ -72,7 +72,7 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
|
|||||||
)
|
)
|
||||||
// @formatter:on
|
// @formatter:on
|
||||||
|
|
||||||
rest.updateLibManga(track.remote_id, jsonObject("data" to data))
|
rest.updateLibManga(track.media_id, jsonObject("data" to data))
|
||||||
.map { track }
|
.map { track }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -88,7 +88,7 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun findLibManga(track: Track, userId: String): Observable<Track?> {
|
fun findLibManga(track: Track, userId: String): Observable<Track?> {
|
||||||
return rest.findLibManga(track.remote_id, userId)
|
return rest.findLibManga(track.media_id, userId)
|
||||||
.map { json ->
|
.map { json ->
|
||||||
val data = json["data"].array
|
val data = json["data"].array
|
||||||
if (data.size() > 0) {
|
if (data.size() > 0) {
|
||||||
@ -101,7 +101,7 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun getLibManga(track: Track): Observable<Track> {
|
fun getLibManga(track: Track): Observable<Track> {
|
||||||
return rest.getLibManga(track.remote_id)
|
return rest.getLibManga(track.media_id)
|
||||||
.map { json ->
|
.map { json ->
|
||||||
val data = json["data"].array
|
val data = json["data"].array
|
||||||
if (data.size() > 0) {
|
if (data.size() > 0) {
|
||||||
|
@ -19,12 +19,12 @@ open class KitsuManga(obj: JsonObject) {
|
|||||||
|
|
||||||
@CallSuper
|
@CallSuper
|
||||||
open fun toTrack() = TrackSearch.create(TrackManager.KITSU).apply {
|
open fun toTrack() = TrackSearch.create(TrackManager.KITSU).apply {
|
||||||
remote_id = this@KitsuManga.id
|
media_id = this@KitsuManga.id
|
||||||
title = canonicalTitle
|
title = canonicalTitle
|
||||||
total_chapters = chapterCount ?: 0
|
total_chapters = chapterCount ?: 0
|
||||||
cover_url = original
|
cover_url = original
|
||||||
summary = synopsis
|
summary = synopsis
|
||||||
tracking_url = KitsuApi.mangaUrl(remote_id)
|
tracking_url = KitsuApi.mangaUrl(media_id)
|
||||||
publishing_status = this@KitsuManga.status
|
publishing_status = this@KitsuManga.status
|
||||||
publishing_type = type
|
publishing_type = type
|
||||||
start_date = startDate.orEmpty()
|
start_date = startDate.orEmpty()
|
||||||
@ -32,13 +32,13 @@ open class KitsuManga(obj: JsonObject) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class KitsuLibManga(obj: JsonObject, manga: JsonObject) : KitsuManga(manga) {
|
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
|
override val status by obj["attributes"].byString
|
||||||
val ratingTwenty = obj["attributes"].obj.get("ratingTwenty").nullString
|
val ratingTwenty = obj["attributes"].obj.get("ratingTwenty").nullString
|
||||||
val progress by obj["attributes"].byInt
|
val progress by obj["attributes"].byInt
|
||||||
|
|
||||||
override fun toTrack() = super.toTrack().apply {
|
override fun toTrack() = super.toTrack().apply {
|
||||||
remote_id = remoteId
|
media_id = libraryId // TODO migrate media ids to library ids
|
||||||
status = toTrackStatus()
|
status = toTrackStatus()
|
||||||
score = ratingTwenty?.let { it.toInt() / 2f } ?: 0f
|
score = ratingTwenty?.let { it.toInt() / 2f } ?: 0f
|
||||||
last_chapter_read = progress
|
last_chapter_read = progress
|
||||||
|
@ -10,7 +10,9 @@ class TrackSearch : Track {
|
|||||||
|
|
||||||
override var sync_id: Int = 0
|
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
|
override lateinit var title: String
|
||||||
|
|
||||||
@ -42,13 +44,13 @@ class TrackSearch : Track {
|
|||||||
|
|
||||||
if (manga_id != other.manga_id) return false
|
if (manga_id != other.manga_id) return false
|
||||||
if (sync_id != other.sync_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 {
|
override fun hashCode(): Int {
|
||||||
var result = (manga_id xor manga_id.ushr(32)).toInt()
|
var result = (manga_id xor manga_id.ushr(32)).toInt()
|
||||||
result = 31 * result + sync_id
|
result = 31 * result + sync_id
|
||||||
result = 31 * result + remote_id
|
result = 31 * result + media_id
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
companion object {
|
companion object {
|
||||||
|
@ -54,11 +54,11 @@ class MyanimelistApi(private val client: OkHttpClient, username: String, passwor
|
|||||||
.map {
|
.map {
|
||||||
TrackSearch.create(TrackManager.MYANIMELIST).apply {
|
TrackSearch.create(TrackManager.MYANIMELIST).apply {
|
||||||
title = it.selectText("title")!!
|
title = it.selectText("title")!!
|
||||||
remote_id = it.selectInt("id")
|
media_id = it.selectInt("id")
|
||||||
total_chapters = it.selectInt("chapters")
|
total_chapters = it.selectInt("chapters")
|
||||||
summary = it.selectText("synopsis")!!
|
summary = it.selectText("synopsis")!!
|
||||||
cover_url = it.selectText("image")!!
|
cover_url = it.selectText("image")!!
|
||||||
tracking_url = MyanimelistApi.mangaUrl(remote_id)
|
tracking_url = MyanimelistApi.mangaUrl(media_id)
|
||||||
publishing_status = it.selectText("status")!!
|
publishing_status = it.selectText("status")!!
|
||||||
publishing_type = it.selectText("type")!!
|
publishing_type = it.selectText("type")!!
|
||||||
start_date = it.selectText("start_date")!!
|
start_date = it.selectText("start_date")!!
|
||||||
@ -77,13 +77,13 @@ class MyanimelistApi(private val client: OkHttpClient, username: String, passwor
|
|||||||
.map {
|
.map {
|
||||||
TrackSearch.create(TrackManager.MYANIMELIST).apply {
|
TrackSearch.create(TrackManager.MYANIMELIST).apply {
|
||||||
title = it.selectText("series_title")!!
|
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")
|
last_chapter_read = it.selectInt("my_read_chapters")
|
||||||
status = it.selectInt("my_status")
|
status = it.selectInt("my_status")
|
||||||
score = it.selectInt("my_score").toFloat()
|
score = it.selectInt("my_score").toFloat()
|
||||||
total_chapters = it.selectInt("series_chapters")
|
total_chapters = it.selectInt("series_chapters")
|
||||||
cover_url = it.selectText("series_image")!!
|
cover_url = it.selectText("series_image")!!
|
||||||
tracking_url = MyanimelistApi.mangaUrl(remote_id)
|
tracking_url = MyanimelistApi.mangaUrl(media_id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.toList()
|
.toList()
|
||||||
@ -91,7 +91,7 @@ class MyanimelistApi(private val client: OkHttpClient, username: String, passwor
|
|||||||
|
|
||||||
fun findLibManga(track: Track, username: String): Observable<Track?> {
|
fun findLibManga(track: Track, username: String): Observable<Track?> {
|
||||||
return getList(username)
|
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<Track> {
|
fun getLibManga(track: Track, username: String): Observable<Track> {
|
||||||
@ -169,12 +169,12 @@ class MyanimelistApi(private val client: OkHttpClient, username: String, passwor
|
|||||||
|
|
||||||
fun getUpdateUrl(track: Track) = Uri.parse(baseUrl).buildUpon()
|
fun getUpdateUrl(track: Track) = Uri.parse(baseUrl).buildUpon()
|
||||||
.appendEncodedPath("api/mangalist/update")
|
.appendEncodedPath("api/mangalist/update")
|
||||||
.appendPath("${track.remote_id}.xml")
|
.appendPath("${track.media_id}.xml")
|
||||||
.toString()
|
.toString()
|
||||||
|
|
||||||
fun getAddUrl(track: Track) = Uri.parse(baseUrl).buildUpon()
|
fun getAddUrl(track: Track) = Uri.parse(baseUrl).buildUpon()
|
||||||
.appendEncodedPath("api/mangalist/add")
|
.appendEncodedPath("api/mangalist/add")
|
||||||
.appendPath("${track.remote_id}.xml")
|
.appendPath("${track.media_id}.xml")
|
||||||
.toString()
|
.toString()
|
||||||
|
|
||||||
fun createHeaders(username: String, password: String): Headers {
|
fun createHeaders(username: String, password: String): Headers {
|
||||||
|
@ -23,9 +23,10 @@ class AnilistLoginActivity : AppCompatActivity() {
|
|||||||
val view = ProgressBar(this)
|
val view = ProgressBar(this)
|
||||||
setContentView(view, FrameLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT, CENTER))
|
setContentView(view, FrameLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT, CENTER))
|
||||||
|
|
||||||
val code = intent.data?.getQueryParameter("code")
|
val regex = "(?:access_token=)(.*?)(?:&)".toRegex()
|
||||||
if (code != null) {
|
val matchResult = regex.find(intent.data?.fragment.toString())
|
||||||
trackManager.aniList.login(code)
|
if (matchResult?.groups?.get(1) != null) {
|
||||||
|
trackManager.aniList.login(matchResult.groups[1]!!.value)
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe({
|
.subscribe({
|
||||||
|
@ -385,6 +385,7 @@
|
|||||||
<string name="dropped">Dropped</string>
|
<string name="dropped">Dropped</string>
|
||||||
<string name="on_hold">On hold</string>
|
<string name="on_hold">On hold</string>
|
||||||
<string name="plan_to_read">Plan to read</string>
|
<string name="plan_to_read">Plan to read</string>
|
||||||
|
<string name="repeating">Re-reading</string>
|
||||||
<string name="score">Score</string>
|
<string name="score">Score</string>
|
||||||
<string name="title">Title</string>
|
<string name="title">Title</string>
|
||||||
<string name="status">Status</string>
|
<string name="status">Status</string>
|
||||||
|
Loading…
Reference in New Issue
Block a user