package link.roadio.riff.player.library import link.roadio.cuetree.Album import link.roadio.cuetree.CatalogView import link.roadio.cuetree.Flags import link.roadio.cuetree.NodeKind import link.roadio.riff.indexer.core.media.MediaId import link.roadio.riff.player.storage.StoredAudiobookOverride import link.roadio.riff.storage.hash.nativeXxh3 import link.roadio.riff.ui.BrowserItem import link.roadio.riff.ui.BrowserKind import link.roadio.riff.ui.PlaybackSourceRef import link.roadio.riff.ui.TocChapter import link.roadio.riff.ui.trackReactionKey /** Bounded playback lookups; browsing need not have constructed a single row yet. */ interface PlayerPlaybackCatalog { val generation: Long val projection: PlayerLibraryProjection fun lookup( mediaId: MediaId, contextKey: String? = null, ordinal: Int = -1, ): PlaybackCatalogTrack } data class PlaybackCatalogTrack( val row: BrowserItem?, val sources: List, val chapters: Map>, val contextBindingId: Long? = null, ) /** * AI-INTENT: Browse and cold restore share identity, override precedence and physical bindings. * Only compact source-to-album indices are eager; decoded media records have a bounded cache. * https://wiki.271.pics/Riff/LATEST/engineering-verification */ internal class CuetreeTrackCatalog( private val snapshot: CatalogView, override val projection: PlayerLibraryProjection, hash: (ByteArray) -> Long = ::nativeXxh3, val facts: CuetreeProfileFacts = CuetreeProfileFacts(snapshot, projection.audiobookOverrides, hash), ) : PlayerPlaybackCatalog { override val generation get() = snapshot.generation private val nodes = snapshot.nodes private val bindings = CuetreePlaybackBindings(snapshot, hash) var contextOccurrence: ((String, Int, MediaId) -> Pair?)? = null private val lookupCache = object : LinkedHashMap, PlaybackCatalogTrack>(128, 0.75f, true) { override fun removeEldestEntry(eldest: MutableMap.MutableEntry, PlaybackCatalogTrack>) = size > 128 } private val knownChapters = object : LinkedHashMap>(128, 0.75f, true) { override fun removeEldestEntry(eldest: MutableMap.MutableEntry>) = size > 128 } val resolvedChapters: Map> = object : AbstractMap>() { override fun get(key: Long): List? = synchronized(knownChapters) { knownChapters[key] } override val entries: Set>> get() = synchronized( knownChapters, ) { knownChapters.toMap().entries } } fun albumFor(index: Int): Album? = facts.albumBySource[index].takeIf { it >= 0 }?.let(snapshot.albums::get) fun binding( index: Int, base: Int, ): CatalogPlaybackBinding? = bindings.binding(index, base)?.also { value -> synchronized(knownChapters) { knownChapters[value.source.bindingId] = value.chapters } } fun bindings(index: Int): List { val node = nodes[index] val bases = if (node.kind == NodeKind.FRAGMENT) snapshot.baseSources(node) else listOf(index) return bases.mapNotNull { binding(index, it) } } fun id(index: Int): MediaId = snapshot.nodeId(index).let { MediaId(it.hi, it.lo ?: 0L, it.lo != null) } private fun sources(mediaId: MediaId): List = snapshot .sources( link.roadio.cuetree.MediaId(mediaId.high, mediaId.low.takeIf { mediaId.lowPresent }), ).filter { index -> when (snapshot.nodeKind(index)) { NodeKind.MEDIA, NodeKind.FRAGMENT -> true NodeKind.FAILEDL1 -> snapshot.nodeFlags(index) and Flags.IDHI == 0 else -> false } } fun automatic(mediaId: MediaId): Boolean = sources(mediaId).firstOrNull()?.let(facts::automaticAt) == true fun reaction(mediaId: MediaId): MediaId { val members = sources(mediaId) val canonical = members.firstOrNull()?.let(::id) ?: mediaId return if (members.any { snapshot.nodeFlags(it) and Flags.SHRT != 0 }) MediaId(canonical.high, 0, false) else canonical } fun book(mediaId: MediaId): Boolean = sources(mediaId).firstOrNull()?.let(facts::bookAt) == true fun track( index: Int, folder: Boolean = false, albumContext: Album? = null, baseOverride: Int? = null, resolveBinding: Boolean = true, ): BrowserItem { val node = nodes[index] val mediaId = id(index) val source = if (resolveBinding) baseOverride?.let { binding(index, it)?.source } ?: bindings(index).firstOrNull()?.source else null val cachedPlayable = node.playable && (node.kind != NodeKind.FRAGMENT || snapshot.baseSources(node).any(snapshot::nodeAvailable)) val album = albumContext ?: albumFor(index) val fragment = node.kind == NodeKind.FRAGMENT val occurrenceKey = "${mediaId.hex()}:${node.parentId}:${node.source?.name}:${node.span}:${node.attributes.toSortedMap()}:$index" return BrowserItem( id = if (folder) "folder-track:$occurrenceKey" else "track:$occurrenceKey", label = node.attributes["title"]?.firstOrNull() ?: if (fragment) node.name else node.source?.name.orEmpty(), kind = BrowserKind.TRACK, playable = if (resolveBinding) source != null else cachedPlayable, enabled = if (resolveBinding) source != null else cachedPlayable, mediaId = mediaId, artist = node.attributes["artist"].orEmpty().joinToString(", "), album = album?.playerDisplayTitle() ?: node.attributes["album"]?.firstOrNull().orEmpty(), sourceFileName = source ?.segments ?.lastOrNull() ?.displayName ?: baseOverride?.let(snapshot::nodeSourceName) ?: node.source?.name.orEmpty(), fragment = fragment, discNumber = node.attributes["discNumber"]?.firstOrNull()?.toLongOrNull(), trackNumber = node.attributes["trackNumber"]?.firstOrNull()?.toLongOrNull(), reactionKey = trackReactionKey(reaction(mediaId)), sourceBindingId = source?.bindingId, playbackSource = source, audiobook = facts.bookAt(index), automaticAudiobook = facts.automaticAt(index), aiReactionEligible = !facts.bookAt(index), albumGroupId = album?.id, ) } override fun lookup( mediaId: MediaId, contextKey: String?, ordinal: Int, ): PlaybackCatalogTrack { synchronized(lookupCache) { val key = Triple(mediaId, contextKey, ordinal) return lookupCache.getOrPut(key) { lookupUncached(mediaId, contextKey, ordinal) } } } private fun lookupUncached( mediaId: MediaId, contextKey: String?, ordinal: Int, ): PlaybackCatalogTrack { val members = sources(mediaId) val all = members.flatMap(::bindings).distinctBy { it.source.bindingId } val albumId = contextKey ?.takeIf { it.startsWith("album:") } ?.substringAfter(':') ?.toULongOrNull(16) ?.toLong() val album = albumId?.let { id -> val index = snapshot.albumIndex(id) snapshot.albums.getOrNull(index) } val reference = contextKey?.let { contextOccurrence?.invoke(it, ordinal, mediaId) } val contextIndex = reference?.first?.takeIf { it in members } ?: album?.sources?.getOrNull(ordinal)?.takeIf { it in members } val selectedIndex = contextIndex ?: members.firstOrNull { bindings(it).isNotEmpty() } val row = selectedIndex?.let { track(it, albumContext = album.takeIf { contextIndex != null }, baseOverride = reference?.second) } return PlaybackCatalogTrack( row, all.map { it.source }, all.associate { it.source.bindingId to it.chapters }, row?.sourceBindingId.takeIf { contextIndex != null }, ) } }