mirror of
https://github.com/librespot-org/librespot.git
synced 2024-12-18 17:11:53 +00:00
Merge pull request #154 from Spotifyd/events-on-prev-next
Run onstart/onstop when a new song is loaded
This commit is contained in:
commit
21d7b618cb
4 changed files with 130 additions and 72 deletions
|
@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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 {
|
(Player { commands: Some(cmd_tx), thread_handle: Some(handle) },
|
||||||
commands: Some(cmd_tx),
|
event_receiver)
|
||||||
thread_handle: Some(handle),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
match mem::replace(self, Invalid) {
|
||||||
Paused { end_of_track, .. } |
|
Playing { track_id, end_of_track, ..} => {
|
||||||
Playing { end_of_track, .. } => {
|
|
||||||
let _ = end_of_track.send(());
|
let _ = end_of_track.send(());
|
||||||
}
|
*self = EndOfTrack { track_id };
|
||||||
|
},
|
||||||
Stopped => warn!("signal_end_of_track from stopped state"),
|
_ => panic!("Called playing_to_end_of_track in non-playing state.")
|
||||||
Invalid => panic!("invalid 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();
|
self.state.playing_to_end_of_track();
|
||||||
|
|
||||||
let old_state = mem::replace(&mut self.state, PlayerState::Stopped);
|
|
||||||
old_state.signal_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() {
|
match self.state {
|
||||||
self.run_onstart();
|
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.send_event(PlayerEvent::Stopped { track_id });
|
||||||
self.state = PlayerState::Stopped;
|
|
||||||
}
|
|
||||||
PlayerState::Paused { .. } => {
|
|
||||||
self.state = PlayerState::Stopped;
|
self.state = PlayerState::Stopped;
|
||||||
},
|
},
|
||||||
PlayerState::Stopped => {
|
PlayerState::Stopped => {
|
||||||
|
@ -434,16 +463,8 @@ impl PlayerInternal {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run_onstart(&self) {
|
fn send_event(&mut self, event: PlayerEvent) {
|
||||||
if let Some(ref program) = self.config.onstart {
|
let _ = self.event_sender.unbounded_send(event.clone());
|
||||||
run_program(program)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn run_onstop(&self) {
|
|
||||||
if let Some(ref program) = self.config.onstop {
|
|
||||||
run_program(program)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
30
src/main.rs
30
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("", "onevent", "Run PROGRAM when playback is about to begin.", "PROGRAM")
|
||||||
.optopt("", "onstop", "Run PROGRAM when playback has ended.", "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
src/player_event_handler.rs
Normal file
33
src/player_event_handler.rs
Normal file
|
@ -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);
|
||||||
|
}
|
Loading…
Reference in a new issue