diff --git a/playback/src/config.rs b/playback/src/config.rs index d44e937a..f0fd13d7 100644 --- a/playback/src/config.rs +++ b/playback/src/config.rs @@ -1,4 +1,6 @@ use std::str::FromStr; +use core::spotify_id::SpotifyId; +use std::sync::mpsc::Sender; #[derive(Clone, Copy, Debug, Hash, PartialOrd, Ord, PartialEq, Eq)] pub enum Bitrate { @@ -25,19 +27,33 @@ impl Default for Bitrate { } } +#[derive(Debug, Clone)] +pub enum PlayerEvent { + Started { + track_id: SpotifyId, + }, + + Changed { + old_track_id: SpotifyId, + new_track_id: SpotifyId, + }, + + Stopped { + track_id: SpotifyId, + } +} + #[derive(Clone, Debug)] pub struct PlayerConfig { pub bitrate: Bitrate, - pub onstart: Option, - pub onstop: Option, + pub event_sender : Option>, } impl Default for PlayerConfig { fn default() -> PlayerConfig { PlayerConfig { bitrate: Bitrate::default(), - onstart: None, - onstop: None, + event_sender: None, } } } diff --git a/playback/src/player.rs b/playback/src/player.rs index bbcdb370..593e7f1d 100644 --- a/playback/src/player.rs +++ b/playback/src/player.rs @@ -4,12 +4,11 @@ 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; -use config::{Bitrate, PlayerConfig}; +use config::{Bitrate, PlayerConfig, PlayerEvent}; use core::session::Session; use core::spotify_id::SpotifyId; @@ -121,14 +120,16 @@ type Decoder = VorbisDecoder>>; enum PlayerState { Stopped, Paused { + track_id: SpotifyId, decoder: Decoder, end_of_track: oneshot::Sender<()>, }, Playing { + track_id: SpotifyId, decoder: Decoder, end_of_track: oneshot::Sender<()>, }, - + EndOfTrack { track_id: SpotifyId }, Invalid, } @@ -136,7 +137,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"), } @@ -145,7 +146,7 @@ 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"), @@ -160,6 +161,7 @@ impl PlayerState { let _ = end_of_track.send(()); } + EndOfTrack { .. } => warn!("signal_end_of_track from end of track state"), Stopped => warn!("signal_end_of_track from stopped state"), Invalid => panic!("invalid state"), } @@ -168,10 +170,11 @@ impl PlayerState { fn paused_to_playing(&mut self) { use self::PlayerState::*; match ::std::mem::replace(self, Invalid) { - Paused { decoder, end_of_track } => { + Paused { decoder, end_of_track, track_id } => { *self = Playing { decoder: decoder, end_of_track: end_of_track, + track_id: track_id, }; } _ => panic!("invalid state"), @@ -181,10 +184,11 @@ impl PlayerState { fn playing_to_paused(&mut self) { use self::PlayerState::*; match ::std::mem::replace(self, Invalid) { - Playing { decoder, end_of_track } => { + Playing { decoder, end_of_track, track_id } => { *self = Paused { decoder: decoder, end_of_track: end_of_track, + track_id: track_id, }; } _ => panic!("invalid state"), @@ -274,9 +278,15 @@ impl PlayerInternal { None => { self.stop_sink(); - self.run_onstop(); - let old_state = mem::replace(&mut self.state, PlayerState::Stopped); + let new_state = match self.state { + PlayerState::Playing { track_id, .. } + | PlayerState::Paused { track_id, .. } => + PlayerState::EndOfTrack { track_id }, + _ => PlayerState::Stopped, + }; + + let old_state = mem::replace(&mut self.state, new_state); old_state.signal_end_of_track(); } } @@ -288,24 +298,35 @@ impl PlayerInternal { PlayerCommand::Load(track_id, play, position, end_of_track) => { if self.state.is_playing() { self.stop_sink_if_running(); - self.run_onstop(); } match self.load_track(track_id, position as i64) { Some(decoder) => { if play { - 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, }; } else { self.state = PlayerState::Paused { + track_id: track_id, decoder: decoder, end_of_track: end_of_track, }; + self.send_event(PlayerEvent::Stopped { track_id }); } } @@ -327,10 +348,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"); @@ -338,11 +359,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"); } @@ -350,12 +371,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 => { @@ -367,15 +387,14 @@ 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) { + match self.config.event_sender { + Some(ref s) => + match s.send(event.clone()) { + Ok(_) => info!("Sent event {:?} to event listener.", event), + Err(err) => error!("Failed to send event {:?} to listener: {:?}", event, err) + } + None => () } } @@ -509,13 +528,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 01c34421..8251c3f5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -31,6 +31,9 @@ use librespot::playback::mixer::{self, Mixer}; use librespot::playback::player::Player; use librespot::connect::spirc::{Spirc, SpircTask}; +mod player_event_handler; +use player_event_handler::run_program_on_events; + fn usage(program: &str, opts: &getopts::Options) -> String { let brief = format!("Usage: {} [options]", program); opts.usage(&brief) @@ -94,6 +97,7 @@ fn setup(args: &[String]) -> Setup { .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("", "onchange", "Run PROGRAM between two tracks.", "PROGRAM") .optflag("v", "verbose", "Enable verbose output") .optopt("u", "username", "Username to sign in with", "USERNAME") .optopt("p", "password", "Password", "PASSWORD") @@ -185,8 +189,9 @@ fn setup(args: &[String]) -> Setup { PlayerConfig { bitrate: bitrate, - onstart: matches.opt_str("onstart"), - onstop: matches.opt_str("onstop"), + event_sender: run_program_on_events(matches.opt_str("onstart"), + matches.opt_str("onstop"), + matches.opt_str("onchange")) } }; diff --git a/src/player_event_handler.rs b/src/player_event_handler.rs new file mode 100644 index 00000000..25724d17 --- /dev/null +++ b/src/player_event_handler.rs @@ -0,0 +1,50 @@ +use std::process::Command; +use std::sync::mpsc::{channel, Sender}; +use std::thread; +use librespot::playback::config::PlayerEvent; + +fn run_program(program: &str, args: Vec) { + info!("Running {}", program); + let mut v: Vec<&str> = program.split_whitespace().collect(); + let status = Command::new(&v.remove(0)) + .args(&v) + .args(args) + .status() + .expect("program failed to start"); + info!("Exit status: {}", status); +} + +pub fn run_program_on_events(onstart: Option, + onstop: Option, + onchange: Option) -> Option> { + if onstart.is_none() && onstop.is_none() && onchange.is_none() { + None + } else { + let (sender, receiver) = channel(); + thread::spawn(move || { + while let Ok(msg) = receiver.recv() { + match msg { + PlayerEvent::Changed { old_track_id, new_track_id } => { + let args = vec![old_track_id.to_base16(), new_track_id.to_base16()]; + if let Some(ref onchange) = onchange.as_ref() { + run_program(onchange, args); + } + }, + PlayerEvent::Started { track_id } => { + let args = vec![track_id.to_base16()]; + if let Some(ref onstart) = onstart.as_ref() { + run_program(onstart, args); + } + } + PlayerEvent::Stopped { track_id } => { + let args = vec![track_id.to_base16()]; + if let Some(ref onstop) = onstop.as_ref() { + run_program(onstop, args); + } + } + } + } + }); + Some(sender) + } +}