Score formatting. Hide API from Anilist/Kitsu services.

This commit is contained in:
len 2016-12-21 22:39:46 +01:00
parent 091c0c0c71
commit 8d749df290
17 changed files with 573 additions and 455 deletions

View file

@ -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)
} }

View file

@ -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,110 +26,12 @@ 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 login(username: String, password: String) = login(password)
fun login(authCode: String): Completable {
// Create a new api with the default client to avoid request interceptions.
return AnilistApi.createService(client)
// Request the access token from the API with the authorization code.
.requestAccessToken(authCode)
// Save the token in the interceptor.
.doOnNext { interceptor.setAuth(it) }
// Obtain the authenticated user from the API.
.zipWith(api.getCurrentUser().map {
preferences.anilistScoreType().set(it["score_type"].int)
it["id"].string
}, { oauth, user -> Pair(user, oauth.refresh_token!!) })
// Save service credentials (username and refresh token).
.doOnNext { saveCredentials(it.first, it.second) }
// Logout on any error.
.doOnError { logout() }
.toCompletable()
}
override fun logout() {
super.logout()
interceptor.setAuth(null)
}
override fun search(query: String): Observable<List<Track>> {
return api.search(query, 1)
.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> {
return api.addManga(track.remote_id, track.last_chapter_read, track.getAnilistStatus())
.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> {
if (track.total_chapters != 0 && track.last_chapter_read == track.total_chapters) {
track.status = COMPLETED
}
return api.updateManga(track.remote_id, track.last_chapter_read, track.getAnilistStatus(),
track.getAnilistScore())
.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> {
return getList()
.flatMap { userlist ->
track.sync_id = id
val remoteTrack = userlist.find { it.remote_id == track.remote_id }
if (remoteTrack != null) {
track.copyPersonalFrom(remoteTrack)
update(track)
} else {
// Set default fields if it's not found in the list
track.score = DEFAULT_SCORE.toFloat()
track.status = DEFAULT_STATUS
add(track)
}
}
}
override fun refresh(track: Track): Observable<Track> {
return getList()
.map { myList ->
val remoteTrack = myList.find { it.remote_id == track.remote_id }
if (remoteTrack != null) {
track.copyPersonalFrom(remoteTrack)
track.total_chapters = remoteTrack.total_chapters
track
} else {
throw Exception("Could not find manga")
}
}
}
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, PLAN_TO_READ)
} }
@ -148,43 +47,118 @@ class Anilist(private val context: Context, id: Int) : TrackService(id) {
} }
} }
private fun Track.getAnilistStatus() = when (status) { override fun getScoreList(): List<String> {
READING -> "reading" return when (preferences.anilistScoreType().getOrDefault()) {
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 // 10 point
0 -> Math.floor(score.toDouble() / 10).toInt().toString() 0 -> IntRange(0, 10).map(Int::toString)
// 100 point // 100 point
1 -> score.toInt().toString() 1 -> IntRange(0, 100).map(Int::toString)
// 5 stars // 5 stars
2 -> when { 2 -> IntRange(0, 5).map { "$it" }
score == 0f -> "0"
score < 30 -> "1"
score < 50 -> "2"
score < 70 -> "3"
score < 90 -> "4"
else -> "5"
}
// Smiley // Smiley
3 -> when { 3 -> listOf("-", "😦", "😐", "😊")
score == 0f -> "0"
score <= 30 -> ":("
score <= 60 -> ":|"
else -> ":)"
}
// 10 point decimal // 10 point decimal
4 -> (score / 10).toString() 4 -> IntRange(0, 100).map { (it / 10f).toString() }
else -> throw Exception("Unknown score type") else -> throw Exception("Unknown score type")
} }
}
override fun formatScore(track: Track): String { override fun indexToScore(index: Int): Float {
return track.getAnilistScore() 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)
fun login(authCode: String): Completable {
return api.login(authCode)
// Save the token in the interceptor.
.doOnNext { interceptor.setAuth(it) }
// Obtain the authenticated user from the API.
.zipWith(api.getCurrentUser().map { pair ->
preferences.anilistScoreType().set(pair.second)
pair.first
}, { oauth, user -> Pair(user, oauth.refresh_token!!) })
// Save service credentials (username and refresh token).
.doOnNext { saveCredentials(it.first, it.second) }
// Logout on any error.
.doOnError { logout() }
.toCompletable()
}
override fun logout() {
super.logout()
interceptor.setAuth(null)
}
override fun search(query: String): Observable<List<Track>> {
return api.search(query)
}
override fun add(track: Track): Observable<Track> {
return api.addLibManga(track)
}
override fun update(track: Track): Observable<Track> {
if (track.total_chapters != 0 && track.last_chapter_read == track.total_chapters) {
track.status = COMPLETED
}
return api.updateLibManga(track)
}
override fun bind(track: Track): Observable<Track> {
return api.findLibManga(getUsername(), track)
.flatMap { remoteTrack ->
if (remoteTrack != null) {
track.copyPersonalFrom(remoteTrack)
update(track)
} else {
// Set default fields if it's not found in the list
track.score = DEFAULT_SCORE.toFloat()
track.status = DEFAULT_STATUS
add(track)
}
}
}
override fun refresh(track: Track): Observable<Track> {
// TODO getLibManga method?
return api.findLibManga(getUsername(), track)
.map { remoteTrack ->
if (remoteTrack != null) {
track.copyPersonalFrom(remoteTrack)
track.total_chapters = remoteTrack.total_chapters
track
} else {
throw Exception("Could not find manga")
}
}
} }
} }

