diff --git a/CHANGELOG.md b/CHANGELOG.md index 65a2b888c..0e74f1e22 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### 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` +- [playback] Add `SetQueue` player event, emitting when the queue changes (context loaded, track added to queue, or queue set via Spotify Connect). Gated behind `ConnectConfig::emit_set_queue_events` ### Changed diff --git a/connect/src/spirc.rs b/connect/src/spirc.rs index f2c302b5d..fec6057c4 100644 --- a/connect/src/spirc.rs +++ b/connect/src/spirc.rs @@ -14,7 +14,7 @@ use crate::{ model::{LoadRequest, PlayingTrack, SpircPlayStatus}, playback::{ mixer::Mixer, - player::{Player, PlayerEvent, PlayerEventChannel}, + player::{Player, PlayerEvent, PlayerEventChannel, QueueTrack}, }, protocol::{ connect::{Cluster, ClusterUpdate, LogoutCommand, SetVolumeCommand}, @@ -95,6 +95,8 @@ struct SpircTask { context_resolver: ContextResolver, + emit_set_queue_events: bool, + shutdown: bool, session: Session, @@ -173,6 +175,7 @@ impl Spirc { let spirc_id = SPIRC_COUNTER.fetch_add(1, Ordering::AcqRel); debug!("new Spirc[{spirc_id}]"); + let emit_set_queue_events = config.emit_set_queue_events; let connect_state = ConnectState::new(config, &session); let connection_id_update = session @@ -249,6 +252,8 @@ impl Spirc { context_resolver: ContextResolver::new(session.clone()), + emit_set_queue_events, + shutdown: false, session, @@ -636,10 +641,52 @@ impl SpircTask { false }; + // Fire set queue event if context was successfully loaded + if update_state { + self.emit_set_queue_event(); + } + self.context_resolver.remove_used_and_invalid(); update_state } + /// Emit set queue event via PlayerEvent + fn emit_set_queue_event(&self) { + if !self.emit_set_queue_events { + return; + } + + let state_player = self.connect_state.player(); + + let current_track = state_player.track.as_ref().map(|t| QueueTrack { + uri: t.uri.clone(), + provider: t.provider.clone(), + }); + + let next_tracks: Vec<_> = state_player + .next_tracks + .iter() + .map(|t| QueueTrack { + uri: t.uri.clone(), + provider: t.provider.clone(), + }) + .collect(); + + let prev_tracks: Vec<_> = state_player + .prev_tracks + .iter() + .map(|t| QueueTrack { + uri: t.uri.clone(), + provider: t.provider.clone(), + }) + .collect(); + + let context_uri = self.connect_state.context_uri().clone(); + + self.player + .emit_set_queue_event(context_uri, current_track, next_tracks, prev_tracks); + } + // todo: is the time_delta still necessary? fn now_ms(&self) -> i64 { let dur = SystemTime::now() @@ -1084,13 +1131,13 @@ impl SpircTask { } SetRepeatingTrack(repeat_track) => self.handle_repeat_track(repeat_track.value), 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); - } + self.emit_set_queue_event(); + } + SetQueue(set_queue) => { + self.connect_state.handle_set_queue(set_queue); + self.emit_set_queue_event(); } - SetQueue(set_queue) => self.connect_state.handle_set_queue(set_queue), SetOptions(set_options) => { if let Some(repeat_context) = set_options.repeating_context { self.handle_repeat_context(repeat_context)? @@ -1460,6 +1507,8 @@ impl SpircTask { .connect_state .update_context(ctx, ContextType::Default)?; + self.emit_set_queue_event(); + Ok(()) } @@ -1598,11 +1647,8 @@ impl SpircTask { ..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); - } } + self.emit_set_queue_event(); } fn handle_preload_next_track(&mut self) { diff --git a/connect/src/state.rs b/connect/src/state.rs index b4ee61b2f..f810d8518 100644 --- a/connect/src/state.rs +++ b/connect/src/state.rs @@ -90,6 +90,8 @@ pub struct ConnectConfig { pub disable_volume: bool, /// Number of incremental steps (default: 64) pub volume_steps: u16, + /// Emit `SetQueue` player events when the queue changes (default: false) + pub emit_set_queue_events: bool, } impl Default for ConnectConfig { @@ -101,6 +103,7 @@ impl Default for ConnectConfig { initial_volume: u16::MAX / 2, disable_volume: false, volume_steps: 64, + emit_set_queue_events: false, } } } diff --git a/playback/src/player.rs b/playback/src/player.rs index 95d49e8c2..f3d803d51 100644 --- a/playback/src/player.rs +++ b/playback/src/player.rs @@ -138,7 +138,19 @@ enum PlayerCommand { track: bool, }, EmitAutoPlayChangedEvent(bool), - EmitAddedToQueueEvent(SpotifyUri), + EmitSetQueueEvent { + context_uri: String, + current_track: Option, + next_tracks: Vec, + prev_tracks: Vec, + }, +} + +/// Represents a track in the queue with its URI and provider. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct QueueTrack { + pub uri: String, + pub provider: String, } #[derive(Debug, Clone)] @@ -147,9 +159,6 @@ 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, @@ -252,6 +261,13 @@ pub enum PlayerEvent { FilterExplicitContentChanged { filter: bool, }, + /// Fired when the queue is set or context is loaded with its track list. + SetQueue { + context_uri: String, + current_track: Option, + next_tracks: Vec, + prev_tracks: Vec, + }, } impl PlayerEvent { @@ -652,8 +668,19 @@ impl Player { self.command(PlayerCommand::EmitAutoPlayChangedEvent(auto_play)); } - pub fn emit_added_to_queue_event(&self, track_id: SpotifyUri) { - self.command(PlayerCommand::EmitAddedToQueueEvent(track_id)); + pub fn emit_set_queue_event( + &self, + context_uri: String, + current_track: Option, + next_tracks: Vec, + prev_tracks: Vec, + ) { + self.command(PlayerCommand::EmitSetQueueEvent { + context_uri, + current_track, + next_tracks, + prev_tracks, + }); } } @@ -2343,9 +2370,17 @@ impl PlayerInternal { self.auto_normalise_as_album = setting } - PlayerCommand::EmitAddedToQueueEvent(track_id) => { - self.send_event(PlayerEvent::AddedToQueue { track_id }) - } + PlayerCommand::EmitSetQueueEvent { + context_uri, + current_track, + next_tracks, + prev_tracks, + } => self.send_event(PlayerEvent::SetQueue { + context_uri, + current_track, + next_tracks, + prev_tracks, + }), PlayerCommand::EmitFilterExplicitContentChangedEvent(filter) => { self.send_event(PlayerEvent::FilterExplicitContentChanged { filter }); @@ -2548,9 +2583,16 @@ impl fmt::Debug for PlayerCommand { .debug_tuple("EmitAutoPlayChangedEvent") .field(&auto_play) .finish(), - PlayerCommand::EmitAddedToQueueEvent(track_id) => f - .debug_tuple("EmitAddedToQueueEvent") - .field(&track_id) + PlayerCommand::EmitSetQueueEvent { + context_uri, + next_tracks, + prev_tracks, + .. + } => f + .debug_tuple("EmitSetQueueEvent") + .field(&context_uri) + .field(&next_tracks.len()) + .field(&prev_tracks.len()) .finish(), } } diff --git a/src/main.rs b/src/main.rs index 7e7209e1b..16ba1946f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1534,6 +1534,7 @@ async fn get_setup() -> Setup { initial_volume, disable_volume, volume_steps, + emit_set_queue_events: false, } }; diff --git a/src/player_event_handler.rs b/src/player_event_handler.rs index fccafe4c7..9ca2dddce 100644 --- a/src/player_event_handler.rs +++ b/src/player_event_handler.rs @@ -27,10 +27,6 @@ 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()); @@ -234,6 +230,37 @@ impl EventHandler { ); env_vars.insert("FILTER", filter.to_string()); } + PlayerEvent::SetQueue { + context_uri, + current_track, + next_tracks, + prev_tracks, + } => { + env_vars.insert("PLAYER_EVENT", "set_queue".to_string()); + env_vars.insert("CONTEXT_URI", context_uri); + if let Some(track) = current_track { + env_vars.insert( + "CURRENT_TRACK", + format!("{}\t{}", track.uri, track.provider), + ); + } + env_vars.insert( + "NEXT_TRACKS", + next_tracks + .into_iter() + .map(|t| format!("{}\t{}", t.uri, t.provider)) + .collect::>() + .join("\n"), + ); + env_vars.insert( + "PREV_TRACKS", + prev_tracks + .into_iter() + .map(|t| format!("{}\t{}", t.uri, t.provider)) + .collect::>() + .join("\n"), + ); + } // Ignore event irrelevant for standalone binary like PositionChanged _ => {} }