From 0e5ba830d2fc85efc355a78c067d9f797b0ebac4 Mon Sep 17 00:00:00 2001 From: Aniket Tuli Date: Fri, 12 Jun 2026 02:37:10 -0700 Subject: [PATCH 1/2] fix: stop scrobbling next episode as watched and surface Trakt save errors Fixes #1196 Fixes #1192 Co-Authored-By: Claude Fable 5 --- .../app/features/library/LibraryRepository.kt | 10 +++++++++- .../player/PlayerScreenRuntimeEffects.kt | 11 ++++++++++- .../PlayerScreenRuntimePlaybackActions.kt | 17 ++++++++++++++++- 3 files changed, 35 insertions(+), 3 deletions(-) diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/library/LibraryRepository.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/library/LibraryRepository.kt index c4c86bb08..7e5242e13 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/library/LibraryRepository.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/library/LibraryRepository.kt @@ -2,6 +2,7 @@ package com.nuvio.app.features.library import co.touchlab.kermit.Logger import com.nuvio.app.core.auth.AuthRepository +import com.nuvio.app.core.ui.NuvioToastController import com.nuvio.app.core.auth.AuthState import com.nuvio.app.core.network.SupabaseProvider import com.nuvio.app.features.home.PosterShape @@ -40,6 +41,7 @@ import kotlinx.serialization.json.put import nuvio.composeapp.generated.resources.Res import nuvio.composeapp.generated.resources.library_local_tab_title import nuvio.composeapp.generated.resources.library_other +import nuvio.composeapp.generated.resources.trakt_lists_update_failed import org.jetbrains.compose.resources.StringResource import org.jetbrains.compose.resources.getString @@ -218,7 +220,13 @@ object LibraryRepository { if (isTraktLibrarySourceActive()) { syncScope.launch { runCatching { TraktLibraryRepository.toggleWatchlist(item) } - .onFailure { e -> log.e(e) { "Failed to toggle Trakt watchlist" } } + .onFailure { e -> + log.e(e) { "Failed to toggle Trakt watchlist" } + NuvioToastController.show( + e.message?.takeIf { it.isNotBlank() } + ?: getString(Res.string.trakt_lists_update_failed), + ) + } publish() } return diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerScreenRuntimeEffects.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerScreenRuntimeEffects.kt index e1e5f81fc..f688458e4 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerScreenRuntimeEffects.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerScreenRuntimeEffects.kt @@ -244,8 +244,17 @@ internal fun PlayerScreenRuntime.BindPlayerRuntimeEffects() { BindPlayerMetadataAndSkipEffects() DisposableEffect(playbackSession.videoId, activeSourceUrl, activeSourceAudioUrl) { + val effectVideoId = playbackSession.videoId + val effectSourceUrl = activeSourceUrl + val effectSourceAudioUrl = activeSourceAudioUrl onDispose { - flushWatchProgress() + if ( + playbackSession.videoId == effectVideoId && + activeSourceUrl == effectSourceUrl && + activeSourceAudioUrl == effectSourceAudioUrl + ) { + flushWatchProgress() + } } } diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerScreenRuntimePlaybackActions.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerScreenRuntimePlaybackActions.kt index 22828532e..98c843d35 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerScreenRuntimePlaybackActions.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerScreenRuntimePlaybackActions.kt @@ -120,8 +120,23 @@ internal fun PlayerScreenRuntime.emitTraktScrobbleStop(progressPercent: Float? = val percent = provided ?: currentPlaybackProgressPercent() val itemSnapshot = currentTraktScrobbleItem + val contentTypeSnapshot = contentType ?: parentMetaType + val parentMetaIdSnapshot = parentMetaId + val videoIdSnapshot = activeVideoId + val titleSnapshot = title + val seasonNumberSnapshot = activeSeasonNumber + val episodeNumberSnapshot = activeEpisodeNumber + val episodeTitleSnapshot = activeEpisodeTitle scope.launch(NonCancellable) { - val item = itemSnapshot ?: currentTraktScrobbleItem() ?: return@launch + val item = itemSnapshot ?: TraktScrobbleRepository.buildItem( + contentType = contentTypeSnapshot, + parentMetaId = parentMetaIdSnapshot, + videoId = videoIdSnapshot, + title = titleSnapshot, + seasonNumber = seasonNumberSnapshot, + episodeNumber = episodeNumberSnapshot, + episodeTitle = episodeTitleSnapshot, + ) ?: return@launch TraktScrobbleRepository.scrobbleStop( item = item, progressPercent = percent, From 582e0ff40ecc7e6ea6115be369c7db1bd86098a1 Mon Sep 17 00:00:00 2001 From: Aniket Tuli Date: Fri, 12 Jun 2026 03:03:41 -0700 Subject: [PATCH 2/2] refactor(trakt): snapshot scrobble item inputs through a single holder The stop-scrobble path snapshots its inputs via snapshotTraktScrobbleItemInputs() returning a TraktScrobbleItemInputs holder instead of seven loose locals, so adding a field to the item cannot silently miss the snapshot. Co-Authored-By: Claude Fable 5 --- .../PlayerScreenRuntimePlaybackActions.kt | 53 +++++++++++-------- 1 file changed, 31 insertions(+), 22 deletions(-) diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerScreenRuntimePlaybackActions.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerScreenRuntimePlaybackActions.kt index 98c843d35..52ee175e5 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerScreenRuntimePlaybackActions.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerScreenRuntimePlaybackActions.kt @@ -80,17 +80,40 @@ internal fun PlayerScreenRuntime.currentPlaybackProgressPercent( .coerceIn(0f, 100f) } -internal suspend fun PlayerScreenRuntime.currentTraktScrobbleItem() = +internal data class TraktScrobbleItemInputs( + val contentType: String, + val parentMetaId: String, + val videoId: String?, + val title: String, + val seasonNumber: Int?, + val episodeNumber: Int?, + val episodeTitle: String?, +) + +internal fun PlayerScreenRuntime.snapshotTraktScrobbleItemInputs() = TraktScrobbleItemInputs( + contentType = contentType ?: parentMetaType, + parentMetaId = parentMetaId, + videoId = activeVideoId, + title = title, + seasonNumber = activeSeasonNumber, + episodeNumber = activeEpisodeNumber, + episodeTitle = activeEpisodeTitle, +) + +private suspend fun TraktScrobbleItemInputs.buildItem() = TraktScrobbleRepository.buildItem( - contentType = contentType ?: parentMetaType, + contentType = contentType, parentMetaId = parentMetaId, - videoId = activeVideoId, + videoId = videoId, title = title, - seasonNumber = activeSeasonNumber, - episodeNumber = activeEpisodeNumber, - episodeTitle = activeEpisodeTitle, + seasonNumber = seasonNumber, + episodeNumber = episodeNumber, + episodeTitle = episodeTitle, ) +internal suspend fun PlayerScreenRuntime.currentTraktScrobbleItem() = + snapshotTraktScrobbleItemInputs().buildItem() + internal fun PlayerScreenRuntime.emitTraktScrobbleStart() { if (hasRequestedScrobbleStartForCurrentItem) return hasRequestedScrobbleStartForCurrentItem = true @@ -120,23 +143,9 @@ internal fun PlayerScreenRuntime.emitTraktScrobbleStop(progressPercent: Float? = val percent = provided ?: currentPlaybackProgressPercent() val itemSnapshot = currentTraktScrobbleItem - val contentTypeSnapshot = contentType ?: parentMetaType - val parentMetaIdSnapshot = parentMetaId - val videoIdSnapshot = activeVideoId - val titleSnapshot = title - val seasonNumberSnapshot = activeSeasonNumber - val episodeNumberSnapshot = activeEpisodeNumber - val episodeTitleSnapshot = activeEpisodeTitle + val inputsSnapshot = snapshotTraktScrobbleItemInputs() scope.launch(NonCancellable) { - val item = itemSnapshot ?: TraktScrobbleRepository.buildItem( - contentType = contentTypeSnapshot, - parentMetaId = parentMetaIdSnapshot, - videoId = videoIdSnapshot, - title = titleSnapshot, - seasonNumber = seasonNumberSnapshot, - episodeNumber = episodeNumberSnapshot, - episodeTitle = episodeTitleSnapshot, - ) ?: return@launch + val item = itemSnapshot ?: inputsSnapshot.buildItem() ?: return@launch TraktScrobbleRepository.scrobbleStop( item = item, progressPercent = percent,