diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerScreen.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerScreen.kt index 4cc99511f..9bd6d364a 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerScreen.kt @@ -269,6 +269,12 @@ fun PlayerScreen( activeSeasonNumber, activeEpisodeNumber, ) { mutableStateOf(false) } + var scrobbleStartRequestGeneration by remember( + activeSourceUrl, + activeVideoId, + activeSeasonNumber, + activeEpisodeNumber, + ) { mutableStateOf(0L) } var hasSentCompletionScrobbleForCurrentItem by remember( activeVideoId, activeSeasonNumber, @@ -422,6 +428,8 @@ fun PlayerScreen( fun emitTraktScrobbleStart() { if (hasRequestedScrobbleStartForCurrentItem) return hasRequestedScrobbleStartForCurrentItem = true + val requestGeneration = scrobbleStartRequestGeneration + 1L + scrobbleStartRequestGeneration = requestGeneration scope.launch { val item = currentTraktScrobbleItem() @@ -429,6 +437,9 @@ fun PlayerScreen( hasRequestedScrobbleStartForCurrentItem = false return@launch } + if (requestGeneration != scrobbleStartRequestGeneration || !hasRequestedScrobbleStartForCurrentItem) { + return@launch + } currentTraktScrobbleItem = item TraktScrobbleRepository.scrobbleStart( item = item, @@ -452,6 +463,7 @@ fun PlayerScreen( } currentTraktScrobbleItem = null hasRequestedScrobbleStartForCurrentItem = false + scrobbleStartRequestGeneration += 1L } fun emitStopScrobbleForCurrentProgress() { diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/watched/WatchedRepository.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/watched/WatchedRepository.kt index 3735763f8..fffae188f 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/watched/WatchedRepository.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/watched/WatchedRepository.kt @@ -28,6 +28,16 @@ private data class StoredWatchedPayload( val lastSuccessfulPushEpochMs: Long = 0L, ) +internal enum class WatchedTraktHistorySync { + Mirror, + Skip, +} + +internal fun shouldMirrorWatchedMarkToTraktHistory( + sync: WatchedTraktHistorySync, + isTraktAuthenticated: Boolean, +): Boolean = sync == WatchedTraktHistorySync.Mirror && isTraktAuthenticated + object WatchedRepository { private const val watchedItemsPageSize = 900 @@ -134,6 +144,17 @@ object WatchedRepository { } fun markWatched(items: Collection) { + markWatched(items = items, traktHistorySync = WatchedTraktHistorySync.Mirror) + } + + internal fun markWatchedFromPlaybackCompletion(item: WatchedItem) { + markWatched(items = listOf(item), traktHistorySync = WatchedTraktHistorySync.Skip) + } + + private fun markWatched( + items: Collection, + traktHistorySync: WatchedTraktHistorySync, + ) { ensureLoaded() if (items.isEmpty()) return val markedAt = WatchedClock.nowEpochMs() @@ -146,7 +167,7 @@ object WatchedRepository { } publish() persist() - pushMarksToServer(timestampedItems) + pushMarksToServer(timestampedItems, traktHistorySync) } fun unmarkWatched(item: WatchedItem) { @@ -223,13 +244,22 @@ object WatchedRepository { } } - private fun pushMarksToServer(items: Collection) { + private fun pushMarksToServer( + items: Collection, + traktHistorySync: WatchedTraktHistorySync, + ) { syncScope.launch { runCatching { if (items.isEmpty()) return@runCatching val profileId = ProfileRepository.activeProfileId - pushToActiveTargets(profileId = profileId, items = items) - recordSuccessfulPush(profileId = profileId, items = items) + val pushed = pushToActiveTargets( + profileId = profileId, + items = items, + traktHistorySync = traktHistorySync, + ) + if (pushed) { + recordSuccessfulPush(profileId = profileId, items = items) + } }.onFailure { e -> log.e(e) { "Failed to push watched items" } } @@ -296,16 +326,24 @@ object WatchedRepository { private suspend fun pushToActiveTargets( profileId: Int, items: Collection, - ) { + traktHistorySync: WatchedTraktHistorySync, + ): Boolean { + val shouldMirrorToTrakt = shouldMirrorWatchedMarkToTraktHistory( + sync = traktHistorySync, + isTraktAuthenticated = TraktAuthRepository.isAuthenticated.value, + ) + if (shouldUseTraktWatchedSync()) { + if (!shouldMirrorToTrakt) return false TraktWatchedSyncAdapter.push(profileId = profileId, items = items) - return + return true } syncAdapter.push(profileId = profileId, items = items) - if (TraktAuthRepository.isAuthenticated.value) { + if (shouldMirrorToTrakt) { TraktWatchedSyncAdapter.push(profileId = profileId, items = items) } + return true } private suspend fun deleteFromActiveTargets( diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/watching/application/WatchingActions.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/watching/application/WatchingActions.kt index 4d2074226..0b73570a3 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/watching/application/WatchingActions.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/watching/application/WatchingActions.kt @@ -129,7 +129,7 @@ object WatchingActions { episode = entry.episodeNumber, markedAtEpochMs = entry.lastUpdatedEpochMs, ) - WatchedRepository.markWatched(watchedItem) + WatchedRepository.markWatchedFromPlaybackCompletion(watchedItem) if (!entry.isEpisode) return actionScope.launch { diff --git a/composeApp/src/commonTest/kotlin/com/nuvio/app/features/watched/WatchedRepositoryTest.kt b/composeApp/src/commonTest/kotlin/com/nuvio/app/features/watched/WatchedRepositoryTest.kt index 433568373..346457289 100644 --- a/composeApp/src/commonTest/kotlin/com/nuvio/app/features/watched/WatchedRepositoryTest.kt +++ b/composeApp/src/commonTest/kotlin/com/nuvio/app/features/watched/WatchedRepositoryTest.kt @@ -4,6 +4,7 @@ import com.nuvio.app.features.details.MetaDetails import com.nuvio.app.features.details.MetaVideo import kotlin.test.Test import kotlin.test.assertEquals +import kotlin.test.assertFalse import kotlin.test.assertTrue class WatchedRepositoryTest { @@ -97,4 +98,26 @@ class WatchedRepositoryTest { assertTrue(merged.isEmpty()) } + + @Test + fun playbackCompletionWatchedMarks_doNotMirrorToTraktHistory() { + assertFalse( + shouldMirrorWatchedMarkToTraktHistory( + sync = WatchedTraktHistorySync.Skip, + isTraktAuthenticated = true, + ), + ) + assertTrue( + shouldMirrorWatchedMarkToTraktHistory( + sync = WatchedTraktHistorySync.Mirror, + isTraktAuthenticated = true, + ), + ) + assertFalse( + shouldMirrorWatchedMarkToTraktHistory( + sync = WatchedTraktHistorySync.Mirror, + isTraktAuthenticated = false, + ), + ) + } }