Add activate and load functions to Spirc (#1075)

This commit is contained in:
Oliver Cooper 2022-12-04 00:25:27 +13:00 committed by GitHub
parent 98c985ffab
commit edf646d4bb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 210 additions and 60 deletions

View file

@ -93,6 +93,7 @@ https://github.com/librespot-org/librespot
disabled such content. Applications that use librespot as a library without disabled such content. Applications that use librespot as a library without
Connect should use the 'filter-explicit-content' user attribute in the session. Connect should use the 'filter-explicit-content' user attribute in the session.
- [playback] Add metadata support via a `TrackChanged` event - [playback] Add metadata support via a `TrackChanged` event
- [connect] Add `activate` and `load` functions to `Spirc`, allowing control over local connect sessions
### Fixed ### Fixed

View file

@ -122,6 +122,36 @@ pub enum SpircCommand {
Disconnect, Disconnect,
SetPosition(u32), SetPosition(u32),
SetVolume(u16), 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<TrackRef>,
}
impl From<SpircLoadCommand> 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; const CONTEXT_TRACKS_HISTORY: usize = 10;
@ -469,6 +499,12 @@ impl Spirc {
pub fn disconnect(&self) -> Result<(), Error> { pub fn disconnect(&self) -> Result<(), Error> {
Ok(self.commands.send(SpircCommand::Disconnect)?) 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 { impl SpircTask {
@ -666,13 +702,26 @@ impl SpircTask {
self.set_volume(volume); self.set_volume(volume);
self.notify(None) self.notify(None)
} }
SpircCommand::Load(command) => {
self.handle_load(&command.into())?;
self.notify(None)
}
_ => Ok(()), _ => Ok(()),
} }
} else { } else {
match cmd {
SpircCommand::Activate => {
trace!("Received SpircCommand::{:?}", cmd);
self.handle_activate();
self.notify(None)
}
_ => {
warn!("SpircCommand::{:?} will be ignored while Not Active", cmd); warn!("SpircCommand::{:?} will be ignored while Not Active", cmd);
Ok(()) Ok(())
} }
} }
}
}
fn handle_player_event(&mut self, event: PlayerEvent) -> Result<(), Error> { fn handle_player_event(&mut self, event: PlayerEvent) -> Result<(), Error> {
// we only process events if the play_request_id matches. If it doesn't, it is // we only process events if the play_request_id matches. If it doesn't, it is
@ -889,57 +938,7 @@ impl SpircTask {
MessageType::kMessageTypeHello => self.notify(Some(ident)), MessageType::kMessageTypeHello => self.notify(Some(ident)),
MessageType::kMessageTypeLoad => { MessageType::kMessageTypeLoad => {
if !self.device.get_is_active() { self.handle_load(update.get_state())?;
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.notify(None) self.notify(None)
} }
@ -1021,7 +1020,7 @@ impl SpircTask {
return Err(SpircError::UnsupportedLocalPlayBack.into()); return Err(SpircError::UnsupportedLocalPlayBack.into());
} }
self.update_tracks(&update); self.update_tracks(update.get_state());
if let SpircPlayStatus::Playing { if let SpircPlayStatus::Playing {
preloading_of_next_track_triggered, preloading_of_next_track_triggered,
@ -1075,6 +1074,60 @@ impl SpircTask {
self.player.stop(); 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) { fn handle_play(&mut self) {
match self.play_status { match self.play_status {
SpircPlayStatus::Paused { SpircPlayStatus::Paused {
@ -1372,12 +1425,12 @@ impl SpircTask {
} }
} }
fn update_tracks(&mut self, frame: &protocol::spirc::Frame) { fn update_tracks(&mut self, state: &State) {
trace!("State: {:#?}", frame.get_state()); trace!("State: {:#?}", state);
let index = frame.get_state().get_playing_track_index(); let index = state.get_playing_track_index();
let context_uri = frame.get_state().get_context_uri(); let context_uri = state.get_context_uri();
let tracks = frame.get_state().get_track(); let tracks = state.get_track();
trace!("Frame has {:?} tracks", tracks.len()); trace!("Frame has {:?} tracks", tracks.len());
@ -1395,7 +1448,7 @@ impl SpircTask {
// has_shuffle/repeat seem to always be true in these replace msgs, // has_shuffle/repeat seem to always be true in these replace msgs,
// but to replicate the behaviour of the Android client we have to // but to replicate the behaviour of the Android client we have to
// ignore false values. // ignore false values.
let state = frame.get_state(); let state = state;
if state.get_repeat() { if state.get_repeat() {
self.state.set_repeat(true); self.state.set_repeat(true);
} }

96
examples/play_connect.rs Normal file
View file

@ -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();
});
}