diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/TrackManager.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/TrackManager.kt index 46f3c99e9b..73fa15c550 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/TrackManager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/TrackManager.kt @@ -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) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/TrackService.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/TrackService.kt index 5e14c66d61..39b5db83fc 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/TrackService.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/TrackService.kt @@ -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 abstract fun update(track: Track): Observable @@ -54,17 +48,23 @@ abstract class TrackService(val id: Int) { abstract fun refresh(track: Track): Observable - 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) + } + } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/Anilist.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/Anilist.kt index 54ca0151c3..25fafc6a5d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/Anilist.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/Anilist.kt @@ -93,6 +93,46 @@ class Anilist(private val context: Context, id: Int) : TrackService(id) { } } + override fun add(track: Track): Observable { + return api.addLibManga(track) + } + + override fun update(track: Track): Observable { + 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 { + 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> { + return api.search(query) + } + + override fun refresh(track: Track): Observable { + 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> { - return api.search(query) - } - - override fun add(track: Track): Observable { - return api.addLibManga(track) - } - - override fun update(track: Track): Observable { - 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 { - 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 { - // 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") - } - } - } - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistApi.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistApi.kt index 29c4551fca..d8ef82d301 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistApi.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistApi.kt @@ -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 { - return restBuilder() - .client(client) - .build() - .create(Rest::class.java) - .requestAccessToken(authCode) + fun addLibManga(track: Track): Observable { + 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> { - return rest.getCurrentUser() - .map { it["id"].string to it["score_type"].int } + fun updateLibManga(track: Track): Observable { + 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> { @@ -55,27 +60,35 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) { } } - fun addLibManga(track: Track): Observable { - 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 { - 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 { + fun findLibManga(track: Track, username: String) : Observable { // 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 { + return findLibManga(track, username) + .map { it ?: throw Exception("Could not find manga") } + } + + fun login(authCode: String): Observable { + return restBuilder() + .client(client) + .build() + .create(Rest::class.java) + .requestAccessToken(authCode) + } + + fun getCurrentUser(): Observable> { + 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 diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistInterceptor.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistInterceptor.kt index a25b7fe79a..7cdc5dde87 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistInterceptor.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistInterceptor.kt @@ -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 diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/Kitsu.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/Kitsu.kt index 51d3643213..d99475cb8d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/Kitsu.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/Kitsu.kt @@ -62,6 +62,60 @@ class Kitsu(private val context: Context, id: Int) : TrackService(id) { return track.toKitsuScore() } + override fun add(track: Track): Observable { + return api.addLibManga(track, getUserId()) + } + + override fun update(track: Track): Observable { + 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 { + 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> { + return api.search(query) + } + + override fun refresh(track: Track): Observable { + 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> { - return api.search(query) - } - - override fun bind(track: Track): Observable { - 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 { - return api.findLibManga(getUserId(), track.remote_id) - } - - override fun add(track: Track): Observable { - return api.addLibManga(track, getUserId()) - } - - override fun update(track: Track): Observable { - 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 { - return api.getLibManga(track) - .map { remoteTrack -> - track.copyPersonalFrom(remoteTrack) - track.total_chapters = remoteTrack.total_chapters - track - } - } - } \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuApi.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuApi.kt index ce5510818d..45d962d469 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuApi.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuApi.kt @@ -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 { - 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 { - return rest.getCurrentUser().map { it["data"].array[0]["id"].string } - } - - fun search(query: String): Observable> { - return rest.search(query) - .map { json -> - val data = json["data"].array - data.map { KitsuManga(it.obj).toTrack() } - } - } - - fun findLibManga(userId: String, remoteId: Int): Observable { - 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 { return Observable.defer { // @formatter:off @@ -110,6 +75,26 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor) } } + fun search(query: String): Observable> { + return rest.search(query) + .map { json -> + val data = json["data"].array + data.map { KitsuManga(it.obj).toTrack() } + } + } + + fun findLibManga(track: Track, userId: String): Observable { + 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 { 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 { + 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 { + 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 - - @GET("manga") - fun search( - @Query("filter[text]", encoded = true) query: String - ): Observable - - @GET("library-entries") - fun getLibManga( - @Query("filter[id]", encoded = true) remoteId: Int, - @Query("include") includes: String = "media" - ): Observable - - @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 - @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 + @GET("manga") + fun search( + @Query("filter[text]", encoded = true) query: String + ): Observable + + @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 + + @GET("library-entries") + fun getLibManga( + @Query("filter[id]", encoded = true) remoteId: Int, + @Query("include") includes: String = "media" + ): Observable + + @GET("users") + fun getCurrentUser( + @Query("filter[self]", encoded = true) self: Boolean = true + ): Observable + } private interface LoginRest { diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeList.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeList.kt index 9db563151d..a3a131e5a9 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeList.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeList.kt @@ -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> { - 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 { - 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> { - 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 { + return api.addLibManga(track) } override fun update(track: Track): Observable { - 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 { - 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 { - 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> { + return api.search(query, getUsername()) + } + + override fun refresh(track: Track): Observable { + 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() } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyanimelistApi.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyanimelistApi.kt new file mode 100644 index 0000000000..a2f82e909d --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyanimelistApi.kt @@ -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 { + 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 { + 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> { + 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> { + 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 { + return getList(username) + .map { list -> list.find { it.remote_id == track.remote_id } } + } + + fun getLibManga(track: Track, username: String): Observable { + return findLibManga(track, username) + .map { it ?: throw Exception("Could not find manga") } + } + + fun login(username: String, password: String): Observable { + 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:" + } +} \ No newline at end of file