From 4cad4bc481ec09be6873b2cb1be961943f416060 Mon Sep 17 00:00:00 2001 From: austinried <4966622+austinried@users.noreply.github.com> Date: Thu, 24 Sep 2026 12:50:08 +0000 Subject: [PATCH] resolve shuffled queue windows in one batched query --- .../kotlin/com/subtracks/data/db/QueueDao.kt | 39 ++++++++++++ .../subtracks/data/repo/QueueRepository.kt | 60 +++++++++++++++++-- 2 files changed, 95 insertions(+), 4 deletions(-) diff --git a/app/src/main/kotlin/com/subtracks/data/db/QueueDao.kt b/app/src/main/kotlin/com/subtracks/data/db/QueueDao.kt index 9d68f894..ab4ed0f3 100644 --- a/app/src/main/kotlin/com/subtracks/data/db/QueueDao.kt +++ b/app/src/main/kotlin/com/subtracks/data/db/QueueDao.kt @@ -101,4 +101,43 @@ interface QueueDao { offset: Long, limit: Int, ): List + + @Query( + "SELECT songs.id FROM songs WHERE songs.sourceId = :sourceId AND songs.albumId = :albumId " + + "ORDER BY songs.disc, songs.track, songs.id", + ) + suspend fun albumSongIds( + sourceId: Long, + albumId: String, + ): List + + @Query( + "SELECT songs.id FROM songs " + + "LEFT JOIN albums ON albums.sourceId = songs.sourceId AND albums.id = songs.albumId " + + "WHERE songs.sourceId = :sourceId " + + "ORDER BY albums.albumArtist COLLATE NOCASE, songs.album COLLATE NOCASE, songs.disc, songs.track, " + + "songs.title COLLATE NOCASE, songs.id", + ) + suspend fun songIds(sourceId: Long): List + + @Query( + "SELECT songs.id FROM playlist_songs " + + "JOIN songs ON songs.sourceId = playlist_songs.sourceId AND songs.id = playlist_songs.songId " + + "WHERE playlist_songs.sourceId = :sourceId AND playlist_songs.playlistId = :playlistId " + + "ORDER BY playlist_songs.position", + ) + suspend fun playlistSongIds( + sourceId: Long, + playlistId: String, + ): List + + @Query( + "SELECT songs.*, albums.coverArt AS coverArt FROM songs " + + "LEFT JOIN albums ON albums.sourceId = songs.sourceId AND albums.id = songs.albumId " + + "WHERE songs.sourceId = :sourceId AND songs.id IN (:ids)", + ) + suspend fun songsByIds( + sourceId: Long, + ids: List, + ): List } diff --git a/app/src/main/kotlin/com/subtracks/data/repo/QueueRepository.kt b/app/src/main/kotlin/com/subtracks/data/repo/QueueRepository.kt index 5c3a9e95..85a48195 100644 --- a/app/src/main/kotlin/com/subtracks/data/repo/QueueRepository.kt +++ b/app/src/main/kotlin/com/subtracks/data/repo/QueueRepository.kt @@ -61,6 +61,15 @@ class QueueRepository( @Volatile private var cachedShuffleOrder: LongArray? = null + @Volatile + private var queueVersion = 0L + + @Volatile + private var cachedFlatIds: List? = null + + @Volatile + private var cachedFlatVersion = -1L + fun playlistEntry( sourceId: Long, playlistId: String, @@ -139,6 +148,16 @@ class QueueRepository( snapshot: QueueSnapshot, position: Long, ): SongListItem? { + if (snapshot.shuffled) { + val sourceId = + snapshot.entries + .firstOrNull() + ?.entry + ?.sourceId ?: return null + val flat = snapshot.flatPosition(position) ?: return null + val id = flatIds(snapshot).getOrNull(flat.toInt()) ?: return null + return dao.songsByIds(sourceId, listOf(id)).firstOrNull() + } val (entry, offset) = snapshot.locate(position) ?: return null return rows(entry, entry.offset + offset, 1).firstOrNull() } @@ -149,12 +168,21 @@ class QueueRepository( last: Long, ): List { if (snapshot.shuffled) { - val items = mutableListOf() + val sourceId = + snapshot.entries + .firstOrNull() + ?.entry + ?.sourceId ?: return emptyList() + val ids = flatIds(snapshot) + val requested = mutableListOf>() for (position in first..last) { - val (entry, offset) = snapshot.locate(position) ?: continue - rows(entry, entry.offset + offset, 1).firstOrNull()?.let { items += QueueWindowItem(position, it) } + val flat = snapshot.flatPosition(position) ?: continue + val id = ids.getOrNull(flat.toInt()) ?: continue + requested += position to id } - return items + if (requested.isEmpty()) return emptyList() + val byId = dao.songsByIds(sourceId, requested.map { it.second }.distinct()).associateBy { it.song.id } + return requested.mapNotNull { (position, id) -> byId[id]?.let { QueueWindowItem(position, it) } } } val items = mutableListOf() var start = 0L @@ -232,6 +260,30 @@ class QueueRepository( dao.insert(entries.mapIndexed { index, entry -> entry.copy(id = 0, position = index.toLong()) }) } } + queueVersion++ + cachedFlatIds = null + } + + private suspend fun flatIds(snapshot: QueueSnapshot): List { + val cached = cachedFlatIds + if (cached != null && cachedFlatVersion == queueVersion) return cached + val ids = ArrayList(snapshot.size.toInt()) + for (resolved in snapshot.entries) { + val entry = resolved.entry + val entryIds = + when (entry.kind) { + QueueKind.Song -> listOf(entry.refId) + QueueKind.Album -> dao.albumSongIds(entry.sourceId, entry.refId) + QueueKind.Songs -> dao.songIds(entry.sourceId) + QueueKind.Playlist -> dao.playlistSongIds(entry.sourceId, entry.refId) + } + val from = entry.offset.toInt().coerceIn(0, entryIds.size) + val to = (entry.offset + resolved.length).toInt().coerceIn(from, entryIds.size) + ids.addAll(entryIds.subList(from, to)) + } + cachedFlatIds = ids + cachedFlatVersion = queueVersion + return ids } private suspend fun rows(