Fix Trakt progress watched precedence

This commit is contained in:
tapframe 2026-07-06 00:59:53 +05:30
parent 94e1141bd1
commit 4b6a15c950
3 changed files with 90 additions and 13 deletions

View file

@ -11,7 +11,7 @@ import com.nuvio.app.features.watchprogress.WatchProgressSourceTraktHistory
import com.nuvio.app.features.watchprogress.WatchProgressSourceTraktPlayback
import com.nuvio.app.features.watchprogress.WatchProgressSourceTraktShowProgress
import com.nuvio.app.features.watchprogress.buildPlaybackVideoId
import com.nuvio.app.features.watchprogress.shouldTreatAsInProgressForContinueWatching
import com.nuvio.app.features.watchprogress.shouldReplaceProgressSnapshotEntry
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred
@ -1007,18 +1007,6 @@ object TraktProgressRepository {
.sortedByDescending { it.lastUpdatedEpochMs }
}
private fun shouldReplaceProgressSnapshotEntry(
existing: WatchProgressEntry,
candidate: WatchProgressEntry,
): Boolean {
val existingInProgress = existing.shouldTreatAsInProgressForContinueWatching()
val candidateInProgress = candidate.shouldTreatAsInProgressForContinueWatching()
if (existingInProgress != candidateInProgress) {
return candidateInProgress
}
return candidate.lastUpdatedEpochMs > existing.lastUpdatedEpochMs
}
private fun mergeEntriesPreferRichMetadata(
current: List<WatchProgressEntry>,
hydrated: List<WatchProgressEntry>,

View file

@ -13,6 +13,7 @@ import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
internal const val ContinueWatchingLimit = DefaultContinueWatchingLimit
private const val WatchProgressSnapshotConflictToleranceMs = 1_000L
@Serializable
internal data class StoredWatchProgressPayload(
@ -124,6 +125,24 @@ internal fun WatchProgressEntry.shouldUseAsCompletedSeedForContinueWatching(): B
return explicitPercent >= WatchProgressTraktPlaybackNextUpSeedPercentThreshold
}
internal fun shouldReplaceProgressSnapshotEntry(
existing: WatchProgressEntry,
candidate: WatchProgressEntry,
): Boolean {
val normalizedExisting = existing.normalizedCompletion()
val normalizedCandidate = candidate.normalizedCompletion()
val existingInProgress = normalizedExisting.shouldTreatAsInProgressForContinueWatching()
val candidateInProgress = normalizedCandidate.shouldTreatAsInProgressForContinueWatching()
if (existingInProgress != candidateInProgress) {
val inProgressEntry = if (candidateInProgress) normalizedCandidate else normalizedExisting
val completedEntry = if (candidateInProgress) normalizedExisting else normalizedCandidate
val inProgressIsCurrentEnough =
inProgressEntry.lastUpdatedEpochMs >= completedEntry.lastUpdatedEpochMs - WatchProgressSnapshotConflictToleranceMs
return if (candidateInProgress) inProgressIsCurrentEnough else !inProgressIsCurrentEnough
}
return normalizedCandidate.lastUpdatedEpochMs > normalizedExisting.lastUpdatedEpochMs
}
internal fun shouldCascadeCompletedProgressToWatchedHistory(
entry: WatchProgressEntry,
isUsingTraktProgress: Boolean,

View file

@ -191,6 +191,76 @@ class WatchProgressRulesTest {
assertFalse(history.shouldTreatAsInProgressForContinueWatching())
}
@Test
fun `Trakt playback does not replace watched history when watched timestamp is newer`() {
val watched = entry(
videoId = "show:1:4",
parentMetaId = "show",
seasonNumber = 1,
episodeNumber = 4,
lastUpdatedEpochMs = 3_000L,
isCompleted = true,
progressPercent = 100f,
source = WatchProgressSourceTraktHistory,
)
val playback = watched.copy(
lastUpdatedEpochMs = 1_000L,
isCompleted = false,
progressPercent = 25f,
source = WatchProgressSourceTraktPlayback,
)
assertFalse(shouldReplaceProgressSnapshotEntry(existing = watched, candidate = playback))
assertTrue(shouldReplaceProgressSnapshotEntry(existing = playback, candidate = watched))
}
@Test
fun `Trakt playback replaces watched history when playback timestamp is newer`() {
val watched = entry(
videoId = "show:1:4",
parentMetaId = "show",
seasonNumber = 1,
episodeNumber = 4,
lastUpdatedEpochMs = 1_000L,
isCompleted = true,
progressPercent = 100f,
source = WatchProgressSourceTraktHistory,
)
val playback = watched.copy(
lastUpdatedEpochMs = 3_000L,
isCompleted = false,
progressPercent = 25f,
source = WatchProgressSourceTraktPlayback,
)
assertTrue(shouldReplaceProgressSnapshotEntry(existing = watched, candidate = playback))
assertFalse(shouldReplaceProgressSnapshotEntry(existing = playback, candidate = watched))
}
@Test
fun `Trakt playback uses TV timestamp tolerance against watched history`() {
val watched = entry(
videoId = "show:1:4",
parentMetaId = "show",
seasonNumber = 1,
episodeNumber = 4,
lastUpdatedEpochMs = 2_000L,
isCompleted = true,
progressPercent = 100f,
source = WatchProgressSourceTraktHistory,
)
val insideTolerance = watched.copy(
lastUpdatedEpochMs = 1_001L,
isCompleted = false,
progressPercent = 25f,
source = WatchProgressSourceTraktPlayback,
)
val outsideTolerance = insideTolerance.copy(lastUpdatedEpochMs = 999L)
assertTrue(shouldReplaceProgressSnapshotEntry(existing = watched, candidate = insideTolerance))
assertFalse(shouldReplaceProgressSnapshotEntry(existing = watched, candidate = outsideTolerance))
}
@Test
fun `completed progress does not cascade to watched history while Trakt progress is active`() {
val completed = entry(