Skip to content

Commit 5a411df

Browse files
committed
merge librespot:dev
2 parents ce33846 + 33bf3a7 commit 5a411df

9 files changed

Lines changed: 214 additions & 6 deletions

File tree

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Added
11+
12+
- [connect] Add method `add_to_queue` to `Spirc` to add tracks, episodes, albums and playlists to the queue
13+
- [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`
14+
1015
### Changed
1116

1217
- [core] Made `SpotifyId::to_base62`, `SpotifyId::to_base16`, `FileId::to_base16`, `SpotifyUri::to_id`, `SpotifyUri::to_uri` infallible (breaking)
1318

1419
### Fixed
1520

21+
- [audio] Fixed integer overflow in throughput calculation
1622
- [main] Fixed `--volume-ctrl fixed` not disabling volume control
1723
- [core] Fix default permissions on credentials file and warn user if file is world readable
1824
- [core] Try all resolved addresses for the dealer connection instead of failing after the first one.

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

audio/src/fetch/receive.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -120,8 +120,8 @@ async fn receive_data(
120120
if measure_throughput {
121121
let duration = Instant::now().duration_since(request_time).as_millis();
122122
if actual_length > 0 && duration > 0 {
123-
let throughput = ONE_SECOND.as_millis() as usize * actual_length / duration as usize;
124-
file_data_tx.send(ReceivedData::Throughput(throughput))?;
123+
let throughput = ONE_SECOND.as_millis() * actual_length as u128 / duration;
124+
file_data_tx.send(ReceivedData::Throughput(throughput as usize))?;
125125
}
126126
}
127127

connect/src/spirc.rs

Lines changed: 109 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,13 @@ use crate::{
1414
model::{LoadRequest, PlayingTrack, SpircPlayStatus},
1515
playback::{
1616
mixer::Mixer,
17-
player::{Player, PlayerEvent, PlayerEventChannel},
17+
player::{Player, PlayerEvent, PlayerEventChannel, QueueTrack},
1818
},
1919
protocol::{
2020
connect::{Cluster, ClusterUpdate, LogoutCommand, SetVolumeCommand},
2121
context::Context,
2222
explicit_content_pubsub::UserAttributesUpdate,
23+
player::ProvidedTrack,
2324
playlist4_external::PlaylistModificationInfo,
2425
social_connect_v2::SessionUpdate,
2526
transfer_state::TransferState,
@@ -94,6 +95,8 @@ struct SpircTask {
9495

9596
context_resolver: ContextResolver,
9697

98+
emit_set_queue_events: bool,
99+
97100
shutdown: bool,
98101
session: Session,
99102

@@ -132,6 +135,7 @@ enum SpircCommand {
132135
Activate,
133136
Transfer(Option<TransferRequest>),
134137
Load(LoadRequest),
138+
AddToQueue(SpotifyUri),
135139
}
136140

137141
const CONTEXT_FETCH_THRESHOLD: usize = 2;
@@ -171,6 +175,7 @@ impl Spirc {
171175
let spirc_id = SPIRC_COUNTER.fetch_add(1, Ordering::AcqRel);
172176
debug!("new Spirc[{spirc_id}]");
173177

178+
let emit_set_queue_events = config.emit_set_queue_events;
174179
let connect_state = ConnectState::new(config, &session);
175180

176181
let connection_id_update = session
@@ -247,6 +252,8 @@ impl Spirc {
247252

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

255+
emit_set_queue_events,
256+
250257
shutdown: false,
251258
session,
252259

@@ -388,6 +395,24 @@ impl Spirc {
388395
Ok(self.commands.send(SpircCommand::Load(command))?)
389396
}
390397

398+
/// Adds a track, episode, album or playlist to the queue.
399+
///
400+
/// Does nothing if we are not the active device.
401+
///
402+
/// For albums and playlists, all tracks/episodes are resolved and added to the queue.
403+
pub fn add_to_queue(&self, uri: SpotifyUri) -> Result<(), Error> {
404+
if !matches!(
405+
uri,
406+
SpotifyUri::Track { .. }
407+
| SpotifyUri::Episode { .. }
408+
| SpotifyUri::Album { .. }
409+
| SpotifyUri::Playlist { .. }
410+
) {
411+
return Err(Error::invalid_argument("uri"));
412+
}
413+
Ok(self.commands.send(SpircCommand::AddToQueue(uri))?)
414+
}
415+
391416
/// Disconnects the current device and pauses the playback according the value.
392417
///
393418
/// Does nothing if we are not the active device.
@@ -616,10 +641,52 @@ impl SpircTask {
616641
false
617642
};
618643

644+
// Fire set queue event if context was successfully loaded
645+
if update_state {
646+
self.emit_set_queue_event();
647+
}
648+
619649
self.context_resolver.remove_used_and_invalid();
620650
update_state
621651
}
622652

653+
/// Emit set queue event via PlayerEvent
654+
fn emit_set_queue_event(&self) {
655+
if !self.emit_set_queue_events {
656+
return;
657+
}
658+
659+
let state_player = self.connect_state.player();
660+
661+
let current_track = state_player.track.as_ref().map(|t| QueueTrack {
662+
uri: t.uri.clone(),
663+
provider: t.provider.clone(),
664+
});
665+
666+
let next_tracks: Vec<_> = state_player
667+
.next_tracks
668+
.iter()
669+
.map(|t| QueueTrack {
670+
uri: t.uri.clone(),
671+
provider: t.provider.clone(),
672+
})
673+
.collect();
674+
675+
let prev_tracks: Vec<_> = state_player
676+
.prev_tracks
677+
.iter()
678+
.map(|t| QueueTrack {
679+
uri: t.uri.clone(),
680+
provider: t.provider.clone(),
681+
})
682+
.collect();
683+
684+
let context_uri = self.connect_state.context_uri().clone();
685+
686+
self.player
687+
.emit_set_queue_event(context_uri, current_track, next_tracks, prev_tracks);
688+
}
689+
623690
// todo: is the time_delta still necessary?
624691
fn now_ms(&self) -> i64 {
625692
let dur = SystemTime::now()
@@ -679,6 +746,7 @@ impl SpircTask {
679746
SpircCommand::SetPosition(position) => self.handle_seek(position),
680747
SpircCommand::SetVolume(volume) => self.set_volume(volume),
681748
SpircCommand::Load(command) => self.handle_load(command, None, None).await?,
749+
SpircCommand::AddToQueue(uri) => self.handle_add_to_queue(uri).await,
682750
};
683751

684752
self.notify().await
@@ -1062,8 +1130,14 @@ impl SpircTask {
10621130
self.handle_repeat_context(repeat_context.value)?
10631131
}
10641132
SetRepeatingTrack(repeat_track) => self.handle_repeat_track(repeat_track.value),
1065-
AddToQueue(add_to_queue) => self.connect_state.add_to_queue(add_to_queue.track, true),
1066-
SetQueue(set_queue) => self.connect_state.handle_set_queue(set_queue),
1133+
AddToQueue(add_to_queue) => {
1134+
self.connect_state.add_to_queue(add_to_queue.track, true);
1135+
self.emit_set_queue_event();
1136+
}
1137+
SetQueue(set_queue) => {
1138+
self.connect_state.handle_set_queue(set_queue);
1139+
self.emit_set_queue_event();
1140+
}
10671141
SetOptions(set_options) => {
10681142
if let Some(repeat_context) = set_options.repeating_context {
10691143
self.handle_repeat_context(repeat_context)?
@@ -1433,6 +1507,8 @@ impl SpircTask {
14331507
.connect_state
14341508
.update_context(ctx, ContextType::Default)?;
14351509

1510+
self.emit_set_queue_event();
1511+
14361512
Ok(())
14371513
}
14381514

@@ -1545,6 +1621,36 @@ impl SpircTask {
15451621
self.connect_state.set_repeat_track(repeat);
15461622
}
15471623

1624+
async fn handle_add_to_queue(&mut self, uri: SpotifyUri) {
1625+
let track_uris: Vec<String> = match uri {
1626+
SpotifyUri::Track { .. } | SpotifyUri::Episode { .. } => vec![uri.to_uri()],
1627+
SpotifyUri::Album { .. } | SpotifyUri::Playlist { .. } => {
1628+
match self.session.spclient().get_context(&uri.to_uri()).await {
1629+
Ok(context) => context
1630+
.pages
1631+
.iter()
1632+
.flat_map(|page| page.tracks.iter())
1633+
.filter_map(|track| track.uri.clone())
1634+
.collect(),
1635+
Err(e) => {
1636+
error!("failed to resolve context for {}: {e}", uri.item_type());
1637+
return;
1638+
}
1639+
}
1640+
}
1641+
_ => return,
1642+
};
1643+
1644+
for track_uri in track_uris {
1645+
let track = ProvidedTrack {
1646+
uri: track_uri.clone(),
1647+
..Default::default()
1648+
};
1649+
self.connect_state.add_to_queue(track, true);
1650+
}
1651+
self.emit_set_queue_event();
1652+
}
1653+
15481654
fn handle_preload_next_track(&mut self) {
15491655
// Requests the player thread to preload the next track
15501656
match self.play_status {

connect/src/state.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,8 @@ pub struct ConnectConfig {
9090
pub disable_volume: bool,
9191
/// Number of incremental steps (default: 64)
9292
pub volume_steps: u16,
93+
/// Emit `SetQueue` player events when the queue changes (default: false)
94+
pub emit_set_queue_events: bool,
9395
}
9496

9597
impl Default for ConnectConfig {
@@ -101,6 +103,7 @@ impl Default for ConnectConfig {
101103
initial_volume: ConnectConfig::DEFAULT_INITIAL_VOLUME,
102104
disable_volume: false,
103105
volume_steps: ConnectConfig::DEFAULT_VOLUME_STEPS,
106+
emit_set_queue_events: false,
104107
}
105108
}
106109
}

core/Cargo.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,9 +113,11 @@ clap = { version = "4.5.54", default-features = false }
113113
[build-dependencies]
114114
rand = { version = "0.9", default-features = false, features = ["thread_rng"] }
115115
rand_distr = "0.5"
116-
vergen-gitcl = { version = "1.0", default-features = false, features = [
116+
vergen-gitcl = { version = "1.0.8", default-features = false, features = [
117117
"build",
118118
] }
119+
# fix for https://github.com/rustyhorde/vergen/issues/478#issuecomment-3769340357
120+
vergen = "=9.0.6"
119121

120122
[dev-dependencies]
121123
tokio = { version = "1", features = ["macros"] }

playback/src/player.rs

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,19 @@ enum PlayerCommand {
138138
track: bool,
139139
},
140140
EmitAutoPlayChangedEvent(bool),
141+
EmitSetQueueEvent {
142+
context_uri: String,
143+
current_track: Option<QueueTrack>,
144+
next_tracks: Vec<QueueTrack>,
145+
prev_tracks: Vec<QueueTrack>,
146+
},
147+
}
148+
149+
/// Represents a track in the queue with its URI and provider.
150+
#[derive(Clone, Debug, PartialEq, Eq)]
151+
pub struct QueueTrack {
152+
pub uri: String,
153+
pub provider: String,
141154
}
142155

143156
#[derive(Debug, Clone)]
@@ -248,6 +261,13 @@ pub enum PlayerEvent {
248261
FilterExplicitContentChanged {
249262
filter: bool,
250263
},
264+
/// Fired when the queue is set or context is loaded with its track list.
265+
SetQueue {
266+
context_uri: String,
267+
current_track: Option<QueueTrack>,
268+
next_tracks: Vec<QueueTrack>,
269+
prev_tracks: Vec<QueueTrack>,
270+
},
251271
}
252272

253273
impl PlayerEvent {
@@ -647,6 +667,21 @@ impl Player {
647667
pub fn emit_auto_play_changed_event(&self, auto_play: bool) {
648668
self.command(PlayerCommand::EmitAutoPlayChangedEvent(auto_play));
649669
}
670+
671+
pub fn emit_set_queue_event(
672+
&self,
673+
context_uri: String,
674+
current_track: Option<QueueTrack>,
675+
next_tracks: Vec<QueueTrack>,
676+
prev_tracks: Vec<QueueTrack>,
677+
) {
678+
self.command(PlayerCommand::EmitSetQueueEvent {
679+
context_uri,
680+
current_track,
681+
next_tracks,
682+
prev_tracks,
683+
});
684+
}
650685
}
651686

652687
impl Drop for Player {
@@ -2335,6 +2370,18 @@ impl PlayerInternal {
23352370
self.auto_normalise_as_album = setting
23362371
}
23372372

2373+
PlayerCommand::EmitSetQueueEvent {
2374+
context_uri,
2375+
current_track,
2376+
next_tracks,
2377+
prev_tracks,
2378+
} => self.send_event(PlayerEvent::SetQueue {
2379+
context_uri,
2380+
current_track,
2381+
next_tracks,
2382+
prev_tracks,
2383+
}),
2384+
23382385
PlayerCommand::EmitFilterExplicitContentChangedEvent(filter) => {
23392386
self.send_event(PlayerEvent::FilterExplicitContentChanged { filter });
23402387

@@ -2536,6 +2583,17 @@ impl fmt::Debug for PlayerCommand {
25362583
.debug_tuple("EmitAutoPlayChangedEvent")
25372584
.field(&auto_play)
25382585
.finish(),
2586+
PlayerCommand::EmitSetQueueEvent {
2587+
context_uri,
2588+
next_tracks,
2589+
prev_tracks,
2590+
..
2591+
} => f
2592+
.debug_tuple("EmitSetQueueEvent")
2593+
.field(&context_uri)
2594+
.field(&next_tracks.len())
2595+
.field(&prev_tracks.len())
2596+
.finish(),
25392597
}
25402598
}
25412599
}

src/config/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -720,6 +720,7 @@ impl Config {
720720
initial_volume,
721721
disable_volume: matches!(mixer_config.volume_ctrl, VolumeCtrl::Fixed),
722722
volume_steps: conf.connect_config.volume_steps,
723+
emit_set_queue_events: false,
723724
}
724725
};
725726

0 commit comments

Comments
 (0)