Prechádzať zdrojové kódy

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

Run onstart/onstop when a new song is loaded
Sasha Hilton 7 rokov pred
rodič
commit
21d7b618cb
4 zmenil súbory, kde vykonal 130 pridanie a 72 odobranie
  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)]
 pub struct PlayerConfig {
     pub bitrate: Bitrate,
-    pub onstart: Option<String>,
-    pub onstop: Option<String>,
     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,
         }
     }
-}
+}

+ 72 - 61
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>,
     sink_running: bool,
     audio_filter: Option<Box<AudioFilter + Send>>,
+    event_sender: futures::sync::mpsc::UnboundedSender<PlayerEvent>,
 }
 
 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<PlayerEvent>;
+
 #[derive(Clone, Copy, Debug)]
 struct NormalisationData {
     track_gain_db: f32,
@@ -90,10 +109,11 @@ impl NormalisationData {
 impl Player {
     pub fn new<F>(config: PlayerConfig, session: Session,
                   audio_filter: Option<Box<AudioFilter + Send>>,
-                  sink_builder: F) -> Player
+                  sink_builder: F) -> (Player, PlayerEventChannel)
         where F: FnOnce() -> Box<Sink> + 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<Subfile<AudioDecrypt<AudioFile>>>;
 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<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 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<Credentials>,
     enable_discovery: bool,
     zeroconf_port: u16,
+    player_event_program: Option<String>,
 }
 
 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::<f32>().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<Future<Item=Session, Error=io::Error>>,
 
     shutdown: bool,
+
+    player_event_channel: Option<UnboundedReceiver<PlayerEvent>>,
+    player_event_program: Option<String>,
 }
 
 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);
             }

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