package link.roadio.riff.player.library import link.roadio.cuetree.CatalogView import link.roadio.riff.indexer.core.analysis.AnalysisSnapshot import link.roadio.riff.indexer.core.analysis.AnalysisState import link.roadio.riff.indexer.core.analysis.resolve import link.roadio.riff.indexer.core.media.MediaId import link.roadio.riff.player.core.selection.AiFeaturePoint /** Optional analysis replaces only augmentation, preserving catalog rows and transport identity. */ internal data class PlayerAnalysisFeatures( val generation: Long?, val ai: Map, val pte: Map, val gains: Map, val genres: Map, ) internal fun projectAnalysis( snapshot: CatalogView, analysis: AnalysisSnapshot?, aliases: Map, excludedGenreIds: Set, aiGenreDisplay: Map>, ): PlayerAnalysisFeatures { val aiFeatures = linkedMapOf() val pteFeatures = linkedMapOf() val gainValues = linkedMapOf() val topGenres = linkedMapOf() val genreNames = snapshot.genres .mapNotNull { genre -> genre.ref.dictionaryId?.let { it to genre.name } }.toMap() analysis?.resolve(snapshot)?.forEach { (resolvedId, record) -> val mediaId = MediaId(resolvedId.hi, resolvedId.lo ?: 0L, resolvedId.lo != null) if (record.aiState == AnalysisState.AVAILABLE) { if (record.arousal != null && record.valence != null) { aiFeatures[mediaId] = AiFeaturePoint( mediaId, requireNotNull(record.arousal), requireNotNull(record.valence), requireNotNull(record.arousalConfidence), requireNotNull(record.valenceConfidence), ) } if (record.pte != null) { pteFeatures[mediaId] = PlayerPteFeature( requireNotNull(record.pte), requireNotNull(record.pteConfidence), ) } record.genres .sortedWith( compareByDescending { it.confidence }.thenBy { it.genreId }, ).firstNotNullOfOrNull { genre -> val visible = aiGenreDisplay[genre.genreId] val visibleId = visible?.first ?: genre.genreId if (genre.genreId in excludedGenreIds || visibleId in excludedGenreIds) { null } else { (visible?.second ?: genreNames[visibleId])?.let { name -> PlayerTopInferredGenre(genre.genreId, name, genre.confidence) } } }?.let { topGenres[mediaId] = it } } if (record.trackGainQ8 != null || record.albumGainQ8 != null) { gainValues[mediaId] = PlayerReplayGainValue( record.trackGainQ8, record.trackPeak, record.albumGainQ8, record.albumPeak, ) } } return PlayerAnalysisFeatures( analysis?.generation, aiFeatures.includingAliases(aliases), pteFeatures.includingAliases(aliases), gainValues.includingAliases(aliases), topGenres.includingAliases(aliases), ) } internal fun PlayerLibraryModel.withAnalysis(features: PlayerAnalysisFeatures): PlayerLibraryModel = copy( aiFeaturesByMediaId = features.ai, pteByMediaId = features.pte, replayGainByMediaId = features.gains, topInferredGenreByMediaId = features.genres, augmentationGeneration = features.generation, replayGainGeneration = features.generation, ) internal fun PlayerLibraryModels.withAnalysis(features: PlayerAnalysisFeatures) = PlayerLibraryModels(music.withAnalysis(features), audiobook.withAnalysis(features)) internal fun catalogAliases(snapshot: CatalogView): Map = snapshot.nodes.indices .asSequence() .filter { snapshot.nodeFlags(it) and link.roadio.cuetree.Flags.SHRT != 0 } .map(snapshot::nodeId) .filter { it.lo != null } .associate { MediaId(it.hi, 0L, false) to MediaId(it.hi, requireNotNull(it.lo), true) }