From 065ca870187487ec053c22d5b639053053d67a7b Mon Sep 17 00:00:00 2001 From: Ralph von der Heyden Date: Thu, 22 Jan 2026 17:54:37 +0100 Subject: [PATCH 01/12] Emit `Player::SetQueue` event --- connect/src/spirc.rs | 64 ++++++++++++++++++++++++++++++++++++- playback/src/player.rs | 51 +++++++++++++++++++++++++++++ src/player_event_handler.rs | 29 +++++++++++++++++ 3 files changed, 143 insertions(+), 1 deletion(-) diff --git a/connect/src/spirc.rs b/connect/src/spirc.rs index f2c302b5d..7b7f4bb4e 100644 --- a/connect/src/spirc.rs +++ b/connect/src/spirc.rs @@ -636,10 +636,41 @@ 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) { + let context_uri = self.connect_state.context_uri().clone(); + let state_player = self.connect_state.player(); + + let current_track = state_player + .track + .as_ref() + .map(|t| (t.uri.clone(), t.provider.clone())); + + let next_tracks: Vec<_> = state_player + .next_tracks + .iter() + .map(|t| (t.uri.clone(), t.provider.clone())) + .collect(); + + let prev_tracks: Vec<_> = state_player + .prev_tracks + .iter() + .map(|t| (t.uri.clone(), t.provider.clone())) + .collect(); + + 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() @@ -1090,7 +1121,36 @@ impl SpircTask { self.player.emit_added_to_queue_event(uri); } } - SetQueue(set_queue) => self.connect_state.handle_set_queue(set_queue), + SetQueue(set_queue) => { + // Extract track data before consuming set_queue + let context_uri = self.connect_state.context_uri().clone(); + let state_player = self.connect_state.player(); + + let current_track = state_player + .track + .as_ref() + .map(|t| (t.uri.clone(), t.provider.clone())); + + let next_tracks: Vec<(String, String)> = set_queue + .next_tracks + .iter() + .map(|t| (t.uri.clone(), t.provider.clone())) + .collect(); + + let prev_tracks: Vec<(String, String)> = set_queue + .prev_tracks + .iter() + .map(|t| (t.uri.clone(), t.provider.clone())) + .collect(); + + self.connect_state.handle_set_queue(set_queue); + self.player.emit_set_queue_event( + context_uri, + current_track, + next_tracks, + prev_tracks, + ); + } SetOptions(set_options) => { if let Some(repeat_context) = set_options.repeating_context { self.handle_repeat_context(repeat_context)? @@ -1460,6 +1520,8 @@ impl SpircTask { .connect_state .update_context(ctx, ContextType::Default)?; + self.emit_set_queue_event(); + Ok(()) } diff --git a/playback/src/player.rs b/playback/src/player.rs index 95d49e8c2..504c456bc 100644 --- a/playback/src/player.rs +++ b/playback/src/player.rs @@ -139,6 +139,12 @@ enum PlayerCommand { }, EmitAutoPlayChangedEvent(bool), EmitAddedToQueueEvent(SpotifyUri), + EmitSetQueueEvent { + context_uri: String, + current_track: Option<(String, String)>, // (uri, provider) + next_tracks: Vec<(String, String)>, // (uri, provider) + prev_tracks: Vec<(String, String)>, // (uri, provider) + }, } #[derive(Debug, Clone)] @@ -252,6 +258,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<(String, String)>, // (uri, provider) + next_tracks: Vec<(String, String)>, // (uri, provider) + prev_tracks: Vec<(String, String)>, // (uri, provider) + }, } impl PlayerEvent { @@ -655,6 +668,21 @@ impl Player { 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<(String, String)>, + next_tracks: Vec<(String, String)>, + prev_tracks: Vec<(String, String)>, + ) { + self.command(PlayerCommand::EmitSetQueueEvent { + context_uri, + current_track, + next_tracks, + prev_tracks, + }); + } } impl Drop for Player { @@ -2347,6 +2375,18 @@ impl PlayerInternal { 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 }); @@ -2552,6 +2592,17 @@ impl fmt::Debug for PlayerCommand { .debug_tuple("EmitAddedToQueueEvent") .field(&track_id) .finish(), + 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/player_event_handler.rs b/src/player_event_handler.rs index fccafe4c7..bd7dc515d 100644 --- a/src/player_event_handler.rs +++ b/src/player_event_handler.rs @@ -234,6 +234,35 @@ 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((uri, provider)) = current_track { + env_vars + .insert("CURRENT_TRACK", format!("{uri}\t{provider}")); + } + env_vars.insert( + "NEXT_TRACKS", + next_tracks + .into_iter() + .map(|(uri, provider)| format!("{uri}\t{provider}")) + .collect::>() + .join("\n"), + ); + env_vars.insert( + "PREV_TRACKS", + prev_tracks + .into_iter() + .map(|(uri, provider)| format!("{uri}\t{provider}")) + .collect::>() + .join("\n"), + ); + } // Ignore event irrelevant for standalone binary like PositionChanged _ => {} } From d0dd9a7184696b088d8822a338ca194735c5b1ad Mon Sep 17 00:00:00 2001 From: Ralph von der Heyden Date: Thu, 22 Jan 2026 17:56:06 +0100 Subject: [PATCH 02/12] Remve AddToQueue, switch to SetQueue --- connect/src/spirc.rs | 10 ++-------- playback/src/player.rs | 16 ---------------- src/player_event_handler.rs | 4 ---- 3 files changed, 2 insertions(+), 28 deletions(-) diff --git a/connect/src/spirc.rs b/connect/src/spirc.rs index 7b7f4bb4e..4f76cd497 100644 --- a/connect/src/spirc.rs +++ b/connect/src/spirc.rs @@ -1115,11 +1115,8 @@ 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) => { // Extract track data before consuming set_queue @@ -1660,10 +1657,7 @@ 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(); } } diff --git a/playback/src/player.rs b/playback/src/player.rs index 504c456bc..bb8f4077b 100644 --- a/playback/src/player.rs +++ b/playback/src/player.rs @@ -138,7 +138,6 @@ enum PlayerCommand { track: bool, }, EmitAutoPlayChangedEvent(bool), - EmitAddedToQueueEvent(SpotifyUri), EmitSetQueueEvent { context_uri: String, current_track: Option<(String, String)>, // (uri, provider) @@ -153,9 +152,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, @@ -665,10 +661,6 @@ 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, @@ -2371,10 +2363,6 @@ 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, @@ -2588,10 +2576,6 @@ impl fmt::Debug for PlayerCommand { .debug_tuple("EmitAutoPlayChangedEvent") .field(&auto_play) .finish(), - PlayerCommand::EmitAddedToQueueEvent(track_id) => f - .debug_tuple("EmitAddedToQueueEvent") - .field(&track_id) - .finish(), PlayerCommand::EmitSetQueueEvent { context_uri, next_tracks, diff --git a/src/player_event_handler.rs b/src/player_event_handler.rs index bd7dc515d..3d085e4f8 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()); From 6a07e0c40f413f013c7307cb7dd469c31f96f867 Mon Sep 17 00:00:00 2001 From: Ralph von der Heyden Date: Thu, 22 Jan 2026 18:12:12 +0100 Subject: [PATCH 03/12] Add CHANGELOG, reformat --- CHANGELOG.md | 2 +- src/player_event_handler.rs | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 65a2b888c..349559529 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) ### Changed diff --git a/src/player_event_handler.rs b/src/player_event_handler.rs index 3d085e4f8..164aa0d0c 100644 --- a/src/player_event_handler.rs +++ b/src/player_event_handler.rs @@ -239,8 +239,7 @@ impl EventHandler { env_vars.insert("PLAYER_EVENT", "set_queue".to_string()); env_vars.insert("CONTEXT_URI", context_uri); if let Some((uri, provider)) = current_track { - env_vars - .insert("CURRENT_TRACK", format!("{uri}\t{provider}")); + env_vars.insert("CURRENT_TRACK", format!("{uri}\t{provider}")); } env_vars.insert( "NEXT_TRACKS", From 22c7bb37d5d38ee7e6b6d69c652adb513ead5324 Mon Sep 17 00:00:00 2001 From: Ralph von der Heyden <> Date: Thu, 22 Jan 2026 21:14:34 +0100 Subject: [PATCH 04/12] Emit `SetQueue` once when a track collection is added --- connect/src/spirc.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/connect/src/spirc.rs b/connect/src/spirc.rs index 4f76cd497..707ed1b52 100644 --- a/connect/src/spirc.rs +++ b/connect/src/spirc.rs @@ -1657,8 +1657,8 @@ impl SpircTask { ..Default::default() }; self.connect_state.add_to_queue(track, true); - self.emit_set_queue_event(); } + self.emit_set_queue_event(); } fn handle_preload_next_track(&mut self) { From 8042798ef9128fe0981b20f1d93c78c2b57d1a55 Mon Sep 17 00:00:00 2001 From: Ralph von der Heyden Date: Thu, 22 Jan 2026 21:19:27 +0100 Subject: [PATCH 05/12] Clarify wording Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- connect/src/spirc.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/connect/src/spirc.rs b/connect/src/spirc.rs index 707ed1b52..614b33242 100644 --- a/connect/src/spirc.rs +++ b/connect/src/spirc.rs @@ -1119,7 +1119,7 @@ impl SpircTask { self.emit_set_queue_event(); } SetQueue(set_queue) => { - // Extract track data before consuming set_queue + // Extract current state and new queue data for the event before updating the state let context_uri = self.connect_state.context_uri().clone(); let state_player = self.connect_state.player(); From 32b00004dee1cf8d2fec9230bb4f554d526c63dd Mon Sep 17 00:00:00 2001 From: Ralph von der Heyden Date: Sun, 25 Jan 2026 14:51:54 +0100 Subject: [PATCH 06/12] Just call `emit_set_queue_event()` instead of duplicating --- connect/src/spirc.rs | 28 +--------------------------- 1 file changed, 1 insertion(+), 27 deletions(-) diff --git a/connect/src/spirc.rs b/connect/src/spirc.rs index 614b33242..62bde38b4 100644 --- a/connect/src/spirc.rs +++ b/connect/src/spirc.rs @@ -1119,34 +1119,8 @@ impl SpircTask { self.emit_set_queue_event(); } SetQueue(set_queue) => { - // Extract current state and new queue data for the event before updating the state - let context_uri = self.connect_state.context_uri().clone(); - let state_player = self.connect_state.player(); - - let current_track = state_player - .track - .as_ref() - .map(|t| (t.uri.clone(), t.provider.clone())); - - let next_tracks: Vec<(String, String)> = set_queue - .next_tracks - .iter() - .map(|t| (t.uri.clone(), t.provider.clone())) - .collect(); - - let prev_tracks: Vec<(String, String)> = set_queue - .prev_tracks - .iter() - .map(|t| (t.uri.clone(), t.provider.clone())) - .collect(); - self.connect_state.handle_set_queue(set_queue); - self.player.emit_set_queue_event( - context_uri, - current_track, - next_tracks, - prev_tracks, - ); + self.emit_set_queue_event(); } SetOptions(set_options) => { if let Some(repeat_context) = set_options.repeating_context { From 64edaf842c3709984502fcc1765b939198013577 Mon Sep 17 00:00:00 2001 From: Ralph von der Heyden <> Date: Wed, 28 Jan 2026 20:49:56 +0100 Subject: [PATCH 07/12] Use QueueTrack struct instead of `(String, String)` --- connect/src/spirc.rs | 20 +++++++++++++------- playback/src/player.rs | 25 ++++++++++++++++--------- src/player_event_handler.rs | 11 +++++++---- 3 files changed, 36 insertions(+), 20 deletions(-) diff --git a/connect/src/spirc.rs b/connect/src/spirc.rs index 62bde38b4..3285f6244 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}, @@ -650,21 +650,27 @@ impl SpircTask { let context_uri = self.connect_state.context_uri().clone(); let state_player = self.connect_state.player(); - let current_track = state_player - .track - .as_ref() - .map(|t| (t.uri.clone(), t.provider.clone())); + 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| (t.uri.clone(), t.provider.clone())) + .map(|t| QueueTrack { + uri: t.uri.clone(), + provider: t.provider.clone(), + }) .collect(); let prev_tracks: Vec<_> = state_player .prev_tracks .iter() - .map(|t| (t.uri.clone(), t.provider.clone())) + .map(|t| QueueTrack { + uri: t.uri.clone(), + provider: t.provider.clone(), + }) .collect(); self.player diff --git a/playback/src/player.rs b/playback/src/player.rs index bb8f4077b..f3d803d51 100644 --- a/playback/src/player.rs +++ b/playback/src/player.rs @@ -140,12 +140,19 @@ enum PlayerCommand { EmitAutoPlayChangedEvent(bool), EmitSetQueueEvent { context_uri: String, - current_track: Option<(String, String)>, // (uri, provider) - next_tracks: Vec<(String, String)>, // (uri, provider) - prev_tracks: Vec<(String, String)>, // (uri, provider) + 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)] pub enum PlayerEvent { // Play request id changed @@ -257,9 +264,9 @@ pub enum PlayerEvent { /// Fired when the queue is set or context is loaded with its track list. SetQueue { context_uri: String, - current_track: Option<(String, String)>, // (uri, provider) - next_tracks: Vec<(String, String)>, // (uri, provider) - prev_tracks: Vec<(String, String)>, // (uri, provider) + current_track: Option, + next_tracks: Vec, + prev_tracks: Vec, }, } @@ -664,9 +671,9 @@ impl Player { pub fn emit_set_queue_event( &self, context_uri: String, - current_track: Option<(String, String)>, - next_tracks: Vec<(String, String)>, - prev_tracks: Vec<(String, String)>, + current_track: Option, + next_tracks: Vec, + prev_tracks: Vec, ) { self.command(PlayerCommand::EmitSetQueueEvent { context_uri, diff --git a/src/player_event_handler.rs b/src/player_event_handler.rs index 164aa0d0c..9ca2dddce 100644 --- a/src/player_event_handler.rs +++ b/src/player_event_handler.rs @@ -238,14 +238,17 @@ impl EventHandler { } => { env_vars.insert("PLAYER_EVENT", "set_queue".to_string()); env_vars.insert("CONTEXT_URI", context_uri); - if let Some((uri, provider)) = current_track { - env_vars.insert("CURRENT_TRACK", format!("{uri}\t{provider}")); + 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(|(uri, provider)| format!("{uri}\t{provider}")) + .map(|t| format!("{}\t{}", t.uri, t.provider)) .collect::>() .join("\n"), ); @@ -253,7 +256,7 @@ impl EventHandler { "PREV_TRACKS", prev_tracks .into_iter() - .map(|(uri, provider)| format!("{uri}\t{provider}")) + .map(|t| format!("{}\t{}", t.uri, t.provider)) .collect::>() .join("\n"), ); From 5bf1c5b7eca080e1f8cf801a625af04821444eac Mon Sep 17 00:00:00 2001 From: Ralph von der Heyden <> Date: Fri, 30 Jan 2026 10:37:53 +0100 Subject: [PATCH 08/12] Make SetQueue an opt-in event Other expensive events might be added to `OptInPlayerEvents` in the future. --- CHANGELOG.md | 2 +- playback/src/player.rs | 61 +++++++++++++++++++++++++++++++++++++----- src/main.rs | 5 ++-- 3 files changed, 58 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 349559529..3cd112193 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 `SetQueue` player event, emitting when the queue changes (context loaded, track added to queue, or queue set via Spotify Connect) +- [playback] Add `SetQueue` player event (opt-in), emitting when the queue changes (context loaded, track added to queue, or queue set via Spotify Connect) ### Changed diff --git a/playback/src/player.rs b/playback/src/player.rs index f3d803d51..ac596f3f6 100644 --- a/playback/src/player.rs +++ b/playback/src/player.rs @@ -80,7 +80,7 @@ struct PlayerInternal { sink_status: SinkStatus, sink_event_callback: Option, volume_getter: Box, - event_senders: Vec>, + event_senders: Vec, converter: Converter, normalisation_integrators: [f64; 2], @@ -113,7 +113,10 @@ enum PlayerCommand { Stop, Seek(u32), SetSession(Session), - AddEventSender(mpsc::UnboundedSender), + AddEventSender { + sender: mpsc::UnboundedSender, + opt_in_events: OptInPlayerEvents, + }, SetSinkEventCallback(Option), EmitVolumeChangedEvent(u16), SetAutoNormaliseAsAlbum(bool), @@ -153,6 +156,20 @@ pub struct QueueTrack { pub provider: String, } +/// Opt-in player events that are not delivered to subscribers by default. +/// +/// Use [`Player::get_player_event_channel_with`] to subscribe to these. +#[derive(Clone, Copy, Debug, Default)] +pub struct OptInPlayerEvents { + /// Subscribe to [`PlayerEvent::SetQueue`] events. + pub set_queue: bool, +} + +struct EventSubscriber { + sender: mpsc::UnboundedSender, + opt_in_events: OptInPlayerEvents, +} + #[derive(Debug, Clone)] pub enum PlayerEvent { // Play request id changed @@ -304,6 +321,14 @@ impl PlayerEvent { _ => None, } } + + /// Whether this event should be sent to a subscriber with the given opt-in events. + fn should_send(&self, opt_in: &OptInPlayerEvents) -> bool { + match self { + PlayerEvent::SetQueue { .. } => opt_in.set_queue, + _ => true, + } + } } pub type PlayerEventChannel = mpsc::UnboundedReceiver; @@ -594,8 +619,18 @@ impl Player { } pub fn get_player_event_channel(&self) -> PlayerEventChannel { + self.get_player_event_channel_with(OptInPlayerEvents::default()) + } + + pub fn get_player_event_channel_with( + &self, + opt_in_events: OptInPlayerEvents, + ) -> PlayerEventChannel { let (event_sender, event_receiver) = mpsc::unbounded_channel(); - self.command(PlayerCommand::AddEventSender(event_sender)); + self.command(PlayerCommand::AddEventSender { + sender: event_sender, + opt_in_events, + }); event_receiver } @@ -2318,7 +2353,15 @@ impl PlayerInternal { PlayerCommand::SetSession(session) => self.session = session, - PlayerCommand::AddEventSender(sender) => self.event_senders.push(sender), + PlayerCommand::AddEventSender { + sender, + opt_in_events, + } => { + self.event_senders.push(EventSubscriber { + sender, + opt_in_events, + }); + } PlayerCommand::SetSinkEventCallback(callback) => self.sink_event_callback = callback, @@ -2419,8 +2462,12 @@ impl PlayerInternal { } fn send_event(&mut self, event: PlayerEvent) { - self.event_senders - .retain(|sender| sender.send(event.clone()).is_ok()); + self.event_senders.retain(|sub| { + if !event.should_send(&sub.opt_in_events) { + return true; // keep subscriber, skip this event + } + sub.sender.send(event.clone()).is_ok() + }); } fn load_track( @@ -2526,7 +2573,7 @@ impl fmt::Debug for PlayerCommand { PlayerCommand::Stop => f.debug_tuple("Stop").finish(), PlayerCommand::Seek(position) => f.debug_tuple("Seek").field(&position).finish(), PlayerCommand::SetSession(_) => f.debug_tuple("SetSession").finish(), - PlayerCommand::AddEventSender(_) => f.debug_tuple("AddEventSender").finish(), + PlayerCommand::AddEventSender { .. } => f.debug_tuple("AddEventSender").finish(), PlayerCommand::SetSinkEventCallback(_) => { f.debug_tuple("SetSinkEventCallback").finish() } diff --git a/src/main.rs b/src/main.rs index 7e7209e1b..75a8b507d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -16,7 +16,7 @@ use librespot::{ }, dither, mixer::{self, MixerConfig, MixerFn}, - player::{Player, coefficient_to_duration, duration_to_coefficient}, + player::{OptInPlayerEvents, Player, coefficient_to_duration, duration_to_coefficient}, }, }; use librespot_oauth::OAuthClientBuilder; @@ -1994,8 +1994,9 @@ async fn main() { }); if let Some(player_event_program) = setup.player_event_program.clone() { + let opt_in_events = OptInPlayerEvents { set_queue: true }; _event_handler = Some(EventHandler::new( - player.get_player_event_channel(), + player.get_player_event_channel_with(opt_in_events), &player_event_program, )); From 2a5f698502032d9d2488000a4de9177afa7c3553 Mon Sep 17 00:00:00 2001 From: Ralph von der Heyden Date: Wed, 4 Feb 2026 11:41:52 +0100 Subject: [PATCH 09/12] Refactor to use `ConnectConfig` for opting in to `SetQueue` events --- CHANGELOG.md | 2 +- connect/src/spirc.rs | 9 +++++++ connect/src/state.rs | 3 +++ playback/src/player.rs | 61 +++++------------------------------------- src/main.rs | 6 ++--- 5 files changed, 23 insertions(+), 58 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3cd112193..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 `SetQueue` player event (opt-in), emitting when the queue changes (context loaded, track added to queue, or queue set via Spotify Connect) +- [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 3285f6244..6ab3347d0 100644 --- a/connect/src/spirc.rs +++ b/connect/src/spirc.rs @@ -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, @@ -647,6 +652,10 @@ impl SpircTask { /// Emit set queue event via PlayerEvent fn emit_set_queue_event(&self) { + if !self.emit_set_queue_events { + return; + } + let context_uri = self.connect_state.context_uri().clone(); let state_player = self.connect_state.player(); 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 ac596f3f6..f3d803d51 100644 --- a/playback/src/player.rs +++ b/playback/src/player.rs @@ -80,7 +80,7 @@ struct PlayerInternal { sink_status: SinkStatus, sink_event_callback: Option, volume_getter: Box, - event_senders: Vec, + event_senders: Vec>, converter: Converter, normalisation_integrators: [f64; 2], @@ -113,10 +113,7 @@ enum PlayerCommand { Stop, Seek(u32), SetSession(Session), - AddEventSender { - sender: mpsc::UnboundedSender, - opt_in_events: OptInPlayerEvents, - }, + AddEventSender(mpsc::UnboundedSender), SetSinkEventCallback(Option), EmitVolumeChangedEvent(u16), SetAutoNormaliseAsAlbum(bool), @@ -156,20 +153,6 @@ pub struct QueueTrack { pub provider: String, } -/// Opt-in player events that are not delivered to subscribers by default. -/// -/// Use [`Player::get_player_event_channel_with`] to subscribe to these. -#[derive(Clone, Copy, Debug, Default)] -pub struct OptInPlayerEvents { - /// Subscribe to [`PlayerEvent::SetQueue`] events. - pub set_queue: bool, -} - -struct EventSubscriber { - sender: mpsc::UnboundedSender, - opt_in_events: OptInPlayerEvents, -} - #[derive(Debug, Clone)] pub enum PlayerEvent { // Play request id changed @@ -321,14 +304,6 @@ impl PlayerEvent { _ => None, } } - - /// Whether this event should be sent to a subscriber with the given opt-in events. - fn should_send(&self, opt_in: &OptInPlayerEvents) -> bool { - match self { - PlayerEvent::SetQueue { .. } => opt_in.set_queue, - _ => true, - } - } } pub type PlayerEventChannel = mpsc::UnboundedReceiver; @@ -619,18 +594,8 @@ impl Player { } pub fn get_player_event_channel(&self) -> PlayerEventChannel { - self.get_player_event_channel_with(OptInPlayerEvents::default()) - } - - pub fn get_player_event_channel_with( - &self, - opt_in_events: OptInPlayerEvents, - ) -> PlayerEventChannel { let (event_sender, event_receiver) = mpsc::unbounded_channel(); - self.command(PlayerCommand::AddEventSender { - sender: event_sender, - opt_in_events, - }); + self.command(PlayerCommand::AddEventSender(event_sender)); event_receiver } @@ -2353,15 +2318,7 @@ impl PlayerInternal { PlayerCommand::SetSession(session) => self.session = session, - PlayerCommand::AddEventSender { - sender, - opt_in_events, - } => { - self.event_senders.push(EventSubscriber { - sender, - opt_in_events, - }); - } + PlayerCommand::AddEventSender(sender) => self.event_senders.push(sender), PlayerCommand::SetSinkEventCallback(callback) => self.sink_event_callback = callback, @@ -2462,12 +2419,8 @@ impl PlayerInternal { } fn send_event(&mut self, event: PlayerEvent) { - self.event_senders.retain(|sub| { - if !event.should_send(&sub.opt_in_events) { - return true; // keep subscriber, skip this event - } - sub.sender.send(event.clone()).is_ok() - }); + self.event_senders + .retain(|sender| sender.send(event.clone()).is_ok()); } fn load_track( @@ -2573,7 +2526,7 @@ impl fmt::Debug for PlayerCommand { PlayerCommand::Stop => f.debug_tuple("Stop").finish(), PlayerCommand::Seek(position) => f.debug_tuple("Seek").field(&position).finish(), PlayerCommand::SetSession(_) => f.debug_tuple("SetSession").finish(), - PlayerCommand::AddEventSender { .. } => f.debug_tuple("AddEventSender").finish(), + PlayerCommand::AddEventSender(_) => f.debug_tuple("AddEventSender").finish(), PlayerCommand::SetSinkEventCallback(_) => { f.debug_tuple("SetSinkEventCallback").finish() } diff --git a/src/main.rs b/src/main.rs index 75a8b507d..74514c0eb 100644 --- a/src/main.rs +++ b/src/main.rs @@ -16,7 +16,7 @@ use librespot::{ }, dither, mixer::{self, MixerConfig, MixerFn}, - player::{OptInPlayerEvents, Player, coefficient_to_duration, duration_to_coefficient}, + player::{Player, coefficient_to_duration, duration_to_coefficient}, }, }; use librespot_oauth::OAuthClientBuilder; @@ -1534,6 +1534,7 @@ async fn get_setup() -> Setup { initial_volume, disable_volume, volume_steps, + ..ConnectConfig::default() } }; @@ -1994,9 +1995,8 @@ async fn main() { }); if let Some(player_event_program) = setup.player_event_program.clone() { - let opt_in_events = OptInPlayerEvents { set_queue: true }; _event_handler = Some(EventHandler::new( - player.get_player_event_channel_with(opt_in_events), + player.get_player_event_channel(), &player_event_program, )); From 8fb664f9068df9a5f30f4899916d1c07fac71ea1 Mon Sep 17 00:00:00 2001 From: Ralph von der Heyden Date: Mon, 9 Feb 2026 20:06:24 +0100 Subject: [PATCH 10/12] Use explicit initializer value Co-authored-by: Felix Prillwitz --- src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main.rs b/src/main.rs index 74514c0eb..c9824feff 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1534,7 +1534,7 @@ async fn get_setup() -> Setup { initial_volume, disable_volume, volume_steps, - ..ConnectConfig::default() + emit_set_queue_events: false } }; From cf7c22f4f0ecfa4774c7f2090361a28181ebc033 Mon Sep 17 00:00:00 2001 From: Ralph von der Heyden Date: Mon, 9 Feb 2026 20:02:18 +0100 Subject: [PATCH 11/12] Move variable closer to usage --- connect/src/spirc.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/connect/src/spirc.rs b/connect/src/spirc.rs index 6ab3347d0..fec6057c4 100644 --- a/connect/src/spirc.rs +++ b/connect/src/spirc.rs @@ -656,7 +656,6 @@ impl SpircTask { return; } - let context_uri = self.connect_state.context_uri().clone(); let state_player = self.connect_state.player(); let current_track = state_player.track.as_ref().map(|t| QueueTrack { @@ -682,6 +681,8 @@ impl SpircTask { }) .collect(); + let context_uri = self.connect_state.context_uri().clone(); + self.player .emit_set_queue_event(context_uri, current_track, next_tracks, prev_tracks); } From 94497167568894bd91502021c09197813244cb2d Mon Sep 17 00:00:00 2001 From: Ralph von der Heyden Date: Mon, 9 Feb 2026 20:24:40 +0100 Subject: [PATCH 12/12] Fix formatting --- src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main.rs b/src/main.rs index c9824feff..16ba1946f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1534,7 +1534,7 @@ async fn get_setup() -> Setup { initial_volume, disable_volume, volume_steps, - emit_set_queue_events: false + emit_set_queue_events: false, } };