feat: complete wiring bingeGroup handling for stream selection and update related components

This commit is contained in:
tapframe 2026-04-08 14:32:15 +05:30
parent 3107ea162e
commit f37c37d85d
5 changed files with 217 additions and 11 deletions

View file

@ -964,6 +964,7 @@ private fun MainAppContent(
episodeThumbnail = route.episodeThumbnail,
streamTitle = stream.streamLabel,
streamSubtitle = stream.streamSubtitle,
bingeGroup = stream.behaviorHints.bingeGroup,
pauseDescription = pauseDescription,
providerName = stream.addonName,
providerAddonId = stream.addonId,
@ -1039,6 +1040,7 @@ private fun MainAppContent(
episodeThumbnail = route.episodeThumbnail,
streamTitle = stream.streamLabel,
streamSubtitle = stream.streamSubtitle,
bingeGroup = stream.behaviorHints.bingeGroup,
pauseDescription = pauseDescription,
providerName = stream.addonName,
providerAddonId = stream.addonId,
@ -1101,6 +1103,7 @@ private fun MainAppContent(
episodeThumbnail = launch.episodeThumbnail,
streamTitle = launch.streamTitle,
streamSubtitle = launch.streamSubtitle,
initialBingeGroup = launch.bingeGroup,
pauseDescription = launch.pauseDescription,
providerName = launch.providerName,
providerAddonId = launch.providerAddonId,

View file

@ -21,6 +21,7 @@ data class PlayerLaunch(
val episodeThumbnail: String? = null,
val streamTitle: String,
val streamSubtitle: String? = null,
val bingeGroup: String? = null,
val pauseDescription: String? = null,
val providerName: String,
val providerAddonId: String? = null,

View file

@ -88,6 +88,7 @@ fun PlayerScreen(
providerName: String,
streamTitle: String,
streamSubtitle: String?,
initialBingeGroup: String? = null,
pauseDescription: String? = null,
onBack: () -> Unit,
modifier: Modifier = Modifier,
@ -135,6 +136,7 @@ fun PlayerScreen(
var activeStreamSubtitle by rememberSaveable { mutableStateOf(streamSubtitle) }
var activeProviderName by rememberSaveable { mutableStateOf(providerName) }
var activeProviderAddonId by rememberSaveable { mutableStateOf(providerAddonId) }
var currentStreamBingeGroup by rememberSaveable { mutableStateOf(initialBingeGroup) }
var activeSeasonNumber by rememberSaveable { mutableStateOf(seasonNumber) }
var activeEpisodeNumber by rememberSaveable { mutableStateOf(episodeNumber) }
var activeEpisodeTitle by rememberSaveable { mutableStateOf(episodeTitle) }
@ -531,6 +533,7 @@ fun PlayerScreen(
activeStreamSubtitle = stream.streamSubtitle
activeProviderName = stream.addonName
activeProviderAddonId = stream.addonId
currentStreamBingeGroup = stream.behaviorHints.bingeGroup
activeInitialPositionMs = currentPositionMs
activeInitialProgressFraction = null
showSourcesPanel = false
@ -584,6 +587,7 @@ fun PlayerScreen(
activeStreamSubtitle = stream.streamSubtitle
activeProviderName = stream.addonName
activeProviderAddonId = stream.addonId
currentStreamBingeGroup = stream.behaviorHints.bingeGroup
activeSeasonNumber = episode.season
activeEpisodeNumber = episode.episode
activeEpisodeTitle = episode.title
@ -606,19 +610,39 @@ fun PlayerScreen(
val type = contentType ?: parentMetaType
val settings = playerSettingsUiState
val shouldAutoSelectInManualMode =
settings.streamAutoPlayMode == StreamAutoPlayMode.MANUAL &&
(
settings.streamAutoPlayNextEpisodeEnabled ||
settings.streamAutoPlayPreferBingeGroup
)
// Determine auto-play mode for next episode
val effectiveMode = if (settings.streamAutoPlayMode == StreamAutoPlayMode.MANUAL) {
if (settings.streamAutoPlayNextEpisodeEnabled) StreamAutoPlayMode.FIRST_STREAM
else StreamAutoPlayMode.MANUAL
val effectiveMode = if (shouldAutoSelectInManualMode) {
StreamAutoPlayMode.FIRST_STREAM
} else {
settings.streamAutoPlayMode
}
val currentBingeGroup = if (settings.streamAutoPlayPreferBingeGroup) {
// Try to find binge group of current stream (not directly available, pass empty)
val effectiveSource = if (shouldAutoSelectInManualMode) {
com.nuvio.app.features.streams.StreamAutoPlaySource.ALL_SOURCES
} else {
settings.streamAutoPlaySource
}
val effectiveSelectedAddons = if (shouldAutoSelectInManualMode) {
emptySet()
} else {
settings.streamAutoPlaySelectedAddons
}
val effectiveSelectedPlugins = if (shouldAutoSelectInManualMode) {
emptySet()
} else {
settings.streamAutoPlaySelectedPlugins
}
val effectiveRegex = if (shouldAutoSelectInManualMode) {
""
} else ""
} else {
settings.streamAutoPlayRegex
}
nextEpisodeAutoPlayJob = scope.launch {
PlayerStreamsRepository.loadEpisodeStreams(
@ -646,11 +670,17 @@ fun PlayerScreen(
StreamAutoPlaySelector.selectAutoPlayStream(
streams = allStreams,
mode = effectiveMode,
regexPattern = settings.streamAutoPlayRegex,
source = settings.streamAutoPlaySource,
regexPattern = effectiveRegex,
source = effectiveSource,
installedAddonNames = installedAddonNames,
selectedAddons = settings.streamAutoPlaySelectedAddons,
selectedPlugins = settings.streamAutoPlaySelectedPlugins,
selectedAddons = effectiveSelectedAddons,
selectedPlugins = effectiveSelectedPlugins,
preferredBingeGroup = if (settings.streamAutoPlayPreferBingeGroup) {
currentStreamBingeGroup
} else {
null
},
preferBingeGroupInSelection = settings.streamAutoPlayPreferBingeGroup,
)
} else null

View file

@ -10,6 +10,8 @@ object StreamAutoPlaySelector {
installedAddonNames: Set<String>,
selectedAddons: Set<String>,
selectedPlugins: Set<String>,
preferredBingeGroup: String? = null,
preferBingeGroupInSelection: Boolean = false,
): StreamItem? {
if (streams.isEmpty()) return null
@ -29,6 +31,14 @@ object StreamAutoPlaySelector {
if (candidateStreams.isEmpty()) return null
if (mode == StreamAutoPlayMode.MANUAL) return null
val targetBingeGroup = preferredBingeGroup?.trim().orEmpty()
if (preferBingeGroupInSelection && targetBingeGroup.isNotEmpty()) {
val bingeGroupMatch = candidateStreams.firstOrNull { stream ->
stream.behaviorHints.bingeGroup == targetBingeGroup && stream.directPlaybackUrl != null
}
if (bingeGroupMatch != null) return bingeGroupMatch
}
return when (mode) {
StreamAutoPlayMode.MANUAL -> null
StreamAutoPlayMode.FIRST_STREAM -> candidateStreams.firstOrNull { it.directPlaybackUrl != null }

View file

@ -0,0 +1,162 @@
package com.nuvio.app.features.streams
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNull
class StreamAutoPlaySelectorTest {
@Test
fun `bingeGroup-first selects matching stream before first stream mode`() {
val first = stream(
addonName = "AddonA",
url = "https://example.com/first.m3u8",
name = "1080p",
bingeGroup = "other-group",
)
val preferred = stream(
addonName = "AddonB",
url = "https://example.com/preferred.m3u8",
name = "720p",
bingeGroup = "same-group",
)
val selected = StreamAutoPlaySelector.selectAutoPlayStream(
streams = listOf(first, preferred),
mode = StreamAutoPlayMode.FIRST_STREAM,
regexPattern = "",
source = StreamAutoPlaySource.ALL_SOURCES,
installedAddonNames = setOf("AddonA", "AddonB"),
selectedAddons = emptySet(),
selectedPlugins = emptySet(),
preferredBingeGroup = "same-group",
preferBingeGroupInSelection = true,
)
assertEquals(preferred, selected)
}
@Test
fun `falls back to normal mode when no bingeGroup match exists`() {
val first = stream(
addonName = "AddonA",
url = "https://example.com/first.m3u8",
name = "First",
bingeGroup = "group-a",
)
val second = stream(
addonName = "AddonB",
url = "https://example.com/second.m3u8",
name = "Second",
bingeGroup = "group-b",
)
val selected = StreamAutoPlaySelector.selectAutoPlayStream(
streams = listOf(first, second),
mode = StreamAutoPlayMode.FIRST_STREAM,
regexPattern = "",
source = StreamAutoPlaySource.ALL_SOURCES,
installedAddonNames = setOf("AddonA", "AddonB"),
selectedAddons = emptySet(),
selectedPlugins = emptySet(),
preferredBingeGroup = "missing-group",
preferBingeGroupInSelection = true,
)
assertEquals(first, selected)
}
@Test
fun `bingeGroup-first respects source and addon plugin filters`() {
val filteredOutAddonMatch = stream(
addonName = "AddonFilteredOut",
url = "https://example.com/addon-match.m3u8",
bingeGroup = "same-group",
)
val allowedPluginMatch = stream(
addonName = "PluginAllowed",
url = "https://example.com/plugin-match.m3u8",
bingeGroup = "same-group",
)
val selected = StreamAutoPlaySelector.selectAutoPlayStream(
streams = listOf(filteredOutAddonMatch, allowedPluginMatch),
mode = StreamAutoPlayMode.FIRST_STREAM,
regexPattern = "",
source = StreamAutoPlaySource.ENABLED_PLUGINS_ONLY,
installedAddonNames = setOf("AddonFilteredOut"),
selectedAddons = emptySet(),
selectedPlugins = setOf("PluginAllowed"),
preferredBingeGroup = "same-group",
preferBingeGroupInSelection = true,
)
assertEquals(allowedPluginMatch, selected)
}
@Test
fun `blank preferredBingeGroup behaves as disabled`() {
val first = stream(
addonName = "AddonA",
url = "https://example.com/first.m3u8",
bingeGroup = "group-a",
)
val second = stream(
addonName = "AddonB",
url = "https://example.com/second.m3u8",
bingeGroup = "group-b",
)
val selected = StreamAutoPlaySelector.selectAutoPlayStream(
streams = listOf(first, second),
mode = StreamAutoPlayMode.FIRST_STREAM,
regexPattern = "",
source = StreamAutoPlaySource.ALL_SOURCES,
installedAddonNames = setOf("AddonA", "AddonB"),
selectedAddons = emptySet(),
selectedPlugins = emptySet(),
preferredBingeGroup = " ",
preferBingeGroupInSelection = true,
)
assertEquals(first, selected)
}
@Test
fun `manual mode remains manual even with matching bingeGroup`() {
val matched = stream(
addonName = "AddonA",
url = "https://example.com/match.m3u8",
bingeGroup = "same-group",
)
val selected = StreamAutoPlaySelector.selectAutoPlayStream(
streams = listOf(matched),
mode = StreamAutoPlayMode.MANUAL,
regexPattern = "",
source = StreamAutoPlaySource.ALL_SOURCES,
installedAddonNames = setOf("AddonA"),
selectedAddons = emptySet(),
selectedPlugins = emptySet(),
preferredBingeGroup = "same-group",
preferBingeGroupInSelection = true,
)
assertNull(selected)
}
private fun stream(
addonName: String,
url: String? = null,
name: String? = null,
bingeGroup: String? = null,
): StreamItem = StreamItem(
name = name,
url = url,
addonName = addonName,
addonId = addonName,
behaviorHints = StreamBehaviorHints(
bingeGroup = bingeGroup,
),
)
}