mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-05-16 23:12:12 +00:00
parent
17c747e5c1
commit
ee66440bf5
9 changed files with 135 additions and 21 deletions
|
|
@ -1338,7 +1338,13 @@ private fun MainAppContent(
|
|||
reuseHandled = true
|
||||
if (launch.manualSelection) return@LaunchedEffect
|
||||
if (!playerSettings.streamReuseLastLinkEnabled) return@LaunchedEffect
|
||||
val cacheKey = StreamLinkCacheRepository.contentKey(launch.type, effectiveVideoId)
|
||||
val cacheKey = StreamLinkCacheRepository.contentKey(
|
||||
type = launch.type,
|
||||
videoId = effectiveVideoId,
|
||||
parentMetaId = launch.parentMetaId,
|
||||
season = launch.seasonNumber,
|
||||
episode = launch.episodeNumber,
|
||||
)
|
||||
val maxAgeMs = playerSettings.streamReuseLastLinkCacheHours * 60L * 60L * 1000L
|
||||
val cached = StreamLinkCacheRepository.getValid(cacheKey, maxAgeMs)
|
||||
if (cached != null) {
|
||||
|
|
@ -1378,17 +1384,37 @@ private fun MainAppContent(
|
|||
}
|
||||
|
||||
val streamsUiState by StreamsRepository.uiState.collectAsStateWithLifecycle()
|
||||
val expectedStreamsRequestToken = StreamsRepository.requestToken(
|
||||
type = launch.type,
|
||||
videoId = effectiveVideoId,
|
||||
season = launch.seasonNumber,
|
||||
episode = launch.episodeNumber,
|
||||
manualSelection = launch.manualSelection,
|
||||
)
|
||||
var autoPlayHandled by rememberSaveable(launch.videoId, effectiveVideoId) { mutableStateOf(false) }
|
||||
LaunchedEffect(streamsUiState.autoPlayStream, reuseHandled, launch.manualSelection) {
|
||||
LaunchedEffect(
|
||||
streamsUiState.autoPlayStream,
|
||||
streamsUiState.requestToken,
|
||||
expectedStreamsRequestToken,
|
||||
reuseHandled,
|
||||
launch.manualSelection,
|
||||
) {
|
||||
if (!reuseHandled) return@LaunchedEffect
|
||||
if (launch.manualSelection) return@LaunchedEffect
|
||||
if (reuseNavigated) return@LaunchedEffect
|
||||
if (autoPlayHandled) return@LaunchedEffect
|
||||
if (streamsUiState.requestToken != expectedStreamsRequestToken) return@LaunchedEffect
|
||||
val stream = streamsUiState.autoPlayStream ?: return@LaunchedEffect
|
||||
val sourceUrl = stream.directPlaybackUrl ?: return@LaunchedEffect
|
||||
autoPlayHandled = true
|
||||
if (playerSettings.streamReuseLastLinkEnabled) {
|
||||
val cacheKey = StreamLinkCacheRepository.contentKey(launch.type, effectiveVideoId)
|
||||
val cacheKey = StreamLinkCacheRepository.contentKey(
|
||||
type = launch.type,
|
||||
videoId = effectiveVideoId,
|
||||
parentMetaId = launch.parentMetaId,
|
||||
season = launch.seasonNumber,
|
||||
episode = launch.episodeNumber,
|
||||
)
|
||||
StreamLinkCacheRepository.save(
|
||||
contentKey = cacheKey,
|
||||
url = sourceUrl,
|
||||
|
|
@ -1468,7 +1494,13 @@ private fun MainAppContent(
|
|||
if (sourceUrl != null) {
|
||||
// Persist for Reuse Last Link
|
||||
if (playerSettings.streamReuseLastLinkEnabled) {
|
||||
val cacheKey = StreamLinkCacheRepository.contentKey(launch.type, effectiveVideoId)
|
||||
val cacheKey = StreamLinkCacheRepository.contentKey(
|
||||
type = launch.type,
|
||||
videoId = effectiveVideoId,
|
||||
parentMetaId = launch.parentMetaId,
|
||||
season = launch.seasonNumber,
|
||||
episode = launch.episodeNumber,
|
||||
)
|
||||
StreamLinkCacheRepository.save(
|
||||
contentKey = cacheKey,
|
||||
url = sourceUrl,
|
||||
|
|
|
|||
|
|
@ -791,8 +791,11 @@ fun PlayerScreen(
|
|||
flushWatchProgress()
|
||||
if (playerSettingsUiState.streamReuseLastLinkEnabled && activeVideoId != null) {
|
||||
val cacheKey = StreamLinkCacheRepository.contentKey(
|
||||
contentType ?: parentMetaType,
|
||||
activeVideoId!!,
|
||||
type = contentType ?: parentMetaType,
|
||||
videoId = activeVideoId!!,
|
||||
parentMetaId = parentMetaId,
|
||||
season = activeSeasonNumber,
|
||||
episode = activeEpisodeNumber,
|
||||
)
|
||||
StreamLinkCacheRepository.save(
|
||||
contentKey = cacheKey,
|
||||
|
|
@ -851,8 +854,11 @@ fun PlayerScreen(
|
|||
val epResumePositionMs = epEntry?.lastPositionMs?.takeIf { it > 0L } ?: 0L
|
||||
if (playerSettingsUiState.streamReuseLastLinkEnabled) {
|
||||
val cacheKey = StreamLinkCacheRepository.contentKey(
|
||||
contentType ?: parentMetaType,
|
||||
epVideoId,
|
||||
type = contentType ?: parentMetaType,
|
||||
videoId = epVideoId,
|
||||
parentMetaId = parentMetaId,
|
||||
season = episode.season,
|
||||
episode = episode.episode,
|
||||
)
|
||||
StreamLinkCacheRepository.save(
|
||||
contentKey = cacheKey,
|
||||
|
|
@ -1563,8 +1569,11 @@ fun PlayerScreen(
|
|||
val currentVideoId = activeVideoId
|
||||
if (currentVideoId != null) {
|
||||
val cacheKey = StreamLinkCacheRepository.contentKey(
|
||||
contentType ?: parentMetaType,
|
||||
currentVideoId,
|
||||
type = contentType ?: parentMetaType,
|
||||
videoId = currentVideoId,
|
||||
parentMetaId = parentMetaId,
|
||||
season = activeSeasonNumber,
|
||||
episode = activeEpisodeNumber,
|
||||
)
|
||||
StreamLinkCacheRepository.remove(cacheKey)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,8 +22,20 @@ internal expect fun epochMs(): Long
|
|||
object StreamLinkCacheRepository {
|
||||
private val json = Json { ignoreUnknownKeys = true }
|
||||
|
||||
fun contentKey(type: String, videoId: String): String =
|
||||
"${type.lowercase()}|$videoId"
|
||||
fun contentKey(
|
||||
type: String,
|
||||
videoId: String,
|
||||
parentMetaId: String? = null,
|
||||
season: Int? = null,
|
||||
episode: Int? = null,
|
||||
): String {
|
||||
val normalizedType = type.lowercase()
|
||||
return if (!parentMetaId.isNullOrBlank() && season != null && episode != null) {
|
||||
"$normalizedType|${parentMetaId.trim()}|s$season|e$episode|$videoId"
|
||||
} else {
|
||||
"$normalizedType|$videoId"
|
||||
}
|
||||
}
|
||||
|
||||
fun save(
|
||||
contentKey: String,
|
||||
|
|
|
|||
|
|
@ -66,6 +66,7 @@ enum class StreamsEmptyStateReason {
|
|||
}
|
||||
|
||||
data class StreamsUiState(
|
||||
val requestToken: String? = null,
|
||||
val groups: List<AddonStreamGroup> = emptyList(),
|
||||
val activeAddonIds: Set<String> = emptySet(),
|
||||
val selectedFilter: String? = null,
|
||||
|
|
|
|||
|
|
@ -36,6 +36,15 @@ object StreamsRepository {
|
|||
private var activeJob: Job? = null
|
||||
private var activeRequestKey: String? = null
|
||||
|
||||
fun requestToken(
|
||||
type: String,
|
||||
videoId: String,
|
||||
season: Int? = null,
|
||||
episode: Int? = null,
|
||||
manualSelection: Boolean = false,
|
||||
): String =
|
||||
"$type::$videoId::$season::$episode::$manualSelection"
|
||||
|
||||
fun load(type: String, videoId: String, season: Int? = null, episode: Int? = null, manualSelection: Boolean = false) {
|
||||
load(
|
||||
type = type,
|
||||
|
|
@ -65,7 +74,14 @@ object StreamsRepository {
|
|||
} else {
|
||||
PluginsUiState(pluginsEnabled = false)
|
||||
}
|
||||
val requestKey = "$type::$videoId::$season::$episode::$manualSelection::pluginsGrouped=${pluginUiState.groupStreamsByRepository}"
|
||||
val requestToken = requestToken(
|
||||
type = type,
|
||||
videoId = videoId,
|
||||
season = season,
|
||||
episode = episode,
|
||||
manualSelection = manualSelection,
|
||||
)
|
||||
val requestKey = "$requestToken::pluginsGrouped=${pluginUiState.groupStreamsByRepository}"
|
||||
val currentState = _uiState.value
|
||||
if (
|
||||
!forceRefresh &&
|
||||
|
|
@ -78,7 +94,7 @@ object StreamsRepository {
|
|||
|
||||
activeRequestKey = requestKey
|
||||
activeJob?.cancel()
|
||||
_uiState.value = StreamsUiState()
|
||||
_uiState.value = StreamsUiState(requestToken = requestToken)
|
||||
|
||||
PlayerSettingsRepository.ensureLoaded()
|
||||
val playerSettings = PlayerSettingsRepository.uiState.value
|
||||
|
|
@ -90,6 +106,7 @@ object StreamsRepository {
|
|||
|
||||
if (isDirectAutoPlayFlow) {
|
||||
_uiState.value = StreamsUiState(
|
||||
requestToken = requestToken,
|
||||
isDirectAutoPlayFlow = true,
|
||||
showDirectAutoPlayOverlay = true,
|
||||
)
|
||||
|
|
@ -105,6 +122,7 @@ object StreamsRepository {
|
|||
isLoading = false,
|
||||
)
|
||||
_uiState.value = StreamsUiState(
|
||||
requestToken = requestToken,
|
||||
groups = listOf(group),
|
||||
activeAddonIds = setOf("embedded"),
|
||||
isAnyLoading = false,
|
||||
|
|
@ -125,6 +143,7 @@ object StreamsRepository {
|
|||
|
||||
if (installedAddons.isEmpty() && pluginProviderGroups.isEmpty()) {
|
||||
_uiState.value = StreamsUiState(
|
||||
requestToken = requestToken,
|
||||
isAnyLoading = false,
|
||||
emptyStateReason = StreamsEmptyStateReason.NoAddonsInstalled,
|
||||
)
|
||||
|
|
@ -151,8 +170,9 @@ object StreamsRepository {
|
|||
|
||||
log.d { "Found ${streamAddons.size} addons for stream type=$type id=$videoId" }
|
||||
|
||||
if (streamAddons.isEmpty() && pluginProviderGroups.isEmpty()) {
|
||||
if (streamAddons.isEmpty() && pluginProviderGroups.isEmpty()) {
|
||||
_uiState.value = StreamsUiState(
|
||||
requestToken = requestToken,
|
||||
isAnyLoading = false,
|
||||
emptyStateReason = StreamsEmptyStateReason.NoCompatibleAddons,
|
||||
)
|
||||
|
|
@ -176,6 +196,7 @@ object StreamsRepository {
|
|||
)
|
||||
}
|
||||
_uiState.value = StreamsUiState(
|
||||
requestToken = requestToken,
|
||||
groups = initialGroups,
|
||||
activeAddonIds = initialGroups.map { it.addonId }.toSet(),
|
||||
isAnyLoading = true,
|
||||
|
|
|
|||
|
|
@ -160,7 +160,7 @@ fun StreamsScreen(
|
|||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(type, videoId, manualSelection) {
|
||||
LaunchedEffect(type, videoId, seasonNumber, episodeNumber, manualSelection) {
|
||||
StreamsRepository.load(
|
||||
type = type,
|
||||
videoId = videoId,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,39 @@
|
|||
package com.nuvio.app.features.streams
|
||||
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNotEquals
|
||||
|
||||
class StreamLinkCacheRepositoryTest {
|
||||
|
||||
@Test
|
||||
fun `movie cache key keeps legacy type and video id shape`() {
|
||||
val key = StreamLinkCacheRepository.contentKey(
|
||||
type = "movie",
|
||||
videoId = "tt123",
|
||||
)
|
||||
|
||||
assertEquals("movie|tt123", key)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `episode cache key is scoped to parent show and episode`() {
|
||||
val firstEpisode = StreamLinkCacheRepository.contentKey(
|
||||
type = "series",
|
||||
videoId = "video-id",
|
||||
parentMetaId = "tt999",
|
||||
season = 1,
|
||||
episode = 1,
|
||||
)
|
||||
val secondEpisode = StreamLinkCacheRepository.contentKey(
|
||||
type = "series",
|
||||
videoId = "video-id",
|
||||
parentMetaId = "tt999",
|
||||
season = 1,
|
||||
episode = 2,
|
||||
)
|
||||
|
||||
assertNotEquals(firstEpisode, secondEpisode)
|
||||
assertEquals("series|tt999|s1|e1|video-id", firstEpisode)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,14 +1,14 @@
|
|||
#Kotlin
|
||||
kotlin.code.style=official
|
||||
kotlin.daemon.jvmargs=-Xmx4096M
|
||||
kotlin.native.jvmArgs=-Xmx6144M
|
||||
kotlin.daemon.jvmargs=-Xmx6144M
|
||||
kotlin.native.jvmArgs=-Xmx12288M
|
||||
kotlin.mpp.enableCInteropCommonization=true
|
||||
|
||||
#Gradle
|
||||
org.gradle.jvmargs=-Xmx6144M -Dfile.encoding=UTF-8 -XX:MaxMetaspaceSize=1024m
|
||||
org.gradle.jvmargs=-Xmx8192M -Dfile.encoding=UTF-8 -XX:MaxMetaspaceSize=1536m
|
||||
org.gradle.configuration-cache=true
|
||||
org.gradle.caching=true
|
||||
|
||||
#Android
|
||||
android.nonTransitiveRClass=true
|
||||
android.useAndroidX=true
|
||||
android.useAndroidX=true
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
CURRENT_PROJECT_VERSION=58
|
||||
MARKETING_VERSION=0.1.18
|
||||
MARKETING_VERSION=0.1.0
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue