From 97f0eeb5daf0c129fb3a280efbb92e9220e5d9fa Mon Sep 17 00:00:00 2001 From: tapframe <85391825+tapframe@users.noreply.github.com> Date: Mon, 30 Mar 2026 22:12:08 +0530 Subject: [PATCH] feat: implement 'Up Next from furthest episode' preference in Continue Watching settings and update related logic across components --- .../app/features/details/MetaDetailsScreen.kt | 5 +- .../details/SeriesPlaybackResolver.kt | 7 +- .../com/nuvio/app/features/home/HomeScreen.kt | 3 +- .../settings/ContinueWatchingSettingsPage.kt | 17 ++++ .../settings/SettingsFullScreenPages.kt | 1 + .../watching/application/WatchingState.kt | 2 + .../watching/domain/SeriesContinuity.kt | 12 ++- .../watching/domain/WatchingPolicies.kt | 85 ++++++++++--------- .../ContinueWatchingPreferencesRepository.kt | 9 ++ .../watchprogress/WatchProgressModels.kt | 1 + .../watching/domain/SeriesContinuityTest.kt | 70 +++++++++++++++ .../watching/domain/WatchingPoliciesTest.kt | 59 +++++++++++++ 12 files changed, 224 insertions(+), 47 deletions(-) diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/MetaDetailsScreen.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/MetaDetailsScreen.kt index 2a8a09d0f..f7bbff5e0 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/MetaDetailsScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/MetaDetailsScreen.kt @@ -55,6 +55,7 @@ import com.nuvio.app.features.watched.releasedEpisodesForSeason import com.nuvio.app.features.watchprogress.CurrentDateProvider import com.nuvio.app.features.watchprogress.WatchProgressRepository import com.nuvio.app.features.watchprogress.buildPlaybackVideoId +import com.nuvio.app.features.watchprogress.ContinueWatchingPreferencesRepository import com.nuvio.app.features.watching.application.WatchingActions import com.nuvio.app.features.watching.application.WatchingState @@ -148,11 +149,13 @@ fun MetaDetailsScreen( } val movieProgress = watchProgressUiState.byVideoId[meta.id] ?.takeUnless { it.isCompleted } - val seriesAction = remember(watchProgressUiState.entries, watchedUiState.items, meta, todayIsoDate) { + val cwPrefs by ContinueWatchingPreferencesRepository.uiState.collectAsStateWithLifecycle() + val seriesAction = remember(watchProgressUiState.entries, watchedUiState.items, meta, todayIsoDate, cwPrefs.upNextFromFurthestEpisode) { meta.seriesPrimaryAction( entries = watchProgressUiState.entries, watchedItems = watchedUiState.items, todayIsoDate = todayIsoDate, + preferFurthestEpisode = cwPrefs.upNextFromFurthestEpisode, ) } val seriesPauseDescription = remember(seriesAction, meta.id, meta.videos) { diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/SeriesPlaybackResolver.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/SeriesPlaybackResolver.kt index f374a3edd..8b30b1d7d 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/SeriesPlaybackResolver.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/SeriesPlaybackResolver.kt @@ -50,7 +50,7 @@ internal fun MetaDetails.nextReleasedEpisodeAfter( seasonNumber = seasonNumber, episodeNumber = episodeNumber, ) - return sortedEpisodes + val candidates = sortedEpisodes .dropWhile { episode -> buildPlaybackVideoId( content = WatchingContentRef(type = type, id = id), @@ -60,9 +60,10 @@ internal fun MetaDetails.nextReleasedEpisodeAfter( ) != watchedVideoId } .drop(1) - .firstOrNull { episode -> + .filter { episode -> isReleasedBy(todayIsoDate = todayIsoDate, releasedDate = episode.released) } + return candidates.firstOrNull { normalizeSeasonNumber(it.season) > 0 } } internal data class SeriesPrimaryAction( @@ -79,6 +80,7 @@ internal fun MetaDetails.seriesPrimaryAction( entries: List, watchedItems: List, todayIsoDate: String, + preferFurthestEpisode: Boolean = true, ): SeriesPrimaryAction? = decideSeriesPrimaryAction( content = WatchingContentRef(type = type, id = id), @@ -86,6 +88,7 @@ internal fun MetaDetails.seriesPrimaryAction( progressRecords = entries.map(WatchProgressEntry::toDomainProgressRecord), watchedRecords = watchedItems.map(WatchedItem::toDomainWatchedRecord), todayIsoDate = todayIsoDate, + preferFurthestEpisode = preferFurthestEpisode, )?.toLegacySeriesPrimaryAction() internal fun MetaVideo.playLabel(): String = diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/home/HomeScreen.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/home/HomeScreen.kt index a97551f93..1e97acc2c 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/home/HomeScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/home/HomeScreen.kt @@ -56,10 +56,11 @@ fun HomeScreen( val watchedUiState by WatchedRepository.uiState.collectAsStateWithLifecycle() val watchProgressUiState by WatchProgressRepository.uiState.collectAsStateWithLifecycle() - val latestCompletedBySeries = remember(watchProgressUiState.entries, watchedUiState.items) { + val latestCompletedBySeries = remember(watchProgressUiState.entries, watchedUiState.items, continueWatchingPreferences.upNextFromFurthestEpisode) { WatchingState.latestCompletedBySeries( progressEntries = watchProgressUiState.entries, watchedItems = watchedUiState.items, + preferFurthestEpisode = continueWatchingPreferences.upNextFromFurthestEpisode, ) } val completedSeriesCandidates = remember(latestCompletedBySeries) { diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/ContinueWatchingSettingsPage.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/ContinueWatchingSettingsPage.kt index 2372da6f7..976b6ed45 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/ContinueWatchingSettingsPage.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/ContinueWatchingSettingsPage.kt @@ -29,6 +29,7 @@ internal fun LazyListScope.continueWatchingSettingsContent( isTablet: Boolean, isVisible: Boolean, style: ContinueWatchingSectionStyle, + upNextFromFurthestEpisode: Boolean, ) { item { SettingsSection( @@ -58,6 +59,22 @@ internal fun LazyListScope.continueWatchingSettingsContent( ) } } + item { + SettingsSection( + title = "UP NEXT BEHAVIOR", + isTablet = isTablet, + ) { + SettingsGroup(isTablet = isTablet) { + SettingsSwitchRow( + title = "Up Next from furthest episode", + description = "When enabled, Up Next always continues from the furthest watched episode. When disabled, it follows from the most recently watched episode — useful if you rewatch earlier episodes.", + checked = upNextFromFurthestEpisode, + isTablet = isTablet, + onCheckedChange = ContinueWatchingPreferencesRepository::setUpNextFromFurthestEpisode, + ) + } + } + } } @Composable diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/SettingsFullScreenPages.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/SettingsFullScreenPages.kt index 4002623a5..4cf840e65 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/SettingsFullScreenPages.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/SettingsFullScreenPages.kt @@ -74,6 +74,7 @@ fun ContinueWatchingSettingsScreen( isTablet = false, isVisible = continueWatchingPreferencesUiState.isVisible, style = continueWatchingPreferencesUiState.style, + upNextFromFurthestEpisode = continueWatchingPreferencesUiState.upNextFromFurthestEpisode, ) } } diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/watching/application/WatchingState.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/watching/application/WatchingState.kt index f2f48c2f5..43c05175f 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/watching/application/WatchingState.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/watching/application/WatchingState.kt @@ -49,6 +49,7 @@ object WatchingState { fun latestCompletedBySeries( progressEntries: List, watchedItems: List, + preferFurthestEpisode: Boolean = true, ): Map { val contentRefs = buildSet { progressEntries.forEach { entry -> @@ -65,6 +66,7 @@ object WatchingState { content = content, progressRecords = progressRecords, watchedRecords = watchedRecords, + preferFurthestEpisode = preferFurthestEpisode, )?.let { completed -> content to completed } }.toMap() } diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/watching/domain/SeriesContinuity.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/watching/domain/SeriesContinuity.kt index bb3aed52f..f77e2740d 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/watching/domain/SeriesContinuity.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/watching/domain/SeriesContinuity.kt @@ -42,10 +42,11 @@ fun nextReleasedEpisodeAfter( compareBy({ normalizeSeasonNumber(it.seasonNumber) }, { it.episodeNumber ?: 0 }), ) val watchedVideoId = buildPlaybackVideoId(content, seasonNumber, episodeNumber) - return sortedEpisodes + val candidates = sortedEpisodes .dropWhile { episode -> buildPlaybackVideoId(content, episode.seasonNumber, episode.episodeNumber, episode.videoId) != watchedVideoId } .drop(1) - .firstOrNull { episode -> isReleasedBy(todayIsoDate = todayIsoDate, releasedDate = episode.releasedDate) } + .filter { episode -> isReleasedBy(todayIsoDate = todayIsoDate, releasedDate = episode.releasedDate) } + return candidates.firstOrNull { normalizeSeasonNumber(it.seasonNumber) > 0 } } fun decideSeriesPrimaryAction( @@ -54,6 +55,7 @@ fun decideSeriesPrimaryAction( progressRecords: List, watchedRecords: List, todayIsoDate: String, + preferFurthestEpisode: Boolean = true, ): WatchingSeriesPrimaryAction? { val resumeRecord = resumeProgressForSeries( content = content, @@ -63,6 +65,7 @@ fun decideSeriesPrimaryAction( content = content, progressRecords = progressRecords, watchedRecords = watchedRecords, + preferFurthestEpisode = preferFurthestEpisode, ) if (shouldPreferResume(resumeRecord = resumeRecord, latestCompletedEpisode = latestCompletedEpisode)) { @@ -78,9 +81,10 @@ fun decideSeriesPrimaryAction( todayIsoDate = todayIsoDate, ) } else { - episodes + val sorted = episodes .sortedWith(compareBy({ normalizeSeasonNumber(it.seasonNumber) }, { it.episodeNumber ?: 0 })) - .firstOrNull { episode -> isReleasedBy(todayIsoDate = todayIsoDate, releasedDate = episode.releasedDate) } + val released = sorted.filter { episode -> isReleasedBy(todayIsoDate = todayIsoDate, releasedDate = episode.releasedDate) } + released.firstOrNull { normalizeSeasonNumber(it.seasonNumber) > 0 } ?: released.firstOrNull() } return nextEpisode?.let { episode -> diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/watching/domain/WatchingPolicies.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/watching/domain/WatchingPolicies.kt index 162d0fb1c..d79c3bc2e 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/watching/domain/WatchingPolicies.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/watching/domain/WatchingPolicies.kt @@ -81,46 +81,53 @@ fun latestCompletedSeriesEpisode( content: WatchingContentRef, progressRecords: List, watchedRecords: List, + preferFurthestEpisode: Boolean = true, ): WatchingCompletedEpisode? { - val progressMarker = progressRecords - .asSequence() - .filter { record -> - record.content == content && - record.isCompleted && - record.seasonNumber != null && - record.episodeNumber != null - } - .mapNotNull { record -> - val seasonNumber = record.seasonNumber ?: return@mapNotNull null - val episodeNumber = record.episodeNumber ?: return@mapNotNull null - WatchingCompletedEpisode( - seasonNumber = seasonNumber, - episodeNumber = episodeNumber, - markedAtEpochMs = record.lastUpdatedEpochMs, - ) - } - .maxByOrNull { marker -> marker.markedAtEpochMs } - - val watchedMarker = watchedRecords - .asSequence() - .filter { record -> - record.content == content && - record.seasonNumber != null && - record.episodeNumber != null - } - .mapNotNull { record -> - val seasonNumber = record.seasonNumber ?: return@mapNotNull null - val episodeNumber = record.episodeNumber ?: return@mapNotNull null - WatchingCompletedEpisode( - seasonNumber = seasonNumber, - episodeNumber = episodeNumber, - markedAtEpochMs = record.markedAtEpochMs, - ) - } - .maxByOrNull { marker -> marker.markedAtEpochMs } - - return listOfNotNull(progressMarker, watchedMarker) - .maxByOrNull { marker -> marker.markedAtEpochMs } + val ordering = if (preferFurthestEpisode) { + compareBy( + { normalizeSeasonNumber(it.seasonNumber) }, + { it.episodeNumber }, + { it.markedAtEpochMs }, + ) + } else { + compareBy { it.markedAtEpochMs } + } + val allMarkers = buildList { + progressRecords + .asSequence() + .filter { record -> + record.content == content && + record.isCompleted && + record.seasonNumber != null && + record.episodeNumber != null + } + .mapNotNullTo(this) { record -> + val seasonNumber = record.seasonNumber ?: return@mapNotNullTo null + val episodeNumber = record.episodeNumber ?: return@mapNotNullTo null + WatchingCompletedEpisode( + seasonNumber = seasonNumber, + episodeNumber = episodeNumber, + markedAtEpochMs = record.lastUpdatedEpochMs, + ) + } + watchedRecords + .asSequence() + .filter { record -> + record.content == content && + record.seasonNumber != null && + record.episodeNumber != null + } + .mapNotNullTo(this) { record -> + val seasonNumber = record.seasonNumber ?: return@mapNotNullTo null + val episodeNumber = record.episodeNumber ?: return@mapNotNullTo null + WatchingCompletedEpisode( + seasonNumber = seasonNumber, + episodeNumber = episodeNumber, + markedAtEpochMs = record.markedAtEpochMs, + ) + } + } + return allMarkers.maxWithOrNull(ordering) } fun normalizeSeasonNumber(seasonNumber: Int?): Int = seasonNumber?.coerceAtLeast(0) ?: 0 diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/watchprogress/ContinueWatchingPreferencesRepository.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/watchprogress/ContinueWatchingPreferencesRepository.kt index 09e819a69..0f4cc9061 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/watchprogress/ContinueWatchingPreferencesRepository.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/watchprogress/ContinueWatchingPreferencesRepository.kt @@ -12,6 +12,7 @@ import kotlinx.serialization.json.Json private data class StoredContinueWatchingPreferences( val isVisible: Boolean = true, val style: ContinueWatchingSectionStyle = ContinueWatchingSectionStyle.Wide, + val upNextFromFurthestEpisode: Boolean = true, ) object ContinueWatchingPreferencesRepository { @@ -56,6 +57,7 @@ object ContinueWatchingPreferencesRepository { ContinueWatchingPreferencesUiState( isVisible = stored.isVisible, style = stored.style, + upNextFromFurthestEpisode = stored.upNextFromFurthestEpisode, ) } else { ContinueWatchingPreferencesUiState() @@ -74,12 +76,19 @@ object ContinueWatchingPreferencesRepository { persist() } + fun setUpNextFromFurthestEpisode(enabled: Boolean) { + ensureLoaded() + _uiState.value = _uiState.value.copy(upNextFromFurthestEpisode = enabled) + persist() + } + private fun persist() { ContinueWatchingPreferencesStorage.savePayload( json.encodeToString( StoredContinueWatchingPreferences( isVisible = _uiState.value.isVisible, style = _uiState.value.style, + upNextFromFurthestEpisode = _uiState.value.upNextFromFurthestEpisode, ), ), ) diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/watchprogress/WatchProgressModels.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/watchprogress/WatchProgressModels.kt index ab5aa99de..eb90775bc 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/watchprogress/WatchProgressModels.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/watchprogress/WatchProgressModels.kt @@ -103,6 +103,7 @@ data class ContinueWatchingItem( data class ContinueWatchingPreferencesUiState( val isVisible: Boolean = true, val style: ContinueWatchingSectionStyle = ContinueWatchingSectionStyle.Wide, + val upNextFromFurthestEpisode: Boolean = true, ) internal fun WatchProgressEntry.toContinueWatchingItem(): ContinueWatchingItem { diff --git a/composeApp/src/commonTest/kotlin/com/nuvio/app/features/watching/domain/SeriesContinuityTest.kt b/composeApp/src/commonTest/kotlin/com/nuvio/app/features/watching/domain/SeriesContinuityTest.kt index 38781346c..51727f2ef 100644 --- a/composeApp/src/commonTest/kotlin/com/nuvio/app/features/watching/domain/SeriesContinuityTest.kt +++ b/composeApp/src/commonTest/kotlin/com/nuvio/app/features/watching/domain/SeriesContinuityTest.kt @@ -75,4 +75,74 @@ class SeriesContinuityTest { assertEquals("show:1:2", action.videoId) assertEquals(1_500L, action.resumePositionMs) } + + @Test + fun decideSeriesPrimaryAction_skips_specials_for_initial_play() { + val episodesWithSpecials = listOf( + WatchingReleasedEpisode(videoId = "sp1", seasonNumber = 0, episodeNumber = 1, title = "Special 1", releasedDate = "2026-01-01"), + WatchingReleasedEpisode(videoId = "ep1", seasonNumber = 1, episodeNumber = 1, title = "Pilot", releasedDate = "2026-01-15"), + WatchingReleasedEpisode(videoId = "ep2", seasonNumber = 1, episodeNumber = 2, title = "Episode 2", releasedDate = "2026-01-22"), + WatchingReleasedEpisode(videoId = "ep3", seasonNumber = 2, episodeNumber = 1, title = "S2 Premiere", releasedDate = "2026-03-01"), + ) + val action = decideSeriesPrimaryAction( + content = show, + episodes = episodesWithSpecials, + progressRecords = emptyList(), + watchedRecords = emptyList(), + todayIsoDate = "2026-03-30", + ) + + assertNotNull(action) + assertEquals("Play S1E1", action.label) + assertEquals("show:1:1", action.videoId) + } + + @Test + fun decideSeriesPrimaryAction_falls_back_to_specials_when_no_main_season() { + val specialsOnly = listOf( + WatchingReleasedEpisode(videoId = "sp1", seasonNumber = 0, episodeNumber = 1, title = "Special 1", releasedDate = "2026-01-01"), + WatchingReleasedEpisode(videoId = "sp2", seasonNumber = 0, episodeNumber = 2, title = "Special 2", releasedDate = "2026-01-15"), + ) + val action = decideSeriesPrimaryAction( + content = show, + episodes = specialsOnly, + progressRecords = emptyList(), + watchedRecords = emptyList(), + todayIsoDate = "2026-03-30", + ) + + assertNotNull(action) + assertEquals("Play S0E1", action.label) + } + + @Test + fun decideSeriesPrimaryAction_skips_watched_episodes_after_rewatch() { + val twoSeasons = listOf( + WatchingReleasedEpisode(videoId = "ep1", seasonNumber = 1, episodeNumber = 1, title = "S1E1", releasedDate = "2026-01-01"), + WatchingReleasedEpisode(videoId = "ep2", seasonNumber = 1, episodeNumber = 2, title = "S1E2", releasedDate = "2026-01-08"), + WatchingReleasedEpisode(videoId = "ep3", seasonNumber = 1, episodeNumber = 3, title = "S1E3", releasedDate = "2026-01-15"), + WatchingReleasedEpisode(videoId = "ep4", seasonNumber = 2, episodeNumber = 1, title = "S2E1", releasedDate = "2026-03-01"), + WatchingReleasedEpisode(videoId = "ep5", seasonNumber = 2, episodeNumber = 2, title = "S2E2", releasedDate = "2026-03-08"), + ) + val action = decideSeriesPrimaryAction( + content = show, + episodes = twoSeasons, + progressRecords = listOf( + // All of season 1 completed + WatchingProgressRecord(content = show, videoId = "show:1:1", seasonNumber = 1, episodeNumber = 1, lastUpdatedEpochMs = 100L, isCompleted = true), + WatchingProgressRecord(content = show, videoId = "show:1:2", seasonNumber = 1, episodeNumber = 2, lastUpdatedEpochMs = 200L, isCompleted = true), + WatchingProgressRecord(content = show, videoId = "show:1:3", seasonNumber = 1, episodeNumber = 3, lastUpdatedEpochMs = 300L, isCompleted = true), + // S2E1 completed + WatchingProgressRecord(content = show, videoId = "show:2:1", seasonNumber = 2, episodeNumber = 1, lastUpdatedEpochMs = 400L, isCompleted = true), + // Re-watched S1E1 recently — newer timestamp but earlier episode + WatchingProgressRecord(content = show, videoId = "show:1:1", seasonNumber = 1, episodeNumber = 1, lastUpdatedEpochMs = 900L, isCompleted = true), + ), + watchedRecords = emptyList(), + todayIsoDate = "2026-03-30", + ) + + assertNotNull(action) + assertEquals("Up Next S2E2", action.label) + assertEquals("show:2:2", action.videoId) + } } diff --git a/composeApp/src/commonTest/kotlin/com/nuvio/app/features/watching/domain/WatchingPoliciesTest.kt b/composeApp/src/commonTest/kotlin/com/nuvio/app/features/watching/domain/WatchingPoliciesTest.kt index 0df28b1b4..7d5b6d62a 100644 --- a/composeApp/src/commonTest/kotlin/com/nuvio/app/features/watching/domain/WatchingPoliciesTest.kt +++ b/composeApp/src/commonTest/kotlin/com/nuvio/app/features/watching/domain/WatchingPoliciesTest.kt @@ -84,4 +84,63 @@ class WatchingPoliciesTest { assertEquals(3, latestCompleted.episodeNumber) assertEquals(300L, latestCompleted.markedAtEpochMs) } + + @Test + fun latestCompletedSeriesEpisode_picks_furthest_episode_even_with_older_timestamp() { + val latestCompleted = latestCompletedSeriesEpisode( + content = show, + progressRecords = listOf( + WatchingProgressRecord( + content = show, + videoId = "show:1:5", + seasonNumber = 1, + episodeNumber = 5, + lastUpdatedEpochMs = 100L, + isCompleted = true, + ), + WatchingProgressRecord( + content = show, + videoId = "show:1:1", + seasonNumber = 1, + episodeNumber = 1, + lastUpdatedEpochMs = 600L, + isCompleted = true, + ), + ), + watchedRecords = emptyList(), + ) + + assertNotNull(latestCompleted) + assertEquals(1, latestCompleted.seasonNumber) + assertEquals(5, latestCompleted.episodeNumber) + } + + @Test + fun latestCompletedSeriesEpisode_picks_furthest_season_over_recent_rewatch() { + val latestCompleted = latestCompletedSeriesEpisode( + content = show, + progressRecords = listOf( + WatchingProgressRecord( + content = show, + videoId = "show:2:3", + seasonNumber = 2, + episodeNumber = 3, + lastUpdatedEpochMs = 200L, + isCompleted = true, + ), + ), + watchedRecords = listOf( + WatchingWatchedRecord( + content = show, + seasonNumber = 1, + episodeNumber = 1, + markedAtEpochMs = 500L, + ), + ), + ) + + assertNotNull(latestCompleted) + assertEquals(2, latestCompleted.seasonNumber) + assertEquals(3, latestCompleted.episodeNumber) + } }