Ver código fonte

Merge pull request #154 from Spotifyd/events-on-prev-next

Run onstart/onstop when a new song is loaded
Sasha Hilton 7 anos atrás
pai
commit
21d7b618cb
4 arquivos alterados com 130 adições e 72 exclusões
  1. 1 5
      playback/src/config.rs
  2. 72 61
      playback/src/player.rs
  3. 24 6
      src/main.rs
  4. 33 0
      src/player_event_handler.rs

+ 1 - 5
playback/src/config.rs

@@ -28,8 +28,6 @@ impl Default for Bitrate {
 #[derive(Clone, Debug)]
 #[derive(Clone, Debug)]
 pub struct PlayerConfig {
 pub struct PlayerConfig {
     pub bitrate: Bitrate,
     pub bitrate: Bitrate,
-    pub onstart: Option<String>,
-    pub onstop: Option<String>,
     pub normalisation: bool,
     pub normalisation: bool,
     pub normalisation_pregain: f32,
     pub normalisation_pregain: f32,
 }
 }
@@ -38,10 +36,8 @@ impl Default for PlayerConfig {
     fn default() -> PlayerConfig {
     fn default() -> PlayerConfig {
         PlayerConfig {
         PlayerConfig {
             bitrate: Bitrate::default(),
             bitrate: Bitrate::default(),
-            onstart: None,
-            onstop: None,
             normalisation: false,
             normalisation: false,
             normalisation_pregain: 0.0,
             normalisation_pregain: 0.0,
         }
         }
     }
     }
-}
+}

+ 72 - 61
playback/src/player.rs

@@ -1,11 +1,11 @@
 use byteorder::{LittleEndian, ReadBytesExt};
 use byteorder::{LittleEndian, ReadBytesExt};
 use futures::sync::oneshot;
 use futures::sync::oneshot;
 use futures::{future, Future};
 use futures::{future, Future};
+use futures;
 use std;
 use std;
 use std::borrow::Cow;
 use std::borrow::Cow;
 use std::io::{Read, Seek, SeekFrom, Result};
 use std::io::{Read, Seek, SeekFrom, Result};
 use std::mem;
 use std::mem;
-use std::process::Command;
 use std::sync::mpsc::{RecvError, TryRecvError, RecvTimeoutError};
 use std::sync::mpsc::{RecvError, TryRecvError, RecvTimeoutError};
 use std::thread;
 use std::thread;
 use std::time::Duration;
 use std::time::Duration;
