Score formatting. Hide API from Anilist/Kitsu services.

pull/602/head
len 8 years ago
parent 091c0c0c71
commit 8d749df290

@ -21,6 +21,23 @@ abstract class TrackService(val id: Int) {
// Name of the manga sync service to display // Name of the manga sync service to display
abstract val name: String abstract val name: String
@DrawableRes
abstract fun getLogo(): Int
abstract fun getLogoColor(): Int
abstract fun getStatusList(): List<Int>
abstract fun getStatus(status: Int): String
abstract fun getScoreList(): List<String>
open fun indexToScore(index: Int): Float {
return index.toFloat()
}
abstract fun displayScore(track: Track): String
abstract fun login(username: String, password: String): Completable abstract fun login(username: String, password: String): Completable
open val isLogged: Boolean open val isLogged: Boolean
@ -37,20 +54,6 @@ abstract class TrackService(val id: Int) {
abstract fun refresh(track: Track): Observable<Track> abstract fun refresh(track: Track): Observable<Track>
abstract fun getStatus(status: Int): String
abstract fun getStatusList(): List<Int>
@DrawableRes
abstract fun getLogo(): Int
abstract fun getLogoColor(): Int
// TODO better support (decimals)
abstract fun maxScore(): Int
abstract fun formatScore(track: Track): String
fun saveCredentials(username: String, password: String) { fun saveCredentials(username: String, password: String) {
preferences.setTrackCredentials(this, username, password) preferences.setTrackCredentials(this, username, password)
} }

@ -2,15 +2,12 @@ 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.github.salomonbrys.kotson.int
import com.github.salomonbrys.kotson.string
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
import eu.kanade.tachiyomi.data.track.TrackService import eu.kanade.tachiyomi.data.track.TrackService
import rx.Completable import rx.Completable
import rx.Observable import rx.Observable
import timber.log.Timber
class Anilist(private val context: Context, id: Int) : TrackService(id) { class Anilist(private val context: Context, id: Int) : TrackService(id) {
@ -29,31 +26,83 @@ class Anilist(private val context: Context, id: Int) : TrackService(id) {
private val interceptor by lazy { AnilistInterceptor(getPassword()) } private val interceptor by lazy { AnilistInterceptor(getPassword()) }
private val api by lazy { private val api by lazy { AnilistApi(client, interceptor) }
AnilistApi.createService(networkService.client.newBuilder()
.addInterceptor(interceptor)
.build())
}
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 maxScore() = 100 override fun getStatusList(): List<Int> {
return listOf(READING, COMPLETED, ON_HOLD, DROPPED, PLAN_TO_READ)
}
override fun getStatus(status: Int): String = with(context) {
when (status) {
READING -> getString(R.string.reading)
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)
else -> ""
}
}
override fun getScoreList(): List<String> {
return when (preferences.anilistScoreType().getOrDefault()) {
// 10 point
0 -> IntRange(0, 10).map(Int::toString)
// 100 point
1 -> IntRange(0, 100).map(Int::toString)
// 5 stars
2 -> IntRange(0, 5).map { "$it" }
// Smiley
3 -> listOf("-", "😦", "😐", "😊")
// 10 point decimal
4 -> IntRange(0, 100).map { (it / 10f).toString() }
else -> throw Exception("Unknown score type")
}
}
override fun indexToScore(index: Int): Float {
return when (preferences.anilistScoreType().getOrDefault()) {
// 10 point
0 -> index * 10f
// 100 point
1 -> index.toFloat()
// 5 stars
2 -> index * 20f
// Smiley
3 -> index * 30f
// 10 point decimal
4 -> index / 10f
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 {
score == 0f -> "0"
score <= 30 -> "😦"
score <= 60 -> "😐"
else -> "😊"
}
else -> track.toAnilistScore()
}
}
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(authCode: String): Completable {
// Create a new api with the default client to avoid request interceptions. return api.login(authCode)
return AnilistApi.createService(client)
// Request the access token from the API with the authorization code.
.requestAccessToken(authCode)
// Save the token in the interceptor. // Save the token in the interceptor.
.doOnNext { interceptor.setAuth(it) } .doOnNext { interceptor.setAuth(it) }
// Obtain the authenticated user from the API. // Obtain the authenticated user from the API.
.zipWith(api.getCurrentUser().map { .zipWith(api.getCurrentUser().map { pair ->
preferences.anilistScoreType().set(it["score_type"].int) preferences.anilistScoreType().set(pair.second)
it["id"].string pair.first
}, { oauth, user -> Pair(user, oauth.refresh_token!!) }) }, { oauth, user -> Pair(user, oauth.refresh_token!!) })
// Save service credentials (username and refresh token). // Save service credentials (username and refresh token).
.doOnNext { saveCredentials(it.first, it.second) } .doOnNext { saveCredentials(it.first, it.second) }
@ -68,45 +117,24 @@ class Anilist(private val context: Context, id: Int) : TrackService(id) {
} }
override fun search(query: String): Observable<List<Track>> { override fun search(query: String): Observable<List<Track>> {
return api.search(query, 1) return api.search(query)
.flatMap { Observable.from(it) }
.filter { it.type != "Novel" }
.map { it.toTrack() }
.toList()
}
fun getList(): Observable<List<Track>> {
return api.getList(getUsername())
.flatMap { Observable.from(it.flatten()) }
.map { it.toTrack() }
.toList()
} }
override fun add(track: Track): Observable<Track> { override fun add(track: Track): Observable<Track> {
return api.addManga(track.remote_id, track.last_chapter_read, track.getAnilistStatus()) return api.addLibManga(track)
.doOnNext { it.body().close() }
.doOnNext { if (!it.isSuccessful) throw Exception("Could not add manga") }
.doOnError { Timber.e(it) }
.map { track }
} }
override fun update(track: Track): Observable<Track> { override fun update(track: Track): Observable<Track> {
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
} }
return api.updateManga(track.remote_id, track.last_chapter_read, track.getAnilistStatus(),
track.getAnilistScore()) return api.updateLibManga(track)
.doOnNext { it.body().close() }
.doOnNext { if (!it.isSuccessful) throw Exception("Could not update manga") }
.doOnError { Timber.e(it) }
.map { track }
} }
override fun bind(track: Track): Observable<Track> { override fun bind(track: Track): Observable<Track> {
return getList() return api.findLibManga(getUsername(), track)
.flatMap { userlist -> .flatMap { remoteTrack ->
track.sync_id = id
val remoteTrack = userlist.find { it.remote_id == track.remote_id }
if (remoteTrack != null) { if (remoteTrack != null) {
track.copyPersonalFrom(remoteTrack) track.copyPersonalFrom(remoteTrack)
update(track) update(track)
@ -120,9 +148,9 @@ 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 getList() // TODO getLibManga method?
.map { myList -> return api.findLibManga(getUsername(), track)
val remoteTrack = myList.find { it.remote_id == track.remote_id } .map { remoteTrack ->
if (remoteTrack != null) { if (remoteTrack != null) {
track.copyPersonalFrom(remoteTrack) track.copyPersonalFrom(remoteTrack)
track.total_chapters = remoteTrack.total_chapters track.total_chapters = remoteTrack.total_chapters
@ -133,59 +161,5 @@ class Anilist(private val context: Context, id: Int) : TrackService(id) {
} }
} }
override fun getStatusList(): List<Int> {
return listOf(READING, COMPLETED, ON_HOLD, DROPPED, PLAN_TO_READ)
}
override fun getStatus(status: Int): String = with(context) {
when (status) {
READING -> getString(R.string.reading)
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)
else -> ""
}
}
private fun Track.getAnilistStatus() = when (status) {
READING -> "reading"
COMPLETED -> "completed"
ON_HOLD -> "on-hold"
DROPPED -> "dropped"
PLAN_TO_READ -> "plan to read"
else -> throw NotImplementedError("Unknown status")
}
fun Track.getAnilistScore(): String = when (preferences.anilistScoreType().getOrDefault()) {
// 10 point
0 -> Math.floor(score.toDouble() / 10).toInt().toString()
// 100 point
1 -> score.toInt().toString()
// 5 stars
2 -> when {
score == 0f -> "0"
score < 30 -> "1"
score < 50 -> "2"
score < 70 -> "3"
score < 90 -> "4"
else -> "5"
}
// Smiley
3 -> when {
score == 0f -> "0"
score <= 30 -> ":("
score <= 60 -> ":|"
else -> ":)"
}
// 10 point decimal
4 -> (score / 10).toString()
else -> throw Exception("Unknown score type")
}
override fun formatScore(track: Track): String {
return track.getAnilistScore()
}
} }

@ -1,11 +1,11 @@
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.string
import com.google.gson.JsonObject import com.google.gson.JsonObject
import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.network.POST import eu.kanade.tachiyomi.data.network.POST
import eu.kanade.tachiyomi.data.track.anilist.model.ALManga
import eu.kanade.tachiyomi.data.track.anilist.model.ALUserLists
import eu.kanade.tachiyomi.data.track.anilist.model.OAuth
import okhttp3.FormBody import okhttp3.FormBody
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.ResponseBody import okhttp3.ResponseBody
@ -16,39 +16,68 @@ import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.http.* import retrofit2.http.*
import rx.Observable import rx.Observable
interface AnilistApi { class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
companion object { private val rest = restBuilder()
private const val clientId = "tachiyomi-hrtje" .client(client.newBuilder().addInterceptor(interceptor).build())
private const val clientSecret = "nlGB5OmgE9YWq5dr3gIDbTQV0C"
private const val clientUrl = "tachiyomi://anilist-auth"
private const val baseUrl = "https://anilist.co/api/"
fun authUrl() = Uri.parse("${baseUrl}auth/authorize").buildUpon()
.appendQueryParameter("grant_type", "authorization_code")
.appendQueryParameter("client_id", clientId)
.appendQueryParameter("redirect_uri", clientUrl)
.appendQueryParameter("response_type", "code")
.build() .build()
.create(Rest::class.java)
fun refreshTokenRequest(token: String) = POST("${baseUrl}auth/access_token", private fun restBuilder() = Retrofit.Builder()
body = FormBody.Builder()
.add("grant_type", "refresh_token")
.add("client_id", clientId)
.add("client_secret", clientSecret)
.add("refresh_token", token)
.build())
fun createService(client: OkHttpClient) = Retrofit.Builder()
.baseUrl(baseUrl) .baseUrl(baseUrl)
.client(client)
.addConverterFactory(GsonConverterFactory.create()) .addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJavaCallAdapterFactory.create()) .addCallAdapterFactory(RxJavaCallAdapterFactory.create())
fun login(authCode: String): Observable<OAuth> {
return restBuilder()
.client(client)
.build() .build()
.create(AnilistApi::class.java) .create(Rest::class.java)
.requestAccessToken(authCode)
}
fun getCurrentUser(): Observable<Pair<String, Int>> {
return rest.getCurrentUser()
.map { it["id"].string to it["score_type"].int }
}
fun search(query: String): Observable<List<Track>> {
return rest.search(query, 1)
.map { list ->
list.filter { it.type != "Novel" }.map { it.toTrack() }
}
}
fun getList(username: String): Observable<List<Track>> {
return rest.getLib(username)
.map { lib ->
lib.flatten().map { it.toTrack() }
}
}
fun addLibManga(track: Track): Observable<Track> {
return rest.addLibManga(track.remote_id, track.last_chapter_read, track.toAnilistStatus())
.doOnNext { it.body().close() }
.doOnNext { if (!it.isSuccessful) throw Exception("Could not add manga") }
.map { track }
} }
fun updateLibManga(track: Track): Observable<Track> {
return rest.updateLibManga(track.remote_id, track.last_chapter_read, track.toAnilistStatus(),
track.toAnilistScore())
.doOnNext { it.body().close() }
.doOnNext { if (!it.isSuccessful) throw Exception("Could not update manga") }
.map { track }
}
fun findLibManga(username: String, track: Track) : Observable<Track?> {
// TODO avoid getting the entire list
return getList(username)
.map { list -> list.find { it.remote_id == track.remote_id } }
}
private interface Rest {
@FormUrlEncoded @FormUrlEncoded
@POST("auth/access_token") @POST("auth/access_token")
fun requestAccessToken( fun requestAccessToken(
@ -56,33 +85,63 @@ interface AnilistApi {
@Field("grant_type") grant_type: String = "authorization_code", @Field("grant_type") grant_type: String = "authorization_code",
@Field("client_id") client_id: String = clientId, @Field("client_id") client_id: String = clientId,
@Field("client_secret") client_secret: String = clientSecret, @Field("client_secret") client_secret: String = clientSecret,
@Field("redirect_uri") redirect_uri: String = clientUrl) @Field("redirect_uri") redirect_uri: String = clientUrl
: Observable<OAuth> ) : Observable<OAuth>
@GET("user") @GET("user")
fun getCurrentUser(): Observable<JsonObject> fun getCurrentUser(): Observable<JsonObject>
@GET("manga/search/{query}") @GET("manga/search/{query}")
fun search(@Path("query") query: String, @Query("page") page: Int): Observable<List<ALManga>> fun search(
@Path("query") query: String,
@Query("page") page: Int
): Observable<List<ALManga>>
@GET("user/{username}/mangalist") @GET("user/{username}/mangalist")
fun getList(@Path("username") username: String): Observable<ALUserLists> fun getLib(
@Path("username") username: String
): Observable<ALUserLists>
@FormUrlEncoded @FormUrlEncoded
@PUT("mangalist") @PUT("mangalist")
fun addManga( fun addLibManga(
@Field("id") id: Int, @Field("id") id: Int,
@Field("chapters_read") chapters_read: Int, @Field("chapters_read") chapters_read: Int,
@Field("list_status") list_status: String) @Field("list_status") list_status: String
: Observable<Response<ResponseBody>> ) : Observable<Response<ResponseBody>>
@FormUrlEncoded @FormUrlEncoded
@PUT("mangalist") @PUT("mangalist")
fun updateManga( fun updateLibManga(
@Field("id") id: Int, @Field("id") id: Int,
@Field("chapters_read") chapters_read: Int, @Field("chapters_read") chapters_read: Int,
@Field("list_status") list_status: String, @Field("list_status") list_status: String,
@Field("score") score_raw: String) @Field("score") score_raw: String
: Observable<Response<ResponseBody>> ) : Observable<Response<ResponseBody>>
}
companion object {
private const val clientId = "tachiyomi-hrtje"
private const val clientSecret = "nlGB5OmgE9YWq5dr3gIDbTQV0C"
private const val clientUrl = "tachiyomi://anilist-auth"
private const val baseUrl = "https://anilist.co/api/"
fun authUrl() = Uri.parse("${baseUrl}auth/authorize").buildUpon()
.appendQueryParameter("grant_type", "authorization_code")
.appendQueryParameter("client_id", clientId)
.appendQueryParameter("redirect_uri", clientUrl)
.appendQueryParameter("response_type", "code")
.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,7 +1,7 @@
package eu.kanade.tachiyomi.data.track.anilist package eu.kanade.tachiyomi.data.track.anilist
import com.google.gson.Gson import com.google.gson.Gson
import eu.kanade.tachiyomi.data.track.anilist.model.OAuth import eu.kanade.tachiyomi.data.track.anilist.OAuth
import okhttp3.Interceptor import okhttp3.Interceptor
import okhttp3.Response import okhttp3.Response

@ -0,0 +1,86 @@
package eu.kanade.tachiyomi.data.track.anilist
import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.getOrDefault
import eu.kanade.tachiyomi.data.track.TrackManager
import uy.kohesive.injekt.injectLazy
data class ALManga(
val id: Int,
val title_romaji: String,
val type: String,
val total_chapters: Int) {
fun toTrack() = Track.create(TrackManager.ANILIST).apply {
remote_id = this@ALManga.id
title = title_romaji
total_chapters = this@ALManga.total_chapters
}
}
data class ALUserManga(
val id: Int,
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
status = toTrackStatus()
score = score_raw.toFloat()
last_chapter_read = chapters_read
}
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
else -> throw NotImplementedError("Unknown status")
}
}
data class ALUserLists(val lists: Map<String, List<ALUserManga>>) {
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"
else -> throw NotImplementedError("Unknown status")
}
private val preferences: PreferencesHelper by injectLazy()
fun Track.toAnilistScore(): String = when (preferences.anilistScoreType().getOrDefault()) {
// 10 point
0 -> Math.floor(score.toDouble() / 10).toInt().toString()
// 100 point
1 -> score.toInt().toString()
// 5 stars
2 -> when {
score == 0f -> "0"
score < 30 -> "1"
score < 50 -> "2"
score < 70 -> "3"
score < 90 -> "4"
else -> "5"
}
// Smiley
3 -> when {
score == 0f -> "0"
score <= 30 -> ":("
score <= 60 -> ":|"
else -> ":)"
}
// 10 point decimal
4 -> (score / 10).toString()
else -> throw Exception("Unknown score type")
}

@ -1,4 +1,4 @@
package eu.kanade.tachiyomi.data.track.anilist.model package eu.kanade.tachiyomi.data.track.anilist
data class OAuth( data class OAuth(
val access_token: String, val access_token: String,

@ -1,17 +0,0 @@
package eu.kanade.tachiyomi.data.track.anilist.model
import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.track.TrackManager
data class ALManga(
val id: Int,
val title_romaji: String,
val type: String,
val total_chapters: Int) {
fun toTrack() = Track.create(TrackManager.ANILIST).apply {
remote_id = this@ALManga.id
title = title_romaji
total_chapters = this@ALManga.total_chapters
}
}

@ -1,6 +0,0 @@
package eu.kanade.tachiyomi.data.track.anilist.model
data class ALUserLists(val lists: Map<String, List<ALUserManga>>) {
fun flatten() = lists.values.flatten()
}

@ -1,29 +0,0 @@
package eu.kanade.tachiyomi.data.track.anilist.model
import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.data.track.anilist.Anilist
data class ALUserManga(
val id: Int,
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
status = toTrackStatus()
score = score_raw.toFloat()
last_chapter_read = chapters_read
}
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
else -> throw NotImplementedError("Unknown status")
}
}

@ -2,14 +2,12 @@ package eu.kanade.tachiyomi.data.track.kitsu
import android.content.Context import android.content.Context
import android.graphics.Color import android.graphics.Color
import com.github.salomonbrys.kotson.*
import com.google.gson.Gson 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.track.TrackService import eu.kanade.tachiyomi.data.track.TrackService
import rx.Completable import rx.Completable
import rx.Observable import rx.Observable
import timber.log.Timber
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
class Kitsu(private val context: Context, id: Int) : TrackService(id) { class Kitsu(private val context: Context, id: Int) : TrackService(id) {
@ -31,10 +29,37 @@ class Kitsu(private val context: Context, id: Int) : TrackService(id) {
private val interceptor by lazy { KitsuInterceptor(this, gson) } private val interceptor by lazy { KitsuInterceptor(this, gson) }
private val api by lazy { private val api by lazy { KitsuApi(client, interceptor) }
KitsuApi.createService(client.newBuilder()
.addInterceptor(interceptor) override fun getLogo(): Int {
.build()) return R.drawable.kitsu
}
override fun getLogoColor(): Int {
return Color.rgb(51, 37, 50)
}
override fun getStatusList(): List<Int> {
return listOf(READING, COMPLETED, ON_HOLD, DROPPED, PLAN_TO_READ)
}
override fun getStatus(status: Int): String = with(context) {
when (status) {
READING -> getString(R.string.reading)
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)
else -> ""
}
}
override fun getScoreList(): List<String> {
return IntRange(0, 10).map { (it.toFloat() / 2).toString() }
}
override fun displayScore(track: Track): String {
return track.toKitsuScore()
} }
private fun getUserId(): String { private fun getUserId(): String {
@ -55,10 +80,9 @@ class Kitsu(private val context: Context, id: Int) : TrackService(id) {
} }
override fun login(username: String, password: String): Completable { override fun login(username: String, password: String): Completable {
return KitsuApi.createLoginService(client) return api.login(username, password)
.requestAccessToken(username, password)
.doOnNext { interceptor.newAuth(it) } .doOnNext { interceptor.newAuth(it) }
.flatMap { api.getCurrentUser().map { it["data"].array[0]["id"].string } } .flatMap { api.getCurrentUser() }
.doOnNext { userId -> saveCredentials(username, userId) } .doOnNext { userId -> saveCredentials(username, userId) }
.doOnError { logout() } .doOnError { logout() }
.toCompletable() .toCompletable()
@ -71,11 +95,6 @@ class Kitsu(private val context: Context, id: Int) : TrackService(id) {
override fun search(query: String): Observable<List<Track>> { override fun search(query: String): Observable<List<Track>> {
return api.search(query) return api.search(query)
.map { json ->
val data = json["data"].array
data.map { KitsuManga(it.obj).toTrack() }
}
.doOnError { Timber.e(it) }
} }
override fun bind(track: Track): Observable<Track> { override fun bind(track: Track): Observable<Track> {
@ -95,125 +114,26 @@ class Kitsu(private val context: Context, id: Int) : TrackService(id) {
private fun find(track: Track): Observable<Track?> { private fun find(track: Track): Observable<Track?> {
return api.findLibManga(getUserId(), track.remote_id) return api.findLibManga(getUserId(), track.remote_id)
.map { json ->
val data = json["data"].array
if (data.size() > 0) {
KitsuLibManga(data[0].obj, json["included"].array[0].obj).toTrack()
} else {
null
}
}
} }
override fun add(track: Track): Observable<Track> { override fun add(track: Track): Observable<Track> {
// @formatter:off return api.addLibManga(track, getUserId())
val data = jsonObject(
"type" to "libraryEntries",
"attributes" to jsonObject(
"status" to track.getKitsuStatus(),
"progress" to track.last_chapter_read
),
"relationships" to jsonObject(
"user" to jsonObject(
"data" to jsonObject(
"id" to getUserId(),
"type" to "users"
)
),
"media" to jsonObject(
"data" to jsonObject(
"id" to track.remote_id,
"type" to "manga"
)
)
)
)
// @formatter:on
return api.addLibManga(jsonObject("data" to data))
.doOnNext { json -> track.remote_id = json["data"]["id"].int }
.doOnError { Timber.e(it) }
.map { track }
} }
override fun update(track: Track): Observable<Track> { override fun update(track: Track): Observable<Track> {
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
} }
// @formatter:off
val data = jsonObject(
"type" to "libraryEntries",
"id" to track.remote_id,
"attributes" to jsonObject(
"status" to track.getKitsuStatus(),
"progress" to track.last_chapter_read,
"rating" to track.getKitsuScore()
)
)
// @formatter:on
return api.updateLibManga(track.remote_id, jsonObject("data" to data)) return api.updateLibManga(track)
.map { track }
} }
override fun refresh(track: Track): Observable<Track> { override fun refresh(track: Track): Observable<Track> {
return api.getLibManga(track.remote_id) return api.getLibManga(track)
.map { json -> .doOnNext { remoteTrack ->
val data = json["data"].array
if (data.size() > 0) {
val include = json["included"].array[0].obj
val remoteTrack = KitsuLibManga(data[0].obj, include).toTrack()
track.copyPersonalFrom(remoteTrack) track.copyPersonalFrom(remoteTrack)
track.total_chapters = remoteTrack.total_chapters track.total_chapters = remoteTrack.total_chapters
track
} else {
throw Exception("Could not find manga")
} }
} }
}
override fun getStatusList(): List<Int> {
return listOf(READING, COMPLETED, ON_HOLD, DROPPED, PLAN_TO_READ)
}
override fun getStatus(status: Int): String = with(context) {
when (status) {
READING -> getString(R.string.reading)
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)
else -> ""
}
}
private fun Track.getKitsuStatus() = when (status) {
READING -> "current"
COMPLETED -> "completed"
ON_HOLD -> "on_hold"
DROPPED -> "dropped"
PLAN_TO_READ -> "planned"
else -> throw Exception("Unknown status")
}
private fun Track.getKitsuScore(): String {
return if (score > 0) (score / 2).toString() else ""
}
override fun getLogo(): Int {
return R.drawable.kitsu
}
override fun getLogoColor(): Int {
return Color.rgb(51, 37, 50)
}
override fun maxScore(): Int {
return 10
}
override fun formatScore(track: Track): String {
return track.getKitsuScore()
}
} }

@ -1,6 +1,8 @@
package eu.kanade.tachiyomi.data.track.kitsu package eu.kanade.tachiyomi.data.track.kitsu
import com.github.salomonbrys.kotson.*
import com.google.gson.JsonObject import com.google.gson.JsonObject
import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.network.POST import eu.kanade.tachiyomi.data.network.POST
import okhttp3.FormBody import okhttp3.FormBody
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
@ -10,53 +12,123 @@ import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.http.* import retrofit2.http.*
import rx.Observable import rx.Observable
interface KitsuApi { class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor) {
companion object { private val rest = Retrofit.Builder()
private const val clientId = "dd031b32d2f56c990b1425efe6c42ad847e7fe3ab46bf1299f05ecd856bdb7dd"
private const val clientSecret = "54d7307928f63414defd96399fc31ba847961ceaecef3a5fd93144e960c0e151"
private const val baseUrl = "https://kitsu.io/api/edge/"
private const val loginUrl = "https://kitsu.io/api/"
fun createService(client: OkHttpClient) = Retrofit.Builder()
.baseUrl(baseUrl) .baseUrl(baseUrl)
.client(client) .client(client.newBuilder().addInterceptor(interceptor).build())
.addConverterFactory(GsonConverterFactory.create()) .addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJavaCallAdapterFactory.create()) .addCallAdapterFactory(RxJavaCallAdapterFactory.create())
.build() .build()
.create(KitsuApi::class.java) .create(KitsuApi.Rest::class.java)
fun createLoginService(client: OkHttpClient) = Retrofit.Builder() fun login(username: String, password: String): Observable<OAuth> {
return Retrofit.Builder()
.baseUrl(loginUrl) .baseUrl(loginUrl)
.client(client) .client(client)
.addConverterFactory(GsonConverterFactory.create()) .addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJavaCallAdapterFactory.create()) .addCallAdapterFactory(RxJavaCallAdapterFactory.create())
.build() .build()
.create(KitsuApi::class.java) .create(KitsuApi.LoginRest::class.java)
.requestAccessToken(username, password)
}
fun refreshTokenRequest(token: String) = POST("${loginUrl}oauth/token", fun getCurrentUser(): Observable<String> {
body = FormBody.Builder() return rest.getCurrentUser().map { it["data"].array[0]["id"].string }
.add("grant_type", "refresh_token")
.add("client_id", clientId)
.add("client_secret", clientSecret)
.add("refresh_token", token)
.build())
} }
@FormUrlEncoded fun search(query: String): Observable<List<Track>> {
@POST("oauth/token") return rest.search(query)
fun requestAccessToken( .map { json ->
@Field("username") username: String, val data = json["data"].array
@Field("password") password: String, data.map { KitsuManga(it.obj).toTrack() }
@Field("grant_type") grantType: String = "password", }
@Field("client_id") client_id: String = clientId, }
@Field("client_secret") client_secret: String = clientSecret
) : Observable<OAuth> fun findLibManga(userId: String, remoteId: Int): Observable<Track?> {
return rest.findLibManga(userId, remoteId)
.map { json ->
val data = json["data"].array
if (data.size() > 0) {
KitsuLibManga(data[0].obj, json["included"].array[0].obj).toTrack()
} else {
null
}
}
}
fun addLibManga(track: Track, userId: String): Observable<Track> {
return Observable.defer {
// @formatter:off
val data = jsonObject(
"type" to "libraryEntries",
"attributes" to jsonObject(
"status" to track.toKitsuStatus(),
"progress" to track.last_chapter_read
),
"relationships" to jsonObject(
"user" to jsonObject(
"data" to jsonObject(
"id" to userId,
"type" to "users"
)
),
"media" to jsonObject(
"data" to jsonObject(
"id" to track.remote_id,
"type" to "manga"
)
)
)
)
// @formatter:on
rest.addLibManga(jsonObject("data" to data))
.map { json ->
track.remote_id = json["data"]["id"].int
track
}
}
}
fun updateLibManga(track: Track): Observable<Track> {
return Observable.defer {
// @formatter:off
val data = jsonObject(
"type" to "libraryEntries",
"id" to track.remote_id,
"attributes" to jsonObject(
"status" to track.toKitsuStatus(),
"progress" to track.last_chapter_read,
"rating" to track.toKitsuScore()
)
)
// @formatter:on
rest.updateLibManga(track.remote_id, jsonObject("data" to data))
.map { track }
}
}
fun getLibManga(track: Track): Observable<Track> {
return rest.getLibManga(track.remote_id)
.map { json ->
val data = json["data"].array
if (data.size() > 0) {
val include = json["included"].array[0].obj
KitsuLibManga(data[0].obj, include).toTrack()
} else {
throw Exception("Could not find manga")
}
}
}
private interface Rest {
@GET("users") @GET("users")
fun getCurrentUser( fun getCurrentUser(
@Query("filter[self]", encoded = true) self: Boolean = true @Query("filter[self]", encoded = true) self: Boolean = true
) : Observable<JsonObject> ): Observable<JsonObject>
@GET("manga") @GET("manga")
fun search( fun search(
@ -67,7 +139,7 @@ interface KitsuApi {
fun getLibManga( fun getLibManga(
@Query("filter[id]", encoded = true) remoteId: Int, @Query("filter[id]", encoded = true) remoteId: Int,
@Query("include") includes: String = "media" @Query("include") includes: String = "media"
) : Observable<JsonObject> ): Observable<JsonObject>
@GET("library-entries") @GET("library-entries")
fun findLibManga( fun findLibManga(
@ -75,19 +147,52 @@ interface KitsuApi {
@Query("filter[media_id]", encoded = true) remoteId: Int, @Query("filter[media_id]", encoded = true) remoteId: Int,
@Query("page[limit]", encoded = true) limit: Int = 10000, @Query("page[limit]", encoded = true) limit: Int = 10000,
@Query("include") includes: String = "media" @Query("include") includes: String = "media"
) : Observable<JsonObject> ): Observable<JsonObject>
@Headers("Content-Type: application/vnd.api+json") @Headers("Content-Type: application/vnd.api+json")
@POST("library-entries") @POST("library-entries")
fun addLibManga( fun addLibManga(
@Body data: JsonObject @Body data: JsonObject
) : Observable<JsonObject> ): Observable<JsonObject>
@Headers("Content-Type: application/vnd.api+json") @Headers("Content-Type: application/vnd.api+json")
@PATCH("library-entries/{id}") @PATCH("library-entries/{id}")
fun updateLibManga( fun updateLibManga(
@Path("id") remoteId: Int, @Path("id") remoteId: Int,
@Body data: JsonObject @Body data: JsonObject
) : Observable<JsonObject> ): Observable<JsonObject>
}
private interface LoginRest {
@FormUrlEncoded
@POST("oauth/token")
fun requestAccessToken(
@Field("username") username: String,
@Field("password") password: String,
@Field("grant_type") grantType: String = "password",
@Field("client_id") client_id: String = clientId,
@Field("client_secret") client_secret: String = clientSecret
): Observable<OAuth>
}
companion object {
private const val clientId = "dd031b32d2f56c990b1425efe6c42ad847e7fe3ab46bf1299f05ecd856bdb7dd"
private const val clientSecret = "54d7307928f63414defd96399fc31ba847961ceaecef3a5fd93144e960c0e151"
private const val baseUrl = "https://kitsu.io/api/edge/"
private const val loginUrl = "https://kitsu.io/api/"
fun refreshTokenRequest(token: String) = POST("${loginUrl}oauth/token",
body = FormBody.Builder()
.add("grant_type", "refresh_token")
.add("client_id", clientId)
.add("client_secret", clientSecret)
.add("refresh_token", token)
.build())
}
} }

@ -42,3 +42,16 @@ class KitsuLibManga(obj: JsonObject, manga: JsonObject) : KitsuManga(manga) {
} }
} }
fun Track.toKitsuStatus() = when (status) {
Kitsu.READING -> "current"
Kitsu.COMPLETED -> "completed"
Kitsu.ON_HOLD -> "on_hold"
Kitsu.DROPPED -> "dropped"
Kitsu.PLAN_TO_READ -> "planned"
else -> throw Exception("Unknown status")
}
fun Track.toKitsuScore(): String {
return if (score > 0) (score / 2).toString() else ""
}

@ -62,9 +62,26 @@ class MyAnimeList(private val context: Context, id: Int) : TrackService(id) {
override fun getLogoColor() = Color.rgb(46, 81, 162) override fun getLogoColor() = Color.rgb(46, 81, 162)
override fun maxScore() = 10 override fun getStatus(status: Int): String = with(context) {
when (status) {
READING -> getString(R.string.reading)
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)
else -> ""
}
}
override fun getStatusList(): List<Int> {
return listOf(READING, COMPLETED, ON_HOLD, DROPPED, PLAN_TO_READ)
}
override fun getScoreList(): List<String> {
return IntRange(0, 10).map(Int::toString)
}
override fun formatScore(track: Track): String { override fun displayScore(track: Track): String {
return track.score.toInt().toString() return track.score.toInt().toString()
} }
@ -238,21 +255,6 @@ class MyAnimeList(private val context: Context, id: Int) : TrackService(id) {
} }
} }
override fun getStatus(status: Int): String = with(context) {
when (status) {
READING -> getString(R.string.reading)
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)
else -> ""
}
}
override fun getStatusList(): List<Int> {
return listOf(READING, COMPLETED, ON_HOLD, DROPPED, PLAN_TO_READ)
}
fun createHeaders(username: String, password: String) { fun createHeaders(username: String, password: String) {
val builder = Headers.Builder() val builder = Headers.Builder()
builder.add("Authorization", Credentials.basic(username, password)) builder.add("Authorization", Credentials.basic(username, password))

@ -157,9 +157,16 @@ class TrackFragment : BaseRxFragment<TrackPresenter>() {
val view = dialog.customView val view = dialog.customView
if (view != null) { if (view != null) {
val np = view.findViewById(R.id.score_picker) as NumberPicker val np = view.findViewById(R.id.score_picker) as NumberPicker
np.maxValue = item.service.maxScore() val scores = item.service.getScoreList().toTypedArray()
np.maxValue = scores.size - 1
np.displayedValues = scores
// Set initial value // Set initial value
np.value = item.track.score.toInt() val displayedScore = item.service.displayScore(item.track)
if (displayedScore != "-") {
val index = scores.indexOf(displayedScore)
np.value = if (index != -1) index else 0
}
} }
} }

@ -30,7 +30,7 @@ class TrackHolder(private val view: View, private val fragment: TrackFragment)
track_chapters.text = "${track.last_chapter_read}/" + track_chapters.text = "${track.last_chapter_read}/" +
if (track.total_chapters > 0) track.total_chapters else "-" if (track.total_chapters > 0) track.total_chapters else "-"
track_status.text = item.service.getStatus(track.status) track_status.text = item.service.getStatus(track.status)
track_score.text = if (track.score == 0f) "-" else item.service.formatScore(track) track_score.text = if (track.score == 0f) "-" else item.service.displayScore(track)
} else { } else {
track_title.setTextAppearance(context, R.style.TextAppearance_Medium_Button) track_title.setTextAppearance(context, R.style.TextAppearance_Medium_Button)
track_title.setText(R.string.action_edit) track_title.setText(R.string.action_edit)

@ -122,9 +122,9 @@ class TrackPresenter : BasePresenter<TrackFragment>() {
updateRemote(track, item.service) updateRemote(track, item.service)
} }
fun setScore(item: TrackItem, score: Int) { fun setScore(item: TrackItem, index: Int) {
val track = item.track!! val track = item.track!!
track.score = score.toFloat() track.score = item.service.indexToScore(index)
updateRemote(track, item.service) updateRemote(track, item.service)
} }

@ -10,6 +10,7 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center" android:layout_gravity="center"
android:descendantFocusability="blocksDescendants"
app:max="10" app:max="10"
app:min="0"/> app:min="0"/>

Loading…
Cancel
Save