diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/MetaDetailsRepository.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/MetaDetailsRepository.kt index 4418d3c9e..b7d646613 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/MetaDetailsRepository.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/MetaDetailsRepository.kt @@ -16,24 +16,70 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import kotlinx.coroutines.withTimeoutOrNull object MetaDetailsRepository { + private data class CachedMetaEntry( + val baseMeta: MetaDetails, + val metaScreenMeta: MetaDetails? = null, + val metaScreenSettingsFingerprint: String? = null, + ) + private val log = Logger.withTag("MetaDetailsRepo") - private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default) + private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main) private val _uiState = MutableStateFlow(MetaDetailsUiState()) val uiState: StateFlow = _uiState.asStateFlow() private var activeRequestKey: String? = null - private val cachedMetaByRequestKey = mutableMapOf() + private val cachedMetaByRequestKey = mutableMapOf() fun load(type: String, id: String) { log.d { "load() called — type=$type id=$id" } val requestKey = "$type:$id" val currentState = _uiState.value + val mdbListSettings = MdbListSettingsRepository.snapshot() + val metaScreenSettingsFingerprint = buildMetaScreenSettingsFingerprint(mdbListSettings) + + cachedMetaByRequestKey[requestKey]?.let { cachedEntry -> + cachedEntry.metaScreenMeta + ?.takeIf { cachedEntry.metaScreenSettingsFingerprint == metaScreenSettingsFingerprint } + ?.let { cachedMeta -> + _uiState.value = MetaDetailsUiState(meta = cachedMeta) + activeRequestKey = requestKey + return + } + + val cachedBaseMeta = cachedEntry.baseMeta + if (!shouldFetchMdbListOnMetaScreen(cachedBaseMeta, id, mdbListSettings)) { + _uiState.value = MetaDetailsUiState(meta = cachedBaseMeta) + activeRequestKey = requestKey + return + } + + if (currentState.isLoading && activeRequestKey == requestKey) { + log.d { "Meta screen enrichment already in flight — type=$type id=$id" } + return + } - cachedMetaByRequestKey[requestKey]?.let { cachedMeta -> - _uiState.value = MetaDetailsUiState(meta = cachedMeta) activeRequestKey = requestKey + _uiState.value = MetaDetailsUiState( + isLoading = true, + meta = cachedBaseMeta, + ) + + scope.launch { + val enrichedMeta = withContext(Dispatchers.Default) { + enrichForMetaScreen( + requestKey = requestKey, + meta = cachedBaseMeta, + fallbackItemId = id, + settings = mdbListSettings, + settingsFingerprint = metaScreenSettingsFingerprint, + ) + } + _uiState.value = MetaDetailsUiState(meta = enrichedMeta) + activeRequestKey = requestKey + } return } @@ -72,10 +118,38 @@ object MetaDetailsRepository { } for (manifest in manifests) { - val result = tryFetchMeta(manifest, type, id) + val result = withContext(Dispatchers.Default) { + tryFetchMeta(manifest, type, id, includeMdbList = false) + } if (result != null) { - cachedMetaByRequestKey[requestKey] = result - _uiState.value = MetaDetailsUiState(meta = result) + var cachedEntry = CachedMetaEntry(baseMeta = result) + cachedMetaByRequestKey[requestKey] = cachedEntry + + if (!shouldFetchMdbListOnMetaScreen(result, id, mdbListSettings)) { + _uiState.value = MetaDetailsUiState(meta = result) + activeRequestKey = requestKey + return@launch + } + + _uiState.value = MetaDetailsUiState( + isLoading = true, + meta = result, + ) + val enrichedMeta = withContext(Dispatchers.Default) { + enrichForMetaScreen( + requestKey = requestKey, + meta = result, + fallbackItemId = id, + settings = mdbListSettings, + settingsFingerprint = metaScreenSettingsFingerprint, + ) + } + cachedEntry = cachedEntry.copy( + metaScreenMeta = enrichedMeta, + metaScreenSettingsFingerprint = metaScreenSettingsFingerprint, + ) + cachedMetaByRequestKey[requestKey] = cachedEntry + _uiState.value = MetaDetailsUiState(meta = enrichedMeta) activeRequestKey = requestKey return@launch } @@ -90,8 +164,14 @@ object MetaDetailsRepository { fun peek(type: String, id: String): MetaDetails? { val requestKey = "$type:$id" - return cachedMetaByRequestKey[requestKey] - ?: _uiState.value.meta?.takeIf { it.type == type && it.id == id } + val currentMeta = _uiState.value.meta?.takeIf { it.type == type && it.id == id } + if (currentMeta != null) return currentMeta + + val metaScreenSettingsFingerprint = buildMetaScreenSettingsFingerprint(MdbListSettingsRepository.snapshot()) + val cachedEntry = cachedMetaByRequestKey[requestKey] ?: return null + return cachedEntry.metaScreenMeta + ?.takeIf { cachedEntry.metaScreenSettingsFingerprint == metaScreenSettingsFingerprint } + ?: cachedEntry.baseMeta } fun clear() { @@ -102,7 +182,7 @@ object MetaDetailsRepository { suspend fun fetch(type: String, id: String): MetaDetails? { val requestKey = "$type:$id" - cachedMetaByRequestKey[requestKey]?.let { return it } + cachedMetaByRequestKey[requestKey]?.let { return it.baseMeta } val manifests = AddonRepository.uiState.value.addons .mapNotNull { it.manifest } @@ -116,10 +196,10 @@ object MetaDetailsRepository { for (manifest in manifests) { val result = withTimeoutOrNull(FETCH_TIMEOUT_MS) { - tryFetchMeta(manifest, type, id) + tryFetchMeta(manifest, type, id, includeMdbList = false) } if (result != null) { - cachedMetaByRequestKey[requestKey] = result + cachedMetaByRequestKey[requestKey] = CachedMetaEntry(baseMeta = result) return result } } @@ -135,10 +215,10 @@ object MetaDetailsRepository { manifest: AddonManifest, type: String, id: String, + includeMdbList: Boolean, ): MetaDetails? { return try { TmdbSettingsRepository.ensureLoaded() - MdbListSettingsRepository.ensureLoaded() val baseUrl = manifest.transportUrl .substringBefore("?") .removeSuffix("/manifest.json") @@ -154,13 +234,18 @@ object MetaDetailsRepository { settings = TmdbSettingsRepository.snapshot(), ) } ?: result - val enriched = withTimeoutOrNull(MDBLIST_ENRICH_TIMEOUT_MS) { - MdbListMetadataService.enrichMeta( - meta = tmdbEnriched, - fallbackItemId = id, - settings = MdbListSettingsRepository.snapshot(), - ) - } ?: tmdbEnriched + val enriched = if (includeMdbList) { + MdbListSettingsRepository.ensureLoaded() + withTimeoutOrNull(MDBLIST_ENRICH_TIMEOUT_MS) { + MdbListMetadataService.enrichMeta( + meta = tmdbEnriched, + fallbackItemId = id, + settings = MdbListSettingsRepository.snapshot(), + ) + } ?: tmdbEnriched + } else { + tmdbEnriched + } log.d { "Parsed meta: type=${enriched.type}, name=${enriched.name}, videos=${enriched.videos.size}" } if (enriched.videos.isNotEmpty()) { val first = enriched.videos.first() @@ -174,6 +259,52 @@ object MetaDetailsRepository { } } + private suspend fun enrichForMetaScreen( + requestKey: String, + meta: MetaDetails, + fallbackItemId: String, + settings: com.nuvio.app.features.mdblist.MdbListSettings, + settingsFingerprint: String, + ): MetaDetails { + val enrichedMeta = withTimeoutOrNull(MDBLIST_ENRICH_TIMEOUT_MS) { + MdbListMetadataService.enrichMeta( + meta = meta, + fallbackItemId = fallbackItemId, + settings = settings, + ) + } ?: meta + + cachedMetaByRequestKey[requestKey] = cachedMetaByRequestKey[requestKey] + ?.copy( + metaScreenMeta = enrichedMeta, + metaScreenSettingsFingerprint = settingsFingerprint, + ) + ?: CachedMetaEntry( + baseMeta = meta, + metaScreenMeta = enrichedMeta, + metaScreenSettingsFingerprint = settingsFingerprint, + ) + + return enrichedMeta + } + + private fun shouldFetchMdbListOnMetaScreen( + meta: MetaDetails, + fallbackItemId: String, + settings: com.nuvio.app.features.mdblist.MdbListSettings, + ): Boolean = MdbListMetadataService.shouldFetchForMeta( + meta = meta, + fallbackItemId = fallbackItemId, + settings = settings, + ) + + private fun buildMetaScreenSettingsFingerprint( + settings: com.nuvio.app.features.mdblist.MdbListSettings, + ): String { + val providers = settings.enabledProvidersInPriorityOrder().joinToString(",") + return "${settings.enabled}:${settings.apiKey.trim()}:$providers" + } + fun findEmbeddedStreams(videoId: String): List { val meta = _uiState.value.meta ?: return emptyList() diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/MetaDetailsScreen.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/MetaDetailsScreen.kt index 4de2e17e0..8636ec341 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/MetaDetailsScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/MetaDetailsScreen.kt @@ -105,7 +105,8 @@ fun MetaDetailsScreen( modifier: Modifier = Modifier, ) { val uiState by MetaDetailsRepository.uiState.collectAsStateWithLifecycle() - val displayedMeta = MetaDetailsRepository.peek(type, id) + val displayedMeta = uiState.meta?.takeIf { it.type == type && it.id == id } + ?: MetaDetailsRepository.peek(type, id) val metaScreenSettingsUiState by remember { MetaScreenSettingsRepository.ensureLoaded() MetaScreenSettingsRepository.uiState diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/components/DetailMetaInfo.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/components/DetailMetaInfo.kt index 5c95eb6f2..8175dece8 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/components/DetailMetaInfo.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/components/DetailMetaInfo.kt @@ -62,7 +62,9 @@ fun DetailMetaInfo( modifier: Modifier = Modifier, ) { Column( - modifier = modifier.fillMaxWidth(), + modifier = modifier + .fillMaxWidth() + .animateContentSize(), verticalArrangement = Arrangement.spacedBy(12.dp), ) { val releaseLine = formatMetaReleaseLineForDetails(meta) diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/mdblist/MdbListMetadataService.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/mdblist/MdbListMetadataService.kt index a84c42de1..b1ca1c1e1 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/mdblist/MdbListMetadataService.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/mdblist/MdbListMetadataService.kt @@ -39,21 +39,32 @@ object MdbListMetadataService { private val ratingsCache = mutableMapOf>() private val imdbRegex = Regex("tt\\d+") + fun shouldFetchForMeta( + meta: MetaDetails, + fallbackItemId: String, + settings: MdbListSettings, + ): Boolean { + if (!settings.enabled) return false + if (settings.apiKey.trim().isBlank()) return false + if (settings.enabledProvidersInPriorityOrder().isEmpty()) return false + return extractImdbId(meta.id) != null || extractImdbId(fallbackItemId) != null + } + suspend fun enrichMeta( meta: MetaDetails, fallbackItemId: String, settings: MdbListSettings, ): MetaDetails { - if (!settings.enabled) return meta.copy(externalRatings = emptyList()) + if (!shouldFetchForMeta(meta, fallbackItemId, settings)) { + return meta.copy(externalRatings = emptyList()) + } val apiKey = settings.apiKey.trim() - if (apiKey.isBlank()) return meta.copy(externalRatings = emptyList()) val imdbId = extractImdbId(meta.id) ?: extractImdbId(fallbackItemId) ?: return meta.copy(externalRatings = emptyList()) val mediaType = toMdbListMediaType(meta.type) val enabledProviders = settings.enabledProvidersInPriorityOrder() - if (enabledProviders.isEmpty()) return meta.copy(externalRatings = emptyList()) val ratings = fetchRatings( imdbId = imdbId,