diff --git a/CHANGELOG.md b/CHANGELOG.md index 06a70e2d0..65a2b888c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- [connect] Add method `add_to_queue` to `Spirc` to add tracks, episodes, albums and playlists to the queue +- [playback] Add `AddedToQueue` player event, emitting when a track was added to the queue with `Spirc::add_to_queue` + ### Changed - [core] Made `SpotifyId::to_base62`, `SpotifyId::to_base16`, `FileId::to_base16`, `SpotifyUri::to_id`, `SpotifyUri::to_uri` infallible (breaking) diff --git a/connect/src/spirc.rs b/connect/src/spirc.rs index d5643df16..f2c302b5d 100644 --- a/connect/src/spirc.rs +++ b/connect/src/spirc.rs @@ -20,6 +20,7 @@ use crate::{ connect::{Cluster, ClusterUpdate, LogoutCommand, SetVolumeCommand}, context::Context, explicit_content_pubsub::UserAttributesUpdate, + player::ProvidedTrack, playlist4_external::PlaylistModificationInfo, social_connect_v2::SessionUpdate, transfer_state::TransferState, @@ -132,6 +133,7 @@ enum SpircCommand { Activate, Transfer(Option), Load(LoadRequest), + AddToQueue(SpotifyUri), } const CONTEXT_FETCH_THRESHOLD: usize = 2; @@ -388,6 +390,24 @@ impl Spirc { Ok(self.commands.send(SpircCommand::Load(command))?) } + /// Adds a track, episode, album or playlist to the queue. + /// + /// Does nothing if we are not the active device. + /// + /// For albums and playlists, all tracks/episodes are resolved and added to the queue. + pub fn add_to_queue(&self, uri: SpotifyUri) -> Result<(), Error> { + if !matches!( + uri, + SpotifyUri::Track { .. } + | SpotifyUri::Episode { .. } + | SpotifyUri::Album { .. } + | SpotifyUri::Playlist { .. } + ) { + return Err(Error::invalid_argument("uri")); + } + Ok(self.commands.send(SpircCommand::AddToQueue(uri))?) + } + /// Disconnects the current device and pauses the playback according the value. /// /// Does nothing if we are not the active device. @@ -679,6 +699,7 @@ impl SpircTask { SpircCommand::SetPosition(position) => self.handle_seek(position), SpircCommand::SetVolume(volume) => self.set_volume(volume), SpircCommand::Load(command) => self.handle_load(command, None, None).await?, + SpircCommand::AddToQueue(uri) => self.handle_add_to_queue(uri).await, }; self.notify().await @@ -1062,7 +1083,13 @@ impl SpircTask { self.handle_repeat_context(repeat_context.value)? } SetRepeatingTrack(repeat_track) => self.handle_repeat_track(repeat_track.value), - AddToQueue(add_to_queue) => self.connect_state.add_to_queue(add_to_queue.track, true), + AddToQueue(add_to_queue) => { + let track = add_to_queue.track.clone(); + self.connect_state.add_to_queue(add_to_queue.track, true); + if let Ok(uri) = SpotifyUri::from_uri(&track.uri) { + self.player.emit_added_to_queue_event(uri); + } + } SetQueue(set_queue) => self.connect_state.handle_set_queue(set_queue), SetOptions(set_options) => { if let Some(repeat_context) = set_options.repeating_context { @@ -1545,6 +1572,39 @@ impl SpircTask { self.connect_state.set_repeat_track(repeat); } + async fn handle_add_to_queue(&mut self, uri: SpotifyUri) { + let track_uris: Vec = match uri { + SpotifyUri::Track { .. } | SpotifyUri::Episode { .. } => vec![uri.to_uri()], + SpotifyUri::Album { .. } | SpotifyUri::Playlist { .. } => { + match self.session.spclient().get_context(&uri.to_uri()).await { + Ok(context) => context + .pages + .iter() + .flat_map(|page| page.tracks.iter()) + .filter_map(|track| track.uri.clone()) + .collect(), + Err(e) => { + error!("failed to resolve context for {}: {e}", uri.item_type()); + return; + } + } + } + _ => return, + }; + + for track_uri in track_uris { + let track = ProvidedTrack { + uri: track_uri.clone(), + ..Default::default() + }; + self.connect_state.add_to_queue(track, true); + + if let Ok(uri) = SpotifyUri::from_uri(&track_uri) { + self.player.emit_added_to_queue_event(uri); + } + } + } + fn handle_preload_next_track(&mut self) { // Requests the player thread to preload the next track match self.play_status { diff --git a/playback/src/player.rs b/playback/src/player.rs index 3f3bbc9f1..95d49e8c2 100644 --- a/playback/src/player.rs +++ b/playback/src/player.rs @@ -138,6 +138,7 @@ enum PlayerCommand { track: bool, }, EmitAutoPlayChangedEvent(bool), + EmitAddedToQueueEvent(SpotifyUri), } #[derive(Debug, Clone)] @@ -146,6 +147,9 @@ pub enum PlayerEvent { PlayRequestIdChanged { play_request_id: u64, }, + AddedToQueue { + track_id: SpotifyUri, + }, // Fired when the player is stopped (e.g. by issuing a "stop" command to the player). Stopped { play_request_id: u64, @@ -647,6 +651,10 @@ impl Player { pub fn emit_auto_play_changed_event(&self, auto_play: bool) { self.command(PlayerCommand::EmitAutoPlayChangedEvent(auto_play)); } + + pub fn emit_added_to_queue_event(&self, track_id: SpotifyUri) { + self.command(PlayerCommand::EmitAddedToQueueEvent(track_id)); + } } impl Drop for Player { @@ -2335,6 +2343,10 @@ impl PlayerInternal { self.auto_normalise_as_album = setting } + PlayerCommand::EmitAddedToQueueEvent(track_id) => { + self.send_event(PlayerEvent::AddedToQueue { track_id }) + } + PlayerCommand::EmitFilterExplicitContentChangedEvent(filter) => { self.send_event(PlayerEvent::FilterExplicitContentChanged { filter }); @@ -2536,6 +2548,10 @@ impl fmt::Debug for PlayerCommand { .debug_tuple("EmitAutoPlayChangedEvent") .field(&auto_play) .finish(), + PlayerCommand::EmitAddedToQueueEvent(track_id) => f + .debug_tuple("EmitAddedToQueueEvent") + .field(&track_id) + .finish(), } } } diff --git a/src/player_event_handler.rs b/src/player_event_handler.rs index e39a94a22..fccafe4c7 100644 --- a/src/player_event_handler.rs +++ b/src/player_event_handler.rs @@ -27,6 +27,10 @@ impl EventHandler { .insert("PLAYER_EVENT", "play_request_id_changed".to_string()); env_vars.insert("PLAY_REQUEST_ID", play_request_id.to_string()); } + PlayerEvent::AddedToQueue { track_id } => { + env_vars.insert("PLAYER_EVENT", "added_to_queue".to_string()); + env_vars.insert("TRACK_ID", track_id.to_id()); + } PlayerEvent::TrackChanged { audio_item } => { let id = audio_item.track_id.to_id(); env_vars.insert("PLAYER_EVENT", "track_changed".to_string());