feat(cw): retry hydration with readiness check

This commit is contained in:
tapframe 2026-05-29 03:06:32 +05:30
parent 3473972345
commit 8b03367699

View file

@ -3,7 +3,10 @@ package com.nuvio.app.features.watchprogress
import co.touchlab.kermit.Logger
import com.nuvio.app.core.auth.AuthRepository
import com.nuvio.app.core.auth.AuthState
import com.nuvio.app.features.addons.AddonManifest
import com.nuvio.app.features.addons.AddonRepository
import com.nuvio.app.features.addons.AddonsUiState
import com.nuvio.app.features.addons.enabledAddons
import com.nuvio.app.features.details.MetaDetails
import com.nuvio.app.features.details.MetaDetailsRepository
import com.nuvio.app.features.player.PlayerPlaybackSnapshot
@ -26,6 +29,7 @@ import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
@ -37,6 +41,7 @@ import kotlinx.coroutines.withTimeoutOrNull
private const val NUVIO_SYNC_PERIODIC_INTERVAL_MS = 5L * 60L * 1000L
private const val WATCH_PROGRESS_METADATA_RESOLUTION_CONCURRENCY = 4
private const val WATCH_PROGRESS_METADATA_RESOLUTION_LIMIT = 64
private data class RemoteMetadataResolutionResult(
val key: Pair<String, String>,
@ -44,6 +49,20 @@ private data class RemoteMetadataResolutionResult(
val meta: MetaDetails?,
)
private data class MetadataProviderReadiness(
val providers: List<AddonManifest>,
val isRefreshing: Boolean,
) {
val fingerprint: String
get() = providers.map(AddonManifest::transportUrl).sorted().joinToString(separator = "|")
val isReady: Boolean
get() = providers.isNotEmpty() && !isRefreshing
val isSettledWithoutProviders: Boolean
get() = !isRefreshing && providers.isEmpty()
}
object WatchProgressRepository {
private val syncScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
private val log = Logger.withTag("WatchProgressRepository")
@ -57,6 +76,7 @@ object WatchProgressRepository {
private var metadataResolutionJob: Job? = null
private var isPullingNuvioSyncFromServer = false
private var hasCompletedInitialNuvioSyncPull = false
private var lastAddonMetadataReadyFingerprint: String? = null
internal var syncAdapter: ProgressSyncAdapter = SupabaseProgressSyncAdapter
init {
@ -102,6 +122,12 @@ object WatchProgressRepository {
}
}
syncScope.launch {
AddonRepository.uiState.collectLatest { state ->
retryMetadataResolutionWhenAddonMetaProvidersReady(state)
}
}
syncScope.launch {
while (true) {
delay(NUVIO_SYNC_PERIODIC_INTERVAL_MS)
@ -147,6 +173,7 @@ object WatchProgressRepository {
metadataResolutionJob?.cancel()
hasLoaded = false
currentProfileId = 1
lastAddonMetadataReadyFingerprint = null
entriesByVideoId.clear()
TraktProgressRepository.clearLocalState()
TraktSettingsRepository.clearLocalState()
@ -156,6 +183,7 @@ object WatchProgressRepository {
private fun loadFromDisk(profileId: Int) {
currentProfileId = profileId
hasLoaded = true
lastAddonMetadataReadyFingerprint = null
entriesByVideoId.clear()
val payload = WatchProgressStorage.loadPayload(profileId).orEmpty().trim()
@ -263,30 +291,48 @@ object WatchProgressRepository {
isCompleted = isWatchProgressComplete(position, duration, false),
)
private fun retryMetadataResolutionWhenAddonMetaProvidersReady(state: AddonsUiState) {
if (!hasLoaded || shouldUseTraktProgress()) return
val readiness = state.metadataProviderReadiness()
if (!readiness.isReady) return
val fingerprint = readiness.fingerprint
if (fingerprint == lastAddonMetadataReadyFingerprint) return
lastAddonMetadataReadyFingerprint = fingerprint
if (metadataResolutionJob?.isActive == true) return
resolveRemoteMetadata()
}
private fun resolveRemoteMetadata() {
val missingMetadataEntries = entriesByVideoId.values
.filter { it.poster.isNullOrBlank() || it.background.isNullOrBlank() }
val entriesToResolve = missingMetadataEntries.continueWatchingEntries(limit = ContinueWatchingLimit)
val entriesToResolve = missingMetadataEntries.continueWatchingEntries(
limit = WATCH_PROGRESS_METADATA_RESOLUTION_LIMIT,
)
val needsResolution = entriesToResolve
.groupBy { it.parentMetaId to it.contentType }
if (needsResolution.isEmpty()) {
return
}
if (needsResolution.isEmpty()) return
metadataResolutionJob?.cancel()
metadataResolutionJob = syncScope.launch {
withTimeoutOrNull(30_000L) {
AddonRepository.awaitManifestsLoaded()
} ?: run {
log.w { "Timed out waiting for addon manifests" }
return@launch
val providerReadiness = awaitReadyMetadataProviders() ?: return@launch
lastAddonMetadataReadyFingerprint = providerReadiness.fingerprint
val supportedNeedsResolution = needsResolution.filter { (key, _) ->
val (metaId, metaType) = key
providerReadiness.providers.any { provider ->
provider.supportsMetaRequest(type = metaType, id = metaId)
}
}
if (supportedNeedsResolution.isEmpty()) return@launch
var resolvedEntries = 0
val semaphore = Semaphore(WATCH_PROGRESS_METADATA_RESOLUTION_CONCURRENCY)
val resolutionResults = coroutineScope {
needsResolution.map { (key, entries) ->
supportedNeedsResolution.map { (key, entries) ->
async {
semaphore.withPermit {
fetchRemoteMetadataGroup(key = key, entries = entries)
@ -356,6 +402,20 @@ object WatchProgressRepository {
)
}
private suspend fun awaitReadyMetadataProviders(): MetadataProviderReadiness? {
val current = AddonRepository.uiState.value.metadataProviderReadiness()
if (current.isReady) return current
if (current.isSettledWithoutProviders) return null
val settled = withTimeoutOrNull(30_000L) {
AddonRepository.uiState.first { state ->
val readiness = state.metadataProviderReadiness()
readiness.isReady || readiness.isSettledWithoutProviders
}.metadataProviderReadiness()
}
return settled?.takeIf { it.isReady }
}
fun upsertPlaybackProgress(
session: WatchProgressPlaybackSession,
snapshot: PlayerPlaybackSnapshot,
@ -622,4 +682,24 @@ object WatchProgressRepository {
return shouldUseTraktProgress() && TraktProgressRepository.isShowHiddenFromProgress(contentId)
}
private fun AddonsUiState.metadataProviderReadiness(): MetadataProviderReadiness {
val enabled = addons.enabledAddons()
val providers = enabled
.mapNotNull { addon -> addon.manifest }
.filter { manifest -> manifest.hasMetaResource() }
return MetadataProviderReadiness(
providers = providers,
isRefreshing = enabled.any { addon -> addon.isRefreshing },
)
}
private fun AddonManifest.hasMetaResource(): Boolean =
resources.any { resource -> resource.name == "meta" }
private fun AddonManifest.supportsMetaRequest(type: String, id: String): Boolean =
resources.any { resource ->
resource.name == "meta" &&
resource.types.contains(type) &&
(resource.idPrefixes.isEmpty() || resource.idPrefixes.any { prefix -> id.startsWith(prefix) })
}
}