Hide API implementation from MAL service. Reorder methods and minor changes

This commit is contained in:
len 2016-12-22 21:17:47 +01:00
parent ba428c401d
commit 725ceab00b
9 changed files with 424 additions and 392 deletions

View File

@ -3,7 +3,7 @@ package eu.kanade.tachiyomi.data.track
import android.content.Context import android.content.Context
import eu.kanade.tachiyomi.data.track.anilist.Anilist import eu.kanade.tachiyomi.data.track.anilist.Anilist
import eu.kanade.tachiyomi.data.track.kitsu.Kitsu import eu.kanade.tachiyomi.data.track.kitsu.Kitsu
import eu.kanade.tachiyomi.data.track.myanimelist.MyAnimeList import eu.kanade.tachiyomi.data.track.myanimelist.Myanimelist
class TrackManager(private val context: Context) { class TrackManager(private val context: Context) {
@ -13,7 +13,7 @@ class TrackManager(private val context: Context) {
const val KITSU = 3 const val KITSU = 3
} }
val myAnimeList = MyAnimeList(context, MYANIMELIST) val myAnimeList = Myanimelist(context, MYANIMELIST)
val aniList = Anilist(context, ANILIST) val aniList = Anilist(context, ANILIST)

View File

@ -38,12 +38,6 @@ abstract class TrackService(val id: Int) {
abstract fun displayScore(track: Track): String abstract fun displayScore(track: Track): String
abstract fun login(username: String, password: String): Completable
open val isLogged: Boolean
get() = !getUsername().isEmpty() &&
!getPassword().isEmpty()
abstract fun add(track: Track): Observable<Track> abstract fun add(track: Track): Observable<Track>
abstract fun update(track: Track): Observable<Track> abstract fun update(track: Track): Observable<Track>
@ -54,17 +48,23 @@ abstract class TrackService(val id: Int) {
abstract fun refresh(track: Track): Observable<Track> abstract fun refresh(track: Track): Observable<Track>
fun saveCredentials(username: String, password: String) { abstract fun login(username: String, password: String): Completable
preferences.setTrackCredentials(this, username, password)
}
@CallSuper @CallSuper
open fun logout() { open fun logout() {
preferences.setTrackCredentials(this, "", "") preferences.setTrackCredentials(this, "", "")
} }
open val isLogged: Boolean
get() = !getUsername().isEmpty() &&
!getPassword().isEmpty()
fun getUsername() = preferences.trackUsername(this) fun getUsername() = preferences.trackUsername(this)
fun getPassword() = preferences.trackPassword(this) fun getPassword() = preferences.trackPassword(this)
fun saveCredentials(username: String, password: String) {
preferences.setTrackCredentials(this, username, password)
}
} }

View File

@ -93,6 +93,46 @@ class Anilist(private val context: Context, id: Int) : TrackService(id) {
} }
} }
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(track, getUsername())
.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 search(query: String): Observable<List<Track>> {
return api.search(query)
}
override fun refresh(track: Track): Observable<Track> {
return api.getLibManga(track, getUsername())
.map { remoteTrack ->
track.copyPersonalFrom(remoteTrack)
track.total_chapters = remoteTrack.total_chapters
track
}
}
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 {
@ -116,50 +156,5 @@ class Anilist(private val context: Context, id: Int) : TrackService(id) {
interceptor.setAuth(null) 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

@ -23,22 +23,27 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
.build() .build()
.create(Rest::class.java) .create(Rest::class.java)
private fun restBuilder() = Retrofit.Builder() fun addLibManga(track: Track): Observable<Track> {
.baseUrl(baseUrl) return rest.addLibManga(track.remote_id, track.last_chapter_read, track.toAnilistStatus())
.addConverterFactory(GsonConverterFactory.create()) .map { response ->
.addCallAdapterFactory(RxJavaCallAdapterFactory.create()) response.body().close()
if (!response.isSuccessful) {
fun login(authCode: String): Observable<OAuth> { throw Exception("Could not add manga")
return restBuilder() }
.client(client) track
.build() }
.create(Rest::class.java)
.requestAccessToken(authCode)
} }
fun getCurrentUser(): Observable<Pair<String, Int>> { fun updateLibManga(track: Track): Observable<Track> {
return rest.getCurrentUser() return rest.updateLibManga(track.remote_id, track.last_chapter_read, track.toAnilistStatus(),
.map { it["id"].string to it["score_type"].int } track.toAnilistScore())
.map { response ->
response.body().close()
if (!response.isSuccessful) {
throw Exception("Could not update manga")
}
track
}
} }
fun search(query: String): Observable<List<Track>> { fun search(query: String): Observable<List<Track>> {
@ -55,27 +60,35 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
} }
} }
fun addLibManga(track: Track): Observable<Track> { fun findLibManga(track: Track, username: String) : 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 // TODO avoid getting the entire list
return getList(username) return getList(username)
.map { list -> list.find { it.remote_id == track.remote_id } } .map { list -> list.find { it.remote_id == track.remote_id } }
} }
fun getLibManga(track: Track, username: String): Observable<Track> {
return findLibManga(track, username)
.map { it ?: throw Exception("Could not find manga") }
}
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 }
}
private fun restBuilder() = Retrofit.Builder()
.baseUrl(baseUrl)
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJavaCallAdapterFactory.create())
private interface Rest { private interface Rest {
@FormUrlEncoded @FormUrlEncoded

View File

@ -1,7 +1,6 @@
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.OAuth
import okhttp3.Interceptor import okhttp3.Interceptor
import okhttp3.Response import okhttp3.Response

View File

@ -62,6 +62,60 @@ class Kitsu(private val context: Context, id: Int) : TrackService(id) {
return track.toKitsuScore() return track.toKitsuScore()
} }
override fun add(track: Track): Observable<Track> {
return api.addLibManga(track, getUserId())
}
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(track, getUserId())
.flatMap { remoteTrack ->
if (remoteTrack != null) {
track.copyPersonalFrom(remoteTrack)
track.remote_id = remoteTrack.remote_id
update(track)
} else {
track.score = DEFAULT_SCORE
track.status = DEFAULT_STATUS
add(track)
}
}
}
override fun search(query: String): Observable<List<Track>> {
return api.search(query)
}
override fun refresh(track: Track): Observable<Track> {
return api.getLibManga(track)
.map { remoteTrack ->
track.copyPersonalFrom(remoteTrack)
track.total_chapters = remoteTrack.total_chapters
track
}
}
override fun login(username: String, password: String): Completable {
return api.login(username, password)
.doOnNext { interceptor.newAuth(it) }
.flatMap { api.getCurrentUser() }
.doOnNext { userId -> saveCredentials(username, userId) }
.doOnError { logout() }
.toCompletable()
}
override fun logout() {
super.logout()
interceptor.newAuth(null)
}
private fun getUserId(): String { private fun getUserId(): String {
return getPassword() return getPassword()
} }
@ -79,62 +133,4 @@ class Kitsu(private val context: Context, id: Int) : TrackService(id) {
} }
} }
override fun login(username: String, password: String): Completable {
return api.login(username, password)
.doOnNext { interceptor.newAuth(it) }
.flatMap { api.getCurrentUser() }
.doOnNext { userId -> saveCredentials(username, userId) }
.doOnError { logout() }
.toCompletable()
}
override fun logout() {
super.logout()
interceptor.newAuth(null)
}
override fun search(query: String): Observable<List<Track>> {
return api.search(query)
}
override fun bind(track: Track): Observable<Track> {
return find(track)
.flatMap { remoteTrack ->
if (remoteTrack != null) {
track.copyPersonalFrom(remoteTrack)
track.remote_id = remoteTrack.remote_id
update(track)
} else {
track.score = DEFAULT_SCORE
track.status = DEFAULT_STATUS
add(track)
}
}
}
private fun find(track: Track): Observable<Track?> {
return api.findLibManga(getUserId(), track.remote_id)
}
override fun add(track: Track): Observable<Track> {
return api.addLibManga(track, getUserId())
}
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 refresh(track: Track): Observable<Track> {
return api.getLibManga(track)
.map { remoteTrack ->
track.copyPersonalFrom(remoteTrack)
track.total_chapters = remoteTrack.total_chapters
track
}
}
} }

