From 8039c32a707015586011007cf3a1c63003336670 Mon Sep 17 00:00:00 2001 From: skoruppa Date: Thu, 18 Jun 2026 13:54:59 +0200 Subject: [PATCH] fix: resolve debrid instant streams before download/copy link Long-press menu was blocked for debrid instant streams because playableDirectUrl is null until resolved. Now the menu opens for any stream that has a direct URL or is a debrid candidate. Download and Copy Link actions now resolve the debrid stream URL via DirectDebridPlaybackResolver before enqueuing/copying, matching the same flow used by the player. --- .../app/features/streams/StreamsScreen.kt | 99 +++++++++++++++---- 1 file changed, 82 insertions(+), 17 deletions(-) diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/streams/StreamsScreen.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/streams/StreamsScreen.kt index 8fdf7e116..d8853d5c9 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/streams/StreamsScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/streams/StreamsScreen.kt @@ -84,6 +84,9 @@ import androidx.compose.material3.rememberModalBottomSheetState import coil3.compose.AsyncImage import com.nuvio.app.core.ui.nuvioSafeBottomPadding import com.nuvio.app.features.debrid.DebridSettingsRepository +import com.nuvio.app.features.debrid.DirectDebridPlayableResult +import com.nuvio.app.features.debrid.DirectDebridPlaybackResolver +import com.nuvio.app.features.debrid.toastMessage import com.nuvio.app.features.player.PlayerSettingsRepository import com.nuvio.app.features.watchprogress.WatchProgressRepository import kotlinx.coroutines.launch @@ -144,6 +147,7 @@ fun StreamsScreen( val streamLinkCopiedText = stringResource(Res.string.streams_link_copied) val noDirectStreamLinkText = stringResource(Res.string.streams_no_direct_link) var streamActionsTarget by remember(videoId) { mutableStateOf(null) } + val downloadScope = rememberCoroutineScope() var preferredFilterApplied by remember(videoId) { mutableStateOf(false) } var autoPlayOverlayLogoLoadError by remember(logo) { mutableStateOf(false) } val autoPlayOverlayLogoUrl = logo?.takeIf { it.isNotBlank() } @@ -355,27 +359,88 @@ fun StreamsScreen( if (!directUrl.isNullOrBlank()) { clipboardManager.setText(AnnotatedString(directUrl)) NuvioToastController.show(streamLinkCopiedText) + } else if (DirectDebridPlaybackResolver.shouldResolveToPlayableStream(stream)) { + downloadScope.launch { + val resolved = DirectDebridPlaybackResolver.resolveToPlayableStream( + stream = stream, + season = seasonNumber, + episode = episodeNumber, + ) + when (resolved) { + is DirectDebridPlayableResult.Success -> { + val resolvedUrl = resolved.stream.playableDirectUrl + if (!resolvedUrl.isNullOrBlank()) { + clipboardManager.setText(AnnotatedString(resolvedUrl)) + NuvioToastController.show(streamLinkCopiedText) + } else { + NuvioToastController.show(noDirectStreamLinkText) + } + } + else -> { + val message = resolved.toastMessage() + if (message != null) { + NuvioToastController.show(message) + } + } + } + } } else { NuvioToastController.show(noDirectStreamLinkText) } }, onDownload = { stream -> - val result = DownloadsRepository.enqueueFromStream( - contentType = type, - videoId = videoId, - parentMetaId = parentMetaId, - parentMetaType = parentMetaType, - title = title, - logo = logo, - poster = poster, - background = background, - seasonNumber = seasonNumber, - episodeNumber = episodeNumber, - episodeTitle = episodeTitle, - episodeThumbnail = episodeThumbnail, - stream = stream, - ) - NuvioToastController.show(result.toastMessage()) + if (DirectDebridPlaybackResolver.shouldResolveToPlayableStream(stream)) { + downloadScope.launch { + val resolved = DirectDebridPlaybackResolver.resolveToPlayableStream( + stream = stream, + season = seasonNumber, + episode = episodeNumber, + ) + when (resolved) { + is DirectDebridPlayableResult.Success -> { + val result = DownloadsRepository.enqueueFromStream( + contentType = type, + videoId = videoId, + parentMetaId = parentMetaId, + parentMetaType = parentMetaType, + title = title, + logo = logo, + poster = poster, + background = background, + seasonNumber = seasonNumber, + episodeNumber = episodeNumber, + episodeTitle = episodeTitle, + episodeThumbnail = episodeThumbnail, + stream = resolved.stream, + ) + NuvioToastController.show(result.toastMessage()) + } + else -> { + val message = resolved.toastMessage() + if (message != null) { + NuvioToastController.show(message) + } + } + } + } + } else { + val result = DownloadsRepository.enqueueFromStream( + contentType = type, + videoId = videoId, + parentMetaId = parentMetaId, + parentMetaType = parentMetaType, + title = title, + logo = logo, + poster = poster, + background = background, + seasonNumber = seasonNumber, + episodeNumber = episodeNumber, + episodeTitle = episodeTitle, + episodeThumbnail = episodeThumbnail, + stream = stream, + ) + NuvioToastController.show(result.toastMessage()) + } }, onOpen = { stream, openExternally -> onStreamActionOpen( @@ -911,7 +976,7 @@ private fun LazyListScope.streamSection( } }, onLongClick = { - if (stream.playableDirectUrl != null) { + if (stream.playableDirectUrl != null || stream.isAddonDebridCandidate) { onStreamLongPress(stream) } },