diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerStreamsRepository.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerStreamsRepository.kt index 6e9487ed..013460c3 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerStreamsRepository.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerStreamsRepository.kt @@ -13,6 +13,7 @@ import com.nuvio.app.features.plugins.pluginContentId import com.nuvio.app.features.plugins.PluginRuntimeResult import com.nuvio.app.features.plugins.PluginScraper import com.nuvio.app.features.streams.AddonStreamGroup +import com.nuvio.app.features.streams.StreamAutoPlaySelector import com.nuvio.app.features.streams.StreamItem import com.nuvio.app.features.streams.StreamParser import com.nuvio.app.features.streams.StreamsUiState @@ -21,6 +22,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.async +import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow @@ -199,7 +201,8 @@ object PlayerStreamsRepository { return } - val initialGroups = streamAddons.map { addon -> + val installedAddonOrder = streamAddons.map { it.addonName } + val initialGroups = StreamAutoPlaySelector.orderAddonStreams(streamAddons.map { addon -> AddonStreamGroup( addonName = addon.addonName, addonId = addon.addonId, @@ -220,7 +223,7 @@ object PlayerStreamsRepository { streams = emptyList(), isLoading = true, ) - } + }, installedAddonOrder) stateFlow.value = StreamsUiState( groups = initialGroups, activeAddonIds = initialGroups.map { it.addonId }.toSet(), @@ -299,11 +302,20 @@ object PlayerStreamsRepository { } val jobs = addonJobs + pluginJobs + debridJobs - var debridPreparationLaunched = false + val completions = Channel(capacity = Channel.BUFFERED) jobs.forEach { deferred -> - val result = deferred.await() + launch { + completions.send(deferred.await()) + } + } + var debridPreparationLaunched = false + repeat(jobs.size) { + val result = completions.receive() stateFlow.update { current -> - val updated = current.groups.map { g -> if (g.addonId == result.addonId) result else g } + val updated = StreamAutoPlaySelector.orderAddonStreams( + groups = current.groups.map { g -> if (g.addonId == result.addonId) result else g }, + installedOrder = installedAddonOrder, + ) val anyLoading = updated.any { it.isLoading } current.copy( groups = updated, @@ -340,6 +352,7 @@ object PlayerStreamsRepository { } } } + completions.close() } setJob(job) } diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/streams/StreamAutoPlaySelector.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/streams/StreamAutoPlaySelector.kt index 5fbfb6fc..5917325a 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/streams/StreamAutoPlaySelector.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/streams/StreamAutoPlaySelector.kt @@ -2,6 +2,34 @@ package com.nuvio.app.features.streams object StreamAutoPlaySelector { + fun orderAddonStreams( + groups: List, + installedOrder: List, + ): List { + if (groups.isEmpty()) return groups + + val addonRankByName = HashMap(installedOrder.size) + installedOrder.forEachIndexed { index, addonName -> + if (addonName !in addonRankByName) { + addonRankByName[addonName] = index + } + } + + val (directDebridEntries, remainingEntries) = groups.partition { group -> + group.addonId.startsWith("debrid:") || + group.streams.any { stream -> stream.isDirectDebridStream } + } + if (installedOrder.isEmpty()) return directDebridEntries + remainingEntries + + val (addonEntries, pluginEntries) = remainingEntries.partition { group -> + group.addonName in addonRankByName + } + val orderedAddons = addonEntries.sortedBy { group -> + addonRankByName.getValue(group.addonName) + } + return directDebridEntries + orderedAddons + pluginEntries + } + fun selectAutoPlayStream( streams: List, mode: StreamAutoPlayMode, diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/streams/StreamsRepository.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/streams/StreamsRepository.kt index 1f7d42e1..2fc87a24 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/streams/StreamsRepository.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/streams/StreamsRepository.kt @@ -184,7 +184,8 @@ object StreamsRepository { } // Initialise loading placeholders - val initialGroups = streamAddons.map { addon -> + val installedAddonOrder = streamAddons.map { it.addonName } + val initialGroups = StreamAutoPlaySelector.orderAddonStreams(streamAddons.map { addon -> AddonStreamGroup( addonName = addon.addonName, addonId = addon.addonId, @@ -205,7 +206,7 @@ object StreamsRepository { streams = emptyList(), isLoading = true, ) - } + }, installedAddonOrder) _uiState.value = StreamsUiState( requestToken = requestToken, groups = initialGroups, @@ -226,9 +227,7 @@ object StreamsRepository { pluginProviderGroups.sumOf { it.scrapers.size } + debridTargets.size - val installedAddonNames = installedAddons - .map { it.displayTitle } - .toSet() + val installedAddonNames = installedAddonOrder.toSet() var autoSelectTriggered = false var timeoutElapsed = false var debridPreparationLaunched = false @@ -383,9 +382,12 @@ object StreamsRepository { is StreamLoadCompletion.Addon -> { val result = completion.group _uiState.update { current -> - val updated = current.groups.map { group -> - if (group.addonId == result.addonId) result else group - } + val updated = StreamAutoPlaySelector.orderAddonStreams( + groups = current.groups.map { group -> + if (group.addonId == result.addonId) result else group + }, + installedOrder = installedAddonOrder, + ) val anyLoading = updated.any { it.isLoading } current.copy( groups = updated, @@ -403,28 +405,31 @@ object StreamsRepository { } _uiState.update { current -> - val updated = current.groups.map { group -> - if (group.addonId != completion.addonId) { - group - } else { - val mergedStreams = if (completion.streams.isEmpty()) { - group.streams + val updated = StreamAutoPlaySelector.orderAddonStreams( + groups = current.groups.map { group -> + if (group.addonId != completion.addonId) { + group } else { - (group.streams + completion.streams).sortedForGroupedDisplay() + val mergedStreams = if (completion.streams.isEmpty()) { + group.streams + } else { + (group.streams + completion.streams).sortedForGroupedDisplay() + } + val stillLoading = remaining > 0 + val finalError = if (mergedStreams.isEmpty() && !stillLoading) { + pluginFirstErrorByAddonId[completion.addonId] + } else { + null + } + group.copy( + streams = mergedStreams, + isLoading = stillLoading, + error = finalError, + ) } - val stillLoading = remaining > 0 - val finalError = if (mergedStreams.isEmpty() && !stillLoading) { - pluginFirstErrorByAddonId[completion.addonId] - } else { - null - } - group.copy( - streams = mergedStreams, - isLoading = stillLoading, - error = finalError, - ) - } - } + }, + installedOrder = installedAddonOrder, + ) val anyLoading = updated.any { it.isLoading } current.copy( groups = updated, @@ -437,9 +442,12 @@ object StreamsRepository { is StreamLoadCompletion.Debrid -> { val result = completion.group _uiState.update { current -> - val updated = current.groups.map { group -> - if (group.addonId == result.addonId) result else group - } + val updated = StreamAutoPlaySelector.orderAddonStreams( + groups = current.groups.map { group -> + if (group.addonId == result.addonId) result else group + }, + installedOrder = installedAddonOrder, + ) val anyLoading = updated.any { it.isLoading } current.copy( groups = updated,