feat: implement shuffle play functionality for albums, playlists, and saved tracks

This commit is contained in:
Kingkor Roy Tirtho 2026-09-25 18:47:29 +06:00
parent 50c3b04eb1
commit 78168d269c
11 changed files with 53 additions and 28 deletions

View File

@ -36,9 +36,9 @@ class CollectionPlaybackHelper(
private val audioPlayerQueue: AudioPlayerQueue,
private val blacklistRepository: BlacklistRepository,
) {
suspend fun playAlbum(albumId: String) {
if (audioPlayerQueue.isAlbumPlaying(albumId)) return
val entries = fetchAllAlbumTracks(albumId)
suspend fun playAlbum(albumId: String, shuffle: Boolean = false) {
if (!shuffle && audioPlayerQueue.isAlbumPlaying(albumId)) return
val entries = fetchAllAlbumTracks(albumId).let { if (shuffle) it.shuffled() else it }
if (entries.isNotEmpty()) {
audioPlayerQueue.load(
entries = entries,
@ -91,9 +91,9 @@ class CollectionPlaybackHelper(
)
}
suspend fun playPlaylist(playlistId: String) {
if (audioPlayerQueue.isPlaylistPlaying(playlistId)) return
val entries = fetchAllPlaylistTracks(playlistId)
suspend fun playPlaylist(playlistId: String, shuffle: Boolean = false) {
if (!shuffle && audioPlayerQueue.isPlaylistPlaying(playlistId)) return
val entries = fetchAllPlaylistTracks(playlistId).let { if (shuffle) it.shuffled() else it }
if (entries.isNotEmpty()) {
audioPlayerQueue.load(
entries = entries,
@ -146,9 +146,9 @@ class CollectionPlaybackHelper(
)
}
suspend fun playSavedTracks() {
if (audioPlayerQueue.isSavedTracksPlaying()) return
val entries = fetchAllSavedTracks()
suspend fun playSavedTracks(shuffle: Boolean = false) {
if (!shuffle && audioPlayerQueue.isSavedTracksPlaying()) return
val entries = fetchAllSavedTracks().let { if (shuffle) it.shuffled() else it }
if (entries.isNotEmpty()) {
audioPlayerQueue.load(
entries = entries,

View File

@ -172,7 +172,7 @@ class RemoteControlHandler(
private suspend fun handleCommand(session: WebSocketServerSession, envelope: CommandEnvelope) {
when (val command = envelope.command) {
is RemoteControlCommand.Play -> {
handleCollectionSource(command.source, RemoteCollectionAction.Play)
handleCollectionSource(command.source, RemoteCollectionAction.Play, command.shuffle)
}
is RemoteControlCommand.Pause -> {
audioPlayer.pause()
@ -297,13 +297,17 @@ class RemoteControlHandler(
* Resolves a `spotube://` collection source URI (playlist/album/artist top
* tracks/saved tracks) and applies the requested action on the remote queue.
*/
private suspend fun handleCollectionSource(source: String, action: RemoteCollectionAction) {
private suspend fun handleCollectionSource(
source: String,
action: RemoteCollectionAction,
shuffle: Boolean = false,
) {
logger.d { "Remote collection $action for source: $source" }
when {
source.startsWith(COLLECTION_PLAYLIST_PREFIX) -> {
val id = source.removePrefix(COLLECTION_PLAYLIST_PREFIX)
when (action) {
RemoteCollectionAction.Play -> collectionPlaybackHelper.playPlaylist(id)
RemoteCollectionAction.Play -> collectionPlaybackHelper.playPlaylist(id, shuffle)
RemoteCollectionAction.AddToQueue -> collectionPlaybackHelper.addPlaylistToQueue(id)
RemoteCollectionAction.PlayNext -> collectionPlaybackHelper.playPlaylistNext(id)
}
@ -312,7 +316,7 @@ class RemoteControlHandler(
source.startsWith(COLLECTION_ALBUM_PREFIX) -> {
val id = source.removePrefix(COLLECTION_ALBUM_PREFIX)
when (action) {
RemoteCollectionAction.Play -> collectionPlaybackHelper.playAlbum(id)
RemoteCollectionAction.Play -> collectionPlaybackHelper.playAlbum(id, shuffle)
RemoteCollectionAction.AddToQueue -> collectionPlaybackHelper.addAlbumToQueue(id)
RemoteCollectionAction.PlayNext -> collectionPlaybackHelper.playAlbumNext(id)
}
@ -329,7 +333,7 @@ class RemoteControlHandler(
source == COLLECTION_SAVED_TRACKS -> {
when (action) {
RemoteCollectionAction.Play -> collectionPlaybackHelper.playSavedTracks()
RemoteCollectionAction.Play -> collectionPlaybackHelper.playSavedTracks(shuffle)
RemoteCollectionAction.AddToQueue -> collectionPlaybackHelper.addSavedTracksToQueue()
RemoteCollectionAction.PlayNext -> {
// Saved tracks "play next" is not supported; add to queue instead

View File

@ -25,7 +25,7 @@ import kotlinx.serialization.Serializable
sealed class RemoteControlCommand {
@Serializable
@SerialName("play")
data class Play(val source: String) : RemoteControlCommand()
data class Play(val source: String, val shuffle: Boolean = false) : RemoteControlCommand()
@Serializable
@SerialName("pause")

View File

@ -64,6 +64,7 @@ sealed interface PlaybackDestinationRequest {
val type: RemoteCollectionType,
val id: String,
val startTrack: MetadataTrack? = null,
val shuffle: Boolean = false,
) : PlaybackDestinationRequest
data class Track(
@ -112,8 +113,9 @@ class RemotePlaybackController(
id: String,
title: String,
startTrack: MetadataTrack? = null,
shuffle: Boolean = false,
) {
request(PlaybackDestinationRequest.Collection(title, PlaybackDestinationAction.Play, type, id, startTrack))
request(PlaybackDestinationRequest.Collection(title, PlaybackDestinationAction.Play, type, id, startTrack, shuffle))
}
fun requestCollectionAddToQueue(type: RemoteCollectionType, id: String, title: String) {
@ -239,7 +241,7 @@ class RemotePlaybackController(
if (startTrack != null) {
collectionPlaybackHelper.playPlaylistFromTrack(request.id, startTrack)
} else {
collectionPlaybackHelper.playPlaylist(request.id)
collectionPlaybackHelper.playPlaylist(request.id, request.shuffle)
}
}
@ -252,7 +254,7 @@ class RemotePlaybackController(
if (startTrack != null) {
collectionPlaybackHelper.playAlbumFromTrack(request.id, startTrack)
} else {
collectionPlaybackHelper.playAlbum(request.id)
collectionPlaybackHelper.playAlbum(request.id, request.shuffle)
}
}
@ -271,7 +273,7 @@ class RemotePlaybackController(
if (startTrack != null) {
collectionPlaybackHelper.playSavedTracksFromTrack(startTrack)
} else {
collectionPlaybackHelper.playSavedTracks()
collectionPlaybackHelper.playSavedTracks(request.shuffle)
}
}
@ -346,7 +348,7 @@ class RemotePlaybackController(
RemoteCollectionType.SavedTracks -> "spotube://saved_tracks"
}
when (request.action) {
PlaybackDestinationAction.Play -> RemoteControlCommand.Play(source)
PlaybackDestinationAction.Play -> RemoteControlCommand.Play(source, request.shuffle)
PlaybackDestinationAction.AddToQueue -> RemoteControlCommand.AddToQueue(source)
PlaybackDestinationAction.PlayNext -> RemoteControlCommand.PlayNext(source)
}

View File

@ -78,7 +78,7 @@ fun AlbumScreen(
}
},
onPlay = viewModel::playAlbum,
onShufflePlay = {},
onShufflePlay = viewModel::shufflePlayAlbum,
onAddToQueue = viewModel::addAlbumToQueue,
isPlaying = isAlbumPlaying,
isFollowing = savedAlbumIds.contains(albumId),

View File

@ -224,6 +224,11 @@ class AlbumViewModel(
remotePlaybackController.requestCollectionPlay(RemoteCollectionType.Album, albumId, title)
}
fun shufflePlayAlbum() {
val title = (_state.value as? AlbumScreenState.Data)?.album?.title ?: "Album"
remotePlaybackController.requestCollectionPlay(RemoteCollectionType.Album, albumId, title, shuffle = true)
}
fun addAlbumToQueue() {
val title = (_state.value as? AlbumScreenState.Data)?.album?.title ?: "Album"
remotePlaybackController.requestCollectionAddToQueue(RemoteCollectionType.Album, albumId, title)

View File

@ -111,7 +111,7 @@ fun PlaylistScreen(
ownerImageURL = ownerImageURL,
onOwnerClick = {},
onPlay = viewModel::playPlaylist,
onShufflePlay = {},
onShufflePlay = viewModel::shufflePlayPlaylist,
onAddToQueue = viewModel::addPlaylistToQueue,
isPlaying = isPlaying,
isFollowing = savedPlaylistIds.contains(playlistId),

View File

@ -230,6 +230,11 @@ class PlaylistViewModel(
remotePlaybackController.requestCollectionPlay(RemoteCollectionType.Playlist, playlistId, title)
}
fun shufflePlayPlaylist() {
val title = (_state.value as? PlaylistScreenState.Data)?.playlist?.title ?: "Playlist"
remotePlaybackController.requestCollectionPlay(RemoteCollectionType.Playlist, playlistId, title, shuffle = true)
}
fun addPlaylistToQueue() {
val title = (_state.value as? PlaylistScreenState.Data)?.playlist?.title ?: "Playlist"
remotePlaybackController.requestCollectionAddToQueue(RemoteCollectionType.Playlist, playlistId, title)

View File

@ -70,7 +70,7 @@ fun SavedTracksScreen(
ownerImageURL = null,
onOwnerClick = {},
onPlay = viewModel::playSavedTracks,
onShufflePlay = {},
onShufflePlay = viewModel::shufflePlaySavedTracks,
onAddToQueue = viewModel::addSavedTracksToQueue,
isPlaying = isPlaying,
isFollowing = false,

View File

@ -206,6 +206,15 @@ class SavedTracksViewModel(
remotePlaybackController.requestCollectionPlay(RemoteCollectionType.SavedTracks, SAVED_TRACKS_COLLECTION_ID, "Saved Tracks")
}
fun shufflePlaySavedTracks() {
remotePlaybackController.requestCollectionPlay(
RemoteCollectionType.SavedTracks,
SAVED_TRACKS_COLLECTION_ID,
"Saved Tracks",
shuffle = true,
)
}
fun addSavedTracksToQueue() {
remotePlaybackController.requestCollectionAddToQueue(RemoteCollectionType.SavedTracks, SAVED_TRACKS_COLLECTION_ID, "Saved Tracks")
}

View File

@ -47,7 +47,7 @@ materialKolor = "4.1.1"
mqttClient = "2.1.1"
mqttBuffer = "6.30.8"
murmurhash = "0.4.2"
newpipeextractor = "v0.26.2"
newpipeextractor = "v0.26.5"
newpipeExtractorKmp = "1.3.0"
nucleusNucleusApplication = "2.4.6"
semver = "2.1.0"