mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-05-23 02:02:04 +00:00
proper management of cloud items in cw section
This commit is contained in:
parent
996fd7f949
commit
3d6d0fcfb4
9 changed files with 340 additions and 63 deletions
|
|
@ -106,10 +106,13 @@ import com.nuvio.app.features.addons.AddonRepository
|
|||
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.cloud.CloudLibraryContentType
|
||||
import com.nuvio.app.features.cloud.CloudLibraryFile
|
||||
import com.nuvio.app.features.cloud.CloudLibraryItem
|
||||
import com.nuvio.app.features.cloud.CloudLibraryPlaybackResult
|
||||
import com.nuvio.app.features.cloud.CloudLibraryRepository
|
||||
import com.nuvio.app.features.cloud.playbackVideoId
|
||||
import com.nuvio.app.features.cloud.providerPosterUrl
|
||||
import com.nuvio.app.features.debrid.DirectDebridPlayableResult
|
||||
import com.nuvio.app.features.debrid.DirectDebridPlaybackResolver
|
||||
import com.nuvio.app.features.debrid.toastMessage
|
||||
|
|
@ -842,6 +845,52 @@ private fun MainAppContent(
|
|||
}
|
||||
}
|
||||
|
||||
suspend fun launchCloudLibraryFile(
|
||||
item: CloudLibraryItem,
|
||||
file: CloudLibraryFile,
|
||||
resumePositionMs: Long? = null,
|
||||
resumeProgressFraction: Float? = null,
|
||||
startFromBeginning: Boolean = false,
|
||||
): Boolean {
|
||||
return when (
|
||||
val resolved = CloudLibraryRepository.resolvePlayback(
|
||||
item = item,
|
||||
file = file,
|
||||
)
|
||||
) {
|
||||
is CloudLibraryPlaybackResult.Success -> {
|
||||
val playbackTitle = resolved.filename
|
||||
?.takeIf { it.isNotBlank() }
|
||||
?: file.name.ifBlank { item.name }
|
||||
val playerLaunch = PlayerLaunch(
|
||||
title = playbackTitle,
|
||||
sourceUrl = resolved.url,
|
||||
streamTitle = playbackTitle,
|
||||
streamSubtitle = item.name.takeIf { it != playbackTitle },
|
||||
providerName = item.providerName,
|
||||
providerAddonId = "cloud:${item.providerId}",
|
||||
poster = item.providerPosterUrl(),
|
||||
contentType = CloudLibraryContentType,
|
||||
videoId = item.playbackVideoId(file),
|
||||
parentMetaId = item.stableKey,
|
||||
parentMetaType = CloudLibraryContentType,
|
||||
initialPositionMs = if (startFromBeginning) 0L else (resumePositionMs ?: 0L),
|
||||
initialProgressFraction = if (startFromBeginning) null else resumeProgressFraction,
|
||||
)
|
||||
if (playerSettingsUiState.externalPlayerEnabled) {
|
||||
openExternalPlayback(playerLaunch)
|
||||
true
|
||||
} else {
|
||||
val launchId = PlayerLaunchStore.put(playerLaunch)
|
||||
navController.navigate(PlayerRoute(launchId = launchId))
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
fun launchPlaybackWithDownloadPreference(
|
||||
type: String,
|
||||
videoId: String,
|
||||
|
|
@ -1012,25 +1061,46 @@ private fun MainAppContent(
|
|||
}
|
||||
|
||||
val openContinueWatching: (ContinueWatchingItem, Boolean, Boolean) -> Unit = { item, manualSelection, 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,
|
||||
)
|
||||
if (item.isCloudLibraryContinueWatchingItem()) {
|
||||
coroutineScope.launch {
|
||||
val target = CloudLibraryRepository.findPlaybackTargetForProgress(
|
||||
contentId = item.parentMetaId,
|
||||
videoId = item.videoId,
|
||||
)
|
||||
val launched = target?.let { playbackTarget ->
|
||||
launchCloudLibraryFile(
|
||||
item = playbackTarget.item,
|
||||
file = playbackTarget.file,
|
||||
resumePositionMs = item.resumePositionMs,
|
||||
resumeProgressFraction = item.resumeProgressFraction,
|
||||
startFromBeginning = startFromBeginning,
|
||||
)
|
||||
} == true
|
||||
if (!launched) {
|
||||
NuvioToastController.show(cloudLibraryPlayFailedText)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val onContinueWatchingClick: (ContinueWatchingItem) -> Unit = { item ->
|
||||
|
|
@ -1165,36 +1235,8 @@ private fun MainAppContent(
|
|||
onLibrarySectionViewAllClick = onLibrarySectionViewAllClick,
|
||||
onCloudFilePlay = { item, file ->
|
||||
coroutineScope.launch {
|
||||
when (
|
||||
val resolved = CloudLibraryRepository.resolvePlayback(
|
||||
item = item,
|
||||
file = file,
|
||||
)
|
||||
) {
|
||||
is CloudLibraryPlaybackResult.Success -> {
|
||||
val playbackTitle = file.name.ifBlank { item.name }
|
||||
val playerLaunch = PlayerLaunch(
|
||||
title = playbackTitle,
|
||||
sourceUrl = resolved.url,
|
||||
streamTitle = playbackTitle,
|
||||
streamSubtitle = item.name.takeIf { it != playbackTitle },
|
||||
providerName = item.providerName,
|
||||
providerAddonId = "cloud:${item.providerId}",
|
||||
contentType = "cloud",
|
||||
videoId = "${item.stableKey}:${file.stableKey}",
|
||||
parentMetaId = item.stableKey,
|
||||
parentMetaType = "cloud",
|
||||
)
|
||||
if (playerSettingsUiState.externalPlayerEnabled) {
|
||||
openExternalPlayback(playerLaunch)
|
||||
return@launch
|
||||
}
|
||||
val launchId = PlayerLaunchStore.put(playerLaunch)
|
||||
navController.navigate(PlayerRoute(launchId = launchId))
|
||||
}
|
||||
else -> {
|
||||
NuvioToastController.show(cloudLibraryPlayFailedText)
|
||||
}
|
||||
if (!launchCloudLibraryFile(item = item, file = file)) {
|
||||
NuvioToastController.show(cloudLibraryPlayFailedText)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
@ -2161,6 +2203,7 @@ private fun MainAppContent(
|
|||
NuvioContinueWatchingActionSheet(
|
||||
item = selectedContinueWatchingForActions,
|
||||
showManualPlayOption = StreamAutoPlayPolicy.isEffectivelyEnabled(playerSettingsUiState),
|
||||
showDetailsOption = selectedContinueWatchingForActions?.isCloudLibraryContinueWatchingItem() != true,
|
||||
onDismiss = { selectedContinueWatchingForActions = null },
|
||||
onOpenDetails = {
|
||||
selectedContinueWatchingForActions?.let { item ->
|
||||
|
|
@ -2540,6 +2583,9 @@ private fun TabletFloatingTopBar(
|
|||
}
|
||||
}
|
||||
|
||||
private fun ContinueWatchingItem.isCloudLibraryContinueWatchingItem(): Boolean =
|
||||
parentMetaType.equals(CloudLibraryContentType, ignoreCase = true)
|
||||
|
||||
@Composable
|
||||
private fun TabletTopPillItem(
|
||||
label: String,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package com.nuvio.app.core.ui
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import com.nuvio.app.features.cloud.CloudLibraryContentType
|
||||
import com.nuvio.app.features.watchprogress.ContinueWatchingItem
|
||||
import nuvio.composeapp.generated.resources.*
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
|
|
@ -18,6 +19,8 @@ fun localizedContinueWatchingSubtitle(item: ContinueWatchingItem): String {
|
|||
stringResource(Res.string.compose_player_episode_code_full, seasonNumber, episodeNumber)
|
||||
item.isNextUp ->
|
||||
stringResource(Res.string.continue_watching_up_next)
|
||||
item.parentMetaType.equals(CloudLibraryContentType, ignoreCase = true) ->
|
||||
stringResource(Res.string.library_source_cloud)
|
||||
else ->
|
||||
stringResource(Res.string.media_movie)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,6 +28,8 @@ import androidx.compose.ui.text.font.FontWeight
|
|||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import coil3.compose.AsyncImage
|
||||
import com.nuvio.app.features.cloud.CloudLibraryContentType
|
||||
import com.nuvio.app.features.cloud.cloudLibraryDisplayArtworkUrl
|
||||
import com.nuvio.app.features.watchprogress.ContinueWatchingItem
|
||||
import kotlinx.coroutines.launch
|
||||
import nuvio.composeapp.generated.resources.Res
|
||||
|
|
@ -42,6 +44,7 @@ import org.jetbrains.compose.resources.stringResource
|
|||
fun NuvioContinueWatchingActionSheet(
|
||||
item: ContinueWatchingItem?,
|
||||
showManualPlayOption: Boolean,
|
||||
showDetailsOption: Boolean = true,
|
||||
onDismiss: () -> Unit,
|
||||
onOpenDetails: () -> Unit,
|
||||
onStartFromBeginning: (() -> Unit)? = null,
|
||||
|
|
@ -73,12 +76,14 @@ fun NuvioContinueWatchingActionSheet(
|
|||
.padding(bottom = nuvioSafeBottomPadding(16.dp)),
|
||||
) {
|
||||
ContinueWatchingSheetHeader(item = item)
|
||||
NuvioBottomSheetDivider()
|
||||
NuvioBottomSheetActionRow(
|
||||
icon = Icons.Default.Info,
|
||||
title = stringResource(Res.string.cw_action_go_to_details),
|
||||
onClick = { dismissAfter(onOpenDetails) },
|
||||
)
|
||||
if (showDetailsOption) {
|
||||
NuvioBottomSheetDivider()
|
||||
NuvioBottomSheetActionRow(
|
||||
icon = Icons.Default.Info,
|
||||
title = stringResource(Res.string.cw_action_go_to_details),
|
||||
onClick = { dismissAfter(onOpenDetails) },
|
||||
)
|
||||
}
|
||||
if (showManualPlayOption && onPlayManually != null) {
|
||||
NuvioBottomSheetDivider()
|
||||
NuvioBottomSheetActionRow(
|
||||
|
|
@ -128,10 +133,10 @@ private fun ContinueWatchingSheetHeader(
|
|||
val artwork = item.poster ?: item.imageUrl
|
||||
if (artwork != null) {
|
||||
AsyncImage(
|
||||
model = artwork,
|
||||
model = cloudLibraryDisplayArtworkUrl(artwork),
|
||||
contentDescription = item.title,
|
||||
modifier = Modifier.matchParentSize(),
|
||||
contentScale = ContentScale.Crop,
|
||||
contentScale = if (item.isCloudLibraryItem()) ContentScale.Fit else ContentScale.Crop,
|
||||
)
|
||||
} else {
|
||||
Text(
|
||||
|
|
@ -167,3 +172,6 @@ private fun ContinueWatchingSheetHeader(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun ContinueWatchingItem.isCloudLibraryItem(): Boolean =
|
||||
parentMetaType.equals(CloudLibraryContentType, ignoreCase = true)
|
||||
|
|
|
|||
|
|
@ -39,6 +39,43 @@ data class CloudLibraryItem(
|
|||
get() = files.filter { it.playable }
|
||||
}
|
||||
|
||||
data class CloudLibraryPlaybackTarget(
|
||||
val item: CloudLibraryItem,
|
||||
val file: CloudLibraryFile,
|
||||
)
|
||||
|
||||
const val CloudLibraryContentType = "cloud"
|
||||
const val TorboxCloudLibraryPosterUrl = "https://torbox.app/assets/logo-bb7a9579.svg"
|
||||
const val PremiumizeCloudLibraryPosterUrl = "https://www.premiumize.me/icon_normal.svg"
|
||||
private const val TorboxCloudLibraryPosterDataUrl =
|
||||
"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjM2NyAzMDggNzY2IDg4NCI+PHBvbHlnb24gZmlsbD0iIzAwNDQ0RCIgcG9pbnRzPSI3NDkuOTksNzQ5Ljk5IDc0OS45OSwxMTkxLjk2IDM2Ny4yNSw5NzAuOTcgMzY3LjI1LDUyOS4wMSIvPjxwb2x5Z29uIGZpbGw9IiMzNEJBOTAiIHBvaW50cz0iMTEzMi43NSw1MjkuMDEgMTEzMi43NSw5NzAuOTcgNzQ5Ljk5LDExOTEuOTYgNzQ5Ljk5LDc0OS45OSA4NzIuODcsNjc5LjA1IDk1Ni43MSw2MzAuNjYiLz48cG9seWdvbiBmaWxsPSIjNTJBMTUzIiBwb2ludHM9IjExMzIuNzUsNTI5LjAxIDc0OS45OSw3NDkuOTkgMzY3LjI1LDUyOS4wMSA3NDkuOTksMzA4LjA0Ii8+PHBvbHlnb24gZmlsbD0iI0ZGRkZGRiIgcG9pbnRzPSIxMDQzLjA0LDczOS4zNiA5NTguNjYsMTA1Ny4wOCA5NTIuNCw4NTEuODQgODM5LjcxLDkxNS4zOSA4NzIuODcsNjc5LjA1IDk1Ni43MSw2MzAuNjYgOTMxLjgxLDc5OS4yMSIvPjwvc3ZnPg=="
|
||||
|
||||
fun CloudLibraryItem.playbackVideoId(file: CloudLibraryFile): String =
|
||||
"$stableKey:${file.stableKey}"
|
||||
|
||||
fun CloudLibraryItem.providerPosterUrl(): String? =
|
||||
cloudLibraryProviderPosterUrl(providerId)
|
||||
|
||||
fun cloudLibraryProviderPosterUrl(providerIdOrContentId: String?): String? =
|
||||
when (providerIdOrContentId.normalizedCloudLibraryProviderId()) {
|
||||
"torbox" -> TorboxCloudLibraryPosterUrl
|
||||
"premiumize" -> PremiumizeCloudLibraryPosterUrl
|
||||
else -> null
|
||||
}
|
||||
|
||||
fun cloudLibraryDisplayArtworkUrl(url: String?): String? =
|
||||
when (url?.trim()) {
|
||||
TorboxCloudLibraryPosterUrl -> TorboxCloudLibraryPosterDataUrl
|
||||
else -> url?.trim()
|
||||
}
|
||||
|
||||
private fun String?.normalizedCloudLibraryProviderId(): String =
|
||||
orEmpty()
|
||||
.trim()
|
||||
.removePrefix("$CloudLibraryContentType:")
|
||||
.substringBefore(':')
|
||||
.lowercase()
|
||||
|
||||
data class CloudLibraryProviderState(
|
||||
val provider: DebridProvider,
|
||||
val isLoading: Boolean = false,
|
||||
|
|
|
|||
|
|
@ -121,6 +121,29 @@ object CloudLibraryRepository {
|
|||
}
|
||||
}
|
||||
|
||||
suspend fun findPlaybackTargetForProgress(
|
||||
contentId: String,
|
||||
videoId: String,
|
||||
): CloudLibraryPlaybackTarget? {
|
||||
DebridSettingsRepository.ensureLoaded()
|
||||
if (!DebridSettingsRepository.snapshot().cloudLibraryEnabled) {
|
||||
loadedConnectionKeys = emptyList()
|
||||
_uiState.value = CloudLibraryUiState(isLoaded = true, isEnabled = false)
|
||||
return null
|
||||
}
|
||||
|
||||
_uiState.value.findPlaybackTargetForProgress(
|
||||
contentId = contentId,
|
||||
videoId = videoId,
|
||||
)?.let { target -> return target }
|
||||
|
||||
val refreshed = refreshNow()
|
||||
return refreshed.findPlaybackTargetForProgress(
|
||||
contentId = contentId,
|
||||
videoId = videoId,
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun resolvePlayback(
|
||||
item: CloudLibraryItem,
|
||||
file: CloudLibraryFile,
|
||||
|
|
@ -147,8 +170,47 @@ object CloudLibraryRepository {
|
|||
)
|
||||
}.sortedBy { it.providerId }
|
||||
|
||||
private suspend fun refreshNow(): CloudLibraryUiState {
|
||||
_uiState.update { current ->
|
||||
current.copy(
|
||||
isEnabled = true,
|
||||
isRefreshing = true,
|
||||
providers = current.providers.map { it.copy(isLoading = true, errorMessage = null) },
|
||||
)
|
||||
}
|
||||
val refreshed = store.refresh()
|
||||
loadedConnectionKeys = connectedCloudConnectionKeys()
|
||||
_uiState.value = refreshed
|
||||
return refreshed
|
||||
}
|
||||
|
||||
private data class CloudConnectionKey(
|
||||
val providerId: String,
|
||||
val apiKeyHash: Int,
|
||||
)
|
||||
}
|
||||
|
||||
internal fun CloudLibraryUiState.findPlaybackTargetForProgress(
|
||||
contentId: String,
|
||||
videoId: String,
|
||||
): CloudLibraryPlaybackTarget? {
|
||||
val normalizedContentId = contentId.trim()
|
||||
val normalizedVideoId = videoId.trim()
|
||||
if (normalizedContentId.isBlank()) return null
|
||||
|
||||
val matchingItems = items.filter { item -> item.stableKey == normalizedContentId }
|
||||
if (matchingItems.isEmpty()) return null
|
||||
|
||||
for (item in matchingItems) {
|
||||
val exactFile = item.playableFiles.firstOrNull { file ->
|
||||
item.playbackVideoId(file) == normalizedVideoId
|
||||
}
|
||||
if (exactFile != null) {
|
||||
return CloudLibraryPlaybackTarget(item = item, file = exactFile)
|
||||
}
|
||||
}
|
||||
|
||||
val singleItem = matchingItems.singleOrNull() ?: return null
|
||||
val singleFile = singleItem.playableFiles.singleOrNull() ?: return null
|
||||
return CloudLibraryPlaybackTarget(item = singleItem, file = singleFile)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -45,6 +45,8 @@ import coil3.compose.AsyncImage
|
|||
import com.nuvio.app.core.ui.NuvioProgressBar
|
||||
import com.nuvio.app.core.ui.NuvioShelfSection
|
||||
import com.nuvio.app.core.ui.posterCardClickable
|
||||
import com.nuvio.app.features.cloud.CloudLibraryContentType
|
||||
import com.nuvio.app.features.cloud.cloudLibraryDisplayArtworkUrl
|
||||
import com.nuvio.app.features.home.HomeCatalogSettingsRepository
|
||||
import com.nuvio.app.features.watchprogress.ContinueWatchingItem
|
||||
import com.nuvio.app.features.watchprogress.ContinueWatchingSectionStyle
|
||||
|
|
@ -64,10 +66,15 @@ private fun localizedContinueWatchingMetaLine(item: ContinueWatchingItem): Strin
|
|||
stringResource(Res.string.compose_player_episode_code_full, item.seasonNumber, item.episodeNumber)
|
||||
item.isNextUp ->
|
||||
stringResource(Res.string.continue_watching_up_next)
|
||||
item.isCloudLibraryItem() ->
|
||||
stringResource(Res.string.library_source_cloud)
|
||||
else ->
|
||||
stringResource(Res.string.media_movie)
|
||||
}
|
||||
|
||||
private fun ContinueWatchingItem.isCloudLibraryItem(): Boolean =
|
||||
parentMetaType.equals(CloudLibraryContentType, ignoreCase = true)
|
||||
|
||||
private fun ContinueWatchingItem.continueWatchingArtworkUrl(
|
||||
useEpisodeThumbnails: Boolean,
|
||||
): String? = when {
|
||||
|
|
@ -392,6 +399,7 @@ private fun ContinueWatchingWideCard(
|
|||
imageUrl = artworkUrl,
|
||||
width = layout.widePosterStripWidth,
|
||||
blurred = shouldBlurArtwork,
|
||||
contentScale = if (item.isCloudLibraryItem()) ContentScale.Fit else ContentScale.Crop,
|
||||
modifier = Modifier.fillMaxHeight(),
|
||||
)
|
||||
Column(
|
||||
|
|
@ -504,12 +512,12 @@ private fun ContinueWatchingPosterCard(
|
|||
val imageUrl = item.continueWatchingArtworkUrl(useEpisodeThumbnails)
|
||||
if (imageUrl != null) {
|
||||
AsyncImage(
|
||||
model = imageUrl,
|
||||
model = cloudLibraryDisplayArtworkUrl(imageUrl),
|
||||
contentDescription = item.title,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.then(if (shouldBlurArtwork) Modifier.blur(18.dp) else Modifier),
|
||||
contentScale = ContentScale.Crop,
|
||||
contentScale = if (item.isCloudLibraryItem()) ContentScale.Fit else ContentScale.Crop,
|
||||
)
|
||||
}
|
||||
if (item.progressFraction <= 0f && item.seasonNumber != null && item.episodeNumber != null) {
|
||||
|
|
@ -589,6 +597,7 @@ private fun ArtworkPanel(
|
|||
imageUrl: String?,
|
||||
width: Dp,
|
||||
blurred: Boolean = false,
|
||||
contentScale: ContentScale = ContentScale.Crop,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Box(
|
||||
|
|
@ -598,12 +607,12 @@ private fun ArtworkPanel(
|
|||
) {
|
||||
if (imageUrl != null) {
|
||||
AsyncImage(
|
||||
model = imageUrl,
|
||||
model = cloudLibraryDisplayArtworkUrl(imageUrl),
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.then(if (blurred) Modifier.blur(18.dp) else Modifier),
|
||||
contentScale = ContentScale.Crop,
|
||||
contentScale = contentScale,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
package com.nuvio.app.features.watchprogress
|
||||
|
||||
import com.nuvio.app.features.cloud.CloudLibraryContentType
|
||||
import com.nuvio.app.features.cloud.cloudLibraryProviderPosterUrl
|
||||
import com.nuvio.app.features.details.MetaVideo
|
||||
import com.nuvio.app.features.watching.domain.WatchingContentRef
|
||||
import kotlinx.serialization.Serializable
|
||||
|
|
@ -199,6 +201,7 @@ internal fun nextUpDismissKey(
|
|||
|
||||
internal fun WatchProgressEntry.toContinueWatchingItem(): ContinueWatchingItem {
|
||||
val normalizedEntry = normalizedCompletion()
|
||||
val cloudPosterUrl = normalizedEntry.cloudLibraryPosterFallbackUrl()
|
||||
val explicitResumeProgressFraction = normalizedEntry.normalizedProgressPercent
|
||||
?.takeIf { durationMs <= 0L && it > 0f }
|
||||
?.let { explicitPercent -> (explicitPercent / 100f).coerceIn(0f, 1f) }
|
||||
|
|
@ -213,9 +216,9 @@ internal fun WatchProgressEntry.toContinueWatchingItem(): ContinueWatchingItem {
|
|||
episodeNumber = normalizedEntry.episodeNumber,
|
||||
episodeTitle = normalizedEntry.episodeTitle,
|
||||
),
|
||||
imageUrl = normalizedEntry.episodeThumbnail ?: normalizedEntry.background ?: normalizedEntry.poster,
|
||||
imageUrl = normalizedEntry.episodeThumbnail ?: normalizedEntry.background ?: normalizedEntry.poster ?: cloudPosterUrl,
|
||||
logo = normalizedEntry.logo,
|
||||
poster = normalizedEntry.poster,
|
||||
poster = normalizedEntry.poster ?: cloudPosterUrl,
|
||||
background = normalizedEntry.background,
|
||||
seasonNumber = normalizedEntry.seasonNumber,
|
||||
episodeNumber = normalizedEntry.episodeNumber,
|
||||
|
|
@ -233,6 +236,16 @@ internal fun WatchProgressEntry.toContinueWatchingItem(): ContinueWatchingItem {
|
|||
)
|
||||
}
|
||||
|
||||
private fun WatchProgressEntry.cloudLibraryPosterFallbackUrl(): String? {
|
||||
if (!contentType.equals(CloudLibraryContentType, ignoreCase = true) &&
|
||||
!parentMetaType.equals(CloudLibraryContentType, ignoreCase = true)
|
||||
) {
|
||||
return null
|
||||
}
|
||||
return cloudLibraryProviderPosterUrl(parentMetaId)
|
||||
?: cloudLibraryProviderPosterUrl(providerAddonId)
|
||||
}
|
||||
|
||||
internal fun WatchProgressEntry.toUpNextContinueWatchingItem(
|
||||
nextEpisode: MetaVideo,
|
||||
): ContinueWatchingItem {
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import com.nuvio.app.features.debrid.DebridServiceCredential
|
|||
import kotlinx.coroutines.runBlocking
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNotNull
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class CloudLibraryStoreTest {
|
||||
|
|
@ -68,6 +69,86 @@ class CloudLibraryStoreTest {
|
|||
assertEquals(listOf("cloud"), state.providers.map { it.providerId })
|
||||
assertEquals(listOf("cloud-item"), state.items.map { it.id })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `playback target lookup matches cloud watch progress video id`() {
|
||||
val provider = cloudProvider(id = "torbox", name = "TorBox")
|
||||
val item = CloudLibraryItem(
|
||||
providerId = provider.id,
|
||||
providerName = provider.displayName,
|
||||
id = "29773238",
|
||||
type = CloudLibraryItemType.Torrent,
|
||||
name = "Torrent",
|
||||
files = listOf(
|
||||
CloudLibraryFile(id = "7", name = "sample.mkv", playable = true),
|
||||
CloudLibraryFile(id = "8", name = "movie.mkv", playable = true),
|
||||
),
|
||||
)
|
||||
val state = CloudLibraryUiState(
|
||||
isLoaded = true,
|
||||
providers = listOf(
|
||||
CloudLibraryProviderState(
|
||||
provider = provider,
|
||||
items = listOf(item),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
val target = assertNotNull(
|
||||
state.findPlaybackTargetForProgress(
|
||||
contentId = "torbox:Torrent:29773238",
|
||||
videoId = "torbox:Torrent:29773238:8",
|
||||
),
|
||||
)
|
||||
|
||||
assertEquals(item, target.item)
|
||||
assertEquals("8", target.file.id)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `playback target lookup falls back to single playable file`() {
|
||||
val provider = cloudProvider(id = "torbox", name = "TorBox")
|
||||
val item = cloudItem(provider, "29773238")
|
||||
val state = CloudLibraryUiState(
|
||||
isLoaded = true,
|
||||
providers = listOf(
|
||||
CloudLibraryProviderState(
|
||||
provider = provider,
|
||||
items = listOf(item),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
val target = assertNotNull(
|
||||
state.findPlaybackTargetForProgress(
|
||||
contentId = item.stableKey,
|
||||
videoId = item.stableKey,
|
||||
),
|
||||
)
|
||||
|
||||
assertEquals(item, target.item)
|
||||
assertEquals(item.playableFiles.single(), target.file)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `provider poster urls are mapped for cloud services`() {
|
||||
assertEquals(
|
||||
TorboxCloudLibraryPosterUrl,
|
||||
cloudLibraryProviderPosterUrl("torbox:Torrent:29773238"),
|
||||
)
|
||||
assertEquals(
|
||||
PremiumizeCloudLibraryPosterUrl,
|
||||
cloudLibraryProviderPosterUrl("cloud:premiumize"),
|
||||
)
|
||||
assertTrue(
|
||||
cloudLibraryDisplayArtworkUrl(TorboxCloudLibraryPosterUrl)
|
||||
?.startsWith("data:image/svg+xml;base64,") == true,
|
||||
)
|
||||
assertEquals(
|
||||
PremiumizeCloudLibraryPosterUrl,
|
||||
cloudLibraryDisplayArtworkUrl(PremiumizeCloudLibraryPosterUrl),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private class FakeCloudProviderApi(
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package com.nuvio.app.features.watchprogress
|
||||
|
||||
import com.nuvio.app.features.cloud.TorboxCloudLibraryPosterUrl
|
||||
import com.nuvio.app.features.details.MetaVideo
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
|
@ -95,6 +96,23 @@ class WatchProgressRulesTest {
|
|||
assertEquals(2, result.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `cloud continue watching uses provider poster fallback`() {
|
||||
val item = WatchProgressEntry(
|
||||
contentType = "cloud",
|
||||
parentMetaId = "torbox:Torrent:29773238",
|
||||
parentMetaType = "cloud",
|
||||
videoId = "torbox:Torrent:29773238:8",
|
||||
title = "Cloud file",
|
||||
lastPositionMs = 120_000L,
|
||||
durationMs = 1_000_000L,
|
||||
lastUpdatedEpochMs = 1L,
|
||||
).toContinueWatchingItem()
|
||||
|
||||
assertEquals(TorboxCloudLibraryPosterUrl, item.poster)
|
||||
assertEquals(TorboxCloudLibraryPosterUrl, item.imageUrl)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `continue watching excludes explicit 100 percent entries even when completion flag is false`() {
|
||||
val completedByPercent = entry(
|
||||
|
|
|
|||
Loading…
Reference in a new issue