fix: ajdust tmdb metadata handling for collections

This commit is contained in:
tapframe 2026-04-30 15:05:28 +05:30
parent d37af8e4a9
commit 2c1993c996
4 changed files with 294 additions and 47 deletions

View file

@ -8,6 +8,7 @@ import com.nuvio.app.features.addons.httpGetText
import com.nuvio.app.features.mdblist.MdbListMetadataService
import com.nuvio.app.features.mdblist.MdbListSettingsRepository
import com.nuvio.app.features.tmdb.TmdbMetadataService
import com.nuvio.app.features.tmdb.TmdbService
import com.nuvio.app.features.tmdb.TmdbSettingsRepository
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
@ -101,17 +102,22 @@ object MetaDetailsRepository {
_uiState.value = MetaDetailsUiState(isLoading = true)
scope.launch {
val manifests = AddonRepository.uiState.value.addons
.mapNotNull { it.manifest }
.filter { manifest ->
manifest.resources.any { resource ->
resource.name == "meta" &&
resource.types.contains(type) &&
(resource.idPrefixes.isEmpty() || resource.idPrefixes.any { id.startsWith(it) })
}
}
val metaLookupId = resolveMetaLookupId(itemId = id, itemType = type)
val manifests = findMetaManifests(type = type, id = metaLookupId)
if (manifests.isEmpty()) {
val tmdbMeta = tryFetchTmdbFallbackMeta(type = type, id = id)
if (tmdbMeta != null) {
publishLoadedMeta(
requestKey = requestKey,
meta = tmdbMeta,
fallbackItemId = id,
mdbListSettings = mdbListSettings,
metaScreenSettingsFingerprint = metaScreenSettingsFingerprint,
)
return@launch
}
log.w { "No addon provides meta for type=$type id=$id" }
_uiState.value = MetaDetailsUiState(
errorMessage = getString(Res.string.details_no_addon_meta),
@ -122,42 +128,32 @@ object MetaDetailsRepository {
for (manifest in manifests) {
val result = withContext(Dispatchers.Default) {
tryFetchMeta(manifest, type, id, includeMdbList = false)
tryFetchMeta(manifest, type, metaLookupId, includeMdbList = false)
}
if (result != null) {
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,
publishLoadedMeta(
requestKey = requestKey,
meta = result,
)
val enrichedMeta = withContext(Dispatchers.Default) {
enrichForMetaScreen(
requestKey = requestKey,
meta = result,
fallbackItemId = id,
settings = mdbListSettings,
settingsFingerprint = metaScreenSettingsFingerprint,
)
}
cachedEntry = cachedEntry.copy(
metaScreenMeta = enrichedMeta,
fallbackItemId = metaLookupId,
mdbListSettings = mdbListSettings,
metaScreenSettingsFingerprint = metaScreenSettingsFingerprint,
)
cachedMetaByRequestKey[requestKey] = cachedEntry
_uiState.value = MetaDetailsUiState(meta = enrichedMeta)
activeRequestKey = requestKey
return@launch
}
}
val tmdbMeta = tryFetchTmdbFallbackMeta(type = type, id = id)
if (tmdbMeta != null) {
publishLoadedMeta(
requestKey = requestKey,
meta = tmdbMeta,
fallbackItemId = id,
mdbListSettings = mdbListSettings,
metaScreenSettingsFingerprint = metaScreenSettingsFingerprint,
)
return@launch
}
_uiState.value = MetaDetailsUiState(
errorMessage = getString(Res.string.details_load_failed_all_addons),
)
@ -187,19 +183,12 @@ object MetaDetailsRepository {
val requestKey = "$type:$id"
cachedMetaByRequestKey[requestKey]?.let { return it.baseMeta }
val manifests = AddonRepository.uiState.value.addons
.mapNotNull { it.manifest }
.filter { manifest ->
manifest.resources.any { resource ->
resource.name == "meta" &&
resource.types.contains(type) &&
(resource.idPrefixes.isEmpty() || resource.idPrefixes.any { id.startsWith(it) })
}
}
val metaLookupId = resolveMetaLookupId(itemId = id, itemType = type)
val manifests = findMetaManifests(type = type, id = metaLookupId)
for (manifest in manifests) {
val result = withTimeoutOrNull(FETCH_TIMEOUT_MS) {
tryFetchMeta(manifest, type, id, includeMdbList = false)
tryFetchMeta(manifest, type, metaLookupId, includeMdbList = false)
}
if (result != null) {
cachedMetaByRequestKey[requestKey] = CachedMetaEntry(baseMeta = result)
@ -207,7 +196,9 @@ object MetaDetailsRepository {
}
}
return null
return tryFetchTmdbFallbackMeta(type = type, id = id)?.also { result ->
cachedMetaByRequestKey[requestKey] = CachedMetaEntry(baseMeta = result)
}
}
private const val FETCH_TIMEOUT_MS = 5_000L
@ -265,6 +256,78 @@ object MetaDetailsRepository {
}
}
private fun findMetaManifests(type: String, id: String): List<AddonManifest> =
AddonRepository.uiState.value.addons
.mapNotNull { it.manifest }
.filter { manifest ->
manifest.resources.any { resource ->
resource.name == "meta" &&
resource.types.contains(type) &&
(resource.idPrefixes.isEmpty() || resource.idPrefixes.any { id.startsWith(it) })
}
}
private suspend fun resolveMetaLookupId(itemId: String, itemType: String): String {
val tmdbId = itemId
.takeIf { it.startsWith("tmdb:", ignoreCase = true) }
?.substringAfter(':')
?.substringBefore(':')
?.toIntOrNull()
?: return itemId
return withTimeoutOrNull(FETCH_TIMEOUT_MS) {
TmdbService.tmdbToImdb(tmdbId = tmdbId, mediaType = itemType)
}
?.takeIf { it.isNotBlank() }
?: itemId
}
private suspend fun tryFetchTmdbFallbackMeta(type: String, id: String): MetaDetails? =
withTimeoutOrNull(TMDB_ENRICH_TIMEOUT_MS) {
TmdbMetadataService.fetchStandaloneMeta(
type = type,
id = id,
settings = TmdbSettingsRepository.snapshot(),
)
}
private suspend fun publishLoadedMeta(
requestKey: String,
meta: MetaDetails,
fallbackItemId: String,
mdbListSettings: com.nuvio.app.features.mdblist.MdbListSettings,
metaScreenSettingsFingerprint: String,
) {
val cachedEntry = CachedMetaEntry(baseMeta = meta)
cachedMetaByRequestKey[requestKey] = cachedEntry
if (!shouldFetchMdbListOnMetaScreen(meta, fallbackItemId, mdbListSettings)) {
_uiState.value = MetaDetailsUiState(meta = meta)
activeRequestKey = requestKey
return
}
_uiState.value = MetaDetailsUiState(
isLoading = true,
meta = meta,
)
val enrichedMeta = withContext(Dispatchers.Default) {
enrichForMetaScreen(
requestKey = requestKey,
meta = meta,
fallbackItemId = fallbackItemId,
settings = mdbListSettings,
settingsFingerprint = metaScreenSettingsFingerprint,
)
}
cachedMetaByRequestKey[requestKey] = cachedEntry.copy(
metaScreenMeta = enrichedMeta,
metaScreenSettingsFingerprint = metaScreenSettingsFingerprint,
)
_uiState.value = MetaDetailsUiState(meta = enrichedMeta)
activeRequestKey = requestKey
}
private suspend fun enrichForMetaScreen(
requestKey: String,
meta: MetaDetails,

View file

@ -638,6 +638,69 @@ object TmdbMetadataService {
)
}
suspend fun fetchStandaloneMeta(
type: String,
id: String,
settings: TmdbSettings,
): MetaDetails? {
if (!settings.hasApiKey) return null
val tmdbId = id
.takeIf { it.startsWith("tmdb:", ignoreCase = true) }
?.substringAfter(':')
?.substringBefore(':')
?.toIntOrNull()
?: return null
val tmdbType = normalizeMetaType(type)
val enrichment = fetchEnrichment(
tmdbId = tmdbId.toString(),
mediaType = tmdbType,
language = settings.language,
settings = settings,
) ?: return null
return buildStandaloneMeta(
type = type,
id = id,
tmdbId = tmdbId,
enrichment = enrichment,
)
}
internal fun buildStandaloneMeta(
type: String,
id: String,
tmdbId: Int,
enrichment: TmdbEnrichment,
): MetaDetails =
MetaDetails(
id = id,
type = type,
name = enrichment.localizedTitle ?: "TMDB $tmdbId",
poster = enrichment.poster,
background = enrichment.backdrop,
logo = enrichment.logo,
description = enrichment.description,
releaseInfo = enrichment.releaseInfo,
lastAirDate = enrichment.lastAirDate,
status = enrichment.status,
imdbRating = enrichment.rating?.formatRating(),
ageRating = enrichment.ageRating,
runtime = enrichment.runtimeMinutes?.formatRuntime(),
genres = enrichment.genres,
director = enrichment.director,
writer = enrichment.writer,
cast = enrichment.people,
productionCompanies = enrichment.productionCompanies,
networks = enrichment.networks,
country = enrichment.countries.takeIf { it.isNotEmpty() }?.joinToString(", "),
language = enrichment.language,
moreLikeThis = enrichment.moreLikeThis,
collectionName = enrichment.collectionName,
collectionItems = enrichment.collectionItems,
trailers = enrichment.trailers,
)
internal fun applyEnrichment(
meta: MetaDetails,
enrichment: TmdbEnrichment?,

View file

@ -8,6 +8,47 @@ import kotlin.test.Test
import kotlin.test.assertEquals
class TmdbMetadataServiceTest {
@Test
fun `buildStandaloneMeta maps tmdb enrichment without addon meta`() {
val enrichment = TmdbEnrichment(
localizedTitle = "TMDB Movie",
description = "TMDB description",
genres = listOf("Adventure"),
backdrop = "backdrop",
logo = "logo",
poster = "poster",
people = listOf(MetaPerson(name = "Cast Member", role = "Hero")),
director = listOf("Director"),
writer = listOf("Writer"),
releaseInfo = "2026-01-01",
rating = 8.4,
runtimeMinutes = 105,
ageRating = "PG-13",
status = "Released",
countries = listOf("US", "GB"),
language = "en",
productionCompanies = listOf(MetaCompany(name = "Studio")),
networks = emptyList(),
)
val result = TmdbMetadataService.buildStandaloneMeta(
type = "movie",
id = "tmdb:123",
tmdbId = 123,
enrichment = enrichment,
)
assertEquals("tmdb:123", result.id)
assertEquals("movie", result.type)
assertEquals("TMDB Movie", result.name)
assertEquals("TMDB description", result.description)
assertEquals("8.4", result.imdbRating)
assertEquals("105m", result.runtime)
assertEquals("US, GB", result.country)
assertEquals(listOf("Cast Member"), result.cast.map { it.name })
assertEquals(listOf("Studio"), result.productionCompanies.map { it.name })
}
@Test
fun `applyEnrichment replaces enabled metadata groups`() {
val base = MetaDetails(

View file

@ -0,0 +1,80 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1620"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES"
buildArchitectures = "Automatic">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "E1B229BC363ABB711AF255E3"
BuildableName = "Nuvio.app"
BlueprintName = "iosApp"
ReferencedContainer = "container:iosApp.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
shouldAutocreateTestPlan = "YES">
<Testables>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "E1B229BC363ABB711AF255E3"
BuildableName = "Nuvio.app"
BlueprintName = "iosApp"
ReferencedContainer = "container:iosApp.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "E1B229BC363ABB711AF255E3"
BuildableName = "Nuvio.app"
BlueprintName = "iosApp"
ReferencedContainer = "container:iosApp.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>