From 0974e787adad22e572887ed2ef83b7d31ebeffc4 Mon Sep 17 00:00:00 2001 From: MajorTanya <39014446+MajorTanya@users.noreply.github.com> Date: Thu, 20 Aug 2026 20:12:48 +0200 Subject: [PATCH] Use Shikimori's update endpoint (#3810) No idea why it was originally going via the addition endpoint but Shikimori's v1 and v2 APIs have a dedicated endpoint to update a list entry via PATCH or PUT (interchangeable, both allow partial payloads). Splitting off the update path revealed that findLibManga would return a non-null track, which in turn causes the update call to fail with a 404 due to `library_id` being null. After further investigation, I realised that findLibManga should always return null if the title is not in the user's library, not just in refreshes. --- .../data/track/shikimori/Shikimori.kt | 11 +++-- .../data/track/shikimori/ShikimoriApi.kt | 42 +++++++++++++++---- ...angaResponse.kt => SMLibraryIdResponse.kt} | 2 +- 3 files changed, 41 insertions(+), 14 deletions(-) rename app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/dto/{SMAddMangaResponse.kt => SMLibraryIdResponse.kt} (80%) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/Shikimori.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/Shikimori.kt index 689c89f2b..eecdccec9 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/Shikimori.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/Shikimori.kt @@ -55,7 +55,7 @@ class Shikimori(id: Long) : BaseTracker(id, "Shikimori"), DeletableTracker { } } - return api.updateLibManga(track, getUsername()) + return api.updateLibManga(track) } override suspend fun delete(track: DomainTrack) { @@ -93,11 +93,10 @@ class Shikimori(id: Long) : BaseTracker(id, "Shikimori"), DeletableTracker { } override suspend fun refresh(track: Track): Track { - api.findLibManga(track, isRefresh = true)?.let { remoteTrack -> - track.library_id = remoteTrack.library_id - track.copyPersonalFrom(remoteTrack) - track.total_chapters = remoteTrack.total_chapters - } ?: throw Exception("Could not find manga") + val remoteTrack = api.findLibManga(track) ?: throw Exception("Could not find manga") + track.library_id = remoteTrack.library_id + track.copyPersonalFrom(remoteTrack) + track.total_chapters = remoteTrack.total_chapters return track } 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 caab48cf1..18dbb2d68 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 @@ -4,7 +4,7 @@ import android.net.Uri import androidx.core.net.toUri import eu.kanade.tachiyomi.data.database.models.Track import eu.kanade.tachiyomi.data.track.model.TrackSearch -import eu.kanade.tachiyomi.data.track.shikimori.dto.SMAddMangaResponse +import eu.kanade.tachiyomi.data.track.shikimori.dto.SMLibraryIdResponse import eu.kanade.tachiyomi.data.track.shikimori.dto.SMOAuth import eu.kanade.tachiyomi.data.track.shikimori.dto.SMSearchResult import eu.kanade.tachiyomi.data.track.shikimori.dto.SMUser @@ -12,6 +12,7 @@ import eu.kanade.tachiyomi.data.track.shikimori.dto.SMUserListResult import eu.kanade.tachiyomi.data.track.shikimori.dto.SMUserResult import eu.kanade.tachiyomi.network.DELETE import eu.kanade.tachiyomi.network.POST +import eu.kanade.tachiyomi.network.PUT import eu.kanade.tachiyomi.network.awaitSuccess import eu.kanade.tachiyomi.network.jsonMime import eu.kanade.tachiyomi.network.parseAs @@ -55,7 +56,7 @@ class ShikimoriApi( body = payload.toString().toRequestBody(jsonMime), ), ).awaitSuccess() - .parseAs() + .parseAs() .let { // save id of the entry for possible future delete request track.library_id = it.id @@ -65,7 +66,32 @@ class ShikimoriApi( } } - suspend fun updateLibManga(track: Track, userId: String): Track = addLibManga(track, userId) + suspend fun updateLibManga(track: Track): Track { + return withIOContext { + val payload = buildJsonObject { + putJsonObject("user_rate") { + put("chapters", track.last_chapter_read.toInt()) + put("score", track.score.toInt()) + put("status", track.toShikimoriStatus()) + } + } + + with(json) { + authClient.newCall( + PUT( + "$API_URL/v2/user_rates/${track.library_id}", + body = payload.toString().toRequestBody(jsonMime), + ), + ) + .awaitSuccess() + .parseAs() + .let { + track.library_id = it.id + } + track + } + } + } suspend fun deleteLibManga(track: DomainTrack) { withIOContext { @@ -175,7 +201,7 @@ class ShikimoriApi( } } - suspend fun findLibManga(track: Track, isRefresh: Boolean = false): Track? { + suspend fun findLibManga(track: Track): Track? { return withIOContext { val query = $$""" |query($id: String) { @@ -216,9 +242,11 @@ class ShikimoriApi( // userRate data which will be null if the title is not in the user's list. // If it was removed on Shikimori and is still linked in the app, notify user via returning null here // which throws an exception at the Shikimori.refresh call - if (isRefresh && listResult?.userRate == null) return@with null - - listResult?.toTrack(trackId) + if (listResult?.userRate == null) { + null + } else { + listResult.toTrack(trackId) + } } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/dto/SMAddMangaResponse.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/dto/SMLibraryIdResponse.kt similarity index 80% rename from app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/dto/SMAddMangaResponse.kt rename to app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/dto/SMLibraryIdResponse.kt index be5ee9f58..8fffc3513 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/dto/SMAddMangaResponse.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/dto/SMLibraryIdResponse.kt @@ -3,6 +3,6 @@ package eu.kanade.tachiyomi.data.track.shikimori.dto import kotlinx.serialization.Serializable @Serializable -data class SMAddMangaResponse( +data class SMLibraryIdResponse( val id: Long, )