diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/full/models/BackupTracking.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/full/models/BackupTracking.kt index 2ef022d5d5..fbd6a55003 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/full/models/BackupTracking.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/full/models/BackupTracking.kt @@ -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 diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/legacy/serializer/TrackTypeSerializer.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/legacy/serializer/TrackTypeSerializer.kt index 4691e9a868..1d2ced7ebe 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/legacy/serializer/TrackTypeSerializer.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/legacy/serializer/TrackTypeSerializer.kt @@ -45,7 +45,7 @@ open class TrackBaseSerializer : KSerializer { 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 diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/mappers/TrackTypeMapping.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/mappers/TrackTypeMapping.kt index 24ca7c26e8..764f2325af 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/mappers/TrackTypeMapping.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/mappers/TrackTypeMapping.kt @@ -68,7 +68,7 @@ class TrackGetResolver : DefaultGetResolver() { 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)) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/Track.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/Track.kt index b577451af5..4fb22b91a1 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/Track.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/Track.kt @@ -10,7 +10,7 @@ interface Track : Serializable { var sync_id: Int - var media_id: Int + var media_id: Long var library_id: Long? diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/TrackImpl.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/TrackImpl.kt index 082769b2d6..b447ef56af 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/TrackImpl.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/TrackImpl.kt @@ -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 } } 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 05065920c2..584136bd22 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 @@ -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 } 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 035ddc1ff9..eb660b5de6 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 @@ -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 } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistModels.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistModels.kt index 41cd2fe8ae..120cdc027b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistModels.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistModels.kt @@ -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?, diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/BangumiApi.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/BangumiApi.kt index dc299b3ebb..c2a6d9eb85 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/BangumiApi.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/BangumiApi.kt @@ -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 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 1387d9d01f..7bb088280c 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 @@ -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() .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 } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuModels.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuModels.kt index 1d60178325..453e65ce7a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuModels.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuModels.kt @@ -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 diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/MangaUpdates.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/MangaUpdates.kt new file mode 100644 index 0000000000..8fce9b705b --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/MangaUpdates.kt @@ -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 { + 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 = (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 { + 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) + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/MangaUpdatesApi.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/MangaUpdatesApi.kt new file mode 100644 index 0000000000..d23b308a6a --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/MangaUpdatesApi.kt @@ -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() + + private val authClient by lazy { + client.newBuilder() + .addInterceptor(interceptor) + .build() + } + + suspend fun getSeriesListItem(track: Track): Pair { + val listItem = + authClient.newCall( + GET( + url = "$baseUrl/v1/lists/series/${track.media_id}", + ), + ) + .await() + .parseAs() + + 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() + } 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 { + val body = buildJsonObject { + put("search", query) + } + return client.newCall( + POST( + url = "$baseUrl/v1/series/search", + body = body.toString().toRequestBody(contentType), + ), + ) + .await() + .parseAs() + .let { obj -> + obj["results"]?.jsonArray?.map { element -> + json.decodeFromJsonElement(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() + .let { obj -> + try { + json.decodeFromJsonElement(obj["context"]!!) + } catch (e: Exception) { + logcat(LogPriority.ERROR, e) + null + } + } + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/MangaUpdatesInterceptor.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/MangaUpdatesInterceptor.kt new file mode 100644 index 0000000000..2b283c3b83 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/MangaUpdatesInterceptor.kt @@ -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 + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/Context.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/Context.kt new file mode 100644 index 0000000000..77019cacd2 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/Context.kt @@ -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, +) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/Image.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/Image.kt new file mode 100644 index 0000000000..bed1f2657b --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/Image.kt @@ -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, +) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/ListItem.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/ListItem.kt new file mode 100644 index 0000000000..4ed8bd7059 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/ListItem.kt @@ -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 + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/Rating.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/Rating.kt new file mode 100644 index 0000000000..0de945dd35 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/Rating.kt @@ -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 + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/Record.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/Record.kt new file mode 100644 index 0000000000..6790290aa1 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/Record.kt @@ -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() + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/Series.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/Series.kt new file mode 100644 index 0000000000..261c857372 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/Series.kt @@ -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, +) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/Status.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/Status.kt new file mode 100644 index 0000000000..7320ac2e3d --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/Status.kt @@ -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, +) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/Url.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/Url.kt new file mode 100644 index 0000000000..f295d3bdc7 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/Url.kt @@ -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, +) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/model/TrackSearch.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/model/TrackSearch.kt index 90c689b0d4..1dab62211e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/model/TrackSearch.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/model/TrackSearch.kt @@ -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 } 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 index 5eef11d576..22bd32228f 100644 --- 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 @@ -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() diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/ShikimoriApi.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/ShikimoriApi.kt index 1aeb598eb3..0319c1cdaa 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/ShikimoriApi.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/ShikimoriApi.kt @@ -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() diff --git a/app/src/main/java/eu/kanade/tachiyomi/network/Requests.kt b/app/src/main/java/eu/kanade/tachiyomi/network/Requests.kt index 8931b90b9e..8fb5ec2aa8 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/network/Requests.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/network/Requests.kt @@ -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() +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsTrackingController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsTrackingController.kt index f48068dad0..5c3505d9ca 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsTrackingController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsTrackingController.kt @@ -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) } diff --git a/app/src/main/res/drawable-nodpi/ic_manga_updates.webp b/app/src/main/res/drawable-nodpi/ic_manga_updates.webp new file mode 100644 index 0000000000..eece5d7d65 Binary files /dev/null and b/app/src/main/res/drawable-nodpi/ic_manga_updates.webp differ diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2368300766..bd0e85032d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -643,6 +643,7 @@ This tracker is only compatible with the Komga source. Bangumi Shikimori + MangaUpdates Tracking %d tracker @@ -657,6 +658,11 @@ Paused Plan to read Rereading + Reading List + Wish List + Complete List + On Hold List + Unfinished List Score Title Status