package link.roadio.riff.player.library import link.roadio.cuetree.NodeKind import link.roadio.riff.indexer.core.media.MediaId import link.roadio.riff.ui.BrowserItem import link.roadio.riff.ui.BrowserMembership import link.roadio.riff.ui.BrowserRowKey import link.roadio.riff.ui.BrowserRows /** Queue construction carries catalog references, not recursively allocated display rows. */ internal data class CatalogOccurrence( val index: Int, val base: Int? = null, val album: Int? = null, val folder: Boolean = false, ) internal fun CuetreeBrowseCatalog.occurrences( context: String, playableOnly: Boolean, ignoreLeadingArticles: Boolean = false, ): List { val values = mutableListOf() fun add( index: Int, base: Int? = null, album: Int? = null, folder: Boolean = false, ) { if (!playableOnly || playable(index)) values += CatalogOccurrence(index, base, album, folder) } fun sortAlbums(indices: List): List = indices .map { index -> val key = albumKey(index) val name = link.roadio.riff.ui .browseComparisonName(key.comparisonName, ignoreLeadingArticles) Triple( index, key, java.text.Normalizer .normalize(name, java.text.Normalizer.Form.NFKC) .lowercase(java.util.Locale.ROOT), ) }.sortedWith( compareBy> { it.third }.thenBy { it.second.comparisonName }.thenBy { it.second.id }, ).map { it.first } fun album(index: Int) { snapshot.albums[index].sources.forEach { add(it, album = index) } } fun file(index: Int) { val container = facts.containers.records[index] container.sources.forEach { add(it, container.base, container.album, true) } } fun folder(index: Int) { orderedFolderChildren(index, ignoreLeadingArticles).forEach { child -> if (child < 0) { file(-1 - child) } else if (snapshot.nodeKind(child) == NodeKind.DIRECTORY) { folder(child) } else { add(child, child, folder = true) } } } val number = context.substringAfter(':').toULongOrNull(16)?.toLong() when { context.startsWith("album:") -> { number?.let(::albumIndex)?.takeIf { it >= 0 }?.let(::album) } context.startsWith("artist:") -> { number?.let { id -> snapshot.artists.binarySearchBy(id) { it.id } }?.takeIf { it >= 0 }?.let { index -> snapshot.artists[index] .albums .map(::albumIndex) .filter { it >= 0 && albumVisible(it) } .let(::sortAlbums) .forEach(::album) } } context.startsWith("genre:") -> { number?.let { id -> snapshot.genres.binarySearchBy(id) { it.id } }?.takeIf { it >= 0 }?.let { index -> snapshot.genres[index] .albums .map(::albumIndex) .filter { it >= 0 && albumVisible(it) } .let(::sortAlbums) .forEach(::album) } } context.startsWith("folder:") -> { number?.let(::folderIndex)?.let(::folder) } context.startsWith("media-container:") -> { fileIndex(context)?.let(::file) } context.startsWith("track-row:") || context.startsWith("folder-track-row:") -> { context .substringAfter(':') .substringBefore(':') .toIntOrNull() ?.takeIf { it in snapshot.nodes.indices }?.let { add(it) } } context == "category:folders" -> { rootFolderIndices.forEach(::folder) } context == "category:albums" || context == "root" -> { albumIndices.toList().let(::sortAlbums).forEach(::album) } context == "category:artists" -> { artistIndices.forEach { index -> snapshot.artists[index] .albums .map(::albumIndex) .filter { it >= 0 && albumVisible(it) }.let(::sortAlbums) .forEach(::album) } } context == "category:genres" -> { genreIndices.forEach { index -> snapshot.genres[index] .albums .map(::albumIndex) .filter { it >= 0 && albumVisible(it) }.let(::sortAlbums) .forEach(::album) } } context.startsWith("track:") -> { parseMediaId(context.substringAfter(':'))?.let { wanted -> snapshot .sources(link.roadio.cuetree.MediaId(wanted.high, wanted.low.takeIf { wanted.lowPresent })) .firstOrNull { isMedia(it) && (!playableOnly || playable(it)) } ?.let { add(it) } } } } return if (context.startsWith("album:") || context.startsWith("media-container:") ) { values } else { values.distinctBy { if (it.album != null ) { "${it.album}:${it.index}" } else { id(it.index) } } } } internal fun CuetreeBrowseCatalog.contextTracks( context: String, playableOnly: Boolean, ignoreLeadingArticles: Boolean, ): List { val records = if (ignoreLeadingArticles) occurrences(context, playableOnly, true) else orderedOccurrences(context, playableOnly) return object : BrowserRows() { override val size get() = records.size override fun mediaId(index: Int) = id(records[index].index) override fun rowId(index: Int): String = records[index].let { "${if (it.folder) "folder-track-row" else "track-row"}:${it.index}" + (it.base?.let { base -> ":$base" } ?: "") } override fun key(index: Int): BrowserRowKey = records[index].let { trackKey(it.index, it.folder, it.base) } override fun get(index: Int): BrowserItem = records[index].let { ref -> trackRow(ref.index, ref.folder, ref.base, ref.album?.let(snapshot.albums::get)) } } } internal fun CuetreeBrowseCatalog.contextMediaIds(context: String): Set { if (context.startsWith("track:") && context.count { it == ':' } == 1 ) { parseMediaId(context.substringAfter(':'))?.let { return setOf(it) } } return orderedOccurrences(context, false).mapTo(linkedSetOf()) { id(it.index) } } internal fun CuetreeBrowseCatalog.contextEntities(category: String): List { val refs = when (category) { "category:albums" -> albumIndices.map { albumKey(it).id } "category:artists" -> artistIndices.map { artistKey(it).id } "category:genres" -> genreIndices.map { genreKey(it).id } else -> emptyList() } return object : AbstractList() { override val size get() = refs.size override fun get(index: Int) = BrowserMembership(refs[index], contextMediaIds(refs[index])) } } internal fun CuetreeBrowseCatalog.catalogDirectoryPaths(): Map>> { val result = linkedMapOf>>() retainedIndices.filter(::playable).forEach { index -> val bases = if (snapshot.nodeKind(index) == NodeKind.FRAGMENT ) { snapshot.sources(requireNotNull(snapshot.nodeParentId(index))).filter { snapshot.nodeKind(it) == NodeKind.MEDIA && snapshot.nodeAvailable(it) } } else { listOf(index) } bases.forEach { base -> val path = mutableListOf() val containers = (facts.containers.byBase[base]?.toList() ?: emptyList()).filter { index in facts.containers.records[it].sources } val virtualPaths = containers.map { listOf(fileId(it)) } if (virtualPaths.isNotEmpty()) path += virtualPaths.first() var parent = snapshot.parentOf(base) while (parent != null) { path += "folder:${snapshot.nodeId(parent).hi.toULong().toString(16)}" parent = snapshot.parentOf(parent) } val paths = result.getOrPut(id(index)) { mutableListOf() } val variants = if (virtualPaths.isEmpty()) listOf(path) else virtualPaths.map { it + path.drop(1) } variants.forEach { if (it !in paths) paths += it } } } return result }