shuffle an artist across all of their albums

This commit is contained in:
austinried
2026-10-04 00:30:05 +00:00
parent 699b1dbb5d
commit 336ff1a1d1
8 changed files with 153 additions and 1 deletions
@@ -19,6 +19,14 @@ private const val ALBUM_SONGS_ORDER = " ORDER BY songs.disc, songs.track, songs.
private const val ALBUM_SONGS_SQL = ALBUM_SONGS_SELECT + ALBUM_SONGS_ORDER
private const val ARTIST_SONGS_SELECT =
"SELECT songs.*, albums.coverArt AS coverArt FROM songs " +
"JOIN albums ON albums.sourceId = songs.sourceId AND albums.id = songs.albumId " +
"WHERE songs.sourceId = :sourceId AND albums.artistId = :artistId"
private const val ARTIST_SONGS_ORDER =
" ORDER BY albums.year DESC, albums.name COLLATE NOCASE, albums.id, songs.disc, songs.track, songs.id"
private const val SONG_SQL =
"SELECT songs.*, albums.coverArt AS coverArt FROM songs " +
"LEFT JOIN albums ON albums.sourceId = songs.sourceId AND albums.id = songs.albumId " +
@@ -72,6 +80,16 @@ interface QueueDao {
songId: String,
): Long
@Query(
"SELECT COUNT(*) FROM songs " +
"JOIN albums ON albums.sourceId = songs.sourceId AND albums.id = songs.albumId " +
"WHERE songs.sourceId = :sourceId AND albums.artistId = :artistId",
)
suspend fun artistLength(
sourceId: Long,
artistId: String,
): Long
@Query("$PLAYLIST_SONGS_SQL LIMIT :limit OFFSET :offset")
suspend fun playlistSongs(
sourceId: Long,
@@ -145,6 +163,14 @@ interface QueueDao {
limit: Int,
): List<AlbumSongItem>
@Query("$ARTIST_SONGS_SELECT$ARTIST_SONGS_ORDER LIMIT :limit OFFSET :offset")
suspend fun artistSongs(
sourceId: Long,
artistId: String,
offset: Long,
limit: Int,
): List<AlbumSongItem>
@Query("$SONG_SQL LIMIT :limit OFFSET :offset")
suspend fun song(
sourceId: Long,
@@ -259,7 +259,7 @@ data class DiscKey(
val disc: Long,
)
enum class QueueKind { Playlist, Album, Song }
enum class QueueKind { Playlist, Album, Song, Artist }
@Entity(
tableName = "queue_entries",
@@ -245,6 +245,11 @@ class QueueRepository(
albumId: String,
) = QueueEntry(position = 0, sourceId = sourceId, kind = QueueKind.Album, refId = albumId)
fun artistEntry(
sourceId: Long,
artistId: String,
) = QueueEntry(position = 0, sourceId = sourceId, kind = QueueKind.Artist, refId = artistId)
fun songEntry(
sourceId: Long,
songId: String,
@@ -273,6 +278,14 @@ class QueueRepository(
?.takeIf { it.isNotBlank() }
}
QueueKind.Artist -> {
library
.artist(sourceId, refId)
.first()
?.name
?.takeIf { it.isNotBlank() }
}
// An explicit song entry in the middle of a queue is a reordered context track, so
// label it by its album rather than losing the context name.
QueueKind.Song -> {
@@ -632,6 +645,7 @@ class QueueRepository(
QueueKind.Song -> listOf(ref.refId)
QueueKind.Album -> dao.albumSongIds(ref.sourceId, ref.refId)
QueueKind.Playlist -> dao.playlistSongIds(ref.sourceId, ref.refId)
QueueKind.Artist -> dao.artistSongIds(ref.sourceId, ref.refId)
}
val from = ref.offset.toInt().coerceIn(0, entryIds.size)
val to = (ref.offset + entry.length).toInt().coerceIn(from, entryIds.size)
@@ -731,6 +745,7 @@ class QueueRepository(
when (entry.kind) {
QueueKind.Playlist -> dao.playlistSongs(entry.sourceId, entry.refId, offset, limit)
QueueKind.Album -> dao.albumSongs(entry.sourceId, entry.refId, offset, limit)
QueueKind.Artist -> dao.artistSongs(entry.sourceId, entry.refId, offset, limit)
QueueKind.Song -> emptyList()
}
if (fresh.isNotEmpty()) remember(ref, offset, fresh)
@@ -753,6 +768,7 @@ class QueueRepository(
when (kind) {
QueueKind.Album -> OrderKey(row.song.id, disc = row.song.disc, track = row.song.track)
QueueKind.Playlist -> OrderKey(row.song.id, position = row.playlistPosition)
QueueKind.Artist -> OrderKey(row.song.id)
QueueKind.Song -> OrderKey(row.song.id)
}
@@ -792,6 +808,7 @@ class QueueRepository(
when (entry.kind) {
QueueKind.Playlist -> dao.playlistSongsFrom(entry.sourceId, entry.refId, key.position!!, skip, limit)
QueueKind.Album -> dao.albumSongsFrom(entry.sourceId, entry.refId, key.disc!!, key.track!!, key.id, skip, limit)
QueueKind.Artist -> emptyList()
QueueKind.Song -> emptyList()
}
@@ -804,6 +821,7 @@ class QueueRepository(
when (entry.kind) {
QueueKind.Playlist -> dao.playlistSongsBefore(entry.sourceId, entry.refId, key.position!!, skip, limit)
QueueKind.Album -> dao.albumSongsBefore(entry.sourceId, entry.refId, key.disc!!, key.track!!, key.id, skip, limit)
QueueKind.Artist -> emptyList()
QueueKind.Song -> emptyList()
}
@@ -811,6 +829,7 @@ class QueueRepository(
when (kind) {
QueueKind.Playlist -> position != null
QueueKind.Album -> disc != null && track != null
QueueKind.Artist -> false
QueueKind.Song -> false
}
@@ -850,6 +869,10 @@ class QueueRepository(
QueueKind.Album -> {
dao.albumLength(sourceId, refId)
}
QueueKind.Artist -> {
dao.artistLength(sourceId, refId)
}
}
val available = (total - offset).coerceAtLeast(0)
return count?.coerceAtMost(available) ?: available
@@ -238,6 +238,11 @@ class PlaybackController(
playlistId: String,
) = shuffleContext(queueRepository.playlistEntry(sourceId, playlistId))
fun shuffleArtist(
sourceId: Long,
artistId: String,
) = shuffleContext(queueRepository.artistEntry(sourceId, artistId))
private fun playContext(
entry: QueueEntry,
start: Long,
@@ -190,6 +190,7 @@ fun ArtistDetailRoute(
onMore = { artist?.let { contextMenuHost?.show(MenuTarget.Artist(it, artThumbnail ?: art, downloadStatus), actions) } },
starred = artist?.starred != null,
onToggleStar = artist?.let { a -> { setStar(StarType.Artist, a.id, a.starred == null) } },
onShuffle = viewModel::shuffle,
offline = offline,
)
@@ -104,4 +104,10 @@ class ArtistDetailViewModel(
coverArt: String?,
thumbnail: Boolean,
): CoverArtRef? = sourceRepository.coverArt(coverArt, thumbnail)
fun shuffle() {
viewModelScope.launch {
playbackController.shuffleArtist(sourceId.first(), artistId)
}
}
}
@@ -497,6 +497,7 @@ private fun QueueKind.label(): String =
QueueKind.Album -> "album"
QueueKind.Playlist -> "playlist"
QueueKind.Song -> "song"
QueueKind.Artist -> "artist"
}
private fun formatTime(milliseconds: Long): String {