From cac2c891c040f19fc25cb071533ab5ff656cdbdc Mon Sep 17 00:00:00 2001 From: tapframe <85391825+tapframe@users.noreply.github.com> Date: Wed, 22 Jul 2026 21:19:12 +0530 Subject: [PATCH] fix(trakt): preserve episode mapping identity --- .../app/features/tracking/TrackingMedia.kt | 12 +++++ .../features/trakt/TraktScrobbleRepository.kt | 52 +++++++++++++------ .../features/tracking/TrackingMediaTest.kt | 3 ++ .../trakt/TraktScrobbleRepositoryTest.kt | 28 ++++++++++ 4 files changed, 80 insertions(+), 15 deletions(-) create mode 100644 composeApp/src/commonTest/kotlin/com/nuvio/app/features/trakt/TraktScrobbleRepositoryTest.kt diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/tracking/TrackingMedia.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/tracking/TrackingMedia.kt index 16bbeeb7d..16faa8eb0 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/tracking/TrackingMedia.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/tracking/TrackingMedia.kt @@ -48,12 +48,19 @@ data class TrackingEpisode( val title: String? = null, ) +data class TrackingCatalogReference( + val contentId: String, + val contentType: String, + val videoId: String? = null, +) + data class TrackingMediaReference( val kind: TrackingMediaKind, val title: String? = null, val year: Int? = null, val ids: TrackingExternalIds = TrackingExternalIds(), val episode: TrackingEpisode? = null, + val catalog: TrackingCatalogReference? = null, ) { val hasResolvableIdentity: Boolean get() = ids.hasAny || !title.isNullOrBlank() @@ -146,6 +153,11 @@ fun buildTrackingMediaReference( title = episodeTitle, ) }, + catalog = TrackingCatalogReference( + contentId = parentMetaId.trim(), + contentType = contentType.trim(), + videoId = videoId?.trim()?.takeIf(String::isNotBlank), + ), ) } diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/trakt/TraktScrobbleRepository.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/trakt/TraktScrobbleRepository.kt index 55e7d3045..632f46d33 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/trakt/TraktScrobbleRepository.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/trakt/TraktScrobbleRepository.kt @@ -46,6 +46,33 @@ internal sealed interface TraktScrobbleItem { } } +internal data class TraktEpisodeMappingInput( + val contentId: String, + val contentType: String, + val videoId: String?, + val season: Int, + val episode: Int, + val episodeTitle: String?, +) + +internal fun TrackingMediaReference.toTraktEpisodeMappingInput(): TraktEpisodeMappingInput? { + val episodeReference = episode ?: return null + val season = episodeReference.season ?: return null + val contentId = catalog?.contentId?.takeIf(String::isNotBlank) + ?: ids.imdb?.takeIf(String::isNotBlank) + ?: ids.tmdb?.let { value -> "tmdb:$value" } + ?: ids.trakt?.let { value -> "trakt:$value" } + ?: return null + return TraktEpisodeMappingInput( + contentId = contentId, + contentType = catalog?.contentType?.takeIf(String::isNotBlank) ?: "series", + videoId = catalog?.videoId?.takeIf(String::isNotBlank), + season = season, + episode = episodeReference.number, + episodeTitle = episodeReference.title, + ) +} + internal object TraktScrobbleRepository : TrackingScrobbler { override val providerId: TrackingProviderId = TrackingProviderId.TRAKT @@ -170,27 +197,22 @@ internal object TraktScrobbleRepository : TrackingScrobbler { ) } - val episode = media.episode ?: return null - val season = episode.season ?: return null - val contentId = ids.imdb - ?: ids.tmdb?.let { value -> "tmdb:$value" } - ?: ids.trakt?.let { value -> "trakt:$value" } - ?: return null + val mappingInput = media.toTraktEpisodeMappingInput() ?: return null val mappedEpisode = TraktEpisodeMappingService.resolveEpisodeMapping( - contentId = contentId, - contentType = "series", - videoId = null, - season = season, - episode = episode.number, - episodeTitle = episode.title, + contentId = mappingInput.contentId, + contentType = mappingInput.contentType, + videoId = mappingInput.videoId, + season = mappingInput.season, + episode = mappingInput.episode, + episodeTitle = mappingInput.episodeTitle, ) return TraktScrobbleItem.Episode( showTitle = media.title, showYear = media.year, showIds = ids, - season = mappedEpisode?.season ?: season, - number = mappedEpisode?.episode ?: episode.number, - episodeTitle = episode.title, + season = mappedEpisode?.season ?: mappingInput.season, + number = mappedEpisode?.episode ?: mappingInput.episode, + episodeTitle = mappingInput.episodeTitle, ) } diff --git a/composeApp/src/commonTest/kotlin/com/nuvio/app/features/tracking/TrackingMediaTest.kt b/composeApp/src/commonTest/kotlin/com/nuvio/app/features/tracking/TrackingMediaTest.kt index f6c5d1bec..1bf98f735 100644 --- a/composeApp/src/commonTest/kotlin/com/nuvio/app/features/tracking/TrackingMediaTest.kt +++ b/composeApp/src/commonTest/kotlin/com/nuvio/app/features/tracking/TrackingMediaTest.kt @@ -51,5 +51,8 @@ class TrackingMediaTest { assertEquals(2016, media.year) assertEquals(2, media.episode?.season) assertEquals(7, media.episode?.number) + assertEquals("addon_specific_identifier", media.catalog?.contentId) + assertEquals("series", media.catalog?.contentType) + assertEquals("tt4574334:2:7", media.catalog?.videoId) } } diff --git a/composeApp/src/commonTest/kotlin/com/nuvio/app/features/trakt/TraktScrobbleRepositoryTest.kt b/composeApp/src/commonTest/kotlin/com/nuvio/app/features/trakt/TraktScrobbleRepositoryTest.kt new file mode 100644 index 000000000..bb6f64878 --- /dev/null +++ b/composeApp/src/commonTest/kotlin/com/nuvio/app/features/trakt/TraktScrobbleRepositoryTest.kt @@ -0,0 +1,28 @@ +package com.nuvio.app.features.trakt + +import com.nuvio.app.features.tracking.buildTrackingMediaReference +import kotlin.test.Test +import kotlin.test.assertEquals + +class TraktScrobbleRepositoryTest { + @Test + fun episodeMappingInput_preservesAddonCatalogIdentity() { + val media = buildTrackingMediaReference( + contentType = "anime", + parentMetaId = "anime-addon:the-crow-girl", + videoId = "tt33307200:1:3", + title = "The Crow Girl", + seasonNumber = 1, + episodeNumber = 3, + episodeTitle = "Episode 3", + ) + + val input = media.toTraktEpisodeMappingInput() + + assertEquals("anime-addon:the-crow-girl", input?.contentId) + assertEquals("anime", input?.contentType) + assertEquals("tt33307200:1:3", input?.videoId) + assertEquals(1, input?.season) + assertEquals(3, input?.episode) + } +}