diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/playback/CollectionPlaybackHelper.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/playback/CollectionPlaybackHelper.kt index da55fb38..6d22e903 100644 --- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/playback/CollectionPlaybackHelper.kt +++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/playback/CollectionPlaybackHelper.kt @@ -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, diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/remote/RemoteControlHandler.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/remote/RemoteControlHandler.kt index 602fc9f1..9307ea65 100644 --- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/remote/RemoteControlHandler.kt +++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/remote/RemoteControlHandler.kt @@ -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 @@ -467,4 +471,4 @@ enum class ConnectionRequestResponse { Allow, AllowAlways, Deny, -} \ No newline at end of file +} diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/remote/RemoteControlProtocol.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/remote/RemoteControlProtocol.kt index fc51834e..e2ab1bd9 100644 --- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/remote/RemoteControlProtocol.kt +++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/remote/RemoteControlProtocol.kt @@ -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") @@ -147,4 +147,4 @@ data class RemoteQueueEntry( val album: String?, val coverUrl: String?, val durationMs: Long, -) \ No newline at end of file +) diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/remote/RemotePlaybackController.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/remote/RemotePlaybackController.kt index 40602ea9..57c89e82 100644 --- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/remote/RemotePlaybackController.kt +++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/remote/RemotePlaybackController.kt @@ -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) } @@ -385,4 +387,4 @@ class RemotePlaybackController( album?.id == other.album?.id && artists.map { it.id.ifBlank { it.name } } == other.artists.map { it.id.ifBlank { it.name } } } -} \ No newline at end of file +} diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/album/AlbumScreen.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/album/AlbumScreen.kt index db76747f..6b2ac18d 100644 --- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/album/AlbumScreen.kt +++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/album/AlbumScreen.kt @@ -78,7 +78,7 @@ fun AlbumScreen( } }, onPlay = viewModel::playAlbum, - onShufflePlay = {}, + onShufflePlay = viewModel::shufflePlayAlbum, onAddToQueue = viewModel::addAlbumToQueue, isPlaying = isAlbumPlaying, isFollowing = savedAlbumIds.contains(albumId), diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/album/AlbumViewModel.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/album/AlbumViewModel.kt index 3da95baa..d27d9421 100644 --- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/album/AlbumViewModel.kt +++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/album/AlbumViewModel.kt @@ -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) diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/playlist/PlaylistScreen.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/playlist/PlaylistScreen.kt index b48f1253..a472ac93 100644 --- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/playlist/PlaylistScreen.kt +++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/playlist/PlaylistScreen.kt @@ -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), diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/playlist/PlaylistViewModel.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/playlist/PlaylistViewModel.kt index d1ef1088..5a4c1076 100644 --- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/playlist/PlaylistViewModel.kt +++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/playlist/PlaylistViewModel.kt @@ -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) diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/saved_tracks/SavedTracksScreen.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/saved_tracks/SavedTracksScreen.kt index 191923a0..a7640ebd 100644 --- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/saved_tracks/SavedTracksScreen.kt +++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/saved_tracks/SavedTracksScreen.kt @@ -70,7 +70,7 @@ fun SavedTracksScreen( ownerImageURL = null, onOwnerClick = {}, onPlay = viewModel::playSavedTracks, - onShufflePlay = {}, + onShufflePlay = viewModel::shufflePlaySavedTracks, onAddToQueue = viewModel::addSavedTracksToQueue, isPlaying = isPlaying, isFollowing = false, diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/saved_tracks/SavedTracksViewModel.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/saved_tracks/SavedTracksViewModel.kt index 1025bdbd..bf8fafa4 100644 --- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/saved_tracks/SavedTracksViewModel.kt +++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/saved_tracks/SavedTracksViewModel.kt @@ -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") } @@ -377,4 +386,4 @@ fun rememberIsSavedTracks( } return state -} \ No newline at end of file +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index d0951e49..2e78d2a0 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -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"