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 46f3c99e9..73fa15c55 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 5e14c66d6..39b5db83f 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<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)
+    }
+
 }
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 54ca0151c..25fafc6a5 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<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")
-                    }
-                }
-    }
-
 }
 
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 29c4551fc..d8ef82d30 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<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
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 a25b7fe79..7cdc5dde8 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 51d364321..d99475cb8 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<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
-                }
-    }
-
 }
\ 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 ce5510818..45d962d46 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<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 {
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 9db563151..a3a131e5a 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<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()
     }
 
 }
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 000000000..a2f82e909
--- /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<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:"
+    }
+}
\ No newline at end of file