View File

@ -22,41 +22,6 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
.build() .build()
.create(KitsuApi.Rest::class.java) .create(KitsuApi.Rest::class.java)
fun login(username: String, password: String): Observable<OAuth> {
return Retrofit.Builder()
.baseUrl(loginUrl)
.client(client)
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJavaCallAdapterFactory.create())
.build()
.create(KitsuApi.LoginRest::class.java)
.requestAccessToken(username, password)
}
fun getCurrentUser(): Observable<String> {
return rest.getCurrentUser().map { it["data"].array[0]["id"].string }
}
fun search(query: String): Observable<List<Track>> {
return rest.search(query)
.map { json ->
val data = json["data"].array
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> { fun addLibManga(track: Track, userId: String): Observable<Track> {
return Observable.defer { return Observable.defer {
// @formatter:off // @formatter:off
@ -110,6 +75,26 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
} }
} }
fun search(query: String): Observable<List<Track>> {
return rest.search(query)
.map { json ->
val data = json["data"].array
data.map { KitsuManga(it.obj).toTrack() }
}
}
fun findLibManga(track: Track, userId: String): Observable<Track?> {
return rest.findLibManga(track.remote_id, userId)
.map { json ->
val data = json["data"].array
if (data.size() > 0) {
KitsuLibManga(data[0].obj, json["included"].array[0].obj).toTrack()
} else {
null
}
}
}
fun getLibManga(track: Track): Observable<Track> { fun getLibManga(track: Track): Observable<Track> {
return rest.getLibManga(track.remote_id) return rest.getLibManga(track.remote_id)
.map { json -> .map { json ->
@ -123,32 +108,23 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
} }
} }
fun login(username: String, password: String): Observable<OAuth> {
return Retrofit.Builder()
.baseUrl(loginUrl)
.client(client)
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJavaCallAdapterFactory.create())
.build()
.create(KitsuApi.LoginRest::class.java)
.requestAccessToken(username, password)
}
fun getCurrentUser(): Observable<String> {
return rest.getCurrentUser().map { it["data"].array[0]["id"].string }
}
private interface Rest { private interface Rest {
@GET("users")
fun getCurrentUser(
@Query("filter[self]", encoded = true) self: Boolean = true
): Observable<JsonObject>
@GET("manga")
fun search(
@Query("filter[text]", encoded = true) query: String
): Observable<JsonObject>
@GET("library-entries")
fun getLibManga(
@Query("filter[id]", encoded = true) remoteId: Int,
@Query("include") includes: String = "media"
): Observable<JsonObject>
@GET("library-entries")
fun findLibManga(
@Query("filter[user_id]", encoded = true) userId: String,
@Query("filter[media_id]", encoded = true) remoteId: Int,
@Query("page[limit]", encoded = true) limit: Int = 10000,
@Query("include") includes: String = "media"
): 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(
@ -162,6 +138,30 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
@Body data: JsonObject @Body data: JsonObject
): Observable<JsonObject> ): Observable<JsonObject>
@GET("manga")
fun search(
@Query("filter[text]", encoded = true) query: String
): Observable<JsonObject>
@GET("library-entries")
fun findLibManga(
@Query("filter[media_id]", encoded = true) remoteId: Int,
@Query("filter[user_id]", encoded = true) userId: String,
@Query("page[limit]", encoded = true) limit: Int = 10000,
@Query("include") includes: String = "media"
): Observable<JsonObject>
@GET("library-entries")
fun getLibManga(
@Query("filter[id]", encoded = true) remoteId: Int,
@Query("include") includes: String = "media"
): Observable<JsonObject>
@GET("users")
fun getCurrentUser(
@Query("filter[self]", encoded = true) self: Boolean = true
): Observable<JsonObject>
} }
private interface LoginRest { private interface LoginRest {

View File

@ -2,37 +2,15 @@ package eu.kanade.tachiyomi.data.track.myanimelist
import android.content.Context import android.content.Context
import android.graphics.Color import android.graphics.Color
import android.net.Uri
import android.util.Xml
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.network.GET
import eu.kanade.tachiyomi.data.network.POST
import eu.kanade.tachiyomi.data.network.asObservable
import eu.kanade.tachiyomi.data.track.TrackService import eu.kanade.tachiyomi.data.track.TrackService
import eu.kanade.tachiyomi.util.selectInt
import eu.kanade.tachiyomi.util.selectText
import okhttp3.Credentials
import okhttp3.FormBody
import okhttp3.Headers
import okhttp3.RequestBody
import org.jsoup.Jsoup
import org.xmlpull.v1.XmlSerializer
import rx.Completable import rx.Completable
import rx.Observable import rx.Observable
import java.io.StringWriter
class MyAnimeList(private val context: Context, id: Int) : TrackService(id) { class Myanimelist(private val context: Context, id: Int) : TrackService(id) {
private lateinit var headers: Headers
companion object { companion object {
const val BASE_URL = "https://myanimelist.net"
private val ENTRY_TAG = "entry"
private val CHAPTER_TAG = "chapter"
private val SCORE_TAG = "score"
private val STATUS_TAG = "status"
const val READING = 1 const val READING = 1
const val COMPLETED = 2 const val COMPLETED = 2
@ -42,18 +20,9 @@ class MyAnimeList(private val context: Context, id: Int) : TrackService(id) {
const val DEFAULT_STATUS = READING const val DEFAULT_STATUS = READING
const val DEFAULT_SCORE = 0 const val DEFAULT_SCORE = 0
const val PREFIX_MY = "my:"
} }
init { private val api by lazy { MyanimelistApi(client, getUsername(), getPassword()) }
val username = getUsername()
val password = getPassword()
if (!username.isEmpty() && !password.isEmpty()) {
createHeaders(username, password)
}
}
override val name: String override val name: String
get() = "MyAnimeList" get() = "MyAnimeList"
@ -85,164 +54,21 @@ class MyAnimeList(private val context: Context, id: Int) : TrackService(id) {
return track.score.toInt().toString() return track.score.toInt().toString()
} }
fun getLoginUrl() = Uri.parse(BASE_URL).buildUpon() override fun add(track: Track): Observable<Track> {
.appendEncodedPath("api/account/verify_credentials.xml") return api.addLibManga(track)
.toString()
fun getSearchUrl(query: String) = Uri.parse(BASE_URL).buildUpon()
.appendEncodedPath("api/manga/search.xml")
.appendQueryParameter("q", query)
.toString()
fun getListUrl(username: String) = Uri.parse(BASE_URL).buildUpon()
.appendPath("malappinfo.php")
.appendQueryParameter("u", username)
.appendQueryParameter("status", "all")
.appendQueryParameter("type", "manga")
.toString()
fun getUpdateUrl(track: Track) = Uri.parse(BASE_URL).buildUpon()
.appendEncodedPath("api/mangalist/update")
.appendPath("${track.remote_id}.xml")
.toString()
fun getAddUrl(track: Track) = Uri.parse(BASE_URL).buildUpon()
.appendEncodedPath("api/mangalist/add")
.appendPath("${track.remote_id}.xml")
.toString()
override fun login(username: String, password: String): Completable {
createHeaders(username, password)
return client.newCall(GET(getLoginUrl(), headers))
.asObservable()
.doOnNext { it.close() }
.doOnNext { if (it.code() != 200) throw Exception("Login error") }
.doOnNext { saveCredentials(username, password) }
.doOnError { logout() }
.toCompletable()
}
override fun search(query: String): Observable<List<Track>> {
return if (query.startsWith(PREFIX_MY)) {
val realQuery = query.substring(PREFIX_MY.length).toLowerCase().trim()
getList()
.flatMap { Observable.from(it) }
.filter { realQuery in it.title.toLowerCase() }
.toList()
} else {
client.newCall(GET(getSearchUrl(query), headers))
.asObservable()
.map { Jsoup.parse(it.body().string()) }
.flatMap { Observable.from(it.select("entry")) }
.filter { it.select("type").text() != "Novel" }
.map {
Track.create(id).apply {
title = it.selectText("title")!!
remote_id = it.selectInt("id")
total_chapters = it.selectInt("chapters")
}
}
.toList()
}
}
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")
}
}
}
// MAL doesn't support score with decimals
fun getList(): Observable<List<Track>> {
return networkService.forceCacheClient
.newCall(GET(getListUrl(getUsername()), headers))
.asObservable()
.map { Jsoup.parse(it.body().string()) }
.flatMap { Observable.from(it.select("manga")) }
.map {
Track.create(id).apply {
title = it.selectText("series_title")!!
remote_id = it.selectInt("series_mangadb_id")
last_chapter_read = it.selectInt("my_read_chapters")
status = it.selectInt("my_status")
score = it.selectInt("my_score").toFloat()
total_chapters = it.selectInt("series_chapters")
}
}
.toList()
} }
override fun update(track: Track): Observable<Track> { override fun update(track: Track): Observable<Track> {
return Observable.defer { 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
}
client.newCall(POST(getUpdateUrl(track), headers, getMangaPostPayload(track)))
.asObservable()
.doOnNext { it.close() }
.doOnNext { if (!it.isSuccessful) throw Exception("Could not update manga") }
.map { track }
} }
} return api.updateLibManga(track)
override fun add(track: Track): Observable<Track> {
return Observable.defer {
client.newCall(POST(getAddUrl(track), headers, getMangaPostPayload(track)))
.asObservable()
.doOnNext { it.close() }
.doOnNext { if (!it.isSuccessful) throw Exception("Could not add manga") }
.map { track }
}
}
private fun getMangaPostPayload(track: Track): RequestBody {
val xml = Xml.newSerializer()
val writer = StringWriter()
with(xml) {
setOutput(writer)
startDocument("UTF-8", false)
startTag("", ENTRY_TAG)
// Last chapter read
if (track.last_chapter_read != 0) {
inTag(CHAPTER_TAG, track.last_chapter_read.toString())
}
// Manga status in the list
inTag(STATUS_TAG, track.status.toString())
// Manga score
inTag(SCORE_TAG, track.score.toString())
endTag("", ENTRY_TAG)
endDocument()
}
val form = FormBody.Builder()
form.add("data", writer.toString())
return form.build()
}
fun XmlSerializer.inTag(tag: String, body: String, namespace: String = "") {
startTag(namespace, tag)
text(body)
endTag(namespace, tag)
} }
override fun bind(track: Track): Observable<Track> { override fun bind(track: Track): Observable<Track> {
return getList() return api.findLibManga(track, getUsername())
.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)
@ -255,11 +81,24 @@ class MyAnimeList(private val context: Context, id: Int) : TrackService(id) {
} }
} }
fun createHeaders(username: String, password: String) { override fun search(query: String): Observable<List<Track>> {
val builder = Headers.Builder() return api.search(query, getUsername())
builder.add("Authorization", Credentials.basic(username, password)) }
builder.add("User-Agent", "api-indiv-9F93C52A963974CF674325391990191C")
headers = builder.build() override fun refresh(track: Track): Observable<Track> {
return api.getLibManga(track, getUsername())
.map { remoteTrack ->
track.copyPersonalFrom(remoteTrack)
track.total_chapters = remoteTrack.total_chapters
track
}
}
override fun login(username: String, password: String): Completable {
return api.login(username, password)
.doOnNext { saveCredentials(username, password) }
.doOnError { logout() }
.toCompletable()
} }
} }

