mirror of
https://github.com/librespot-org/librespot.git
synced 2024-12-18 17:11:53 +00:00
First attempt at a better playback event system.
This commit is contained in:
parent
77bea07c4f
commit
b0ee03112f
4 changed files with 125 additions and 45 deletions
|
@ -1,4 +1,6 @@
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
use core::spotify_id::SpotifyId;
|
||||||
|
use std::sync::mpsc::Sender;
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Hash, PartialOrd, Ord, PartialEq, Eq)]
|
#[derive(Clone, Copy, Debug, Hash, PartialOrd, Ord, PartialEq, Eq)]
|
||||||
pub enum Bitrate {
|
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)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct PlayerConfig {
|
pub struct PlayerConfig {
|
||||||
pub bitrate: Bitrate,
|
pub bitrate: Bitrate,
|
||||||
pub onstart: Option<String>,
|
pub event_sender : Option<Sender<PlayerEvent>>,
|
||||||
pub onstop: Option<String>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for PlayerConfig {
|
impl Default for PlayerConfig {
|
||||||
fn default() -> PlayerConfig {
|
fn default() -> PlayerConfig {
|
||||||
PlayerConfig {
|
PlayerConfig {
|
||||||
bitrate: Bitrate::default(),
|
bitrate: Bitrate::default(),
|
||||||
onstart: None,
|
event_sender: None,
|
||||||
onstop: None,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,12 +4,11 @@ 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;
|
||||||
|
|
||||||
use config::{Bitrate, PlayerConfig};
|
use config::{Bitrate, PlayerConfig, PlayerEvent};
|
||||||
use core::session::Session;
|
use core::session::Session;
|
||||||
use core::spotify_id::SpotifyId;
|
use core::spotify_id::SpotifyId;
|
||||||
|
|
||||||
|
@ -121,14 +120,16 @@ 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<()>,
|
||||||
},
|
},
|
||||||
Playing {
|
Playing {
|
||||||
|
track_id: SpotifyId,
|
||||||
decoder: Decoder,
|
decoder: Decoder,
|
||||||
end_of_track: oneshot::Sender<()>,
|
end_of_track: oneshot::Sender<()>,
|
||||||
},
|
},
|
||||||
|
EndOfTrack { track_id: SpotifyId },
|
||||||
Invalid,
|
Invalid,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -136,7 +137,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"),
|
||||||
}
|
}
|
||||||
|
@ -145,7 +146,7 @@ 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"),
|
||||||
|
@ -160,6 +161,7 @@ impl PlayerState {
|
||||||
let _ = end_of_track.send(());
|
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"),
|
Stopped => warn!("signal_end_of_track from stopped state"),
|
||||||
Invalid => panic!("invalid state"),
|
Invalid => panic!("invalid state"),
|
||||||
}
|
}
|
||||||
|
@ -168,10 +170,11 @@ impl PlayerState {
|
||||||
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 } => {
|
Paused { decoder, end_of_track, track_id } => {
|
||||||
*self = Playing {
|
*self = Playing {
|
||||||
decoder: decoder,
|
decoder: decoder,
|
||||||
end_of_track: end_of_track,
|
end_of_track: end_of_track,
|
||||||
|
track_id: track_id,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
_ => panic!("invalid state"),
|
_ => panic!("invalid state"),
|
||||||
|
@ -181,10 +184,11 @@ 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 } => {
|
Playing { decoder, end_of_track, track_id } => {
|
||||||
*self = Paused {
|
*self = Paused {
|
||||||
decoder: decoder,
|
decoder: decoder,
|
||||||
end_of_track: end_of_track,
|
end_of_track: end_of_track,
|
||||||
|
track_id: track_id,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
_ => panic!("invalid state"),
|
_ => panic!("invalid state"),
|
||||||
|
@ -274,9 +278,15 @@ impl PlayerInternal {
|
||||||
|
|
||||||
None => {
|
None => {
|
||||||
self.stop_sink();
|
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();
|
old_state.signal_end_of_track();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -288,24 +298,35 @@ impl PlayerInternal {
|
||||||
PlayerCommand::Load(track_id, play, position, end_of_track) => {
|
PlayerCommand::Load(track_id, play, position, end_of_track) => {
|
||||||
if self.state.is_playing() {
|
if self.state.is_playing() {
|
||||||
self.stop_sink_if_running();
|
self.stop_sink_if_running();
|
||||||
self.run_onstop();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
match self.load_track(track_id, position as i64) {
|
match self.load_track(track_id, position as i64) {
|
||||||
Some(decoder) => {
|
Some(decoder) => {
|
||||||
if play {
|
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.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,
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
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,
|
||||||
};
|
};
|
||||||
|
self.send_event(PlayerEvent::Stopped { track_id });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -327,10 +348,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");
|
||||||
|
@ -338,11 +359,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");
|
||||||
}
|
}
|
||||||
|
@ -350,12 +371,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 => {
|
||||||
|
@ -367,15 +387,14 @@ impl PlayerInternal {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run_onstart(&self) {
|
fn send_event(&mut self, event: PlayerEvent) {
|
||||||
if let Some(ref program) = self.config.onstart {
|
match self.config.event_sender {
|
||||||
run_program(program)
|
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)
|
||||||
fn run_onstop(&self) {
|
}
|
||||||
if let Some(ref program) = self.config.onstop {
|
None => ()
|
||||||
run_program(program)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -509,13 +528,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);
|
|
||||||
}
|
|
||||||
|
|
|
@ -31,6 +31,9 @@ use librespot::playback::mixer::{self, Mixer};
|
||||||
use librespot::playback::player::Player;
|
use librespot::playback::player::Player;
|
||||||
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 usage(program: &str, opts: &getopts::Options) -> String {
|
fn usage(program: &str, opts: &getopts::Options) -> String {
|
||||||
let brief = format!("Usage: {} [options]", program);
|
let brief = format!("Usage: {} [options]", program);
|
||||||
opts.usage(&brief)
|
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("b", "bitrate", "Bitrate (96, 160 or 320). Defaults to 160", "BITRATE")
|
||||||
.optopt("", "onstart", "Run PROGRAM when playback is about to begin.", "PROGRAM")
|
.optopt("", "onstart", "Run PROGRAM when playback is about to begin.", "PROGRAM")
|
||||||
.optopt("", "onstop", "Run PROGRAM when playback has ended.", "PROGRAM")
|
.optopt("", "onstop", "Run PROGRAM when playback has ended.", "PROGRAM")
|
||||||
|
.optopt("", "onchange", "Run PROGRAM between two tracks.", "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")
|
||||||
|
@ -185,8 +189,9 @@ fn setup(args: &[String]) -> Setup {
|
||||||
|
|
||||||
PlayerConfig {
|
PlayerConfig {
|
||||||
bitrate: bitrate,
|
bitrate: bitrate,
|
||||||
onstart: matches.opt_str("onstart"),
|
event_sender: run_program_on_events(matches.opt_str("onstart"),
|
||||||
onstop: matches.opt_str("onstop"),
|
matches.opt_str("onstop"),
|
||||||
|
matches.opt_str("onchange"))
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
50
src/player_event_handler.rs
Normal file
50
src/player_event_handler.rs
Normal file
|
@ -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<String>) {
|
||||||
|
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<String>,
|
||||||
|
onstop: Option<String>,
|
||||||
|
onchange: Option<String>) -> Option<Sender<PlayerEvent>> {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue