Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ internal fun CoroutineScope.launchPlayerNextEpisodeAutoPlay(
contentType: String?,
settings: PlayerSettingsUiState,
currentStreamBingeGroup: String?,
currentStreamTitle: String? = null,
currentStreamProviderName: String? = null,
currentStreamProviderAddonId: String? = null,
currentStreamSubtitle: String? = null,
onDownloadedEpisodeSelected: (DownloadItem, MetaVideo) -> Unit,
onEpisodeStreamSelected: (StreamItem, MetaVideo) -> Unit,
onManualSelectionRequired: (MetaVideo) -> Unit,
Expand Down Expand Up @@ -57,18 +61,22 @@ internal fun CoroutineScope.launchPlayerNextEpisodeAutoPlay(
onCountdownChanged(null)

val type = contentType ?: parentMetaType
val hasPreferredSameName = !currentStreamTitle.isNullOrBlank()
val hasPreferredBingeGroup = !currentStreamBingeGroup.isNullOrBlank() && settings.streamAutoPlayPreferBingeGroup
val hasPreferredMatch = hasPreferredSameName || hasPreferredBingeGroup

val shouldAutoSelectInManualMode =
settings.streamAutoPlayMode == StreamAutoPlayMode.MANUAL &&
(
settings.streamAutoPlayNextEpisodeEnabled ||
settings.streamAutoPlayPreferBingeGroup
hasPreferredMatch
)

val bingeGroupOnlyManualMode =
val preferredOnlyManualMode =
shouldAutoSelectInManualMode &&
(!settings.streamAutoPlayNextEpisodeEnabled ||
!settings.streamAutoPlayNextEpisodeFallbackEnabled) &&
settings.streamAutoPlayPreferBingeGroup
hasPreferredMatch

val effectiveMode = if (shouldAutoSelectInManualMode) {
StreamAutoPlayMode.FIRST_STREAM
Expand Down Expand Up @@ -148,13 +156,18 @@ internal fun CoroutineScope.launchPlayerNextEpisodeAutoPlay(
selectedPlugins = effectiveSelectedPlugins,
preferredBingeGroup = preferredBingeGroup,
preferBingeGroupInSelection = settings.streamAutoPlayPreferBingeGroup,
bingeGroupOnly = bingeGroupOnlyManualMode,
preferredStreamTitle = currentStreamTitle,
preferredProviderName = currentStreamProviderName,
preferredProviderAddonId = currentStreamProviderAddonId,
preferredStreamSubtitle = currentStreamSubtitle,
preferSameNameInSelection = hasPreferredSameName,
bingeGroupOnly = preferredOnlyManualMode,
debridEnabled = debridSettings.canResolvePlayableLinks,
activeResolverProviderId = debridSettings.activeResolverProviderId,
)

fun tryBingeGroupOnly(streams: List<StreamItem>): StreamItem? {
if (preferredBingeGroup == null || !settings.streamAutoPlayPreferBingeGroup) return null
fun tryPreferredOnly(streams: List<StreamItem>): StreamItem? {
if (!hasPreferredMatch) return null
return StreamAutoPlaySelector.selectAutoPlayStream(
streams = streams,
mode = effectiveMode,
Expand All @@ -164,7 +177,12 @@ internal fun CoroutineScope.launchPlayerNextEpisodeAutoPlay(
selectedAddons = effectiveSelectedAddons,
selectedPlugins = effectiveSelectedPlugins,
preferredBingeGroup = preferredBingeGroup,
preferBingeGroupInSelection = true,
preferBingeGroupInSelection = settings.streamAutoPlayPreferBingeGroup,
preferredStreamTitle = currentStreamTitle,
preferredProviderName = currentStreamProviderName,
preferredProviderAddonId = currentStreamProviderAddonId,
preferredStreamSubtitle = currentStreamSubtitle,
preferSameNameInSelection = hasPreferredSameName,
bingeGroupOnly = true,
debridEnabled = debridSettings.canResolvePlayableLinks,
activeResolverProviderId = debridSettings.activeResolverProviderId,
Expand All @@ -173,7 +191,7 @@ internal fun CoroutineScope.launchPlayerNextEpisodeAutoPlay(

val selectionCoordinator = NextEpisodeStreamSelectionCoordinator(
selectAfterDelay = ::trySelectStream,
selectPreferred = ::tryBingeGroupOnly,
selectPreferred = ::tryPreferredOnly,
)

fun applySelectionDecision(decision: NextEpisodeStreamSelectionDecision) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,10 @@ internal fun PlayerScreenRuntime.playNextEpisode() {
contentType = contentType,
settings = playerSettingsUiState,
currentStreamBingeGroup = currentStreamBingeGroup,
currentStreamTitle = activeStreamTitle,
currentStreamProviderName = activeProviderName,
currentStreamProviderAddonId = activeProviderAddonId,
currentStreamSubtitle = activeStreamSubtitle,
onDownloadedEpisodeSelected = { item, episode -> switchToDownloadedEpisode(item, episode) },
onEpisodeStreamSelected = { stream, episode -> switchToEpisodeStream(stream, episode) },
onManualSelectionRequired = { nextVideo ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,11 @@ object StreamAutoPlaySelector {
preferredBingeGroup: String? = null,
preferBingeGroupInSelection: Boolean = false,
bingeGroupOnly: Boolean = false,
preferredStreamTitle: String? = null,
preferredProviderName: String? = null,
preferredProviderAddonId: String? = null,
preferredStreamSubtitle: String? = null,
preferSameNameInSelection: Boolean = false,
debridEnabled: Boolean = true,
activeResolverProviderId: String? = null,
): StreamItem? =
Expand All @@ -57,6 +62,11 @@ object StreamAutoPlaySelector {
preferredBingeGroup = preferredBingeGroup,
preferBingeGroupInSelection = preferBingeGroupInSelection,
bingeGroupOnly = bingeGroupOnly,
preferredStreamTitle = preferredStreamTitle,
preferredProviderName = preferredProviderName,
preferredProviderAddonId = preferredProviderAddonId,
preferredStreamSubtitle = preferredStreamSubtitle,
preferSameNameInSelection = preferSameNameInSelection,
debridEnabled = debridEnabled,
activeResolverProviderId = activeResolverProviderId,
).stream
Expand All @@ -72,6 +82,11 @@ object StreamAutoPlaySelector {
preferredBingeGroup: String? = null,
preferBingeGroupInSelection: Boolean = false,
bingeGroupOnly: Boolean = false,
preferredStreamTitle: String? = null,
preferredProviderName: String? = null,
preferredProviderAddonId: String? = null,
preferredStreamSubtitle: String? = null,
preferSameNameInSelection: Boolean = false,
debridEnabled: Boolean = true,
activeResolverProviderId: String? = null,
): StreamAutoPlayEvaluation {
Expand All @@ -95,37 +110,66 @@ object StreamAutoPlaySelector {
return StreamAutoPlayEvaluation()
}

val targetStreamTitle = preferredStreamTitle?.trim().orEmpty()
val sameNameCandidates = if (preferSameNameInSelection && targetStreamTitle.isNotEmpty()) {
candidateStreams.filter { stream ->
val providerMatches = when {
!preferredProviderAddonId.isNullOrBlank() && stream.addonId.equals(preferredProviderAddonId.trim(), ignoreCase = true) -> true
!preferredProviderName.isNullOrBlank() && stream.addonName.equals(preferredProviderName.trim(), ignoreCase = true) -> true
preferredProviderAddonId.isNullOrBlank() && preferredProviderName.isNullOrBlank() -> true
else -> false
}
if (!providerMatches) return@filter false

stream.streamLabel.trim().equals(targetStreamTitle, ignoreCase = true) ||
stream.name?.trim()?.equals(targetStreamTitle, ignoreCase = true) == true
}
} else {
emptyList()
}
val targetSubtitle = preferredStreamSubtitle?.trim().orEmpty()
val orderedSameNameCandidates = if (sameNameCandidates.size > 1 && targetSubtitle.isNotEmpty()) {
sameNameCandidates.sortedByDescending { stream ->
stream.streamSubtitle?.trim()?.equals(targetSubtitle, ignoreCase = true) == true
}
} else {
sameNameCandidates
}
val preferredSameNameReadyStream = orderedSameNameCandidates.firstOrNull { stream ->
stream.isAutoPlayable(debridEnabled, activeResolverProviderId)
}

val targetBingeGroup = preferredBingeGroup?.trim().orEmpty()
val bingeGroupCandidates = if (preferBingeGroupInSelection && targetBingeGroup.isNotEmpty()) {
candidateStreams.filter { stream -> stream.behaviorHints.bingeGroup == targetBingeGroup }
} else {
emptyList()
}
val preferredReadyStream = bingeGroupCandidates.firstOrNull { stream ->
val preferredBingeGroupReadyStream = bingeGroupCandidates.firstOrNull { stream ->
stream.isAutoPlayable(debridEnabled, activeResolverProviderId)
}

val preferredReadyStream = preferredSameNameReadyStream ?: preferredBingeGroupReadyStream
if (bingeGroupOnly) {
val readyStreams = preferredReadyStream?.let(::listOf).orEmpty()
return StreamAutoPlayEvaluation(
stream = preferredReadyStream,
readyStreams = readyStreams,
hasPendingDebridCandidate = preferredReadyStream == null &&
bingeGroupCandidates.any {
it.isPendingDebridAutoPlay(debridEnabled, activeResolverProviderId)
},
(
orderedSameNameCandidates.any {
it.isPendingDebridAutoPlay(debridEnabled, activeResolverProviderId)
} ||
bingeGroupCandidates.any {
it.isPendingDebridAutoPlay(debridEnabled, activeResolverProviderId)
}
),
)
}
if (mode == StreamAutoPlayMode.MANUAL) {
return StreamAutoPlayEvaluation()
}
val preferredStream = if (preferBingeGroupInSelection && targetBingeGroup.isNotEmpty()) {
candidateStreams.firstOrNull { stream ->
stream.behaviorHints.bingeGroup == targetBingeGroup &&
stream.isAutoPlayable(debridEnabled, activeResolverProviderId)
}
} else {
null
}
val preferredStream = preferredReadyStream
val matchingStreams = when (mode) {
StreamAutoPlayMode.MANUAL -> emptyList()
StreamAutoPlayMode.FIRST_STREAM -> candidateStreams
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ class PlayerExitOrderingTest {
override fun selectAudioTrack(index: Int) = Unit
override fun selectSubtitleTrack(index: Int) = Unit
override fun setSubtitleUri(url: String) = Unit
override fun applyAudioLanguagePreferences(languages: List<String>) = Unit
override fun clearExternalSubtitle() = Unit
override fun clearExternalSubtitleAndSelect(trackIndex: Int) = Unit
override fun releaseBeforeNavigation(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -291,18 +291,135 @@ class StreamAutoPlaySelectorTest {
assertEquals(stream, selected)
}

@Test
fun `same-name stream from same addon is selected first before default first stream mode`() {
val topCartoons = stream(
addonName = "TopCartoons",
url = "https://example.com/topcartoons.m3u8",
name = "TopCartoons",
)
val matchingStream = stream(
addonName = "4KHDHub",
url = "https://example.com/4khdhub-fsl.m3u8",
name = "4KHDHub - FSL 1080p",
)

val selected = StreamAutoPlaySelector.selectAutoPlayStream(
streams = listOf(topCartoons, matchingStream),
mode = StreamAutoPlayMode.FIRST_STREAM,
regexPattern = "",
source = StreamAutoPlaySource.ALL_SOURCES,
installedAddonNames = setOf("TopCartoons", "4KHDHub"),
selectedAddons = emptySet(),
selectedPlugins = emptySet(),
preferredStreamTitle = "4KHDHub - FSL 1080p",
preferredProviderName = "4KHDHub",
preferSameNameInSelection = true,
)

assertEquals(matchingStream, selected)
}

@Test
fun `same-name stream matching prefers matching subtitle when multiple exist`() {
val stream720p = stream(
addonName = "4KHDHub",
url = "https://example.com/720p.m3u8",
name = "4KHDHub - FSL",
description = "720p",
)
val stream1080p = stream(
addonName = "4KHDHub",
url = "https://example.com/1080p.m3u8",
name = "4KHDHub - FSL",
description = "1080p",
)

val selected = StreamAutoPlaySelector.selectAutoPlayStream(
streams = listOf(stream720p, stream1080p),
mode = StreamAutoPlayMode.FIRST_STREAM,
regexPattern = "",
source = StreamAutoPlaySource.ALL_SOURCES,
installedAddonNames = setOf("4KHDHub"),
selectedAddons = emptySet(),
selectedPlugins = emptySet(),
preferredStreamTitle = "4KHDHub - FSL",
preferredProviderName = "4KHDHub",
preferredStreamSubtitle = "1080p",
preferSameNameInSelection = true,
)

assertEquals(stream1080p, selected)
}

@Test
fun `same-name stream matching falls back to default scanning algorithm if same-name not found`() {
val firstStream = stream(
addonName = "TopCartoons",
url = "https://example.com/first.m3u8",
name = "TopCartoons",
)
val otherStream = stream(
addonName = "OtherAddon",
url = "https://example.com/other.m3u8",
name = "OtherAddon 1080p",
)

val selected = StreamAutoPlaySelector.selectAutoPlayStream(
streams = listOf(firstStream, otherStream),
mode = StreamAutoPlayMode.FIRST_STREAM,
regexPattern = "",
source = StreamAutoPlaySource.ALL_SOURCES,
installedAddonNames = setOf("TopCartoons", "OtherAddon"),
selectedAddons = emptySet(),
selectedPlugins = emptySet(),
preferredStreamTitle = "4KHDHub - FSL 1080p",
preferredProviderName = "4KHDHub",
preferSameNameInSelection = true,
)

assertEquals(firstStream, selected)
}

@Test
fun `same-name stream matching works in manual mode when preferred stream is requested`() {
val matchingStream = stream(
addonName = "4KHDHub",
url = "https://example.com/4khdhub-fsl.m3u8",
name = "4KHDHub - FSL 1080p",
)

val selected = StreamAutoPlaySelector.selectAutoPlayStream(
streams = listOf(matchingStream),
mode = StreamAutoPlayMode.MANUAL,
regexPattern = "",
source = StreamAutoPlaySource.ALL_SOURCES,
installedAddonNames = setOf("4KHDHub"),
selectedAddons = emptySet(),
selectedPlugins = emptySet(),
preferredStreamTitle = "4KHDHub - FSL 1080p",
preferredProviderName = "4KHDHub",
preferSameNameInSelection = true,
bingeGroupOnly = true,
)

assertEquals(matchingStream, selected)
}

private fun stream(
addonName: String,
url: String? = null,
externalUrl: String? = null,
name: String? = null,
description: String? = null,
bingeGroup: String? = null,
directDebrid: Boolean = false,
directDebridService: String = "torbox",
infoHash: String? = null,
cacheState: StreamDebridCacheState? = null,
): StreamItem = StreamItem(
name = name,
description = description,
url = url,
externalUrl = externalUrl,
infoHash = infoHash,
Expand Down