mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-05-16 23:12:12 +00:00
fix: ajdust tmdb metadata handling for collections
This commit is contained in:
parent
d37af8e4a9
commit
2c1993c996
4 changed files with 294 additions and 47 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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?,
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
Loading…
Reference in a new issue