mirror of
				https://github.com/mihonapp/mihon.git
				synced 2025-11-03 23:58:55 +01:00 
			
		
		
		
	Add MangaUpdates as a tracker (#7170)
* Add MangaUpdates as a tracker - jobobby04 co-authored for suggestion in BackupTracking.kt Co-authored-by: jobobby04 <jobobby04@users.noreply.github.com> * Changes from code review Co-authored-by: arkon <arkon@users.noreply.github.com> Co-authored-by: jobobby04 <jobobby04@users.noreply.github.com> Co-authored-by: arkon <arkon@users.noreply.github.com>
This commit is contained in:
		@@ -12,7 +12,7 @@ data class BackupTracking(
 | 
			
		||||
    @ProtoNumber(1) var syncId: Int,
 | 
			
		||||
    // LibraryId is not null in 1.x
 | 
			
		||||
    @ProtoNumber(2) var libraryId: Long,
 | 
			
		||||
    @ProtoNumber(3) var mediaId: Int = 0,
 | 
			
		||||
    @Deprecated("Use mediaId instead", level = DeprecationLevel.WARNING) @ProtoNumber(3) var mediaIdInt: Int = 0,
 | 
			
		||||
    // trackingUrl is called mediaUrl in 1.x
 | 
			
		||||
    @ProtoNumber(4) var trackingUrl: String = "",
 | 
			
		||||
    @ProtoNumber(5) var title: String = "",
 | 
			
		||||
@@ -25,11 +25,17 @@ data class BackupTracking(
 | 
			
		||||
    @ProtoNumber(10) var startedReadingDate: Long = 0,
 | 
			
		||||
    // finishedReadingDate is called endReadTime in 1.x
 | 
			
		||||
    @ProtoNumber(11) var finishedReadingDate: Long = 0,
 | 
			
		||||
    @ProtoNumber(100) var mediaId: Long = 0,
 | 
			
		||||
) {
 | 
			
		||||
 | 
			
		||||
    fun getTrackingImpl(): TrackImpl {
 | 
			
		||||
        return TrackImpl().apply {
 | 
			
		||||
            sync_id = this@BackupTracking.syncId
 | 
			
		||||
            media_id = this@BackupTracking.mediaId
 | 
			
		||||
            media_id = if (this@BackupTracking.mediaIdInt != 0) {
 | 
			
		||||
                this@BackupTracking.mediaIdInt.toLong()
 | 
			
		||||
            } else {
 | 
			
		||||
                this@BackupTracking.mediaId
 | 
			
		||||
            }
 | 
			
		||||
            library_id = this@BackupTracking.libraryId
 | 
			
		||||
            title = this@BackupTracking.title
 | 
			
		||||
            last_chapter_read = this@BackupTracking.lastChapterRead
 | 
			
		||||
 
 | 
			
		||||
@@ -45,7 +45,7 @@ open class TrackBaseSerializer<T : Track> : KSerializer<T> {
 | 
			
		||||
            val jsonObject = decoder.decodeJsonElement().jsonObject
 | 
			
		||||
            title = jsonObject[TITLE]!!.jsonPrimitive.content
 | 
			
		||||
            sync_id = jsonObject[SYNC]!!.jsonPrimitive.int
 | 
			
		||||
            media_id = jsonObject[MEDIA]!!.jsonPrimitive.int
 | 
			
		||||
            media_id = jsonObject[MEDIA]!!.jsonPrimitive.long
 | 
			
		||||
            library_id = jsonObject[LIBRARY]!!.jsonPrimitive.long
 | 
			
		||||
            last_chapter_read = jsonObject[LAST_READ]!!.jsonPrimitive.float
 | 
			
		||||
            tracking_url = jsonObject[TRACKING_URL]!!.jsonPrimitive.content
 | 
			
		||||
 
 | 
			
		||||
@@ -68,7 +68,7 @@ class TrackGetResolver : DefaultGetResolver<Track>() {
 | 
			
		||||
        id = cursor.getLong(cursor.getColumnIndexOrThrow(COL_ID))
 | 
			
		||||
        manga_id = cursor.getLong(cursor.getColumnIndexOrThrow(COL_MANGA_ID))
 | 
			
		||||
        sync_id = cursor.getInt(cursor.getColumnIndexOrThrow(COL_SYNC_ID))
 | 
			
		||||
        media_id = cursor.getInt(cursor.getColumnIndexOrThrow(COL_MEDIA_ID))
 | 
			
		||||
        media_id = cursor.getLong(cursor.getColumnIndexOrThrow(COL_MEDIA_ID))
 | 
			
		||||
        library_id = cursor.getLong(cursor.getColumnIndexOrThrow(COL_LIBRARY_ID))
 | 
			
		||||
        title = cursor.getString(cursor.getColumnIndexOrThrow(COL_TITLE))
 | 
			
		||||
        last_chapter_read = cursor.getFloat(cursor.getColumnIndexOrThrow(COL_LAST_CHAPTER_READ))
 | 
			
		||||
 
 | 
			
		||||
@@ -10,7 +10,7 @@ interface Track : Serializable {
 | 
			
		||||
 | 
			
		||||
    var sync_id: Int
 | 
			
		||||
 | 
			
		||||
    var media_id: Int
 | 
			
		||||
    var media_id: Long
 | 
			
		||||
 | 
			
		||||
    var library_id: Long?
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -8,7 +8,7 @@ class TrackImpl : Track {
 | 
			
		||||
 | 
			
		||||
    override var sync_id: Int = 0
 | 
			
		||||
 | 
			
		||||
    override var media_id: Int = 0
 | 
			
		||||
    override var media_id: Long = 0
 | 
			
		||||
 | 
			
		||||
    override var library_id: Long? = null
 | 
			
		||||
 | 
			
		||||
@@ -42,7 +42,7 @@ class TrackImpl : Track {
 | 
			
		||||
    override fun hashCode(): Int {
 | 
			
		||||
        var result = (manga_id xor manga_id.ushr(32)).toInt()
 | 
			
		||||
        result = 31 * result + sync_id
 | 
			
		||||
        result = 31 * result + media_id
 | 
			
		||||
        result = 31 * result + media_id.toInt()
 | 
			
		||||
        return result
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -5,6 +5,7 @@ import eu.kanade.tachiyomi.data.track.anilist.Anilist
 | 
			
		||||
import eu.kanade.tachiyomi.data.track.bangumi.Bangumi
 | 
			
		||||
import eu.kanade.tachiyomi.data.track.kitsu.Kitsu
 | 
			
		||||
import eu.kanade.tachiyomi.data.track.komga.Komga
 | 
			
		||||
import eu.kanade.tachiyomi.data.track.mangaupdates.MangaUpdates
 | 
			
		||||
import eu.kanade.tachiyomi.data.track.myanimelist.MyAnimeList
 | 
			
		||||
import eu.kanade.tachiyomi.data.track.shikimori.Shikimori
 | 
			
		||||
 | 
			
		||||
@@ -17,6 +18,7 @@ class TrackManager(context: Context) {
 | 
			
		||||
        const val SHIKIMORI = 4
 | 
			
		||||
        const val BANGUMI = 5
 | 
			
		||||
        const val KOMGA = 6
 | 
			
		||||
        const val MANGA_UPDATES = 7
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    val myAnimeList = MyAnimeList(context, MYANIMELIST)
 | 
			
		||||
@@ -31,7 +33,9 @@ class TrackManager(context: Context) {
 | 
			
		||||
 | 
			
		||||
    val komga = Komga(context, KOMGA)
 | 
			
		||||
 | 
			
		||||
    val services = listOf(myAnimeList, aniList, kitsu, shikimori, bangumi, komga)
 | 
			
		||||
    val mangaUpdates = MangaUpdates(context, MANGA_UPDATES)
 | 
			
		||||
 | 
			
		||||
    val services = listOf(myAnimeList, aniList, kitsu, shikimori, bangumi, komga, mangaUpdates)
 | 
			
		||||
 | 
			
		||||
    fun getService(id: Int) = services.find { it.id == id }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -268,7 +268,7 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
 | 
			
		||||
 | 
			
		||||
    private fun jsonToALManga(struct: JsonObject): ALManga {
 | 
			
		||||
        return ALManga(
 | 
			
		||||
            struct["id"]!!.jsonPrimitive.int,
 | 
			
		||||
            struct["id"]!!.jsonPrimitive.long,
 | 
			
		||||
            struct["title"]!!.jsonObject["userPreferred"]!!.jsonPrimitive.content,
 | 
			
		||||
            struct["coverImage"]!!.jsonObject["large"]!!.jsonPrimitive.content,
 | 
			
		||||
            struct["description"]!!.jsonPrimitive.contentOrNull,
 | 
			
		||||
@@ -329,7 +329,7 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
 | 
			
		||||
        private const val baseUrl = "https://anilist.co/api/v2/"
 | 
			
		||||
        private const val baseMangaUrl = "https://anilist.co/manga/"
 | 
			
		||||
 | 
			
		||||
        fun mangaUrl(mediaId: Int): String {
 | 
			
		||||
        fun mangaUrl(mediaId: Long): String {
 | 
			
		||||
            return baseMangaUrl + mediaId
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -9,7 +9,7 @@ import java.text.SimpleDateFormat
 | 
			
		||||
import java.util.Locale
 | 
			
		||||
 | 
			
		||||
data class ALManga(
 | 
			
		||||
    val media_id: Int,
 | 
			
		||||
    val media_id: Long,
 | 
			
		||||
    val title_user_pref: String,
 | 
			
		||||
    val image_url_lge: String,
 | 
			
		||||
    val description: String?,
 | 
			
		||||
 
 | 
			
		||||
@@ -18,6 +18,7 @@ import kotlinx.serialization.json.int
 | 
			
		||||
import kotlinx.serialization.json.jsonArray
 | 
			
		||||
import kotlinx.serialization.json.jsonObject
 | 
			
		||||
import kotlinx.serialization.json.jsonPrimitive
 | 
			
		||||
import kotlinx.serialization.json.long
 | 
			
		||||
import okhttp3.CacheControl
 | 
			
		||||
import okhttp3.FormBody
 | 
			
		||||
import okhttp3.OkHttpClient
 | 
			
		||||
@@ -106,7 +107,7 @@ class BangumiApi(private val client: OkHttpClient, interceptor: BangumiIntercept
 | 
			
		||||
            0
 | 
			
		||||
        }
 | 
			
		||||
        return TrackSearch.create(TrackManager.BANGUMI).apply {
 | 
			
		||||
            media_id = obj["id"]!!.jsonPrimitive.int
 | 
			
		||||
            media_id = obj["id"]!!.jsonPrimitive.long
 | 
			
		||||
            title = obj["name_cn"]!!.jsonPrimitive.content
 | 
			
		||||
            cover_url = coverUrl
 | 
			
		||||
            summary = obj["name"]!!.jsonPrimitive.content
 | 
			
		||||
 
 | 
			
		||||
@@ -11,10 +11,10 @@ import eu.kanade.tachiyomi.network.parseAs
 | 
			
		||||
import eu.kanade.tachiyomi.util.lang.withIOContext
 | 
			
		||||
import kotlinx.serialization.json.JsonObject
 | 
			
		||||
import kotlinx.serialization.json.buildJsonObject
 | 
			
		||||
import kotlinx.serialization.json.int
 | 
			
		||||
import kotlinx.serialization.json.jsonArray
 | 
			
		||||
import kotlinx.serialization.json.jsonObject
 | 
			
		||||
import kotlinx.serialization.json.jsonPrimitive
 | 
			
		||||
import kotlinx.serialization.json.long
 | 
			
		||||
import kotlinx.serialization.json.put
 | 
			
		||||
import kotlinx.serialization.json.putJsonObject
 | 
			
		||||
import okhttp3.FormBody
 | 
			
		||||
@@ -70,7 +70,7 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
 | 
			
		||||
                .await()
 | 
			
		||||
                .parseAs<JsonObject>()
 | 
			
		||||
                .let {
 | 
			
		||||
                    track.media_id = it["data"]!!.jsonObject["id"]!!.jsonPrimitive.int
 | 
			
		||||
                    track.media_id = it["data"]!!.jsonObject["id"]!!.jsonPrimitive.long
 | 
			
		||||
                    track
 | 
			
		||||
                }
 | 
			
		||||
        }
 | 
			
		||||
@@ -241,7 +241,7 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
 | 
			
		||||
        private const val algoliaFilter =
 | 
			
		||||
            "&facetFilters=%5B%22kind%3Amanga%22%5D&attributesToRetrieve=%5B%22synopsis%22%2C%22canonicalTitle%22%2C%22chapterCount%22%2C%22posterImage%22%2C%22startDate%22%2C%22subtype%22%2C%22endDate%22%2C%20%22id%22%5D"
 | 
			
		||||
 | 
			
		||||
        fun mangaUrl(remoteId: Int): String {
 | 
			
		||||
        fun mangaUrl(remoteId: Long): String {
 | 
			
		||||
            return baseMangaUrl + remoteId
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -10,12 +10,13 @@ import kotlinx.serialization.json.int
 | 
			
		||||
import kotlinx.serialization.json.intOrNull
 | 
			
		||||
import kotlinx.serialization.json.jsonObject
 | 
			
		||||
import kotlinx.serialization.json.jsonPrimitive
 | 
			
		||||
import kotlinx.serialization.json.long
 | 
			
		||||
import java.text.SimpleDateFormat
 | 
			
		||||
import java.util.Date
 | 
			
		||||
import java.util.Locale
 | 
			
		||||
 | 
			
		||||
class KitsuSearchManga(obj: JsonObject) {
 | 
			
		||||
    val id = obj["id"]!!.jsonPrimitive.int
 | 
			
		||||
    val id = obj["id"]!!.jsonPrimitive.long
 | 
			
		||||
    private val canonicalTitle = obj["canonicalTitle"]!!.jsonPrimitive.content
 | 
			
		||||
    private val chapterCount = obj["chapterCount"]?.jsonPrimitive?.intOrNull
 | 
			
		||||
    val subType = obj["subtype"]?.jsonPrimitive?.contentOrNull
 | 
			
		||||
@@ -60,7 +61,7 @@ class KitsuLibManga(obj: JsonObject, manga: JsonObject) {
 | 
			
		||||
    private val startDate = manga["attributes"]!!.jsonObject["startDate"]?.jsonPrimitive?.contentOrNull.orEmpty()
 | 
			
		||||
    private val startedAt = obj["attributes"]!!.jsonObject["startedAt"]?.jsonPrimitive?.contentOrNull
 | 
			
		||||
    private val finishedAt = obj["attributes"]!!.jsonObject["finishedAt"]?.jsonPrimitive?.contentOrNull
 | 
			
		||||
    private val libraryId = obj["id"]!!.jsonPrimitive.int
 | 
			
		||||
    private val libraryId = obj["id"]!!.jsonPrimitive.long
 | 
			
		||||
    val status = obj["attributes"]!!.jsonObject["status"]!!.jsonPrimitive.content
 | 
			
		||||
    private val ratingTwenty = obj["attributes"]!!.jsonObject["ratingTwenty"]?.jsonPrimitive?.contentOrNull
 | 
			
		||||
    val progress = obj["attributes"]!!.jsonObject["progress"]!!.jsonPrimitive.int
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,97 @@
 | 
			
		||||
package eu.kanade.tachiyomi.data.track.mangaupdates
 | 
			
		||||
 | 
			
		||||
import android.content.Context
 | 
			
		||||
import android.graphics.Color
 | 
			
		||||
import androidx.annotation.StringRes
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
import eu.kanade.tachiyomi.data.database.models.Track
 | 
			
		||||
import eu.kanade.tachiyomi.data.track.TrackService
 | 
			
		||||
import eu.kanade.tachiyomi.data.track.mangaupdates.dto.copyTo
 | 
			
		||||
import eu.kanade.tachiyomi.data.track.mangaupdates.dto.toTrackSearch
 | 
			
		||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
 | 
			
		||||
 | 
			
		||||
class MangaUpdates(private val context: Context, id: Int) : TrackService(id) {
 | 
			
		||||
 | 
			
		||||
    companion object {
 | 
			
		||||
        const val READING_LIST = 0
 | 
			
		||||
        const val WISH_LIST = 1
 | 
			
		||||
        const val COMPLETE_LIST = 2
 | 
			
		||||
        const val UNFINISHED_LIST = 3
 | 
			
		||||
        const val ON_HOLD_LIST = 4
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private val interceptor by lazy { MangaUpdatesInterceptor(this) }
 | 
			
		||||
 | 
			
		||||
    private val api by lazy { MangaUpdatesApi(interceptor, client) }
 | 
			
		||||
 | 
			
		||||
    @StringRes
 | 
			
		||||
    override fun nameRes(): Int = R.string.tracker_manga_updates
 | 
			
		||||
 | 
			
		||||
    override fun getLogo(): Int = R.drawable.ic_manga_updates
 | 
			
		||||
 | 
			
		||||
    override fun getLogoColor(): Int = Color.rgb(146, 160, 173)
 | 
			
		||||
 | 
			
		||||
    override fun getStatusList(): List<Int> {
 | 
			
		||||
        return listOf(READING_LIST, COMPLETE_LIST, ON_HOLD_LIST, UNFINISHED_LIST, WISH_LIST)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun getStatus(status: Int): String = with(context) {
 | 
			
		||||
        when (status) {
 | 
			
		||||
            READING_LIST -> getString(R.string.reading_list)
 | 
			
		||||
            WISH_LIST -> getString(R.string.wish_list)
 | 
			
		||||
            COMPLETE_LIST -> getString(R.string.complete_list)
 | 
			
		||||
            ON_HOLD_LIST -> getString(R.string.on_hold_list)
 | 
			
		||||
            UNFINISHED_LIST -> getString(R.string.unfinished_list)
 | 
			
		||||
            else -> ""
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun getReadingStatus(): Int = READING_LIST
 | 
			
		||||
 | 
			
		||||
    override fun getRereadingStatus(): Int = -1
 | 
			
		||||
 | 
			
		||||
    override fun getCompletionStatus(): Int = COMPLETE_LIST
 | 
			
		||||
 | 
			
		||||
    override fun getScoreList(): List<String> = (0..10).map(Int::toString)
 | 
			
		||||
 | 
			
		||||
    override fun displayScore(track: Track): String = track.score.toInt().toString()
 | 
			
		||||
 | 
			
		||||
    override suspend fun update(track: Track, didReadChapter: Boolean): Track {
 | 
			
		||||
        api.updateSeriesListItem(track)
 | 
			
		||||
        return track
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override suspend fun bind(track: Track, hasReadChapters: Boolean): Track {
 | 
			
		||||
        return try {
 | 
			
		||||
            val (series, rating) = api.getSeriesListItem(track)
 | 
			
		||||
            series.copyTo(track)
 | 
			
		||||
            rating?.copyTo(track) ?: track
 | 
			
		||||
        } catch (e: Exception) {
 | 
			
		||||
            api.addSeriesToList(track, hasReadChapters)
 | 
			
		||||
            track
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override suspend fun search(query: String): List<TrackSearch> {
 | 
			
		||||
        return api.search(query)
 | 
			
		||||
            .map {
 | 
			
		||||
                it.toTrackSearch(id)
 | 
			
		||||
            }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override suspend fun refresh(track: Track): Track {
 | 
			
		||||
        val (series, rating) = api.getSeriesListItem(track)
 | 
			
		||||
        series.copyTo(track)
 | 
			
		||||
        return rating?.copyTo(track) ?: track
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override suspend fun login(username: String, password: String) {
 | 
			
		||||
        val authenticated = api.authenticate(username, password) ?: throw Throwable("Unable to login")
 | 
			
		||||
        saveCredentials(authenticated.uid.toString(), authenticated.sessionToken)
 | 
			
		||||
        interceptor.newAuth(authenticated.sessionToken)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun restoreSession(): String? {
 | 
			
		||||
        return preferences.trackPassword(this)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,189 @@
 | 
			
		||||
package eu.kanade.tachiyomi.data.track.mangaupdates
 | 
			
		||||
 | 
			
		||||
import eu.kanade.tachiyomi.data.database.models.Track
 | 
			
		||||
import eu.kanade.tachiyomi.data.track.mangaupdates.MangaUpdates.Companion.READING_LIST
 | 
			
		||||
import eu.kanade.tachiyomi.data.track.mangaupdates.MangaUpdates.Companion.WISH_LIST
 | 
			
		||||
import eu.kanade.tachiyomi.data.track.mangaupdates.dto.Context
 | 
			
		||||
import eu.kanade.tachiyomi.data.track.mangaupdates.dto.ListItem
 | 
			
		||||
import eu.kanade.tachiyomi.data.track.mangaupdates.dto.Rating
 | 
			
		||||
import eu.kanade.tachiyomi.data.track.mangaupdates.dto.Record
 | 
			
		||||
import eu.kanade.tachiyomi.network.DELETE
 | 
			
		||||
import eu.kanade.tachiyomi.network.GET
 | 
			
		||||
import eu.kanade.tachiyomi.network.POST
 | 
			
		||||
import eu.kanade.tachiyomi.network.PUT
 | 
			
		||||
import eu.kanade.tachiyomi.network.await
 | 
			
		||||
import eu.kanade.tachiyomi.network.parseAs
 | 
			
		||||
import eu.kanade.tachiyomi.util.system.logcat
 | 
			
		||||
import kotlinx.serialization.json.Json
 | 
			
		||||
import kotlinx.serialization.json.JsonObject
 | 
			
		||||
import kotlinx.serialization.json.addJsonObject
 | 
			
		||||
import kotlinx.serialization.json.buildJsonArray
 | 
			
		||||
import kotlinx.serialization.json.buildJsonObject
 | 
			
		||||
import kotlinx.serialization.json.decodeFromJsonElement
 | 
			
		||||
import kotlinx.serialization.json.jsonArray
 | 
			
		||||
import kotlinx.serialization.json.jsonObject
 | 
			
		||||
import kotlinx.serialization.json.put
 | 
			
		||||
import kotlinx.serialization.json.putJsonObject
 | 
			
		||||
import logcat.LogPriority
 | 
			
		||||
import okhttp3.MediaType.Companion.toMediaType
 | 
			
		||||
import okhttp3.OkHttpClient
 | 
			
		||||
import okhttp3.RequestBody.Companion.toRequestBody
 | 
			
		||||
import uy.kohesive.injekt.injectLazy
 | 
			
		||||
 | 
			
		||||
class MangaUpdatesApi(
 | 
			
		||||
    interceptor: MangaUpdatesInterceptor,
 | 
			
		||||
    private val client: OkHttpClient,
 | 
			
		||||
) {
 | 
			
		||||
    private val baseUrl = "https://api.mangaupdates.com"
 | 
			
		||||
    private val contentType = "application/vnd.api+json".toMediaType()
 | 
			
		||||
 | 
			
		||||
    private val json by injectLazy<Json>()
 | 
			
		||||
 | 
			
		||||
    private val authClient by lazy {
 | 
			
		||||
        client.newBuilder()
 | 
			
		||||
            .addInterceptor(interceptor)
 | 
			
		||||
            .build()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    suspend fun getSeriesListItem(track: Track): Pair<ListItem, Rating?> {
 | 
			
		||||
        val listItem =
 | 
			
		||||
            authClient.newCall(
 | 
			
		||||
                GET(
 | 
			
		||||
                    url = "$baseUrl/v1/lists/series/${track.media_id}",
 | 
			
		||||
                ),
 | 
			
		||||
            )
 | 
			
		||||
                .await()
 | 
			
		||||
                .parseAs<ListItem>()
 | 
			
		||||
 | 
			
		||||
        val rating = getSeriesRating(track)
 | 
			
		||||
 | 
			
		||||
        return listItem to rating
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    suspend fun addSeriesToList(track: Track, hasReadChapters: Boolean) {
 | 
			
		||||
        val status = if (hasReadChapters) READING_LIST else WISH_LIST
 | 
			
		||||
        val body = buildJsonArray {
 | 
			
		||||
            addJsonObject {
 | 
			
		||||
                putJsonObject("series") {
 | 
			
		||||
                    put("id", track.media_id)
 | 
			
		||||
                }
 | 
			
		||||
                put("list_id", status)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        authClient.newCall(
 | 
			
		||||
            POST(
 | 
			
		||||
                url = "$baseUrl/v1/lists/series",
 | 
			
		||||
                body = body.toString().toRequestBody(contentType),
 | 
			
		||||
            ),
 | 
			
		||||
        )
 | 
			
		||||
            .await()
 | 
			
		||||
            .let {
 | 
			
		||||
                if (it.code == 200) {
 | 
			
		||||
                    track.status = status
 | 
			
		||||
                    track.last_chapter_read = 1f
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    suspend fun updateSeriesListItem(track: Track) {
 | 
			
		||||
        val body = buildJsonArray {
 | 
			
		||||
            addJsonObject {
 | 
			
		||||
                putJsonObject("series") {
 | 
			
		||||
                    put("id", track.media_id)
 | 
			
		||||
                }
 | 
			
		||||
                put("list_id", track.status)
 | 
			
		||||
                putJsonObject("status") {
 | 
			
		||||
                    put("chapter", track.last_chapter_read.toInt())
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        authClient.newCall(
 | 
			
		||||
            POST(
 | 
			
		||||
                url = "$baseUrl/v1/lists/series/update",
 | 
			
		||||
                body = body.toString().toRequestBody(contentType),
 | 
			
		||||
            ),
 | 
			
		||||
        )
 | 
			
		||||
            .await()
 | 
			
		||||
 | 
			
		||||
        updateSeriesRating(track)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    suspend fun getSeriesRating(track: Track): Rating? {
 | 
			
		||||
        return try {
 | 
			
		||||
            authClient.newCall(
 | 
			
		||||
                GET(
 | 
			
		||||
                    url = "$baseUrl/v1/series/${track.media_id}/rating",
 | 
			
		||||
                ),
 | 
			
		||||
            )
 | 
			
		||||
                .await()
 | 
			
		||||
                .parseAs<Rating>()
 | 
			
		||||
        } catch (e: Exception) {
 | 
			
		||||
            null
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    suspend fun updateSeriesRating(track: Track) {
 | 
			
		||||
        if (track.score != 0f) {
 | 
			
		||||
            val body = buildJsonObject {
 | 
			
		||||
                put("rating", track.score.toInt())
 | 
			
		||||
            }
 | 
			
		||||
            authClient.newCall(
 | 
			
		||||
                PUT(
 | 
			
		||||
                    url = "$baseUrl/v1/series/${track.media_id}/rating",
 | 
			
		||||
                    body = body.toString().toRequestBody(contentType),
 | 
			
		||||
                ),
 | 
			
		||||
            )
 | 
			
		||||
                .await()
 | 
			
		||||
        } else {
 | 
			
		||||
            authClient.newCall(
 | 
			
		||||
                DELETE(
 | 
			
		||||
                    url = "$baseUrl/v1/series/${track.media_id}/rating",
 | 
			
		||||
                ),
 | 
			
		||||
            )
 | 
			
		||||
                .await()
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    suspend fun search(query: String): List<Record> {
 | 
			
		||||
        val body = buildJsonObject {
 | 
			
		||||
            put("search", query)
 | 
			
		||||
        }
 | 
			
		||||
        return client.newCall(
 | 
			
		||||
            POST(
 | 
			
		||||
                url = "$baseUrl/v1/series/search",
 | 
			
		||||
                body = body.toString().toRequestBody(contentType),
 | 
			
		||||
            ),
 | 
			
		||||
        )
 | 
			
		||||
            .await()
 | 
			
		||||
            .parseAs<JsonObject>()
 | 
			
		||||
            .let { obj ->
 | 
			
		||||
                obj["results"]?.jsonArray?.map { element ->
 | 
			
		||||
                    json.decodeFromJsonElement<Record>(element.jsonObject["record"]!!)
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            .orEmpty()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    suspend fun authenticate(username: String, password: String): Context? {
 | 
			
		||||
        val body = buildJsonObject {
 | 
			
		||||
            put("username", username)
 | 
			
		||||
            put("password", password)
 | 
			
		||||
        }
 | 
			
		||||
        return client.newCall(
 | 
			
		||||
            PUT(
 | 
			
		||||
                url = "$baseUrl/v1/account/login",
 | 
			
		||||
                body = body.toString().toRequestBody(contentType),
 | 
			
		||||
            ),
 | 
			
		||||
        )
 | 
			
		||||
            .await()
 | 
			
		||||
            .parseAs<JsonObject>()
 | 
			
		||||
            .let { obj ->
 | 
			
		||||
                try {
 | 
			
		||||
                    json.decodeFromJsonElement<Context>(obj["context"]!!)
 | 
			
		||||
                } catch (e: Exception) {
 | 
			
		||||
                    logcat(LogPriority.ERROR, e)
 | 
			
		||||
                    null
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,29 @@
 | 
			
		||||
package eu.kanade.tachiyomi.data.track.mangaupdates
 | 
			
		||||
 | 
			
		||||
import okhttp3.Interceptor
 | 
			
		||||
import okhttp3.Response
 | 
			
		||||
import java.io.IOException
 | 
			
		||||
 | 
			
		||||
class MangaUpdatesInterceptor(
 | 
			
		||||
    mangaUpdates: MangaUpdates,
 | 
			
		||||
) : Interceptor {
 | 
			
		||||
 | 
			
		||||
    private var token: String? = mangaUpdates.restoreSession()
 | 
			
		||||
 | 
			
		||||
    override fun intercept(chain: Interceptor.Chain): Response {
 | 
			
		||||
        val originalRequest = chain.request()
 | 
			
		||||
 | 
			
		||||
        val token = token ?: throw IOException("Not authenticated with MangaUpdates")
 | 
			
		||||
 | 
			
		||||
        // Add the authorization header to the original request.
 | 
			
		||||
        val authRequest = originalRequest.newBuilder()
 | 
			
		||||
            .addHeader("Authorization", "Bearer $token")
 | 
			
		||||
            .build()
 | 
			
		||||
 | 
			
		||||
        return chain.proceed(authRequest)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun newAuth(token: String?) {
 | 
			
		||||
        this.token = token
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,11 @@
 | 
			
		||||
package eu.kanade.tachiyomi.data.track.mangaupdates.dto
 | 
			
		||||
 | 
			
		||||
import kotlinx.serialization.SerialName
 | 
			
		||||
import kotlinx.serialization.Serializable
 | 
			
		||||
 | 
			
		||||
@Serializable
 | 
			
		||||
data class Context(
 | 
			
		||||
    @SerialName("session_token")
 | 
			
		||||
    val sessionToken: String,
 | 
			
		||||
    val uid: Long,
 | 
			
		||||
)
 | 
			
		||||
@@ -0,0 +1,10 @@
 | 
			
		||||
package eu.kanade.tachiyomi.data.track.mangaupdates.dto
 | 
			
		||||
 | 
			
		||||
import kotlinx.serialization.Serializable
 | 
			
		||||
 | 
			
		||||
@Serializable
 | 
			
		||||
data class Image(
 | 
			
		||||
    val url: Url? = null,
 | 
			
		||||
    val height: Int? = null,
 | 
			
		||||
    val width: Int? = null,
 | 
			
		||||
)
 | 
			
		||||
@@ -0,0 +1,22 @@
 | 
			
		||||
package eu.kanade.tachiyomi.data.track.mangaupdates.dto
 | 
			
		||||
 | 
			
		||||
import eu.kanade.tachiyomi.data.database.models.Track
 | 
			
		||||
import eu.kanade.tachiyomi.data.track.mangaupdates.MangaUpdates.Companion.READING_LIST
 | 
			
		||||
import kotlinx.serialization.SerialName
 | 
			
		||||
import kotlinx.serialization.Serializable
 | 
			
		||||
 | 
			
		||||
@Serializable
 | 
			
		||||
data class ListItem(
 | 
			
		||||
    val series: Series? = null,
 | 
			
		||||
    @SerialName("list_id")
 | 
			
		||||
    val listId: Int? = null,
 | 
			
		||||
    val status: Status? = null,
 | 
			
		||||
    val priority: Int? = null,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
fun ListItem.copyTo(track: Track): Track {
 | 
			
		||||
    return track.apply {
 | 
			
		||||
        this.status = listId ?: READING_LIST
 | 
			
		||||
        this.last_chapter_read = this@copyTo.status?.chapter?.toFloat() ?: 0f
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,15 @@
 | 
			
		||||
package eu.kanade.tachiyomi.data.track.mangaupdates.dto
 | 
			
		||||
 | 
			
		||||
import eu.kanade.tachiyomi.data.database.models.Track
 | 
			
		||||
import kotlinx.serialization.Serializable
 | 
			
		||||
 | 
			
		||||
@Serializable
 | 
			
		||||
data class Rating(
 | 
			
		||||
    val rating: Int? = null,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
fun Rating.copyTo(track: Track): Track {
 | 
			
		||||
    return track.apply {
 | 
			
		||||
        this.score = rating?.toFloat() ?: 0f
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,37 @@
 | 
			
		||||
package eu.kanade.tachiyomi.data.track.mangaupdates.dto
 | 
			
		||||
 | 
			
		||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
 | 
			
		||||
import kotlinx.serialization.SerialName
 | 
			
		||||
import kotlinx.serialization.Serializable
 | 
			
		||||
 | 
			
		||||
@Serializable
 | 
			
		||||
data class Record(
 | 
			
		||||
    @SerialName("series_id")
 | 
			
		||||
    val seriesId: Long? = null,
 | 
			
		||||
    val title: String? = null,
 | 
			
		||||
    val url: String? = null,
 | 
			
		||||
    val description: String? = null,
 | 
			
		||||
    val image: Image? = null,
 | 
			
		||||
    val type: String? = null,
 | 
			
		||||
    val year: String? = null,
 | 
			
		||||
    @SerialName("bayesian_rating")
 | 
			
		||||
    val bayesianRating: Double? = null,
 | 
			
		||||
    @SerialName("rating_votes")
 | 
			
		||||
    val ratingVotes: Int? = null,
 | 
			
		||||
    @SerialName("latest_chapter")
 | 
			
		||||
    val latestChapter: Int? = null,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
fun Record.toTrackSearch(id: Int): TrackSearch {
 | 
			
		||||
    return TrackSearch.create(id).apply {
 | 
			
		||||
        media_id = this@toTrackSearch.seriesId ?: 0L
 | 
			
		||||
        title = this@toTrackSearch.title ?: ""
 | 
			
		||||
        total_chapters = 0
 | 
			
		||||
        cover_url = this@toTrackSearch.image?.url?.original ?: ""
 | 
			
		||||
        summary = this@toTrackSearch.description ?: ""
 | 
			
		||||
        tracking_url = this@toTrackSearch.url ?: ""
 | 
			
		||||
        publishing_status = ""
 | 
			
		||||
        publishing_type = this@toTrackSearch.type.toString()
 | 
			
		||||
        start_date = this@toTrackSearch.year.toString()
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,9 @@
 | 
			
		||||
package eu.kanade.tachiyomi.data.track.mangaupdates.dto
 | 
			
		||||
 | 
			
		||||
import kotlinx.serialization.Serializable
 | 
			
		||||
 | 
			
		||||
@Serializable
 | 
			
		||||
data class Series(
 | 
			
		||||
    val id: Long? = null,
 | 
			
		||||
    val title: String? = null,
 | 
			
		||||
)
 | 
			
		||||
@@ -0,0 +1,9 @@
 | 
			
		||||
package eu.kanade.tachiyomi.data.track.mangaupdates.dto
 | 
			
		||||
 | 
			
		||||
import kotlinx.serialization.Serializable
 | 
			
		||||
 | 
			
		||||
@Serializable
 | 
			
		||||
data class Status(
 | 
			
		||||
    val volume: Int? = null,
 | 
			
		||||
    val chapter: Int? = null,
 | 
			
		||||
)
 | 
			
		||||
@@ -0,0 +1,9 @@
 | 
			
		||||
package eu.kanade.tachiyomi.data.track.mangaupdates.dto
 | 
			
		||||
 | 
			
		||||
import kotlinx.serialization.Serializable
 | 
			
		||||
 | 
			
		||||
@Serializable
 | 
			
		||||
data class Url(
 | 
			
		||||
    val original: String? = null,
 | 
			
		||||
    val thumb: String? = null,
 | 
			
		||||
)
 | 
			
		||||
@@ -10,7 +10,7 @@ class TrackSearch : Track {
 | 
			
		||||
 | 
			
		||||
    override var sync_id: Int = 0
 | 
			
		||||
 | 
			
		||||
    override var media_id: Int = 0
 | 
			
		||||
    override var media_id: Long = 0
 | 
			
		||||
 | 
			
		||||
    override var library_id: Long? = null
 | 
			
		||||
 | 
			
		||||
@@ -54,7 +54,7 @@ class TrackSearch : Track {
 | 
			
		||||
    override fun hashCode(): Int {
 | 
			
		||||
        var result = (manga_id xor manga_id.ushr(32)).toInt()
 | 
			
		||||
        result = 31 * result + sync_id
 | 
			
		||||
        result = 31 * result + media_id
 | 
			
		||||
        result = 31 * result + media_id.toInt()
 | 
			
		||||
        return result
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -21,6 +21,7 @@ import kotlinx.serialization.json.int
 | 
			
		||||
import kotlinx.serialization.json.jsonArray
 | 
			
		||||
import kotlinx.serialization.json.jsonObject
 | 
			
		||||
import kotlinx.serialization.json.jsonPrimitive
 | 
			
		||||
import kotlinx.serialization.json.long
 | 
			
		||||
import okhttp3.FormBody
 | 
			
		||||
import okhttp3.OkHttpClient
 | 
			
		||||
import okhttp3.Request
 | 
			
		||||
@@ -94,7 +95,7 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI
 | 
			
		||||
                .let {
 | 
			
		||||
                    val obj = it.jsonObject
 | 
			
		||||
                    TrackSearch.create(TrackManager.MYANIMELIST).apply {
 | 
			
		||||
                        media_id = obj["id"]!!.jsonPrimitive.int
 | 
			
		||||
                        media_id = obj["id"]!!.jsonPrimitive.long
 | 
			
		||||
                        title = obj["title"]!!.jsonPrimitive.content
 | 
			
		||||
                        summary = obj["synopsis"]?.jsonPrimitive?.content ?: ""
 | 
			
		||||
                        total_chapters = obj["num_chapters"]!!.jsonPrimitive.int
 | 
			
		||||
@@ -251,7 +252,7 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI
 | 
			
		||||
            .appendQueryParameter("response_type", "code")
 | 
			
		||||
            .build()
 | 
			
		||||
 | 
			
		||||
        fun mangaUrl(id: Int): Uri = "$baseApiUrl/manga".toUri().buildUpon()
 | 
			
		||||
        fun mangaUrl(id: Long): Uri = "$baseApiUrl/manga".toUri().buildUpon()
 | 
			
		||||
            .appendPath(id.toString())
 | 
			
		||||
            .appendPath("my_list_status")
 | 
			
		||||
            .build()
 | 
			
		||||
 
 | 
			
		||||
@@ -19,6 +19,7 @@ import kotlinx.serialization.json.float
 | 
			
		||||
import kotlinx.serialization.json.int
 | 
			
		||||
import kotlinx.serialization.json.jsonObject
 | 
			
		||||
import kotlinx.serialization.json.jsonPrimitive
 | 
			
		||||
import kotlinx.serialization.json.long
 | 
			
		||||
import kotlinx.serialization.json.put
 | 
			
		||||
import kotlinx.serialization.json.putJsonObject
 | 
			
		||||
import okhttp3.FormBody
 | 
			
		||||
@@ -73,7 +74,7 @@ class ShikimoriApi(private val client: OkHttpClient, interceptor: ShikimoriInter
 | 
			
		||||
 | 
			
		||||
    private fun jsonToSearch(obj: JsonObject): TrackSearch {
 | 
			
		||||
        return TrackSearch.create(TrackManager.SHIKIMORI).apply {
 | 
			
		||||
            media_id = obj["id"]!!.jsonPrimitive.int
 | 
			
		||||
            media_id = obj["id"]!!.jsonPrimitive.long
 | 
			
		||||
            title = obj["name"]!!.jsonPrimitive.content
 | 
			
		||||
            total_chapters = obj["chapters"]!!.jsonPrimitive.int
 | 
			
		||||
            cover_url = baseUrl + obj["image"]!!.jsonObject["preview"]!!.jsonPrimitive.content
 | 
			
		||||
@@ -88,7 +89,7 @@ class ShikimoriApi(private val client: OkHttpClient, interceptor: ShikimoriInter
 | 
			
		||||
    private fun jsonToTrack(obj: JsonObject, mangas: JsonObject): Track {
 | 
			
		||||
        return Track.create(TrackManager.SHIKIMORI).apply {
 | 
			
		||||
            title = mangas["name"]!!.jsonPrimitive.content
 | 
			
		||||
            media_id = obj["id"]!!.jsonPrimitive.int
 | 
			
		||||
            media_id = obj["id"]!!.jsonPrimitive.long
 | 
			
		||||
            total_chapters = mangas["chapters"]!!.jsonPrimitive.int
 | 
			
		||||
            last_chapter_read = obj["chapters"]!!.jsonPrimitive.float
 | 
			
		||||
            score = (obj["score"]!!.jsonPrimitive.int).toFloat()
 | 
			
		||||
 
 | 
			
		||||
@@ -36,3 +36,31 @@ fun POST(
 | 
			
		||||
        .cacheControl(cache)
 | 
			
		||||
        .build()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fun PUT(
 | 
			
		||||
    url: String,
 | 
			
		||||
    headers: Headers = DEFAULT_HEADERS,
 | 
			
		||||
    body: RequestBody = DEFAULT_BODY,
 | 
			
		||||
    cache: CacheControl = DEFAULT_CACHE_CONTROL,
 | 
			
		||||
): Request {
 | 
			
		||||
    return Request.Builder()
 | 
			
		||||
        .url(url)
 | 
			
		||||
        .put(body)
 | 
			
		||||
        .headers(headers)
 | 
			
		||||
        .cacheControl(cache)
 | 
			
		||||
        .build()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fun DELETE(
 | 
			
		||||
    url: String,
 | 
			
		||||
    headers: Headers = DEFAULT_HEADERS,
 | 
			
		||||
    body: RequestBody = DEFAULT_BODY,
 | 
			
		||||
    cache: CacheControl = DEFAULT_CACHE_CONTROL,
 | 
			
		||||
): Request {
 | 
			
		||||
    return Request.Builder()
 | 
			
		||||
        .url(url)
 | 
			
		||||
        .delete(body)
 | 
			
		||||
        .headers(headers)
 | 
			
		||||
        .cacheControl(cache)
 | 
			
		||||
        .build()
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -63,13 +63,17 @@ class SettingsTrackingController :
 | 
			
		||||
                dialog.targetController = this@SettingsTrackingController
 | 
			
		||||
                dialog.showDialog(router)
 | 
			
		||||
            }
 | 
			
		||||
            trackPreference(trackManager.mangaUpdates) {
 | 
			
		||||
                val dialog = TrackLoginDialog(trackManager.mangaUpdates, R.string.username)
 | 
			
		||||
                dialog.targetController = this@SettingsTrackingController
 | 
			
		||||
                dialog.showDialog(router)
 | 
			
		||||
            }
 | 
			
		||||
            trackPreference(trackManager.shikimori) {
 | 
			
		||||
                activity?.openInBrowser(ShikimoriApi.authUrl(), forceDefaultBrowser = true)
 | 
			
		||||
            }
 | 
			
		||||
            trackPreference(trackManager.bangumi) {
 | 
			
		||||
                activity?.openInBrowser(BangumiApi.authUrl(), forceDefaultBrowser = true)
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            infoPreference(R.string.tracking_info)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user