mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-08-09 00:17:59 +00:00
Remove stream warmup dead code
This commit is contained in:
parent
6188f700d6
commit
485e4e81e9
3 changed files with 5 additions and 340 deletions
|
|
@ -15,7 +15,6 @@ import com.nuvio.app.features.plugins.PluginRepository
|
|||
import com.nuvio.app.features.plugins.pluginContentId
|
||||
import com.nuvio.app.features.plugins.PluginRuntimeResult
|
||||
import com.nuvio.app.features.plugins.PluginScraper
|
||||
import com.nuvio.app.features.streams.AddonStreamWarmupRepository
|
||||
import com.nuvio.app.features.streams.AddonStreamGroup
|
||||
import com.nuvio.app.features.streams.StreamAutoPlaySelector
|
||||
import com.nuvio.app.features.streams.StreamBadgePresentation
|
||||
|
|
@ -214,17 +213,8 @@ object PlayerStreamsRepository {
|
|||
}
|
||||
|
||||
val installedAddonOrder = streamAddons.map { it.addonName }
|
||||
val warmedAddonGroups = if (forceRefresh) {
|
||||
emptyMap()
|
||||
} else {
|
||||
AddonStreamWarmupRepository
|
||||
.cachedGroups(type = type, videoId = videoId, season = season, episode = episode)
|
||||
.orEmpty()
|
||||
.associateBy { it.addonId }
|
||||
}
|
||||
val warmedAddonIds = warmedAddonGroups.keys
|
||||
val initialGroups = StreamAutoPlaySelector.orderAddonStreams(streamAddons.map { addon ->
|
||||
warmedAddonGroups[addon.addonId] ?: AddonStreamGroup(
|
||||
AddonStreamGroup(
|
||||
addonName = addon.addonName,
|
||||
addonId = addon.addonId,
|
||||
streams = emptyList(),
|
||||
|
|
@ -246,7 +236,6 @@ object PlayerStreamsRepository {
|
|||
)
|
||||
|
||||
val job = scope.launch {
|
||||
val pendingStreamAddons = streamAddons.filterNot { it.addonId in warmedAddonIds }
|
||||
val installedAddonIds = streamAddons.map { it.addonId }.toSet()
|
||||
val debridAvailabilityJobs = mutableListOf<Job>()
|
||||
fun emptyStateReason(groups: List<AddonStreamGroup>, anyLoading: Boolean) =
|
||||
|
|
@ -319,7 +308,7 @@ object PlayerStreamsRepository {
|
|||
debridAvailabilityJobs += availabilityJob
|
||||
}
|
||||
|
||||
val addonJobs = pendingStreamAddons.map { addon ->
|
||||
val addonJobs = streamAddons.map { addon ->
|
||||
async {
|
||||
val url = buildAddonResourceUrl(
|
||||
manifestUrl = addon.manifest.transportUrl,
|
||||
|
|
|
|||
|
|
@ -1,318 +0,0 @@
|
|||
package com.nuvio.app.features.streams
|
||||
|
||||
import co.touchlab.kermit.Logger
|
||||
import com.nuvio.app.features.addons.AddonManifest
|
||||
import com.nuvio.app.features.addons.AddonRepository
|
||||
import com.nuvio.app.features.addons.ManagedAddon
|
||||
import com.nuvio.app.features.addons.buildAddonResourceUrl
|
||||
import com.nuvio.app.features.addons.enabledAddons
|
||||
import com.nuvio.app.features.addons.httpGetText
|
||||
import com.nuvio.app.features.debrid.DebridSettings
|
||||
import com.nuvio.app.features.debrid.DebridSettingsRepository
|
||||
import com.nuvio.app.features.debrid.DebridStreamPresentation
|
||||
import com.nuvio.app.features.debrid.DirectDebridStreamPreparer
|
||||
import com.nuvio.app.features.debrid.LocalDebridAvailabilityService
|
||||
import com.nuvio.app.features.player.PlayerSettingsRepository
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.CoroutineStart
|
||||
import kotlinx.coroutines.Deferred
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
|
||||
private const val ADDON_STREAM_WARMUP_CACHE_TTL_MS = 5L * 60L * 1000L
|
||||
|
||||
object AddonStreamWarmupRepository {
|
||||
private val log = Logger.withTag("AddonStreamWarmup")
|
||||
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
||||
private val mutex = Mutex()
|
||||
private val cache = mutableMapOf<AddonStreamWarmupKey, CachedAddonStreamWarmup>()
|
||||
private val inFlight = mutableMapOf<AddonStreamWarmupKey, Deferred<List<AddonStreamGroup>>>()
|
||||
|
||||
fun preload(type: String, videoId: String, season: Int? = null, episode: Int? = null) {
|
||||
val key = currentKey(type = type, videoId = videoId, season = season, episode = episode) ?: return
|
||||
scope.launch {
|
||||
runCatching { fetchWarmup(key) }
|
||||
.onFailure { error ->
|
||||
if (error is CancellationException) throw error
|
||||
log.d(error) { "Addon stream warmup failed" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun cachedGroups(type: String, videoId: String, season: Int? = null, episode: Int? = null): List<AddonStreamGroup>? {
|
||||
val key = currentKey(type = type, videoId = videoId, season = season, episode = episode) ?: return null
|
||||
if (!mutex.tryLock()) return null
|
||||
return try {
|
||||
cachedGroupsLocked(key)
|
||||
} finally {
|
||||
mutex.unlock()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun fetchWarmup(key: AddonStreamWarmupKey): List<AddonStreamGroup> {
|
||||
cachedGroups(key.type, key.videoId, key.season, key.episode)?.let { return it }
|
||||
|
||||
var ownsFetch = false
|
||||
val newFetch = scope.async(start = CoroutineStart.LAZY) {
|
||||
fetchWarmupUncached(key)
|
||||
}
|
||||
val activeFetch = mutex.withLock {
|
||||
cachedGroupsLocked(key)?.let { cached ->
|
||||
return@withLock null to cached
|
||||
}
|
||||
val existing = inFlight[key]
|
||||
if (existing != null) {
|
||||
existing to null
|
||||
} else {
|
||||
inFlight[key] = newFetch
|
||||
ownsFetch = true
|
||||
newFetch to null
|
||||
}
|
||||
}
|
||||
activeFetch.second?.let {
|
||||
newFetch.cancel()
|
||||
return it
|
||||
}
|
||||
val deferred = activeFetch.first ?: return emptyList()
|
||||
if (!ownsFetch) newFetch.cancel()
|
||||
if (ownsFetch) deferred.start()
|
||||
|
||||
return try {
|
||||
val result = deferred.await()
|
||||
val cacheableGroups = result.filter { it.streams.isNotEmpty() }
|
||||
if (ownsFetch && cacheableGroups.isNotEmpty()) {
|
||||
mutex.withLock {
|
||||
cache[key] = CachedAddonStreamWarmup(
|
||||
groups = cacheableGroups,
|
||||
createdAtMs = epochMs(),
|
||||
)
|
||||
}
|
||||
}
|
||||
result
|
||||
} finally {
|
||||
if (ownsFetch) {
|
||||
mutex.withLock {
|
||||
if (inFlight[key] === deferred) {
|
||||
inFlight.remove(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun fetchWarmupUncached(key: AddonStreamWarmupKey): List<AddonStreamGroup> {
|
||||
val targets = key.addonTargets
|
||||
if (targets.isEmpty()) return emptyList()
|
||||
|
||||
val addonIds = targets.map { it.addonId }.toSet()
|
||||
val orderedGroups = coroutineScope {
|
||||
targets.map { target ->
|
||||
async {
|
||||
val group = fetchAddonStreams(
|
||||
target = target,
|
||||
type = key.type,
|
||||
videoId = key.videoId,
|
||||
)
|
||||
val eligibleGroupIds = setOf(group.addonId)
|
||||
val checkingGroup = LocalDebridAvailabilityService.markChecking(
|
||||
groups = listOf(group),
|
||||
eligibleGroupIds = eligibleGroupIds,
|
||||
).firstOrNull() ?: group
|
||||
val availabilityGroup = LocalDebridAvailabilityService.annotateCachedAvailability(
|
||||
groups = listOf(checkingGroup),
|
||||
eligibleGroupIds = eligibleGroupIds,
|
||||
).firstOrNull() ?: checkingGroup
|
||||
val badgeGroup = StreamBadgePresentation.apply(
|
||||
groups = listOf(availabilityGroup),
|
||||
rules = key.streamBadgeRules,
|
||||
).firstOrNull() ?: availabilityGroup
|
||||
DebridStreamPresentation.apply(
|
||||
groups = listOf(badgeGroup),
|
||||
settings = key.settings,
|
||||
).firstOrNull() ?: badgeGroup
|
||||
}
|
||||
}.awaitAll()
|
||||
}.let { groups ->
|
||||
StreamAutoPlaySelector.orderAddonStreams(
|
||||
groups = groups,
|
||||
installedOrder = targets.map { it.addonName },
|
||||
)
|
||||
}
|
||||
|
||||
var preparedGroups = orderedGroups
|
||||
|
||||
PlayerSettingsRepository.ensureLoaded()
|
||||
DirectDebridStreamPreparer.prepare(
|
||||
streams = preparedGroups.flatMap { it.streams },
|
||||
season = key.season,
|
||||
episode = key.episode,
|
||||
playerSettings = PlayerSettingsRepository.uiState.value,
|
||||
installedAddonNames = targets.map { it.addonName }.toSet(),
|
||||
) { original, prepared ->
|
||||
preparedGroups = DirectDebridStreamPreparer.replacePreparedStream(
|
||||
groups = preparedGroups,
|
||||
original = original,
|
||||
prepared = prepared,
|
||||
eligibleGroupIds = addonIds,
|
||||
)
|
||||
}
|
||||
|
||||
return preparedGroups
|
||||
}
|
||||
|
||||
private suspend fun fetchAddonStreams(
|
||||
target: AddonStreamWarmupTarget,
|
||||
type: String,
|
||||
videoId: String,
|
||||
): AddonStreamGroup {
|
||||
val url = buildAddonResourceUrl(
|
||||
manifestUrl = target.manifest.transportUrl,
|
||||
resource = "stream",
|
||||
type = type,
|
||||
id = videoId,
|
||||
)
|
||||
return runCatchingUnlessCancelled {
|
||||
val payload = httpGetText(url)
|
||||
StreamParser.parse(
|
||||
payload = payload,
|
||||
addonName = target.addonName,
|
||||
addonId = target.addonId,
|
||||
)
|
||||
}.fold(
|
||||
onSuccess = { streams ->
|
||||
AddonStreamGroup(
|
||||
addonName = target.addonName,
|
||||
addonId = target.addonId,
|
||||
streams = streams,
|
||||
isLoading = false,
|
||||
)
|
||||
},
|
||||
onFailure = { error ->
|
||||
log.d(error) { "Failed to warm addon stream target ${target.addonName}" }
|
||||
AddonStreamGroup(
|
||||
addonName = target.addonName,
|
||||
addonId = target.addonId,
|
||||
streams = emptyList(),
|
||||
isLoading = false,
|
||||
error = error.message,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private fun currentKey(type: String, videoId: String, season: Int?, episode: Int?): AddonStreamWarmupKey? {
|
||||
val normalizedType = type.trim().lowercase()
|
||||
val normalizedVideoId = videoId.trim()
|
||||
if (normalizedType.isBlank() || normalizedVideoId.isBlank()) return null
|
||||
|
||||
DebridSettingsRepository.ensureLoaded()
|
||||
val settings = DebridSettingsRepository.snapshot()
|
||||
if (!settings.canResolvePlayableLinks || settings.torboxApiKey.isBlank()) return null
|
||||
val streamBadgeRules = StreamBadgeSettingsRepository.snapshot()
|
||||
|
||||
AddonRepository.initialize()
|
||||
val addonTargets = AddonRepository.uiState.value.addons
|
||||
.enabledAddons()
|
||||
.mapNotNull { addon -> addon.toWarmupTarget(normalizedType, normalizedVideoId) }
|
||||
if (addonTargets.isEmpty()) return null
|
||||
|
||||
return AddonStreamWarmupKey(
|
||||
type = normalizedType,
|
||||
videoId = normalizedVideoId,
|
||||
season = season,
|
||||
episode = episode,
|
||||
addonFingerprint = addonTargets.joinToString("|") { it.fingerprint },
|
||||
settingsFingerprint = settings.warmupFingerprint(),
|
||||
streamBadgeRulesFingerprint = streamBadgeRules.toString(),
|
||||
settings = settings,
|
||||
streamBadgeRules = streamBadgeRules,
|
||||
addonTargets = addonTargets,
|
||||
)
|
||||
}
|
||||
|
||||
private fun cachedGroupsLocked(key: AddonStreamWarmupKey): List<AddonStreamGroup>? {
|
||||
val cached = cache[key] ?: return null
|
||||
val age = epochMs() - cached.createdAtMs
|
||||
return if (age in 0..ADDON_STREAM_WARMUP_CACHE_TTL_MS) {
|
||||
cached.groups
|
||||
} else {
|
||||
cache.remove(key)
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private data class AddonStreamWarmupKey(
|
||||
val type: String,
|
||||
val videoId: String,
|
||||
val season: Int?,
|
||||
val episode: Int?,
|
||||
val addonFingerprint: String,
|
||||
val settingsFingerprint: String,
|
||||
val streamBadgeRulesFingerprint: String,
|
||||
val settings: DebridSettings,
|
||||
val streamBadgeRules: StreamBadgeRules,
|
||||
val addonTargets: List<AddonStreamWarmupTarget>,
|
||||
)
|
||||
|
||||
private data class AddonStreamWarmupTarget(
|
||||
val addonName: String,
|
||||
val addonId: String,
|
||||
val manifest: AddonManifest,
|
||||
val fingerprint: String,
|
||||
)
|
||||
|
||||
private data class CachedAddonStreamWarmup(
|
||||
val groups: List<AddonStreamGroup>,
|
||||
val createdAtMs: Long,
|
||||
)
|
||||
|
||||
private fun ManagedAddon.toWarmupTarget(type: String, videoId: String): AddonStreamWarmupTarget? {
|
||||
val manifest = manifest ?: return 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 null
|
||||
|
||||
val addonName = displayTitle.ifBlank { manifest.name }
|
||||
return AddonStreamWarmupTarget(
|
||||
addonName = addonName,
|
||||
addonId = "addon:${manifest.id}:$manifestUrl",
|
||||
manifest = manifest,
|
||||
fingerprint = "$manifestUrl:${manifest.id}:${manifest.version}:$addonName",
|
||||
)
|
||||
}
|
||||
|
||||
private fun DebridSettings.warmupFingerprint(): String =
|
||||
listOf(
|
||||
enabled,
|
||||
torboxApiKey,
|
||||
instantPlaybackPreparationLimit,
|
||||
streamMaxResults,
|
||||
streamSortMode,
|
||||
streamMinimumQuality,
|
||||
streamDolbyVisionFilter,
|
||||
streamHdrFilter,
|
||||
streamCodecFilter,
|
||||
streamPreferences,
|
||||
streamNameTemplate,
|
||||
streamDescriptionTemplate,
|
||||
).joinToString("|")
|
||||
|
||||
private suspend fun <T> runCatchingUnlessCancelled(block: suspend () -> T): Result<T> =
|
||||
try {
|
||||
Result.success(block())
|
||||
} catch (error: CancellationException) {
|
||||
throw error
|
||||
} catch (error: Throwable) {
|
||||
Result.failure(error)
|
||||
}
|
||||
|
|
@ -210,13 +210,8 @@ object StreamsRepository {
|
|||
|
||||
// Initialise loading placeholders
|
||||
val installedAddonOrder = streamAddons.map { it.addonName }
|
||||
val warmedAddonGroups = AddonStreamWarmupRepository
|
||||
.cachedGroups(type = type, videoId = videoId, season = season, episode = episode)
|
||||
.orEmpty()
|
||||
.associateBy { it.addonId }
|
||||
val warmedAddonIds = warmedAddonGroups.keys
|
||||
val initialGroups = StreamAutoPlaySelector.orderAddonStreams(streamAddons.map { addon ->
|
||||
warmedAddonGroups[addon.addonId] ?: AddonStreamGroup(
|
||||
AddonStreamGroup(
|
||||
addonName = addon.addonName,
|
||||
addonId = addon.addonId,
|
||||
streams = emptyList(),
|
||||
|
|
@ -242,13 +237,12 @@ object StreamsRepository {
|
|||
)
|
||||
|
||||
activeJob = scope.launch {
|
||||
val pendingStreamAddons = streamAddons.filterNot { it.addonId in warmedAddonIds }
|
||||
val completions = Channel<StreamLoadCompletion>(capacity = Channel.BUFFERED)
|
||||
val pluginRemainingByAddonId = pluginProviderGroups
|
||||
.associate { it.addonId to it.scrapers.size }
|
||||
.toMutableMap()
|
||||
val pluginFirstErrorByAddonId = mutableMapOf<String, String>()
|
||||
val totalTasks = pendingStreamAddons.size +
|
||||
val totalTasks = streamAddons.size +
|
||||
pluginProviderGroups.sumOf { it.scrapers.size }
|
||||
|
||||
val installedAddonNames = installedAddonOrder.toSet()
|
||||
|
|
@ -439,7 +433,7 @@ object StreamsRepository {
|
|||
null
|
||||
}
|
||||
|
||||
pendingStreamAddons.forEach { addon ->
|
||||
streamAddons.forEach { addon ->
|
||||
launch {
|
||||
val url = buildAddonResourceUrl(
|
||||
manifestUrl = addon.manifest.transportUrl,
|
||||
|
|
|
|||
Loading…
Reference in a new issue