diff --git a/playback/src/config.rs b/playback/src/config.rs index bfd9739..44b8c9b 100644 --- a/playback/src/config.rs +++ b/playback/src/config.rs @@ -28,8 +28,6 @@ impl Default for Bitrate { #[derive(Clone, Debug)] pub struct PlayerConfig { pub bitrate: Bitrate, - pub onstart: Option, - pub onstop: Option, pub normalisation: bool, pub normalisation_pregain: f32, } @@ -38,10 +36,8 @@ impl Default for PlayerConfig { fn default() -> PlayerConfig { PlayerConfig { bitrate: Bitrate::default(), - onstart: None, - onstop: None, normalisation: false, normalisation_pregain: 0.0, } } -} +} \ No newline at end of file diff --git a/playback/src/player.rs b/playback/src/player.rs index 62af77e..47d9998 100644 --- a/playback/src/player.rs +++ b/playback/src/player.rs @@ -1,11 +1,11 @@ use byteorder::{LittleEndian, ReadBytesExt}; use futures::sync::oneshot; use futures::{future, Future}; +use futures; use std; use std::borrow::Cow; use std::io::{Read, Seek, SeekFrom, Result}; use std::mem; -use std::process::Command; use std::sync::mpsc::{RecvError, TryRecvError, RecvTimeoutError}; use std::thread; use std::time::Duration; @@ -34,6 +34,7 @@ struct PlayerInternal { sink: Box, sink_running: bool, audio_filter: Option>, + event_sender: futures::sync::mpsc::UnboundedSender, } enum PlayerCommand { @@ -44,6 +45,24 @@ enum PlayerCommand { Seek(u32), } +#[derive(Debug, Clone)] +pub enum PlayerEvent { + Started { + track_id: SpotifyId, + }, + + Changed { + old_track_id: SpotifyId, + new_track_id: SpotifyId, + }, + + Stopped { + track_id: SpotifyId, + } +} + +type PlayerEventChannel = futures::sync::mpsc::UnboundedReceiver; + #[derive(Clone, Copy, Debug)] struct NormalisationData { track_gain_db: f32, @@ -90,10 +109,11 @@ impl NormalisationData { impl Player { pub fn new(config: PlayerConfig, session: Session, audio_filter: Option>, - sink_builder: F) -> Player + sink_builder: F) -> (Player, PlayerEventChannel) where F: FnOnce() -> Box + Send + 'static { let (cmd_tx, cmd_rx) = std::sync::mpsc::channel(); + let (event_sender, event_receiver) = futures::sync::mpsc::unbounded(); let handle = thread::spawn(move || { debug!("new Player[{}]", session.session_id()); @@ -107,15 +127,14 @@ impl Player { sink: sink_builder(), sink_running: false, audio_filter: audio_filter, + event_sender: event_sender, }; internal.run(); }); - Player { - commands: Some(cmd_tx), - thread_handle: Some(handle), - } + (Player { commands: Some(cmd_tx), thread_handle: Some(handle) }, + event_receiver) } fn command(&self, cmd: PlayerCommand) { @@ -165,16 +184,18 @@ type Decoder = VorbisDecoder>>; enum PlayerState { Stopped, Paused { + track_id: SpotifyId, decoder: Decoder, end_of_track: oneshot::Sender<()>, normalisation_factor: f32, }, Playing { + track_id: SpotifyId, decoder: Decoder, end_of_track: oneshot::Sender<()>, normalisation_factor: f32, }, - + EndOfTrack { track_id: SpotifyId }, Invalid, } @@ -182,7 +203,7 @@ impl PlayerState { fn is_playing(&self) -> bool { use self::PlayerState::*; match *self { - Stopped | Paused { .. } => false, + Stopped | EndOfTrack { .. } | Paused { .. } => false, Playing { .. } => true, Invalid => panic!("invalid state"), } @@ -191,31 +212,30 @@ impl PlayerState { fn decoder(&mut self) -> Option<&mut Decoder> { use self::PlayerState::*; match *self { - Stopped => None, + Stopped | EndOfTrack { .. } => None, Paused { ref mut decoder, .. } | Playing { ref mut decoder, .. } => Some(decoder), Invalid => panic!("invalid state"), } } - fn signal_end_of_track(self) { + fn playing_to_end_of_track(&mut self) { use self::PlayerState::*; - match self { - Paused { end_of_track, .. } | - Playing { end_of_track, .. } => { + match mem::replace(self, Invalid) { + Playing { track_id, end_of_track, ..} => { let _ = end_of_track.send(()); - } - - Stopped => warn!("signal_end_of_track from stopped state"), - Invalid => panic!("invalid state"), + *self = EndOfTrack { track_id }; + }, + _ => panic!("Called playing_to_end_of_track in non-playing state.") } } fn paused_to_playing(&mut self) { use self::PlayerState::*; match ::std::mem::replace(self, Invalid) { - Paused { decoder, end_of_track, normalisation_factor } => { + Paused { track_id, decoder, end_of_track, normalisation_factor } => { *self = Playing { + track_id: track_id, decoder: decoder, end_of_track: end_of_track, normalisation_factor: normalisation_factor, @@ -228,8 +248,9 @@ impl PlayerState { fn playing_to_paused(&mut self) { use self::PlayerState::*; match ::std::mem::replace(self, Invalid) { - Playing { decoder, end_of_track, normalisation_factor } => { + Playing { track_id, decoder, end_of_track, normalisation_factor } => { *self = Paused { + track_id: track_id, decoder: decoder, end_of_track: end_of_track, normalisation_factor: normalisation_factor, @@ -331,10 +352,7 @@ impl PlayerInternal { None => { self.stop_sink(); - self.run_onstop(); - - let old_state = mem::replace(&mut self.state, PlayerState::Stopped); - old_state.signal_end_of_track(); + self.state.playing_to_end_of_track(); } } } @@ -350,34 +368,46 @@ impl PlayerInternal { match self.load_track(track_id, position as i64) { Some((decoder, normalisation_factor)) => { if play { - if !self.state.is_playing() { - self.run_onstart(); + match self.state { + PlayerState::Playing { track_id: old_track_id, ..} + | PlayerState::EndOfTrack { track_id: old_track_id, .. } => + self.send_event(PlayerEvent::Changed { + old_track_id: old_track_id, + new_track_id: track_id + }), + _ => self.send_event(PlayerEvent::Started { track_id }), } + self.start_sink(); self.state = PlayerState::Playing { + track_id: track_id, decoder: decoder, end_of_track: end_of_track, normalisation_factor: normalisation_factor, }; } else { - if self.state.is_playing() { - self.run_onstop(); - } - self.state = PlayerState::Paused { + track_id: track_id, decoder: decoder, end_of_track: end_of_track, normalisation_factor: normalisation_factor, }; + match self.state { + PlayerState::Playing { track_id: old_track_id, ..} + | PlayerState::EndOfTrack { track_id: old_track_id, .. } => + self.send_event(PlayerEvent::Changed { + old_track_id: old_track_id, + new_track_id: track_id + }), + _ => (), + } + self.send_event(PlayerEvent::Stopped { track_id }); } } None => { let _ = end_of_track.send(()); - if self.state.is_playing() { - self.run_onstop(); - } } } } @@ -394,10 +424,10 @@ impl PlayerInternal { } PlayerCommand::Play => { - if let PlayerState::Paused { .. } = self.state { + if let PlayerState::Paused { track_id, .. } = self.state { self.state.paused_to_playing(); - self.run_onstart(); + self.send_event(PlayerEvent::Started { track_id }); self.start_sink(); } else { warn!("Player::play called from invalid state"); @@ -405,11 +435,11 @@ impl PlayerInternal { } PlayerCommand::Pause => { - if let PlayerState::Playing { .. } = self.state { + if let PlayerState::Playing { track_id, .. } = self.state { self.state.playing_to_paused(); self.stop_sink_if_running(); - self.run_onstop(); + self.send_event(PlayerEvent::Stopped { track_id }); } else { warn!("Player::pause called from invalid state"); } @@ -417,12 +447,11 @@ impl PlayerInternal { PlayerCommand::Stop => { match self.state { - PlayerState::Playing { .. } => { + PlayerState::Playing { track_id, .. } + | PlayerState::Paused { track_id, .. } + | PlayerState::EndOfTrack { track_id } => { self.stop_sink_if_running(); - self.run_onstop(); - self.state = PlayerState::Stopped; - } - PlayerState::Paused { .. } => { + self.send_event(PlayerEvent::Stopped { track_id }); self.state = PlayerState::Stopped; }, PlayerState::Stopped => { @@ -434,16 +463,8 @@ impl PlayerInternal { } } - fn run_onstart(&self) { - if let Some(ref program) = self.config.onstart { - run_program(program) - } - } - - fn run_onstop(&self) { - if let Some(ref program) = self.config.onstop { - run_program(program) - } + fn send_event(&mut self, event: PlayerEvent) { + let _ = self.event_sender.unbounded_send(event.clone()); } fn find_available_alternative<'a>(&self, track: &'a Track) -> Option> { @@ -587,13 +608,3 @@ impl Seek for Subfile { } } } - -fn run_program(program: &str) { - info!("Running {}", program); - let mut v: Vec<&str> = program.split_whitespace().collect(); - let status = Command::new(&v.remove(0)) - .args(&v) - .status() - .expect("program failed to start"); - info!("Exit status: {}", status); -} diff --git a/src/main.rs b/src/main.rs index e03504c..a42be82 100644 --- a/src/main.rs +++ b/src/main.rs @@ -10,6 +10,7 @@ extern crate crypto; use env_logger::LogBuilder; use futures::{Future, Async, Poll, Stream}; +use futures::sync::mpsc::UnboundedReceiver; use std::env; use std::io::{self, stderr, Write}; use std::path::PathBuf; @@ -31,9 +32,12 @@ use librespot::playback::audio_backend::{self, Sink, BACKENDS}; use librespot::playback::config::{Bitrate, PlayerConfig}; use librespot::connect::discovery::{discovery, DiscoveryStream}; use librespot::playback::mixer::{self, Mixer}; -use librespot::playback::player::Player; +use librespot::playback::player::{Player, PlayerEvent}; use librespot::connect::spirc::{Spirc, SpircTask}; +mod player_event_handler; +use player_event_handler::run_program_on_events; + fn device_id(name: &str) -> String { let mut h = Sha1::new(); h.input_str(name); @@ -92,6 +96,7 @@ struct Setup { credentials: Option, enable_discovery: bool, zeroconf_port: u16, + player_event_program: Option, } fn setup(args: &[String]) -> Setup { @@ -101,8 +106,7 @@ fn setup(args: &[String]) -> Setup { .reqopt("n", "name", "Device name", "NAME") .optopt("", "device-type", "Displayed device type", "DEVICE_TYPE") .optopt("b", "bitrate", "Bitrate (96, 160 or 320). Defaults to 160", "BITRATE") - .optopt("", "onstart", "Run PROGRAM when playback is about to begin.", "PROGRAM") - .optopt("", "onstop", "Run PROGRAM when playback has ended.", "PROGRAM") + .optopt("", "onevent", "Run PROGRAM when playback is about to begin.", "PROGRAM") .optflag("v", "verbose", "Enable verbose output") .optopt("u", "username", "Username to sign in with", "USERNAME") .optopt("p", "password", "Password", "PASSWORD") @@ -196,8 +200,6 @@ fn setup(args: &[String]) -> Setup { PlayerConfig { bitrate: bitrate, - onstart: matches.opt_str("onstart"), - onstop: matches.opt_str("onstop"), normalisation: matches.opt_present("enable-volume-normalisation"), normalisation_pregain: matches.opt_str("normalisation-pregain") .map(|pregain| pregain.parse::().expect("Invalid pregain float value")) @@ -230,6 +232,7 @@ fn setup(args: &[String]) -> Setup { enable_discovery: enable_discovery, zeroconf_port: zeroconf_port, mixer: mixer, + player_event_program: matches.opt_str("onevent"), } } @@ -251,6 +254,9 @@ struct Main { connect: Box>, shutdown: bool, + + player_event_channel: Option>, + player_event_program: Option, } impl Main { @@ -271,6 +277,9 @@ impl Main { spirc_task: None, shutdown: false, signal: Box::new(tokio_signal::ctrl_c(&handle).flatten_stream()), + + player_event_channel: None, + player_event_program: setup.player_event_program, }; if setup.enable_discovery { @@ -328,13 +337,14 @@ impl Future for Main { let audio_filter = mixer.get_audio_filter(); let backend = self.backend; - let player = Player::new(player_config, session.clone(), audio_filter, move || { + let (player, event_channel) = Player::new(player_config, session.clone(), audio_filter, move || { (backend)(device) }); let (spirc, spirc_task) = Spirc::new(connect_config, session, player, mixer); self.spirc = Some(spirc); self.spirc_task = Some(spirc_task); + self.player_event_channel = Some(event_channel); progress = true; } @@ -362,6 +372,14 @@ impl Future for Main { } } + if let Some(ref mut player_event_channel) = self.player_event_channel { + if let Async::Ready(Some(event)) = player_event_channel.poll().unwrap() { + if let Some(ref program) = self.player_event_program { + run_program_on_events(event, program); + } + } + } + if !progress { return Ok(Async::NotReady); } diff --git a/src/player_event_handler.rs b/src/player_event_handler.rs new file mode 100644 index 0000000..7882718 --- /dev/null +++ b/src/player_event_handler.rs @@ -0,0 +1,33 @@ +use std::process::Command; +use std::collections::HashMap; +use librespot::playback::player::PlayerEvent; + +fn run_program(program: &str, env_vars: HashMap<&str, String>) { + let mut v: Vec<&str> = program.split_whitespace().collect(); + info!("Running {:?} with environment variables {:?}", v, env_vars); + Command::new(&v.remove(0)) + .args(&v) + .envs(env_vars.iter()) + .spawn() + .expect("program failed to start"); +} + +pub fn run_program_on_events(event: PlayerEvent, onevent: &str) { + let mut env_vars = HashMap::new(); + match event { + PlayerEvent::Changed { old_track_id, new_track_id } => { + env_vars.insert("PLAYER_EVENT", "change".to_string()); + env_vars.insert("OLD_TRACK_ID", old_track_id.to_base16()); + env_vars.insert("TRACK_ID", new_track_id.to_base16()); + }, + PlayerEvent::Started { track_id } => { + env_vars.insert("PLAYER_EVENT", "start".to_string()); + env_vars.insert("TRACK_ID", track_id.to_base16()); + } + PlayerEvent::Stopped { track_id } => { + env_vars.insert("PLAYER_EVENT", "stop".to_string()); + env_vars.insert("TRACK_ID", track_id.to_base16()); + } + } + run_program(onevent, env_vars); +}