mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-07-29 23:59:32 +00:00
feat: prefer downloaded files over stream selection
This commit is contained in:
parent
7c2cad51b9
commit
616eec757b
3 changed files with 255 additions and 66 deletions
|
|
@ -88,6 +88,7 @@ import com.nuvio.app.features.auth.AuthScreen
|
|||
import com.nuvio.app.features.catalog.CatalogRepository
|
||||
import com.nuvio.app.features.catalog.CatalogScreen
|
||||
import com.nuvio.app.features.catalog.INTERNAL_LIBRARY_MANIFEST_URL
|
||||
import com.nuvio.app.features.downloads.DownloadsRepository
|
||||
import com.nuvio.app.features.downloads.DownloadsScreen
|
||||
import com.nuvio.app.features.details.MetaDetailsRepository
|
||||
import com.nuvio.app.features.details.MetaDetailsScreen
|
||||
|
|
@ -495,56 +496,137 @@ private fun MainAppContent(
|
|||
}
|
||||
}
|
||||
|
||||
fun launchPlaybackWithDownloadPreference(
|
||||
type: String,
|
||||
videoId: String,
|
||||
parentMetaId: String,
|
||||
parentMetaType: String,
|
||||
title: String,
|
||||
logo: String?,
|
||||
poster: String?,
|
||||
background: String?,
|
||||
seasonNumber: Int?,
|
||||
episodeNumber: Int?,
|
||||
episodeTitle: String?,
|
||||
episodeThumbnail: String?,
|
||||
pauseDescription: String?,
|
||||
resumePositionMs: Long?,
|
||||
resumeProgressFraction: Float?,
|
||||
manualSelection: Boolean,
|
||||
startFromBeginning: Boolean,
|
||||
) {
|
||||
val targetResumePositionMs = if (startFromBeginning) 0L else (resumePositionMs ?: 0L)
|
||||
val targetResumeProgressFraction = if (startFromBeginning) null else resumeProgressFraction
|
||||
|
||||
if (!manualSelection) {
|
||||
val downloadedItem = DownloadsRepository.findPlayableDownload(
|
||||
parentMetaId = parentMetaId,
|
||||
seasonNumber = seasonNumber,
|
||||
episodeNumber = episodeNumber,
|
||||
videoId = videoId,
|
||||
)
|
||||
val localSourceUrl = downloadedItem?.localFileUri
|
||||
if (!localSourceUrl.isNullOrBlank()) {
|
||||
val launchId = PlayerLaunchStore.put(
|
||||
PlayerLaunch(
|
||||
title = title,
|
||||
sourceUrl = localSourceUrl,
|
||||
sourceHeaders = emptyMap(),
|
||||
sourceResponseHeaders = emptyMap(),
|
||||
logo = logo,
|
||||
poster = poster,
|
||||
background = background,
|
||||
seasonNumber = seasonNumber,
|
||||
episodeNumber = episodeNumber,
|
||||
episodeTitle = episodeTitle,
|
||||
episodeThumbnail = episodeThumbnail,
|
||||
streamTitle = downloadedItem.streamTitle.ifBlank { title },
|
||||
streamSubtitle = downloadedItem.streamSubtitle,
|
||||
pauseDescription = pauseDescription,
|
||||
providerName = downloadedItem.providerName.ifBlank { "Downloaded" },
|
||||
providerAddonId = downloadedItem.providerAddonId,
|
||||
contentType = type,
|
||||
videoId = videoId,
|
||||
parentMetaId = parentMetaId,
|
||||
parentMetaType = parentMetaType,
|
||||
initialPositionMs = targetResumePositionMs,
|
||||
initialProgressFraction = targetResumeProgressFraction,
|
||||
),
|
||||
)
|
||||
navController.navigate(PlayerRoute(launchId = launchId))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
val streamContextId = pauseDescription
|
||||
?.takeIf { it.isNotBlank() }
|
||||
?.let { StreamContextStore.put(StreamContext(pauseDescription = it)) }
|
||||
navController.navigate(
|
||||
StreamRoute(
|
||||
type = type,
|
||||
videoId = videoId,
|
||||
parentMetaId = parentMetaId,
|
||||
parentMetaType = parentMetaType,
|
||||
title = title,
|
||||
logo = logo,
|
||||
poster = poster,
|
||||
background = background,
|
||||
seasonNumber = seasonNumber,
|
||||
episodeNumber = episodeNumber,
|
||||
episodeTitle = episodeTitle,
|
||||
episodeThumbnail = episodeThumbnail,
|
||||
streamContextId = streamContextId,
|
||||
resumePositionMs = if (startFromBeginning) 0L else resumePositionMs,
|
||||
resumeProgressFraction = targetResumeProgressFraction,
|
||||
manualSelection = manualSelection,
|
||||
startFromBeginning = startFromBeginning,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
val onPlay: (String, String, String, String, String, String?, String?, String?, Int?, Int?, String?, String?, String?, Long?) -> Unit =
|
||||
{ type, videoId, parentMetaId, parentMetaType, title, logo, poster, background, seasonNumber, episodeNumber, episodeTitle, episodeThumbnail, pauseDescription, resumePositionMs ->
|
||||
val streamContextId = pauseDescription
|
||||
?.takeIf { it.isNotBlank() }
|
||||
?.let { StreamContextStore.put(StreamContext(pauseDescription = it)) }
|
||||
navController.navigate(
|
||||
StreamRoute(
|
||||
type = type,
|
||||
videoId = videoId,
|
||||
parentMetaId = parentMetaId,
|
||||
parentMetaType = parentMetaType,
|
||||
title = title,
|
||||
logo = logo,
|
||||
poster = poster,
|
||||
background = background,
|
||||
seasonNumber = seasonNumber,
|
||||
episodeNumber = episodeNumber,
|
||||
episodeTitle = episodeTitle,
|
||||
episodeThumbnail = episodeThumbnail,
|
||||
streamContextId = streamContextId,
|
||||
resumePositionMs = resumePositionMs,
|
||||
resumeProgressFraction = null,
|
||||
)
|
||||
launchPlaybackWithDownloadPreference(
|
||||
type = type,
|
||||
videoId = videoId,
|
||||
parentMetaId = parentMetaId,
|
||||
parentMetaType = parentMetaType,
|
||||
title = title,
|
||||
logo = logo,
|
||||
poster = poster,
|
||||
background = background,
|
||||
seasonNumber = seasonNumber,
|
||||
episodeNumber = episodeNumber,
|
||||
episodeTitle = episodeTitle,
|
||||
episodeThumbnail = episodeThumbnail,
|
||||
pauseDescription = pauseDescription,
|
||||
resumePositionMs = resumePositionMs,
|
||||
resumeProgressFraction = null,
|
||||
manualSelection = false,
|
||||
startFromBeginning = false,
|
||||
)
|
||||
}
|
||||
|
||||
val onPlayManually: (String, String, String, String, String, String?, String?, String?, Int?, Int?, String?, String?, String?, Long?) -> Unit =
|
||||
{ type, videoId, parentMetaId, parentMetaType, title, logo, poster, background, seasonNumber, episodeNumber, episodeTitle, episodeThumbnail, pauseDescription, resumePositionMs ->
|
||||
val streamContextId = pauseDescription
|
||||
?.takeIf { it.isNotBlank() }
|
||||
?.let { StreamContextStore.put(StreamContext(pauseDescription = it)) }
|
||||
navController.navigate(
|
||||
StreamRoute(
|
||||
type = type,
|
||||
videoId = videoId,
|
||||
parentMetaId = parentMetaId,
|
||||
parentMetaType = parentMetaType,
|
||||
title = title,
|
||||
logo = logo,
|
||||
poster = poster,
|
||||
background = background,
|
||||
seasonNumber = seasonNumber,
|
||||
episodeNumber = episodeNumber,
|
||||
episodeTitle = episodeTitle,
|
||||
episodeThumbnail = episodeThumbnail,
|
||||
streamContextId = streamContextId,
|
||||
resumePositionMs = resumePositionMs,
|
||||
resumeProgressFraction = null,
|
||||
manualSelection = true,
|
||||
)
|
||||
launchPlaybackWithDownloadPreference(
|
||||
type = type,
|
||||
videoId = videoId,
|
||||
parentMetaId = parentMetaId,
|
||||
parentMetaType = parentMetaType,
|
||||
title = title,
|
||||
logo = logo,
|
||||
poster = poster,
|
||||
background = background,
|
||||
seasonNumber = seasonNumber,
|
||||
episodeNumber = episodeNumber,
|
||||
episodeTitle = episodeTitle,
|
||||
episodeThumbnail = episodeThumbnail,
|
||||
pauseDescription = pauseDescription,
|
||||
resumePositionMs = resumePositionMs,
|
||||
resumeProgressFraction = null,
|
||||
manualSelection = true,
|
||||
startFromBeginning = false,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -579,29 +661,24 @@ private fun MainAppContent(
|
|||
}
|
||||
|
||||
val openContinueWatching: (ContinueWatchingItem, Boolean, Boolean) -> Unit = { item, manualSelection, startFromBeginning ->
|
||||
val streamContextId = item.pauseDescription
|
||||
?.takeIf { it.isNotBlank() }
|
||||
?.let { StreamContextStore.put(StreamContext(pauseDescription = it)) }
|
||||
navController.navigate(
|
||||
StreamRoute(
|
||||
type = item.parentMetaType,
|
||||
videoId = item.videoId,
|
||||
parentMetaId = item.parentMetaId,
|
||||
parentMetaType = item.parentMetaType,
|
||||
title = item.title,
|
||||
logo = item.logo,
|
||||
poster = item.poster,
|
||||
background = item.background,
|
||||
seasonNumber = item.seasonNumber,
|
||||
episodeNumber = item.episodeNumber,
|
||||
episodeTitle = item.episodeTitle,
|
||||
episodeThumbnail = item.episodeThumbnail,
|
||||
streamContextId = streamContextId,
|
||||
resumePositionMs = if (startFromBeginning) 0L else item.resumePositionMs,
|
||||
resumeProgressFraction = if (startFromBeginning) null else item.resumeProgressFraction,
|
||||
manualSelection = manualSelection,
|
||||
startFromBeginning = startFromBeginning,
|
||||
),
|
||||
launchPlaybackWithDownloadPreference(
|
||||
type = item.parentMetaType,
|
||||
videoId = item.videoId,
|
||||
parentMetaId = item.parentMetaId,
|
||||
parentMetaType = item.parentMetaType,
|
||||
title = item.title,
|
||||
logo = item.logo,
|
||||
poster = item.poster,
|
||||
background = item.background,
|
||||
seasonNumber = item.seasonNumber,
|
||||
episodeNumber = item.episodeNumber,
|
||||
episodeTitle = item.episodeTitle,
|
||||
episodeThumbnail = item.episodeThumbnail,
|
||||
pauseDescription = item.pauseDescription,
|
||||
resumePositionMs = item.resumePositionMs,
|
||||
resumeProgressFraction = item.resumeProgressFraction,
|
||||
manualSelection = manualSelection,
|
||||
startFromBeginning = startFromBeginning,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -34,6 +34,46 @@ object DownloadsRepository {
|
|||
_uiState.value = DownloadsUiState()
|
||||
}
|
||||
|
||||
fun findPlayableDownloadByVideoId(videoId: String?): DownloadItem? {
|
||||
ensureLoaded()
|
||||
val normalizedVideoId = videoId?.trim().orEmpty()
|
||||
if (normalizedVideoId.isBlank()) return null
|
||||
return _uiState.value.items.firstOrNull { item ->
|
||||
item.videoId == normalizedVideoId && item.isPlayable && !item.localFileUri.isNullOrBlank()
|
||||
}
|
||||
}
|
||||
|
||||
fun findPlayableDownload(
|
||||
parentMetaId: String,
|
||||
seasonNumber: Int? = null,
|
||||
episodeNumber: Int? = null,
|
||||
videoId: String? = null,
|
||||
): DownloadItem? {
|
||||
ensureLoaded()
|
||||
val items = _uiState.value.items
|
||||
val normalizedParentMetaId = parentMetaId.trim()
|
||||
|
||||
findPlayableDownloadByVideoId(videoId)?.let { return it }
|
||||
|
||||
return if (seasonNumber != null && episodeNumber != null) {
|
||||
items.firstOrNull { item ->
|
||||
item.parentMetaId == normalizedParentMetaId &&
|
||||
item.seasonNumber == seasonNumber &&
|
||||
item.episodeNumber == episodeNumber &&
|
||||
item.isPlayable &&
|
||||
!item.localFileUri.isNullOrBlank()
|
||||
}
|
||||
} else {
|
||||
items.firstOrNull { item ->
|
||||
item.parentMetaId == normalizedParentMetaId &&
|
||||
item.seasonNumber == null &&
|
||||
item.episodeNumber == null &&
|
||||
item.isPlayable &&
|
||||
!item.localFileUri.isNullOrBlank()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun enqueueFromStream(
|
||||
contentType: String,
|
||||
videoId: String,
|
||||
|
|
|
|||
|
|
@ -40,6 +40,8 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
|||
import com.nuvio.app.features.addons.AddonRepository
|
||||
import com.nuvio.app.features.details.MetaDetailsRepository
|
||||
import com.nuvio.app.features.details.MetaVideo
|
||||
import com.nuvio.app.features.downloads.DownloadItem
|
||||
import com.nuvio.app.features.downloads.DownloadsRepository
|
||||
import com.nuvio.app.features.player.skip.NextEpisodeCard
|
||||
import com.nuvio.app.features.player.skip.NextEpisodeInfo
|
||||
import com.nuvio.app.features.player.skip.PlayerNextEpisodeRules
|
||||
|
|
@ -793,11 +795,70 @@ fun PlayerScreen(
|
|||
controlsVisible = true
|
||||
}
|
||||
|
||||
fun switchToDownloadedEpisode(downloadItem: DownloadItem, episode: MetaVideo) {
|
||||
val localFileUri = downloadItem.localFileUri ?: return
|
||||
showNextEpisodeCard = false
|
||||
showSourcesPanel = false
|
||||
showEpisodesPanel = false
|
||||
episodeStreamsPanelState = EpisodeStreamsPanelState()
|
||||
nextEpisodeAutoPlayJob?.cancel()
|
||||
nextEpisodeAutoPlaySearching = false
|
||||
nextEpisodeAutoPlaySourceName = null
|
||||
nextEpisodeAutoPlayCountdown = null
|
||||
PlayerStreamsRepository.clearEpisodeStreams()
|
||||
flushWatchProgress()
|
||||
|
||||
val fallbackVideoId = buildPlaybackVideoId(
|
||||
parentMetaId = parentMetaId,
|
||||
seasonNumber = episode.season,
|
||||
episodeNumber = episode.episode,
|
||||
fallbackVideoId = episode.id,
|
||||
)
|
||||
val resolvedVideoId = episode.id.takeIf { it.isNotBlank() } ?: fallbackVideoId
|
||||
val epEntry = WatchProgressRepository.progressForVideo(resolvedVideoId)
|
||||
?.takeIf { !it.isCompleted }
|
||||
val epResumeFraction = epEntry?.progressPercent
|
||||
?.takeIf { it > 0f }
|
||||
?.let { (it / 100f).coerceIn(0f, 1f) }
|
||||
val epResumePositionMs = epEntry?.lastPositionMs?.takeIf { it > 0L } ?: 0L
|
||||
|
||||
activeSourceUrl = localFileUri
|
||||
activeSourceAudioUrl = null
|
||||
activeSourceHeaders = emptyMap()
|
||||
activeSourceResponseHeaders = emptyMap()
|
||||
activeStreamTitle = downloadItem.streamTitle.ifBlank {
|
||||
episode.title.ifBlank { title }
|
||||
}
|
||||
activeStreamSubtitle = downloadItem.streamSubtitle
|
||||
activeProviderName = downloadItem.providerName.ifBlank { "Downloaded" }
|
||||
activeProviderAddonId = downloadItem.providerAddonId
|
||||
currentStreamBingeGroup = null
|
||||
activeSeasonNumber = episode.season
|
||||
activeEpisodeNumber = episode.episode
|
||||
activeEpisodeTitle = episode.title
|
||||
activeEpisodeThumbnail = episode.thumbnail
|
||||
activeVideoId = resolvedVideoId
|
||||
activeInitialPositionMs = epResumePositionMs
|
||||
activeInitialProgressFraction = epResumeFraction
|
||||
controlsVisible = true
|
||||
}
|
||||
|
||||
fun playNextEpisode() {
|
||||
val nextVideoId = nextEpisodeInfo?.videoId ?: return
|
||||
val nextVideo = allEpisodes.firstOrNull { video -> video.id == nextVideoId } ?: return
|
||||
if (nextEpisodeInfo?.hasAired != true) return
|
||||
|
||||
val downloadedNextEpisode = DownloadsRepository.findPlayableDownload(
|
||||
parentMetaId = parentMetaId,
|
||||
seasonNumber = nextVideo.season,
|
||||
episodeNumber = nextVideo.episode,
|
||||
videoId = nextVideo.id,
|
||||
)
|
||||
if (downloadedNextEpisode != null) {
|
||||
switchToDownloadedEpisode(downloadedNextEpisode, nextVideo)
|
||||
return
|
||||
}
|
||||
|
||||
nextEpisodeAutoPlayJob?.cancel()
|
||||
nextEpisodeAutoPlaySearching = true
|
||||
nextEpisodeAutoPlaySourceName = null
|
||||
|
|
@ -1596,6 +1657,17 @@ fun PlayerScreen(
|
|||
),
|
||||
onSeasonSelected = { /* season tab change handled internally */ },
|
||||
onEpisodeSelected = { episode ->
|
||||
val downloadedEpisode = DownloadsRepository.findPlayableDownload(
|
||||
parentMetaId = parentMetaId,
|
||||
seasonNumber = episode.season,
|
||||
episodeNumber = episode.episode,
|
||||
videoId = episode.id,
|
||||
)
|
||||
if (downloadedEpisode != null) {
|
||||
switchToDownloadedEpisode(downloadedEpisode, episode)
|
||||
return@PlayerEpisodesPanel
|
||||
}
|
||||
|
||||
val type = contentType ?: parentMetaType
|
||||
PlayerStreamsRepository.loadEpisodeStreams(
|
||||
type = type,
|
||||
|
|
|
|||
Loading…
Reference in a new issue