Skip to content
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Comment thread
photovoltex marked this conversation as resolved.

### Changed

Expand Down
65 changes: 55 additions & 10 deletions connect/src/spirc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand Down Expand Up @@ -95,6 +95,8 @@ struct SpircTask {

context_resolver: ContextResolver,

emit_set_queue_events: bool,

shutdown: bool,
session: Session,

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -249,6 +252,8 @@ impl Spirc {

context_resolver: ContextResolver::new(session.clone()),

emit_set_queue_events,

shutdown: false,
session,

Expand Down Expand Up @@ -636,10 +641,51 @@ 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 context_uri = self.connect_state.context_uri().clone();
Comment thread
photovoltex marked this conversation as resolved.
Outdated
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();

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()
Expand Down Expand Up @@ -1084,13 +1130,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)?
Expand Down Expand Up @@ -1460,6 +1506,8 @@ impl SpircTask {
.connect_state
.update_context(ctx, ContextType::Default)?;

self.emit_set_queue_event();

Ok(())
}

Expand Down Expand Up @@ -1598,11 +1646,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) {
Expand Down
3 changes: 3 additions & 0 deletions connect/src/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -101,6 +103,7 @@ impl Default for ConnectConfig {
initial_volume: u16::MAX / 2,
disable_volume: false,
volume_steps: 64,
emit_set_queue_events: false,
}
}
}
Expand Down
66 changes: 54 additions & 12 deletions playback/src/player.rs
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,19 @@ enum PlayerCommand {
track: bool,
},
EmitAutoPlayChangedEvent(bool),
EmitAddedToQueueEvent(SpotifyUri),
EmitSetQueueEvent {
context_uri: String,
current_track: Option<QueueTrack>,
next_tracks: Vec<QueueTrack>,
prev_tracks: Vec<QueueTrack>,
},
}

/// 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)]
Expand All @@ -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,
Expand Down Expand Up @@ -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<QueueTrack>,
next_tracks: Vec<QueueTrack>,
prev_tracks: Vec<QueueTrack>,
},
}

impl PlayerEvent {
Expand Down Expand Up @@ -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<QueueTrack>,
next_tracks: Vec<QueueTrack>,
prev_tracks: Vec<QueueTrack>,
) {
self.command(PlayerCommand::EmitSetQueueEvent {
context_uri,
current_track,
next_tracks,
prev_tracks,
});
}
}

Expand Down Expand Up @@ -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 });
Expand Down Expand Up @@ -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(),
}
}
Expand Down
1 change: 1 addition & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1534,6 +1534,7 @@ async fn get_setup() -> Setup {
initial_volume,
disable_volume,
volume_steps,
..ConnectConfig::default()
Comment thread
ralph marked this conversation as resolved.
Outdated
}
};

Expand Down
35 changes: 31 additions & 4 deletions src/player_event_handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand Down Expand Up @@ -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::<Vec<String>>()
.join("\n"),
);
env_vars.insert(
"PREV_TRACKS",
prev_tracks
.into_iter()
.map(|t| format!("{}\t{}", t.uri, t.provider))
.collect::<Vec<String>>()
.join("\n"),
);
}
// Ignore event irrelevant for standalone binary like PositionChanged
_ => {}
}
Expand Down