fix(simkl): map anime episode coordinates

This commit is contained in:
tapframe 2026-07-22 20:26:03 +05:30
parent 942a111eec
commit e020b9700a
4 changed files with 100 additions and 9 deletions

View file

@ -231,11 +231,17 @@ internal fun buildSimklScrobbleBody(
json: Json = SimklMutationJson,
): String {
val media = event.media.toScrobbleMediaDto()
val usesTvStyleAnimeCoordinates = event.media.kind == TrackingMediaKind.ANIME &&
event.media.episode?.season != null
val request = SimklScrobbleRequestDto(
progress = event.progressPercent.clampAndRoundProgress(),
movie = media.takeIf { event.media.kind == TrackingMediaKind.MOVIE },
show = media.takeIf { event.media.kind == TrackingMediaKind.SHOW },
anime = media.takeIf { event.media.kind == TrackingMediaKind.ANIME },
show = media.takeIf {
event.media.kind == TrackingMediaKind.SHOW || usesTvStyleAnimeCoordinates
},
anime = media.takeIf {
event.media.kind == TrackingMediaKind.ANIME && !usesTvStyleAnimeCoordinates
},
episode = event.media.episode?.toEpisodeDto(
includeSeason = true,
includeWatchedAt = false,
@ -316,6 +322,7 @@ private fun buildShowHistoryItem(
includeWatchedAt = includeWatchedAt,
episodes = flatEpisodes,
seasons = seasons,
useTvdbAnimeSeasons = first.media.kind == TrackingMediaKind.ANIME && seasons.isNotEmpty(),
)
}
@ -325,6 +332,7 @@ private fun TrackingMediaReference.toHistoryItemDto(
status: String? = null,
episodes: List<SimklEpisodeMutationDto> = emptyList(),
seasons: List<SimklSeasonMutationDto> = emptyList(),
useTvdbAnimeSeasons: Boolean = false,
): SimklHistoryItemDto = SimklHistoryItemDto(
title = title.nonBlankOrNull(),
year = year,
@ -333,6 +341,7 @@ private fun TrackingMediaReference.toHistoryItemDto(
status = status,
episodes = episodes,
seasons = seasons,
useTvdbAnimeSeasons = useTvdbAnimeSeasons,
)
private fun TrackingMediaReference.toScrobbleMediaDto(): SimklScrobbleMediaDto =
@ -508,6 +517,7 @@ private data class SimklHistoryItemDto(
val status: String? = null,
val episodes: List<SimklEpisodeMutationDto> = emptyList(),
val seasons: List<SimklSeasonMutationDto> = emptyList(),
@SerialName("use_tvdb_anime_seasons") val useTvdbAnimeSeasons: Boolean = false,
)
@Serializable

View file

@ -48,9 +48,9 @@ internal fun SimklSyncSnapshot.toSimklWatchedProjection(): SimklWatchedProjectio
var hasEpisodeHistory = false
entry.seasons.forEach seasonLoop@{ season ->
val seasonNumber = season.number ?: return@seasonLoop
season.episodes.forEach episodeLoop@{ episode ->
val episodeNumber = episode.number ?: return@episodeLoop
val seasonNumber = episode.tvdb?.season ?: season.number ?: return@episodeLoop
val episodeNumber = episode.tvdb?.episode ?: episode.number ?: return@episodeLoop
val watchedAt = parseSimklUtcEpochMs(episode.watchedAt) ?: return@episodeLoop
hasEpisodeHistory = true
watchedItems += WatchedItem(

View file

@ -67,6 +67,40 @@ class SimklMutationRepositoryTest {
assertEquals("2023-11-14T22:13:20Z", movieItem.getValue("watched_at").jsonPrimitive.content)
}
@Test
fun `anime history uses tvdb mapping flag only for seasonal coordinates`() {
val seasonalBody = buildSimklHistoryMutationBody(
listOf(
TrackingHistoryItem(
anime(TrackingEpisode(season = 2, number = 4)),
watchedAtEpochMs = 1_700_000_000_000L,
),
),
).asObject()
val seasonalAnime = seasonalBody.getValue("shows").jsonArray.single().jsonObject
assertTrue(seasonalAnime.getValue("use_tvdb_anime_seasons").jsonPrimitive.content.toBoolean())
val flatBody = buildSimklHistoryMutationBody(
listOf(
TrackingHistoryItem(
anime(TrackingEpisode(number = 4)),
watchedAtEpochMs = 1_700_000_000_000L,
),
),
).asObject()
assertNull(flatBody.getValue("shows").jsonArray.single().jsonObject["use_tvdb_anime_seasons"])
val showBody = buildSimklHistoryMutationBody(
listOf(
TrackingHistoryItem(
show(TrackingEpisode(season = 2, number = 4)),
watchedAtEpochMs = 1_700_000_000_000L,
),
),
).asObject()
assertNull(showBody.getValue("shows").jsonArray.single().jsonObject["use_tvdb_anime_seasons"])
}
@Test
fun `anime removal uses shows array and contains no response-only or watch fields`() {
val body = buildSimklHistoryRemovalBody(
@ -78,6 +112,12 @@ class SimklMutationRepositoryTest {
assertNull(item["watched_at"])
assertNull(item["status"])
assertEquals(4, item.getValue("episodes").jsonArray.single().jsonObject.getValue("number").jsonPrimitive.content.toInt())
val seasonalBody = buildSimklHistoryRemovalBody(
listOf(anime(TrackingEpisode(season = 2, number = 4))),
).asObject()
val seasonalItem = seasonalBody.getValue("shows").jsonArray.single().jsonObject
assertTrue(seasonalItem.getValue("use_tvdb_anime_seasons").jsonPrimitive.content.toBoolean())
}
@Test
@ -89,16 +129,27 @@ class SimklMutationRepositoryTest {
assertTrue("movie" in movieBody)
assertFalse("show" in movieBody)
val animeBody = buildSimklScrobbleBody(
val tvStyleAnimeBody = buildSimklScrobbleBody(
TrackingScrobbleEvent(
anime(TrackingEpisode(season = 2, number = 4)),
progressPercent = 42.236,
),
).asObject()
assertEquals(42.24, animeBody.getValue("progress").jsonPrimitive.content.toDouble())
assertTrue("anime" in animeBody)
assertEquals(2, animeBody.getValue("episode").jsonObject.getValue("season").jsonPrimitive.content.toInt())
assertEquals(4, animeBody.getValue("episode").jsonObject.getValue("number").jsonPrimitive.content.toInt())
assertEquals(42.24, tvStyleAnimeBody.getValue("progress").jsonPrimitive.content.toDouble())
assertTrue("show" in tvStyleAnimeBody)
assertFalse("anime" in tvStyleAnimeBody)
assertEquals(2, tvStyleAnimeBody.getValue("episode").jsonObject.getValue("season").jsonPrimitive.content.toInt())
assertEquals(4, tvStyleAnimeBody.getValue("episode").jsonObject.getValue("number").jsonPrimitive.content.toInt())
val nativeAnimeBody = buildSimklScrobbleBody(
TrackingScrobbleEvent(
anime(TrackingEpisode(number = 4)),
progressPercent = 42.236,
),
).asObject()
assertTrue("anime" in nativeAnimeBody)
assertFalse("show" in nativeAnimeBody)
assertNull(nativeAnimeBody.getValue("episode").jsonObject["season"])
}
@Test

View file

@ -142,6 +142,36 @@ class SimklProjectionsTest {
assertTrue(projection.fullyWatchedSeriesKeys.isEmpty())
}
@Test
fun `anime watched projection prefers mapped tvdb coordinates`() {
val anime = entry(
type = SimklMediaType.ANIME,
status = SimklListStatus.WATCHING,
id = 439744,
imdb = "tt2560140",
seasons = listOf(
SimklSeason(
number = 1,
episodes = listOf(
SimklEpisode(
number = 4,
watchedAt = "2023-11-14T23:13:20Z",
tvdb = SimklEpisodeMapping(season = 2, episode = 4),
),
),
),
),
)
val watched = SimklSyncSnapshot(entries = listOf(anime))
.toSimklWatchedProjection()
.items
.single()
assertEquals(2, watched.season)
assertEquals(4, watched.episode)
}
@Test
fun `playback projection preserves Simkl session identity and percentage`() {
val session = SimklPlaybackSession(