View File

@ -0,0 +1,190 @@
package eu.kanade.tachiyomi.data.track.myanimelist
import android.net.Uri
import android.util.Xml
import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.network.GET
import eu.kanade.tachiyomi.data.network.POST
import eu.kanade.tachiyomi.data.network.asObservable
import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.util.selectInt
import eu.kanade.tachiyomi.util.selectText
import okhttp3.*
import org.jsoup.Jsoup
import org.xmlpull.v1.XmlSerializer
import rx.Observable
import java.io.StringWriter
class MyanimelistApi(private val client: OkHttpClient, username: String, password: String) {
private var headers = createHeaders(username, password)
fun addLibManga(track: Track): Observable<Track> {
return Observable.defer {
client.newCall(POST(getAddUrl(track), headers, getMangaPostPayload(track)))
.asObservable()
.map { response ->
response.body().close()
if (!response.isSuccessful) {
throw Exception("Could not add manga")
}
track
}
}
}
fun updateLibManga(track: Track): Observable<Track> {
return Observable.defer {
client.newCall(POST(getUpdateUrl(track), headers, getMangaPostPayload(track)))
.asObservable()
.map { response ->
response.body().close()
if (!response.isSuccessful) {
throw Exception("Could not update manga")
}
track
}
}
}
fun search(query: String, username: String): Observable<List<Track>> {
return if (query.startsWith(PREFIX_MY)) {
val realQuery = query.substring(PREFIX_MY.length).toLowerCase().trim()
getList(username)
.flatMap { Observable.from(it) }
.filter { realQuery in it.title.toLowerCase() }
.toList()
} else {
client.newCall(GET(getSearchUrl(query), headers))
.asObservable()
.map { Jsoup.parse(it.body().string()) }
.flatMap { Observable.from(it.select("entry")) }
.filter { it.select("type").text() != "Novel" }
.map {
Track.create(TrackManager.MYANIMELIST).apply {
title = it.selectText("title")!!
remote_id = it.selectInt("id")
total_chapters = it.selectInt("chapters")
}
}
.toList()
}
}
fun getList(username: String): Observable<List<Track>> {
return client
.newCall(GET(getListUrl(username), headers))
.asObservable()
.map { Jsoup.parse(it.body().string()) }
.flatMap { Observable.from(it.select("manga")) }
.map {
Track.create(TrackManager.MYANIMELIST).apply {
title = it.selectText("series_title")!!
remote_id = it.selectInt("series_mangadb_id")
last_chapter_read = it.selectInt("my_read_chapters")
status = it.selectInt("my_status")
score = it.selectInt("my_score").toFloat()
total_chapters = it.selectInt("series_chapters")
}
}
.toList()
}
fun findLibManga(track: Track, username: String): Observable<Track?> {
return getList(username)
.map { list -> list.find { it.remote_id == track.remote_id } }
}
fun getLibManga(track: Track, username: String): Observable<Track> {
return findLibManga(track, username)
.map { it ?: throw Exception("Could not find manga") }
}
fun login(username: String, password: String): Observable<Response> {
headers = createHeaders(username, password)
return client.newCall(GET(getLoginUrl(), headers))
.asObservable()
.doOnNext { response ->
response.close()
if (response.code() != 200) throw Exception("Login error")
}
}
private fun getMangaPostPayload(track: Track): RequestBody {
val xml = Xml.newSerializer()
val writer = StringWriter()
with(xml) {
setOutput(writer)
startDocument("UTF-8", false)
startTag("", ENTRY_TAG)
// Last chapter read
if (track.last_chapter_read != 0) {
inTag(CHAPTER_TAG, track.last_chapter_read.toString())
}
// Manga status in the list
inTag(STATUS_TAG, track.status.toString())
// Manga score
inTag(SCORE_TAG, track.score.toString())
endTag("", ENTRY_TAG)
endDocument()
}
val form = FormBody.Builder()
form.add("data", writer.toString())
return form.build()
}
fun XmlSerializer.inTag(tag: String, body: String, namespace: String = "") {
startTag(namespace, tag)
text(body)
endTag(namespace, tag)
}
fun getLoginUrl() = Uri.parse(baseUrl).buildUpon()
.appendEncodedPath("api/account/verify_credentials.xml")
.toString()
fun getSearchUrl(query: String) = Uri.parse(baseUrl).buildUpon()
.appendEncodedPath("api/manga/search.xml")
.appendQueryParameter("q", query)
.toString()
fun getListUrl(username: String) = Uri.parse(baseUrl).buildUpon()
.appendPath("malappinfo.php")
.appendQueryParameter("u", username)
.appendQueryParameter("status", "all")
.appendQueryParameter("type", "manga")
.toString()
fun getUpdateUrl(track: Track) = Uri.parse(baseUrl).buildUpon()
.appendEncodedPath("api/mangalist/update")
.appendPath("${track.remote_id}.xml")
.toString()
fun getAddUrl(track: Track) = Uri.parse(baseUrl).buildUpon()
.appendEncodedPath("api/mangalist/add")
.appendPath("${track.remote_id}.xml")
.toString()
fun createHeaders(username: String, password: String): Headers {
return Headers.Builder()
.add("Authorization", Credentials.basic(username, password))
.add("User-Agent", "api-indiv-9F93C52A963974CF674325391990191C")
.build()
}
companion object {
const val baseUrl = "https://myanimelist.net"
private val ENTRY_TAG = "entry"
private val CHAPTER_TAG = "chapter"
private val SCORE_TAG = "score"
private val STATUS_TAG = "status"
const val PREFIX_MY = "my:"
}
}