View file

@ -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,7 +16,110 @@ 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) {
private val rest = restBuilder()
.client(client.newBuilder().addInterceptor(interceptor).build())
.build()
.create(Rest::class.java)
private fun restBuilder() = Retrofit.Builder()
.baseUrl(baseUrl)
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJavaCallAdapterFactory.create())
fun login(authCode: String): Observable<OAuth> {
return restBuilder()
.client(client)
.build()
.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
@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 = "tachiyomi-hrtje"
@ -39,50 +142,6 @@ interface AnilistApi {
.add("refresh_token", token) .add("refresh_token", token)
.build()) .build())
fun createService(client: OkHttpClient) = Retrofit.Builder()
.baseUrl(baseUrl)
.client(client)
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJavaCallAdapterFactory.create())
.build()
.create(AnilistApi::class.java)
} }
@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 getList(@Path("username") username: String): Observable<ALUserLists>
@FormUrlEncoded
@PUT("mangalist")
fun addManga(
@Field("id") id: Int,
@Field("chapters_read") chapters_read: Int,
@Field("list_status") list_status: String)
: Observable<Response<ResponseBody>>
@FormUrlEncoded
@PUT("mangalist")
fun updateManga(
@Field("id") id: Int,
@Field("chapters_read") chapters_read: Int,
@Field("list_status") list_status: String,
@Field("score") score_raw: String)
: Observable<Response<ResponseBody>>
} }

View file

@ -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

View file

@ -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")
}

View file

@ -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,

View file

@ -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
}
}

View file

@ -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()
}

View file

@ -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")
}
}

View file

@ -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()
}
} }

View file

@ -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",
body = FormBody.Builder()
.add("grant_type", "refresh_token")
.add("client_id", clientId)
.add("client_secret", clientSecret)
.add("refresh_token", token)
.build())
} }
@FormUrlEncoded fun getCurrentUser(): Observable<String> {
@POST("oauth/token") return rest.getCurrentUser().map { it["data"].array[0]["id"].string }
fun requestAccessToken( }
@Field("username") username: String,
@Field("password") password: String, fun search(query: String): Observable<List<Track>> {
@Field("grant_type") grantType: String = "password", return rest.search(query)
@Field("client_id") client_id: String = clientId, .map { json ->
@Field("client_secret") client_secret: String = clientSecret val data = json["data"].array
) : Observable<OAuth> data.map { KitsuManga(it.obj).toTrack() }
}
}
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())
}
} }

View file

@ -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 ""
}

View file

@ -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 formatScore(track: Track): String { 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 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))

View file

@ -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
}
} }
} }

View file

@ -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)

View file

@ -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)
} }

View file

@ -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"/>