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 eu.kanade.tachiyomi.data.track.anilist.Anilist
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) {
@ -13,7 +13,7 @@ class TrackManager(private val context: Context) {
const val KITSU = 3
}
val myAnimeList = MyAnimeList(context, MYANIMELIST)
val myAnimeList = Myanimelist(context, MYANIMELIST)
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 login(username: String, password: String): Completable
open val isLogged: Boolean
get() = !getUsername().isEmpty() &&
!getPassword().isEmpty()
abstract fun add(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>
fun saveCredentials(username: String, password: String) {
preferences.setTrackCredentials(this, username, password)
}
abstract fun login(username: String, password: String): Completable
@CallSuper
open fun logout() {
preferences.setTrackCredentials(this, "", "")
}
open val isLogged: Boolean
get() = !getUsername().isEmpty() &&
!getPassword().isEmpty()
fun getUsername() = preferences.trackUsername(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)
fun login(authCode: String): Completable {
@ -116,50 +156,5 @@ class Anilist(private val context: Context, id: Int) : TrackService(id) {
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()
.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 addLibManga(track: Track): Observable<Track> {
return rest.addLibManga(track.remote_id, track.last_chapter_read, track.toAnilistStatus())
.map { response ->
response.body().close()
if (!response.isSuccessful) {
throw Exception("Could not add manga")
}
track
}
}
fun getCurrentUser(): Observable<Pair<String, Int>> {
return rest.getCurrentUser()
.map { it["id"].string to it["score_type"].int }
fun updateLibManga(track: Track): Observable<Track> {
return rest.updateLibManga(track.remote_id, track.last_chapter_read, track.toAnilistStatus(),
track.toAnilistScore())
.map { response ->
response.body().close()
if (!response.isSuccessful) {
throw Exception("Could not update manga")
}
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> {
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?> {
fun findLibManga(track: Track, username: String) : Observable<Track?> {
// TODO avoid getting the entire list
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(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 {
@FormUrlEncoded

View File

@ -1,7 +1,6 @@
package eu.kanade.tachiyomi.data.track.anilist
import com.google.gson.Gson
import eu.kanade.tachiyomi.data.track.anilist.OAuth
import okhttp3.Interceptor
import okhttp3.Response

View File

@ -62,6 +62,60 @@ class Kitsu(private val context: Context, id: Int) : TrackService(id) {
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 {
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()
.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> {
return Observable.defer {
// @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> {
return rest.getLibManga(track.remote_id)
.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 {
@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")
@POST("library-entries")
fun addLibManga(
@ -162,6 +138,30 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
@Body data: 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 {

View File

@ -2,37 +2,15 @@ package eu.kanade.tachiyomi.data.track.myanimelist
import android.content.Context
import android.graphics.Color
import android.net.Uri
import android.util.Xml
import eu.kanade.tachiyomi.R
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.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.Observable
import java.io.StringWriter
class MyAnimeList(private val context: Context, id: Int) : TrackService(id) {
private lateinit var headers: Headers
class Myanimelist(private val context: Context, id: Int) : TrackService(id) {
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 COMPLETED = 2
@ -42,18 +20,9 @@ class MyAnimeList(private val context: Context, id: Int) : TrackService(id) {
const val DEFAULT_STATUS = READING
const val DEFAULT_SCORE = 0
const val PREFIX_MY = "my:"
}
init {
val username = getUsername()
val password = getPassword()
if (!username.isEmpty() && !password.isEmpty()) {
createHeaders(username, password)
}
}
private val api by lazy { MyanimelistApi(client, getUsername(), getPassword()) }
override val name: String
get() = "MyAnimeList"
@ -85,164 +54,21 @@ class MyAnimeList(private val context: Context, id: Int) : TrackService(id) {
return track.score.toInt().toString()
}
fun getLoginUrl() = Uri.parse(BASE_URL).buildUpon()
.appendEncodedPath("api/account/verify_credentials.xml")
.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 add(track: Track): Observable<Track> {
return api.addLibManga(track)
}
override fun update(track: Track): Observable<Track> {
return Observable.defer {
if (track.total_chapters != 0 && track.last_chapter_read == track.total_chapters) {
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 }
if (track.total_chapters != 0 && track.last_chapter_read == track.total_chapters) {
track.status = COMPLETED
}
}
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)
return api.updateLibManga(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 }
return api.findLibManga(track, getUsername())
.flatMap { remoteTrack ->
if (remoteTrack != null) {
track.copyPersonalFrom(remoteTrack)
update(track)
@ -255,11 +81,24 @@ class MyAnimeList(private val context: Context, id: Int) : TrackService(id) {
}
}
fun createHeaders(username: String, password: String) {
val builder = Headers.Builder()
builder.add("Authorization", Credentials.basic(username, password))
builder.add("User-Agent", "api-indiv-9F93C52A963974CF674325391990191C")
headers = builder.build()
override fun search(query: String): Observable<List<Track>> {
return api.search(query, getUsername())
}
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:"
}
}