diff --git a/CHANGELOG.md b/CHANGELOG.md index 1903db1f..54eaf48b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -93,6 +93,7 @@ https://github.com/librespot-org/librespot disabled such content. Applications that use librespot as a library without Connect should use the 'filter-explicit-content' user attribute in the session. - [playback] Add metadata support via a `TrackChanged` event +- [connect] Add `activate` and `load` functions to `Spirc`, allowing control over local connect sessions ### Fixed diff --git a/connect/src/spirc.rs b/connect/src/spirc.rs index 44a29d7c..84c5e3e6 100644 --- a/connect/src/spirc.rs +++ b/connect/src/spirc.rs @@ -122,6 +122,36 @@ pub enum SpircCommand { Disconnect, SetPosition(u32), SetVolume(u16), + Activate, + Load(SpircLoadCommand), +} + +#[derive(Debug)] +pub struct SpircLoadCommand { + pub context_uri: String, + /// Whether the given tracks should immediately start playing, or just be initially loaded. + pub start_playing: bool, + pub shuffle: bool, + pub repeat: bool, + pub playing_track_index: u32, + pub tracks: Vec, +} + +impl From for State { + fn from(command: SpircLoadCommand) -> Self { + let mut state = State::new(); + state.set_context_uri(command.context_uri); + state.set_status(if command.start_playing { + PlayStatus::kPlayStatusPlay + } else { + PlayStatus::kPlayStatusStop + }); + state.set_shuffle(command.shuffle); + state.set_repeat(command.repeat); + state.set_playing_track_index(command.playing_track_index); + state.set_track(command.tracks.into()); + state + } } const CONTEXT_TRACKS_HISTORY: usize = 10; @@ -469,6 +499,12 @@ impl Spirc { pub fn disconnect(&self) -> Result<(), Error> { Ok(self.commands.send(SpircCommand::Disconnect)?) } + pub fn activate(&self) -> Result<(), Error> { + Ok(self.commands.send(SpircCommand::Activate)?) + } + pub fn load(&self, command: SpircLoadCommand) -> Result<(), Error> { + Ok(self.commands.send(SpircCommand::Load(command))?) + } } impl SpircTask { @@ -666,11 +702,24 @@ impl SpircTask { self.set_volume(volume); self.notify(None) } + SpircCommand::Load(command) => { + self.handle_load(&command.into())?; + self.notify(None) + } _ => Ok(()), } } else { - warn!("SpircCommand::{:?} will be ignored while Not Active", cmd); - Ok(()) + match cmd { + SpircCommand::Activate => { + trace!("Received SpircCommand::{:?}", cmd); + self.handle_activate(); + self.notify(None) + } + _ => { + warn!("SpircCommand::{:?} will be ignored while Not Active", cmd); + Ok(()) + } + } } } @@ -889,57 +938,7 @@ impl SpircTask { MessageType::kMessageTypeHello => self.notify(Some(ident)), MessageType::kMessageTypeLoad => { - if !self.device.get_is_active() { - let now = self.now_ms(); - self.device.set_is_active(true); - self.device.set_became_active_at(now); - self.player.emit_session_connected_event( - self.session.connection_id(), - self.session.username(), - ); - self.player.emit_session_client_changed_event( - self.session.client_id(), - self.session.client_name(), - self.session.client_brand_name(), - self.session.client_model_name(), - ); - - self.player - .emit_volume_changed_event(self.device.get_volume() as u16); - - self.player - .emit_auto_play_changed_event(self.session.autoplay()); - - self.player.emit_filter_explicit_content_changed_event( - self.session.filter_explicit_content(), - ); - - self.player - .emit_shuffle_changed_event(self.state.get_shuffle()); - - self.player - .emit_repeat_changed_event(self.state.get_repeat()); - } - - let context_uri = update.get_state().get_context_uri().to_owned(); - - // completely ignore local playback. - if context_uri.starts_with("spotify:local-files") { - self.notify(None)?; - return Err(SpircError::UnsupportedLocalPlayBack.into()); - } - - self.update_tracks(&update); - - if !self.state.get_track().is_empty() { - let start_playing = - update.get_state().get_status() == PlayStatus::kPlayStatusPlay; - self.load_track(start_playing, update.get_state().get_position_ms()); - } else { - info!("No more tracks left in queue"); - self.handle_stop(); - } - + self.handle_load(update.get_state())?; self.notify(None) } @@ -1021,7 +1020,7 @@ impl SpircTask { return Err(SpircError::UnsupportedLocalPlayBack.into()); } - self.update_tracks(&update); + self.update_tracks(update.get_state()); if let SpircPlayStatus::Playing { preloading_of_next_track_triggered, @@ -1075,6 +1074,60 @@ impl SpircTask { self.player.stop(); } + fn handle_activate(&mut self) { + let now = self.now_ms(); + self.device.set_is_active(true); + self.device.set_became_active_at(now); + self.player + .emit_session_connected_event(self.session.connection_id(), self.session.username()); + self.player.emit_session_client_changed_event( + self.session.client_id(), + self.session.client_name(), + self.session.client_brand_name(), + self.session.client_model_name(), + ); + + self.player + .emit_volume_changed_event(self.device.get_volume() as u16); + + self.player + .emit_auto_play_changed_event(self.session.autoplay()); + + self.player + .emit_filter_explicit_content_changed_event(self.session.filter_explicit_content()); + + self.player + .emit_shuffle_changed_event(self.state.get_shuffle()); + + self.player + .emit_repeat_changed_event(self.state.get_repeat()); + } + + fn handle_load(&mut self, state: &State) -> Result<(), Error> { + if !self.device.get_is_active() { + self.handle_activate(); + } + + let context_uri = state.get_context_uri().to_owned(); + + // completely ignore local playback. + if context_uri.starts_with("spotify:local-files") { + self.notify(None)?; + return Err(SpircError::UnsupportedLocalPlayBack.into()); + } + + self.update_tracks(state); + + if !self.state.get_track().is_empty() { + let start_playing = state.get_status() == PlayStatus::kPlayStatusPlay; + self.load_track(start_playing, state.get_position_ms()); + } else { + info!("No more tracks left in queue"); + self.handle_stop(); + } + Ok(()) + } + fn handle_play(&mut self) { match self.play_status { SpircPlayStatus::Paused { @@ -1372,12 +1425,12 @@ impl SpircTask { } } - fn update_tracks(&mut self, frame: &protocol::spirc::Frame) { - trace!("State: {:#?}", frame.get_state()); + fn update_tracks(&mut self, state: &State) { + trace!("State: {:#?}", state); - let index = frame.get_state().get_playing_track_index(); - let context_uri = frame.get_state().get_context_uri(); - let tracks = frame.get_state().get_track(); + let index = state.get_playing_track_index(); + let context_uri = state.get_context_uri(); + let tracks = state.get_track(); trace!("Frame has {:?} tracks", tracks.len()); @@ -1395,7 +1448,7 @@ impl SpircTask { // has_shuffle/repeat seem to always be true in these replace msgs, // but to replicate the behaviour of the Android client we have to // ignore false values. - let state = frame.get_state(); + let state = state; if state.get_repeat() { self.state.set_repeat(true); } diff --git a/examples/play_connect.rs b/examples/play_connect.rs new file mode 100644 index 00000000..2b23a7d3 --- /dev/null +++ b/examples/play_connect.rs @@ -0,0 +1,96 @@ +use librespot::{ + core::{ + authentication::Credentials, config::SessionConfig, session::Session, spotify_id::SpotifyId, + }, + playback::{ + audio_backend, + config::{AudioFormat, PlayerConfig}, + mixer::NoOpVolume, + player::Player, + }, +}; +use librespot_connect::{ + config::ConnectConfig, + spirc::{Spirc, SpircLoadCommand}, +}; +use librespot_metadata::{Album, Metadata}; +use librespot_playback::mixer::{softmixer::SoftMixer, Mixer, MixerConfig}; +use librespot_protocol::spirc::TrackRef; +use std::env; +use tokio::join; + +#[tokio::main] +async fn main() { + let session_config = SessionConfig::default(); + let player_config = PlayerConfig::default(); + let audio_format = AudioFormat::default(); + let connect_config = ConnectConfig::default(); + + let mut args: Vec<_> = env::args().collect(); + let context_uri = if args.len() == 4 { + args.pop().unwrap() + } else if args.len() == 3 { + String::from("spotify:album:79dL7FLiJFOO0EoehUHQBv") + } else { + eprintln!("Usage: {} USERNAME PASSWORD (ALBUM URI)", args[0]); + return; + }; + + let credentials = Credentials::with_password(&args[1], &args[2]); + let backend = audio_backend::find(None).unwrap(); + + println!("Connecting..."); + let session = Session::new(session_config, None); + + let player = Player::new( + player_config, + session.clone(), + Box::new(NoOpVolume), + move || backend(None, audio_format), + ); + + let (spirc, spirc_task) = Spirc::new( + connect_config, + session.clone(), + credentials, + player, + Box::new(SoftMixer::open(MixerConfig::default())), + ) + .await + .unwrap(); + + join!(spirc_task, async { + let album = Album::get(&session, &SpotifyId::from_uri(&context_uri).unwrap()) + .await + .unwrap(); + let tracks = album + .tracks() + .map(|track_id| { + let mut track = TrackRef::new(); + track.set_gid(Vec::from(track_id.to_raw())); + track + }) + .collect(); + + println!( + "Playing album: {} by {}", + &album.name, + album + .artists + .first() + .map_or("unknown", |artist| &artist.name) + ); + + spirc.activate().unwrap(); + spirc + .load(SpircLoadCommand { + context_uri, + start_playing: true, + shuffle: false, + repeat: false, + playing_track_index: 0, // the index specifies which track in the context starts playing, in this case the first in the album + tracks, + }) + .unwrap(); + }); +}