mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-07-27 23:12:24 +00:00
refactor: mdblist meta details caching and fetching logic
This commit is contained in:
parent
a2fdaa5542
commit
4ddf52be1e
4 changed files with 170 additions and 25 deletions
|
|
@ -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<MetaDetailsUiState> = _uiState.asStateFlow()
|
||||
private var activeRequestKey: String? = null
|
||||
private val cachedMetaByRequestKey = mutableMapOf<String, MetaDetails>()
|
||||
private val cachedMetaByRequestKey = mutableMapOf<String, CachedMetaEntry>()
|
||||
|
||||
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<com.nuvio.app.features.streams.StreamItem> {
|
||||
val meta = _uiState.value.meta ?: return emptyList()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -39,21 +39,32 @@ object MdbListMetadataService {
|
|||
private val ratingsCache = mutableMapOf<String, List<MetaExternalRating>>()
|
||||
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,
|
||||
|
|
|
|||
Loading…
Reference in a new issue