package link.roadio.riff.player.library 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.ui.trackReactionKey /** Compact immutable facts, prepared by the repository worker and shared by both browse modes. * AI-INTENT: Keys and UI navigation must never classify an album's tracks again. * https://wiki.271.pics/Riff/1-1-0/catalog-consumption#prepared-collection-views */ internal class CuetreeProfileFacts( val snapshot: CatalogView, private val overrides: Map, hash: (ByteArray) -> Long, ) { val albumBySource = IntArray(snapshot.nodes.size) { -1 } val albumSources = Array(snapshot.albums.size) { snapshot.albumSources(it).toIntArray() } val albumSourceLookup = Array(albumSources.size) { albumSources[it].sortedArray() } private val mediaFlags = ByteArray(snapshot.nodes.size) private val albumFlags = ByteArray(snapshot.albums.size) private val folderChoices = ByteArray(snapshot.nodes.size) val retainedIndices = snapshot.nodes.indices .filter(::isMedia) .toIntArray() val allIds: Set val aliases = catalogAliases(snapshot) val rootFolders = snapshot.childrenOf(-1).filter(snapshot::nodeAvailable).toIntArray() val folderPlayable = BooleanArray(snapshot.nodes.size) val playableIndices: IntArray val albumLabels = Array(snapshot.albums.size) { snapshot.albums[it].playerDisplayTitle() } val containers = CuetreeFolderContainers(snapshot, albumSources, hash) private val containerFlags = ByteArray(containers.records.size) val artistAlbums = Array( snapshot.artists.size, ) { index -> snapshot .artistAlbums(index) .map(snapshot::albumIndex) .filter { it >= 0 } .sorted() .toIntArray() } val genreAlbums = Array( snapshot.genres.size, ) { index -> snapshot .genreAlbums(index) .map(snapshot::albumIndex) .filter { it >= 0 } .sorted() .toIntArray() } init { val groups = retainedIndices.groupBy(::id) allIds = groups.keys.toSet() val automaticAlbums = BooleanArray(snapshot.nodes.size) albumSources.forEachIndexed { album, sources -> val automatic = snapshot.albumHasAttribute(album, "automaticBook", "1") sources.forEach { source -> albumBySource[source] = album if (automatic) automaticAlbums[source] = true } } groups.forEach { (canonical, members) -> val representative = members.firstOrNull(snapshot::nodeAvailable) ?: members.first() val automatic = members.any { automaticAlbums[it] } || snapshot.nodeHasAttribute(representative, "automaticBook", "1") val reaction = if (members.any { snapshot.nodeFlags(it) and Flags.SHRT != 0 }) MediaId(canonical.high, 0, false) else canonical var book = automatic if (overrides.isNotEmpty()) { val source = representative val album = albumBySource[source] val choice = overrides[trackReactionKey(reaction)] ?: overrides[trackReactionKey(canonical)] ?: album.takeIf { it >= 0 }?.let { overrides["album:${snapshot.albumId(it).toULong().toString(16)}"] } ?: folderChoice(source) when (choice) { StoredAudiobookOverride.BOOK -> book = true StoredAudiobookOverride.MUSIC -> book = false null -> Unit } } val flags = (if (automatic) 1 else 0) or (if (book) 2 else 0) members.forEach { mediaFlags[it] = flags.toByte() } } albumSources.forEachIndexed { album, sources -> var flags = 0 sources.forEach { source -> flags = flags or if (bookAt(source)) 1 else 2 } if (snapshot.albumAvailable(album)) flags = flags or 4 albumFlags[album] = flags.toByte() } containers.records.forEachIndexed { index, container -> containerFlags[index] = ((if (container.sources.any(::bookAt)) 1 else 0) or (if (container.sources.any(::playable)) 2 else 0)).toByte() } playableIndices = retainedIndices.filter(::playable).distinctBy(::id).toIntArray() retainedIndices.filter { snapshot.nodeKind(it) == NodeKind.MEDIA && snapshot.nodeAvailable(it) }.forEach { base -> val views = containers.byBase[base] val available = if (views != null ) { views.any(::containerPlayable) } else { base !in this.containers.structuredBases } if (available) { var parent = snapshot.parentOf(base) while (parent != null && !folderPlayable[parent]) { folderPlayable[parent] = true parent = snapshot.parentOf(parent) } } } } fun id(index: Int): MediaId = snapshot.nodeId(index).let { MediaId(it.hi, it.lo ?: 0L, it.lo != null) } fun isMedia(index: Int): Boolean = when (snapshot.nodeKind(index)) { NodeKind.MEDIA, NodeKind.FRAGMENT -> true NodeKind.FAILEDL1 -> snapshot.nodeFlags(index) and Flags.IDHI == 0 else -> false } fun automaticAt(index: Int) = mediaFlags[index].toInt() and 1 != 0 fun bookAt(index: Int) = mediaFlags[index].toInt() and 2 != 0 fun containerBook(index: Int) = containerFlags[index].toInt() and 1 != 0 fun containerPlayable(index: Int) = containerFlags[index].toInt() and 2 != 0 fun albumBook(index: Int) = albumFlags[index].toInt() and 1 != 0 fun albumVisible( index: Int, book: Boolean, ) = albumFlags[index].toInt() and 4 != 0 && albumFlags[index].toInt() and (if (book) 1 else 2) != 0 fun playable(index: Int): Boolean = snapshot.nodeAvailable(index) && when (snapshot.nodeKind(index)) { NodeKind.MEDIA -> true NodeKind.FRAGMENT -> snapshot.sources(requireNotNull(snapshot.nodeParentId(index))).any(snapshot::nodeAvailable) else -> false } private fun folderChoice(index: Int): StoredAudiobookOverride? { val parent = snapshot.parentOf(index) ?: return null if (snapshot.nodeKind(parent) != NodeKind.DIRECTORY) return folderChoice(parent) if (folderChoices[parent].toInt() == 0) { val choice = overrides["folder:${snapshot.nodeId(parent).hi.toULong().toString(16)}"] ?: folderChoice(parent) folderChoices[parent] = when (choice) { null -> 1 StoredAudiobookOverride.MUSIC -> 2 StoredAudiobookOverride.BOOK -> 3 } } return when (folderChoices[parent].toInt()) { 2 -> StoredAudiobookOverride.MUSIC 3 -> StoredAudiobookOverride.BOOK else -> null } } }