diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerEpisodesPanel.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerEpisodesPanel.kt index a9a7fd52..8661f690 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerEpisodesPanel.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerEpisodesPanel.kt @@ -27,6 +27,7 @@ import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape @@ -535,7 +536,10 @@ private fun EpisodeStreamsSubView( verticalArrangement = Arrangement.spacedBy(6.dp), contentPadding = androidx.compose.foundation.layout.PaddingValues(bottom = 16.dp), ) { - items(streams, key = { "${it.addonId}::${it.url ?: it.name}" }) { stream -> + itemsIndexed( + items = streams, + key = { index, stream -> "${stream.addonId}::${index}::${stream.url ?: stream.infoHash ?: stream.name}" }, + ) { _, stream -> EpisodeSourceStreamRow( stream = stream, onClick = { onStreamSelected(stream, episode) }, diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerSourcesPanel.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerSourcesPanel.kt index 702344dc..e13e2318 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerSourcesPanel.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerSourcesPanel.kt @@ -24,6 +24,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.items import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape @@ -195,7 +196,10 @@ fun PlayerSourcesPanel( verticalArrangement = Arrangement.spacedBy(6.dp), contentPadding = androidx.compose.foundation.layout.PaddingValues(bottom = 16.dp), ) { - items(streams, key = { "${it.addonId}::${it.url ?: it.name}" }) { stream -> + itemsIndexed( + items = streams, + key = { index, stream -> "${stream.addonId}::${index}::${stream.url ?: stream.infoHash ?: stream.name}" }, + ) { _, stream -> val isCurrent = isCurrentStream( stream = stream, currentUrl = currentStreamUrl, 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 fc779c77..b64becba 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 @@ -165,19 +165,22 @@ object PlayerStreamsRepository { return } - val addonDisplayNames = installedAddons.associate { - (it.manifest?.id ?: it.manifestUrl) to it.displayTitle - } - val streamAddons = installedAddons - .mapNotNull { it.manifest } - .filter { manifest -> - manifest.resources.any { resource -> + .mapNotNull { addon -> + val manifest = addon.manifest ?: return@mapNotNull null + val supportsRequestedStream = manifest.resources.any { resource -> resource.name == "stream" && resource.types.contains(type) && (resource.idPrefixes.isEmpty() || resource.idPrefixes.any { videoId.startsWith(it) }) } + if (!supportsRequestedStream) return@mapNotNull null + + PlayerInstalledStreamAddonTarget( + addonName = addon.displayTitle.ifBlank { manifest.name }, + addonId = addon.streamAddonInstanceId(manifest.id), + manifest = manifest, + ) } if (streamAddons.isEmpty() && pluginScrapers.isEmpty()) { @@ -188,10 +191,10 @@ object PlayerStreamsRepository { return } - val initialGroups = streamAddons.map { manifest -> + val initialGroups = streamAddons.map { addon -> AddonStreamGroup( - addonName = addonDisplayNames[manifest.id] ?: manifest.name, - addonId = manifest.id, + addonName = addon.addonName, + addonId = addon.addonId, streams = emptyList(), isLoading = true, ) @@ -210,25 +213,25 @@ object PlayerStreamsRepository { ) val job = scope.launch { - val addonJobs = streamAddons.map { manifest -> + val addonJobs = streamAddons.map { addon -> async { val encodedId = videoId.replace("%", "%25").replace(" ", "%20") - val baseUrl = manifest.transportUrl + val baseUrl = addon.manifest.transportUrl .substringBefore("?") .removeSuffix("/manifest.json") val url = "$baseUrl/stream/$type/$encodedId.json" - val displayName = addonDisplayNames[manifest.id] ?: manifest.name + val displayName = addon.addonName runCatching { val payload = httpGetText(url) - StreamParser.parse(payload, displayName, manifest.id) + StreamParser.parse(payload, displayName, addon.addonId) }.fold( onSuccess = { streams -> - AddonStreamGroup(displayName, manifest.id, streams, isLoading = false) + AddonStreamGroup(displayName, addon.addonId, streams, isLoading = false) }, onFailure = { err -> log.w(err) { "Failed: ${displayName}" } - AddonStreamGroup(displayName, manifest.id, emptyList(), isLoading = false, error = err.message) + AddonStreamGroup(displayName, addon.addonId, emptyList(), isLoading = false, error = err.message) }, ) } @@ -289,6 +292,15 @@ object PlayerStreamsRepository { } } +private data class PlayerInstalledStreamAddonTarget( + val addonName: String, + val addonId: String, + val manifest: com.nuvio.app.features.addons.AddonManifest, +) + +private fun com.nuvio.app.features.addons.ManagedAddon.streamAddonInstanceId(manifestId: String): String = + "addon:$manifestId:$manifestUrl" + private fun PluginRuntimeResult.toStreamItem(scraper: PluginScraper): StreamItem { val subtitleParts = listOfNotNull( quality?.takeIf { it.isNotBlank() }, 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 27878283..cf83d7ea 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 @@ -127,19 +127,22 @@ object StreamsRepository { return } - val addonDisplayNames = installedAddons.associate { - (it.manifest?.id ?: it.manifestUrl) to it.displayTitle - } - val streamAddons = installedAddons - .mapNotNull { it.manifest } - .filter { manifest -> - manifest.resources.any { resource -> + .mapNotNull { addon -> + val manifest = addon.manifest ?: return@mapNotNull null + val supportsRequestedStream = manifest.resources.any { resource -> resource.name == "stream" && resource.types.contains(type) && (resource.idPrefixes.isEmpty() || resource.idPrefixes.any { videoId.startsWith(it) }) } + if (!supportsRequestedStream) return@mapNotNull null + + InstalledStreamAddonTarget( + addonName = addon.displayTitle.ifBlank { manifest.name }, + addonId = addon.streamAddonInstanceId(manifest.id), + manifest = manifest, + ) } log.d { "Found ${streamAddons.size} addons for stream type=$type id=$videoId" } @@ -153,10 +156,10 @@ object StreamsRepository { } // Initialise loading placeholders - val initialGroups = streamAddons.map { manifest -> + val initialGroups = streamAddons.map { addon -> AddonStreamGroup( - addonName = addonDisplayNames[manifest.id] ?: manifest.name, - addonId = manifest.id, + addonName = addon.addonName, + addonId = addon.addonId, streams = emptyList(), isLoading = true, ) @@ -232,29 +235,29 @@ object StreamsRepository { null } - streamAddons.forEach { manifest -> + streamAddons.forEach { addon -> launch { val encodedId = videoId.encodeForPath() - val baseUrl = manifest.transportUrl + val baseUrl = addon.manifest.transportUrl .substringBefore("?") .removeSuffix("/manifest.json") val url = "$baseUrl/stream/$type/$encodedId.json" log.d { "Fetching streams from: $url" } - val displayName = addonDisplayNames[manifest.id] ?: manifest.name + val displayName = addon.addonName val group = runCatching { val payload = httpGetText(url) StreamParser.parse( payload = payload, addonName = displayName, - addonId = manifest.id, + addonId = addon.addonId, ) }.fold( onSuccess = { streams -> log.d { "Got ${streams.size} streams from ${displayName}" } AddonStreamGroup( addonName = displayName, - addonId = manifest.id, + addonId = addon.addonId, streams = streams, isLoading = false, ) @@ -263,7 +266,7 @@ object StreamsRepository { log.w(err) { "Failed to fetch streams from ${displayName}" } AddonStreamGroup( addonName = displayName, - addonId = manifest.id, + addonId = addon.addonId, streams = emptyList(), isLoading = false, error = err.message, @@ -423,6 +426,15 @@ object StreamsRepository { replace("%", "%25").replace(" ", "%20") } +private data class InstalledStreamAddonTarget( + val addonName: String, + val addonId: String, + val manifest: com.nuvio.app.features.addons.AddonManifest, +) + +private fun com.nuvio.app.features.addons.ManagedAddon.streamAddonInstanceId(manifestId: String): String = + "addon:$manifestId:$manifestUrl" + private data class PluginProviderGroup( val addonId: String, val addonName: String,