@@ -34,6 +34,7 @@ struct PlayerInternal {
     sink: Box<Sink>,
     sink: Box<Sink>,
     sink_running: bool,
     sink_running: bool,
     audio_filter: Option<Box<AudioFilter + Send>>,
     audio_filter: Option<Box<AudioFilter + Send>>,
+    event_sender: futures::sync::mpsc::UnboundedSender<PlayerEvent>,
 }
 }
 
 
 enum PlayerCommand {
 enum PlayerCommand {
@@ -44,6 +45,24 @@ enum PlayerCommand {
     Seek(u32),
     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<PlayerEvent>;
+
 #[derive(Clone, Copy, Debug)]
 #[derive(Clone, Copy, Debug)]
 struct NormalisationData {
 struct NormalisationData {
     track_gain_db: f32,
     track_gain_db: f32,
@@ -90,10 +109,11 @@ impl NormalisationData {
 impl Player {
 impl Player {
     pub fn new<F>(config: PlayerConfig, session: Session,
     pub fn new<F>(config: PlayerConfig, session: Session,
                   audio_filter: Option<Box<AudioFilter + Send>>,
                   audio_filter: Option<Box<AudioFilter + Send>>,
-                  sink_builder: F) -> Player
+                  sink_builder: F) -> (Player, PlayerEventChannel)
         where F: FnOnce() -> Box<Sink> + Send + 'static
         where F: FnOnce() -> Box<Sink> + Send + 'static
     {
     {
         let (cmd_tx, cmd_rx) = std::sync::mpsc::channel();
         let (cmd_tx, cmd_rx) = std::sync::mpsc::channel();
+        let (event_sender, event_receiver) = futures::sync::mpsc::unbounded();
 
 
         let handle = thread::spawn(move || {
         let handle = thread::spawn(move || {
             debug!("new Player[{}]", session.session_id());
             debug!("new Player[{}]", session.session_id());
@@ -107,15 +127,14 @@ impl Player {
                 sink: sink_builder(),
                 sink: sink_builder(),
                 sink_running: false,
                 sink_running: false,
                 audio_filter: audio_filter,
                 audio_filter: audio_filter,
+                event_sender: event_sender,
             };
             };
 
 
             internal.run();
             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) {
     fn command(&self, cmd: PlayerCommand) {
@@ -165,16 +184,18 @@ type Decoder = VorbisDecoder<Subfile<AudioDecrypt<AudioFile>>>;
 enum PlayerState {
 enum PlayerState {
     Stopped,
     Stopped,
     Paused {
     Paused {
+        track_id: SpotifyId,
         decoder: Decoder,
         decoder: Decoder,
         end_of_track: oneshot::Sender<()>,
         end_of_track: oneshot::Sender<()>,
         normalisation_factor: f32,
         normalisation_factor: f32,
     },
     },
     Playing {
     Playing {
+        track_id: SpotifyId,
         decoder: Decoder,
         decoder: Decoder,
         end_of_track: oneshot::Sender<()>,
         end_of_track: oneshot::Sender<()>,
         normalisation_factor: f32,
         normalisation_factor: f32,
     },
     },
-
+    EndOfTrack { track_id: SpotifyId },
     Invalid,
     Invalid,
 }
 }
 
 
@@ -182,7 +203,7 @@ impl PlayerState {
     fn is_playing(&self) -> bool {
     fn is_playing(&self) -> bool {
         use self::PlayerState::*;
         use self::PlayerState::*;
         match *self {
         match *self {
-            Stopped | Paused { .. } => false,
+            Stopped | EndOfTrack { .. } | Paused { .. } => false,
             Playing { .. } => true,
             Playing { .. } => true,
             Invalid => panic!("invalid state"),
             Invalid => panic!("invalid state"),
         }
         }
@@ -191,31 +212,30 @@ impl PlayerState {
     fn decoder(&mut self) -> Option<&mut Decoder> {
     fn decoder(&mut self) -> Option<&mut Decoder> {
         use self::PlayerState::*;
         use self::PlayerState::*;
         match *self {
         match *self {
-            Stopped => None,
+            Stopped | EndOfTrack { .. } => None,
             Paused { ref mut decoder, .. } |
             Paused { ref mut decoder, .. } |
             Playing { ref mut decoder, .. } => Some(decoder),
             Playing { ref mut decoder, .. } => Some(decoder),
             Invalid => panic!("invalid state"),
             Invalid => panic!("invalid state"),
         }
         }
     }
     }
 
 
-    fn signal_end_of_track(self) {
+    fn playing_to_end_of_track(&mut self) {
         use self::PlayerState::*;
         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(());
                 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) {
     fn paused_to_playing(&mut self) {
         use self::PlayerState::*;
         use self::PlayerState::*;
         match ::std::mem::replace(self, Invalid) {
         match ::std::mem::replace(self, Invalid) {
-            Paused { decoder, end_of_track, normalisation_factor } => {
+            Paused { track_id, decoder, end_of_track, normalisation_factor } => {
                 *self = Playing {
                 *self = Playing {
+                    track_id: track_id,
                     decoder: decoder,
                     decoder: decoder,
                     end_of_track: end_of_track,
                     end_of_track: end_of_track,
                     normalisation_factor: normalisation_factor,
                     normalisation_factor: normalisation_factor,
@@ -228,8 +248,9 @@ impl PlayerState {
     fn playing_to_paused(&mut self) {
     fn playing_to_paused(&mut self) {
         use self::PlayerState::*;
         use self::PlayerState::*;
         match ::std::mem::replace(self, Invalid) {
         match ::std::mem::replace(self, Invalid) {
-            Playing { decoder, end_of_track, normalisation_factor } => {
+            Playing { track_id, decoder, end_of_track, normalisation_factor } => {
                 *self = Paused {
                 *self = Paused {
+                    track_id: track_id,
                     decoder: decoder,
                     decoder: decoder,
                     end_of_track: end_of_track,
                     end_of_track: end_of_track,
                     normalisation_factor: normalisation_factor,
                     normalisation_factor: normalisation_factor,
@@ -331,10 +352,7 @@ impl PlayerInternal {
 
 
             None => {
             None => {
                 self.stop_sink();
                 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) {
                 match self.load_track(track_id, position as i64) {
                     Some((decoder, normalisation_factor)) => {
                     Some((decoder, normalisation_factor)) => {
                         if play {
                         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.start_sink();
 
 
                             self.state = PlayerState::Playing {
                             self.state = PlayerState::Playing {
+                                track_id: track_id,
                                 decoder: decoder,
                                 decoder: decoder,
                                 end_of_track: end_of_track,
                                 end_of_track: end_of_track,
                                 normalisation_factor: normalisation_factor,
                                 normalisation_factor: normalisation_factor,
                             };
                             };
                         } else {
                         } else {
-                            if self.state.is_playing() {
-                                self.run_onstop();
-                            }
-
                             self.state = PlayerState::Paused {
                             self.state = PlayerState::Paused {
+                                track_id: track_id,
                                 decoder: decoder,
                                 decoder: decoder,
                                 end_of_track: end_of_track,
                                 end_of_track: end_of_track,
                                 normalisation_factor: normalisation_factor,
                                 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 => {
                     None => {
                         let _ = end_of_track.send(());
                         let _ = end_of_track.send(());
-                        if self.state.is_playing() {
-                            self.run_onstop();
-                        }
                     }
                     }
                 }
                 }
             }
             }
@@ -394,10 +424,10 @@ impl PlayerInternal {
             }
             }
 
 
             PlayerCommand::Play => {
             PlayerCommand::Play => {
-                if let PlayerState::Paused { .. } = self.state {
+                if let PlayerState::Paused { track_id, .. } = self.state {
                     self.state.paused_to_playing();
                     self.state.paused_to_playing();
 
 
-                    self.run_onstart();
+                    self.send_event(PlayerEvent::Started { track_id });
                     self.start_sink();
                     self.start_sink();
                 } else {
                 } else {
                     warn!("Player::play called from invalid state");
                     warn!("Player::play called from invalid state");
@@ -405,11 +435,11 @@ impl PlayerInternal {
             }
             }
 
 
             PlayerCommand::Pause => {
             PlayerCommand::Pause => {
-                if let PlayerState::Playing { .. } = self.state {
+                if let PlayerState::Playing { track_id, .. } = self.state {
                     self.state.playing_to_paused();
                     self.state.playing_to_paused();
 
 
                     self.stop_sink_if_running();
                     self.stop_sink_if_running();
-                    self.run_onstop();
+                    self.send_event(PlayerEvent::Stopped { track_id });
                 } else {
                 } else {
                     warn!("Player::pause called from invalid state");
                     warn!("Player::pause called from invalid state");
                 }
                 }
@@ -417,12 +447,11 @@ impl PlayerInternal {
 
 
             PlayerCommand::Stop => {
             PlayerCommand::Stop => {
                 match self.state {
                 match self.state {
-                    PlayerState::Playing { .. } => {
+                    PlayerState::Playing { track_id, .. }
+                    | PlayerState::Paused { track_id, .. }
+                    | PlayerState::EndOfTrack { track_id } => {
                         self.stop_sink_if_running();
                         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;
                         self.state = PlayerState::Stopped;
                     },
                     },
                     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<Cow<'a, Track>> {
     fn find_available_alternative<'a>(&self, track: &'a Track) -> Option<Cow<'a, Track>> {
@@ -587,13 +608,3 @@ impl<T: Read + Seek> Seek for Subfile<T> {
         }
         }
     }
     }
 }
 }
-
-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);
-}

+ 24 - 6
src/main.rs

@@ -10,6 +10,7 @@ extern crate crypto;
 
 
 use env_logger::LogBuilder;
 use env_logger::LogBuilder;
 use futures::{Future, Async, Poll, Stream};
 use futures::{Future, Async, Poll, Stream};
+use futures::sync::mpsc::UnboundedReceiver;
 use std::env;
 use std::env;
 use std::io::{self, stderr, Write};
 use std::io::{self, stderr, Write};
 use std::path::PathBuf;
 use std::path::PathBuf;
@@ -31,9 +32,12 @@ use librespot::playback::audio_backend::{self, Sink, BACKENDS};
 use librespot::playback::config::{Bitrate, PlayerConfig};
 use librespot::playback::config::{Bitrate, PlayerConfig};
 use librespot::connect::discovery::{discovery, DiscoveryStream};
 use librespot::connect::discovery::{discovery, DiscoveryStream};
 use librespot::playback::mixer::{self, Mixer};
 use librespot::playback::mixer::{self, Mixer};
-use librespot::playback::player::Player;
+use librespot::playback::player::{Player, PlayerEvent};
 use librespot::connect::spirc::{Spirc, SpircTask};
 use librespot::connect::spirc::{Spirc, SpircTask};
 
 
+mod player_event_handler;
+use player_event_handler::run_program_on_events;
+
 fn device_id(name: &str) -> String {
 fn device_id(name: &str) -> String {
     let mut h = Sha1::new();
     let mut h = Sha1::new();
     h.input_str(name);
     h.input_str(name);
@@ -92,6 +96,7 @@ struct Setup {
     credentials: Option<Credentials>,
     credentials: Option<Credentials>,
     enable_discovery: bool,
     enable_discovery: bool,
     zeroconf_port: u16,
     zeroconf_port: u16,
+    player_event_program: Option<String>,
 }
 }
 
 
 fn setup(args: &[String]) -> Setup {
 fn setup(args: &[String]) -> Setup {
@@ -101,8 +106,7 @@ fn setup(args: &[String]) -> Setup {
         .reqopt("n", "name", "Device name", "NAME")
         .reqopt("n", "name", "Device name", "NAME")
         .optopt("", "device-type", "Displayed device type", "DEVICE_TYPE")
         .optopt("", "device-type", "Displayed device type", "DEVICE_TYPE")
         .optopt("b", "bitrate", "Bitrate (96, 160 or 320). Defaults to 160", "BITRATE")
         .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")
         .optflag("v", "verbose", "Enable verbose output")
         .optopt("u", "username", "Username to sign in with", "USERNAME")
         .optopt("u", "username", "Username to sign in with", "USERNAME")
         .optopt("p", "password", "Password", "PASSWORD")
         .optopt("p", "password", "Password", "PASSWORD")
@@ -196,8 +200,6 @@ fn setup(args: &[String]) -> Setup {
 
 
         PlayerConfig {
         PlayerConfig {
             bitrate: bitrate,
             bitrate: bitrate,
-            onstart: matches.opt_str("onstart"),
-            onstop: matches.opt_str("onstop"),
             normalisation: matches.opt_present("enable-volume-normalisation"),
             normalisation: matches.opt_present("enable-volume-normalisation"),
             normalisation_pregain: matches.opt_str("normalisation-pregain")
             normalisation_pregain: matches.opt_str("normalisation-pregain")
                 .map(|pregain| pregain.parse::<f32>().expect("Invalid pregain float value"))
                 .map(|pregain| pregain.parse::<f32>().expect("Invalid pregain float value"))
@@ -230,6 +232,7 @@ fn setup(args: &[String]) -> Setup {
         enable_discovery: enable_discovery,
         enable_discovery: enable_discovery,
         zeroconf_port: zeroconf_port,
         zeroconf_port: zeroconf_port,
         mixer: mixer,
         mixer: mixer,
+        player_event_program: matches.opt_str("onevent"),
     }
     }
 }
 }
 
 
@@ -251,6 +254,9 @@ struct Main {
     connect: Box<Future<Item=Session, Error=io::Error>>,
     connect: Box<Future<Item=Session, Error=io::Error>>,
 
 
     shutdown: bool,
     shutdown: bool,
+
+    player_event_channel: Option<UnboundedReceiver<PlayerEvent>>,
+    player_event_program: Option<String>,
 }
 }
 
 
 impl Main {
 impl Main {
@@ -271,6 +277,9 @@ impl Main {
             spirc_task: None,
             spirc_task: None,
             shutdown: false,
             shutdown: false,
             signal: Box::new(tokio_signal::ctrl_c(&handle).flatten_stream()),
             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 {
         if setup.enable_discovery {
@@ -328,13 +337,14 @@ impl Future for Main {
 
 
                 let audio_filter = mixer.get_audio_filter();
                 let audio_filter = mixer.get_audio_filter();
                 let backend = self.backend;
                 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)
                     (backend)(device)
                 });
                 });
 
 
                 let (spirc, spirc_task) = Spirc::new(connect_config, session, player, mixer);
                 let (spirc, spirc_task) = Spirc::new(connect_config, session, player, mixer);
                 self.spirc = Some(spirc);
                 self.spirc = Some(spirc);
                 self.spirc_task = Some(spirc_task);
                 self.spirc_task = Some(spirc_task);
+                self.player_event_channel = Some(event_channel);
 
 
                 progress = true;
                 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 {
             if !progress {
                 return Ok(Async::NotReady);
                 return Ok(Async::NotReady);
             }
             }

+ 33 - 0
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);
+}