mirror of
https://github.com/librespot-org/librespot.git
synced 2024-12-18 17:11:53 +00:00
Add activate and load functions to Spirc
(#1075)
This commit is contained in:
parent
98c985ffab
commit
edf646d4bb
3 changed files with 210 additions and 60 deletions
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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,11 +702,24 @@ 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 {
|
||||||
warn!("SpircCommand::{:?} will be ignored while Not Active", cmd);
|
match cmd {
|
||||||
Ok(())
|
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::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
96
examples/play_connect.rs
Normal 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();
|
||||||
|
});
|
||||||
|
}
|
Loading…
Reference in a new issue