From 6a32bdf736b27169157d26da1348649b38e234b3 Mon Sep 17 00:00:00 2001 From: wiktorekdev Date: Sat, 12 Sep 2026 13:23:32 +0200 Subject: [PATCH] fix(player): support autoplay for selected episodes --- .../PlayerScreenRuntimeSourceActions.kt | 46 +++++++++++++++++++ .../player/skip/PlayerNextEpisodeRules.kt | 3 ++ .../player/skip/PlayerNextEpisodeRulesTest.kt | 29 ++++++++++++ 3 files changed, 78 insertions(+) create mode 100644 composeApp/src/commonTest/kotlin/com/nuvio/app/features/player/skip/PlayerNextEpisodeRulesTest.kt diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerScreenRuntimeSourceActions.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerScreenRuntimeSourceActions.kt index f3da69d504..4b44056a68 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerScreenRuntimeSourceActions.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerScreenRuntimeSourceActions.kt @@ -10,6 +10,8 @@ import com.nuvio.app.features.downloads.DownloadItem import com.nuvio.app.features.downloads.DownloadsRepository import com.nuvio.app.features.p2p.P2pSettingsRepository import com.nuvio.app.features.p2p.P2pStreamingEngine +import com.nuvio.app.features.player.skip.NextEpisodeInfo +import com.nuvio.app.features.player.skip.PlayerNextEpisodeRules import com.nuvio.app.features.streams.StreamItem import com.nuvio.app.features.streams.StreamLinkCacheRepository import com.nuvio.app.features.watchprogress.WatchProgressRepository @@ -402,6 +404,50 @@ internal fun PlayerScreenRuntime.playNextEpisode() { } } +/** + * Plays an episode selected outside the player UI through the same download, + * source resolution, and autoplay policy as next-episode playback. + */ +internal fun PlayerScreenRuntime.playEpisodeAtIndex(index: Int) { + val targetEpisode = PlayerNextEpisodeRules.resolvePlayableEpisodeAtIndex(playerMetaVideos, index) ?: return + scope.launchPlayerNextEpisodeAutoPlay( + previousJob = nextEpisodeAutoPlayJob, + nextEpisodeInfo = NextEpisodeInfo( + videoId = targetEpisode.id, + season = targetEpisode.season ?: return, + episode = targetEpisode.episode ?: return, + title = targetEpisode.title, + thumbnail = targetEpisode.thumbnail, + overview = targetEpisode.overview, + released = targetEpisode.released, + hasAired = true, + isWatched = false, + unairedMessage = null, + ), + allEpisodes = playerMetaVideos, + parentMetaId = parentMetaId, + parentMetaType = parentMetaType, + contentType = contentType, + settings = playerSettingsUiState, + currentStreamBingeGroup = currentStreamBingeGroup, + onDownloadedEpisodeSelected = { item, episode -> switchToDownloadedEpisode(item, episode) }, + onEpisodeStreamSelected = { stream, episode -> switchToEpisodeStream(stream, episode) }, + onManualSelectionRequired = { episode -> + episodeStreamsPanelState = EpisodeStreamsPanelState( + showStreams = true, + selectedEpisode = episode, + ) + showEpisodesPanel = true + }, + onSearchingChanged = { nextEpisodeAutoPlaySearching = it }, + onSourceNameChanged = { nextEpisodeAutoPlaySourceName = it }, + onCountdownChanged = { nextEpisodeAutoPlayCountdown = it }, + onNextEpisodeCardVisibleChanged = { showNextEpisodeCard = it }, + )?.let { job -> + nextEpisodeAutoPlayJob = job + } +} + internal fun PlayerScreenRuntime.openSourcesPanel() { val vid = activeVideoId ?: return PlayerStreamsRepository.loadSources( diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/skip/PlayerNextEpisodeRules.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/skip/PlayerNextEpisodeRules.kt index 54d6067514..1f8d37059f 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/skip/PlayerNextEpisodeRules.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/skip/PlayerNextEpisodeRules.kt @@ -4,6 +4,9 @@ import com.nuvio.app.features.details.MetaVideo object PlayerNextEpisodeRules { + fun resolvePlayableEpisodeAtIndex(videos: List, index: Int): MetaVideo? = + videos.getOrNull(index)?.takeIf { hasEpisodeAired(it.released) } + fun resolveNextEpisode( videos: List, currentSeason: Int?, diff --git a/composeApp/src/commonTest/kotlin/com/nuvio/app/features/player/skip/PlayerNextEpisodeRulesTest.kt b/composeApp/src/commonTest/kotlin/com/nuvio/app/features/player/skip/PlayerNextEpisodeRulesTest.kt new file mode 100644 index 0000000000..063bb2fc81 --- /dev/null +++ b/composeApp/src/commonTest/kotlin/com/nuvio/app/features/player/skip/PlayerNextEpisodeRulesTest.kt @@ -0,0 +1,29 @@ +package com.nuvio.app.features.player.skip + +import com.nuvio.app.features.details.MetaVideo +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNull + +class PlayerNextEpisodeRulesTest { + @Test + fun resolvesAiredEpisodeAtItsOriginalIndex() { + val episodes = listOf( + MetaVideo(id = "s1e2", title = "Episode 2", released = "2020-01-02"), + MetaVideo(id = "s1e1", title = "Episode 1", released = "2020-01-01"), + ) + + assertEquals("s1e1", PlayerNextEpisodeRules.resolvePlayableEpisodeAtIndex(episodes, 1)?.id) + } + + @Test + fun rejectsInvalidAndUnairedEpisodeIndices() { + val episodes = listOf( + MetaVideo(id = "future", title = "Future", released = "2999-01-01"), + ) + + assertNull(PlayerNextEpisodeRules.resolvePlayableEpisodeAtIndex(episodes, -1)) + assertNull(PlayerNextEpisodeRules.resolvePlayableEpisodeAtIndex(episodes, 1)) + assertNull(PlayerNextEpisodeRules.resolvePlayableEpisodeAtIndex(episodes, 0)) + } +}