diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/simkl/SimklSyncEngine.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/simkl/SimklSyncEngine.kt index d7c846e66..b0dbceb90 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/simkl/SimklSyncEngine.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/simkl/SimklSyncEngine.kt @@ -16,18 +16,11 @@ internal class SimklSyncEngine( } if (current.watermark == null) return initialSync() - val delta = remote.fetchAllItems( - SimklAllItemsRequest( - dateFrom = current.watermark, - includeEpisodeDetails = true, - ), - ) + val delta = remote.fetchAllItems(SimklAllItemsRequest.Changes(current.watermark)) var entries = mergeDelta(current.entries, delta) if (hasRemovalActivityChanged(current.activities, activities)) { - val authoritativeIds = remote.fetchAllItems( - SimklAllItemsRequest(idsOnly = true), - ) + val authoritativeIds = remote.fetchAllItems(SimklAllItemsRequest.CurrentIds) entries = reconcileRemovedEntries(entries, authoritativeIds) } @@ -52,7 +45,7 @@ internal class SimklSyncEngine( val entries = buildList { SimklMediaType.entries.forEach { type -> addAll( - remote.fetchAllItems(SimklAllItemsRequest(type = type)) + remote.fetchAllItems(SimklAllItemsRequest.Bootstrap(type)) .entriesFor(type), ) } diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/simkl/SimklSyncRemote.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/simkl/SimklSyncRemote.kt index 9279b0732..f1bad9ca1 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/simkl/SimklSyncRemote.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/simkl/SimklSyncRemote.kt @@ -6,12 +6,23 @@ import kotlinx.serialization.json.JsonArray import kotlinx.serialization.json.JsonNull import kotlinx.serialization.json.decodeFromJsonElement -internal data class SimklAllItemsRequest( - val type: SimklMediaType? = null, - val dateFrom: String? = null, - val includeEpisodeDetails: Boolean = false, - val idsOnly: Boolean = false, -) +internal sealed interface SimklAllItemsRequest { + val type: SimklMediaType? + + data class Bootstrap( + override val type: SimklMediaType, + ) : SimklAllItemsRequest + + data class Changes( + val dateFrom: String, + ) : SimklAllItemsRequest { + override val type: SimklMediaType? = null + } + + data object CurrentIds : SimklAllItemsRequest { + override val type: SimklMediaType? = null + } +} internal interface SimklSyncRemote { suspend fun fetchActivities(): SimklActivities @@ -38,17 +49,22 @@ internal class SimklApiSyncRemote( override suspend fun fetchAllItems(request: SimklAllItemsRequest): SimklAllItemsResponse { val path = request.type?.let { type -> "/sync/all-items/${type.apiValue}" } ?: "/sync/all-items" - val query = buildMap { - request.dateFrom?.let { value -> put("date_from", value) } - when { - request.idsOnly -> put("extended", "simkl_ids_only") - request.includeEpisodeDetails -> { - put("extended", "full_anime_seasons") - put("episode_watched_at", "yes") - put("episode_tvdb_id", "yes") - put("include_all_episodes", "original") - } - } + val query = when (request) { + is SimklAllItemsRequest.Bootstrap -> mapOf( + "extended" to "full", + "episode_watched_at" to "yes", + "include_all_episodes" to "yes", + ) + is SimklAllItemsRequest.Changes -> mapOf( + "date_from" to request.dateFrom, + "extended" to "full_anime_seasons", + "episode_watched_at" to "yes", + "episode_tvdb_id" to "yes", + "include_all_episodes" to "yes", + ) + SimklAllItemsRequest.CurrentIds -> mapOf( + "extended" to "simkl_ids_only", + ) } return client.execute( SimklApiRequest( diff --git a/composeApp/src/commonTest/kotlin/com/nuvio/app/features/simkl/SimklSyncEngineTest.kt b/composeApp/src/commonTest/kotlin/com/nuvio/app/features/simkl/SimklSyncEngineTest.kt index 0a02dfb3c..f9e0d3ab9 100644 --- a/composeApp/src/commonTest/kotlin/com/nuvio/app/features/simkl/SimklSyncEngineTest.kt +++ b/composeApp/src/commonTest/kotlin/com/nuvio/app/features/simkl/SimklSyncEngineTest.kt @@ -50,9 +50,14 @@ class SimklSyncEngineTest { assertEquals(1, result.playback.size) assertEquals(500L, result.lastSyncedAtEpochMs) assertTrue(remote.isExhausted) - assertTrue(remote.allItemsRequests.all { request -> - request.dateFrom == null && !request.includeEpisodeDetails && !request.idsOnly - }) + assertEquals( + listOf( + SimklAllItemsRequest.Bootstrap(SimklMediaType.SHOWS), + SimklAllItemsRequest.Bootstrap(SimklMediaType.MOVIES), + SimklAllItemsRequest.Bootstrap(SimklMediaType.ANIME), + ), + remote.allItemsRequests, + ) } @Test @@ -112,9 +117,13 @@ class SimklSyncEngineTest { ) assertEquals("3", result.playback.single().media?.ids?.idValue("simkl")) assertEquals("v2", result.watermark) - assertTrue(remote.allItemsRequests[0].includeEpisodeDetails) - assertEquals("v1", remote.allItemsRequests[0].dateFrom) - assertTrue(remote.allItemsRequests[1].idsOnly) + assertEquals( + listOf( + SimklAllItemsRequest.Changes("v1"), + SimklAllItemsRequest.CurrentIds, + ), + remote.allItemsRequests, + ) assertTrue(remote.isExhausted) } @@ -139,7 +148,7 @@ class SimklSyncEngineTest { } @Test - fun `remote keeps initial pull minimal and adds rich flags only to dated delta`() = runBlocking { + fun `remote applies documented bootstrap delta and reconciliation parameters`() = runBlocking { var now = 0L val urls = mutableListOf() val engine = SimklHttpEngine { _, url, _, _ -> @@ -156,21 +165,20 @@ class SimklSyncEngineTest { ) val remote = SimklApiSyncRemote(client) - remote.fetchAllItems(SimklAllItemsRequest(type = SimklMediaType.SHOWS)) - remote.fetchAllItems( - SimklAllItemsRequest( - dateFrom = "2026-05-08T14:23:11Z", - includeEpisodeDetails = true, - ), - ) - remote.fetchAllItems(SimklAllItemsRequest(idsOnly = true)) + remote.fetchAllItems(SimklAllItemsRequest.Bootstrap(SimklMediaType.SHOWS)) + remote.fetchAllItems(SimklAllItemsRequest.Changes("2026-05-08T14:23:11Z")) + remote.fetchAllItems(SimklAllItemsRequest.CurrentIds) assertFalse("date_from=" in urls[0]) - assertFalse("extended=" in urls[0]) + assertTrue("extended=full" in urls[0]) + assertTrue("episode_watched_at=yes" in urls[0]) + assertTrue("include_all_episodes=yes" in urls[0]) + assertFalse("episode_tvdb_id=" in urls[0]) assertTrue("date_from=2026-05-08T14%3A23%3A11Z" in urls[1]) assertTrue("extended=full_anime_seasons" in urls[1]) assertTrue("episode_watched_at=yes" in urls[1]) - assertTrue("include_all_episodes=original" in urls[1]) + assertTrue("episode_tvdb_id=yes" in urls[1]) + assertTrue("include_all_episodes=yes" in urls[1]) assertTrue("extended=simkl_ids_only" in urls[2]) } @@ -190,7 +198,7 @@ class SimklSyncEngineTest { ) val response = SimklApiSyncRemote(client).fetchAllItems( - SimklAllItemsRequest(type = SimklMediaType.ANIME), + SimklAllItemsRequest.Bootstrap(SimklMediaType.ANIME), ) assertTrue(response.entriesFor(SimklMediaType.ANIME).isEmpty())