Format according to rustfmt

This commit is contained in:
Sasha Hilton 2018-02-26 02:50:41 +01:00
parent c3745a958a
commit 237ef1e4f9
22 changed files with 502 additions and 360 deletions

View file

@ -7,8 +7,9 @@ use std::ops::Add;
use core::audio_key::AudioKey; use core::audio_key::AudioKey;
const AUDIO_AESIV: &'static [u8] = &[0x72, 0xe0, 0x67, 0xfb, 0xdd, 0xcb, 0xcf, 0x77, 0xeb, 0xe8, const AUDIO_AESIV: &'static [u8] = &[
0xbc, 0x64, 0x3f, 0x63, 0x0d, 0x93]; 0x72, 0xe0, 0x67, 0xfb, 0xdd, 0xcb, 0xcf, 0x77, 0xeb, 0xe8, 0xbc, 0x64, 0x3f, 0x63, 0x0d, 0x93
];
pub struct AudioDecrypt<T: io::Read> { pub struct AudioDecrypt<T: io::Read> {
cipher: Box<SynchronousStreamCipher + 'static>, cipher: Box<SynchronousStreamCipher + 'static>,
@ -44,8 +45,8 @@ impl<T: io::Read + io::Seek> io::Seek for AudioDecrypt<T> {
let skip = newpos % 16; let skip = newpos % 16;
let iv = BigUint::from_bytes_be(AUDIO_AESIV) let iv = BigUint::from_bytes_be(AUDIO_AESIV)
.add(BigUint::from_u64(newpos / 16).unwrap()) .add(BigUint::from_u64(newpos / 16).unwrap())
.to_bytes_be(); .to_bytes_be();
self.cipher = aes::ctr(aes::KeySize::KeySize128, &self.key.0, &iv); self.cipher = aes::ctr(aes::KeySize::KeySize128, &self.key.0, &iv);
let buf = vec![0u8; skip as usize]; let buf = vec![0u8; skip as usize];

View file

@ -1,11 +1,11 @@
use bit_set::BitSet; use bit_set::BitSet;
use byteorder::{ByteOrder, BigEndian, WriteBytesExt}; use byteorder::{BigEndian, ByteOrder, WriteBytesExt};
use futures::{Async, Future, Poll};
use futures::Stream; use futures::Stream;
use futures::sync::{oneshot, mpsc}; use futures::sync::{mpsc, oneshot};
use futures::{Poll, Async, Future};
use std::cmp::min; use std::cmp::min;
use std::fs; use std::fs;
use std::io::{self, Read, Write, Seek, SeekFrom}; use std::io::{self, Read, Seek, SeekFrom, Write};
use std::sync::{Arc, Condvar, Mutex}; use std::sync::{Arc, Condvar, Mutex};
use tempfile::NamedTempFile; use tempfile::NamedTempFile;
@ -71,7 +71,12 @@ impl AudioFileOpenStreaming {
let (seek_tx, seek_rx) = mpsc::unbounded(); let (seek_tx, seek_rx) = mpsc::unbounded();
let fetcher = AudioFileFetch::new( let fetcher = AudioFileFetch::new(
self.session.clone(), shared.clone(), data_rx, write_file, seek_rx, complete_tx self.session.clone(),
shared.clone(),
data_rx,
write_file,
seek_rx,
complete_tx,
); );
self.session.spawn(move |_| fetcher); self.session.spawn(move |_| fetcher);
@ -148,14 +153,16 @@ impl AudioFile {
let session_ = session.clone(); let session_ = session.clone();
session.spawn(move |_| { session.spawn(move |_| {
complete_rx.map(move |mut file| { complete_rx
if let Some(cache) = session_.cache() { .map(move |mut file| {
cache.save_file(file_id, &mut file); if let Some(cache) = session_.cache() {
debug!("File {} complete, saving to cache", file_id); cache.save_file(file_id, &mut file);
} else { debug!("File {} complete, saving to cache", file_id);
debug!("File {} complete", file_id); } else {
} debug!("File {} complete", file_id);
}).or_else(|oneshot::Canceled| Ok(())) }
})
.or_else(|oneshot::Canceled| Ok(()))
}); });
AudioFileOpen::Streaming(open) AudioFileOpen::Streaming(open)
@ -200,11 +207,14 @@ struct AudioFileFetch {
} }
impl AudioFileFetch { impl AudioFileFetch {
fn new(session: Session, shared: Arc<AudioFileShared>, fn new(
data_rx: ChannelData, output: NamedTempFile, session: Session,
seek_rx: mpsc::UnboundedReceiver<u64>, shared: Arc<AudioFileShared>,
complete_tx: oneshot::Sender<NamedTempFile>) -> AudioFileFetch data_rx: ChannelData,
{ output: NamedTempFile,
seek_rx: mpsc::UnboundedReceiver<u64>,
complete_tx: oneshot::Sender<NamedTempFile>,
) -> AudioFileFetch {
AudioFileFetch { AudioFileFetch {
session: session, session: session,
shared: shared, shared: shared,
@ -233,8 +243,11 @@ impl AudioFileFetch {
let offset = self.index * CHUNK_SIZE; let offset = self.index * CHUNK_SIZE;
self.output.as_mut().unwrap() self.output
.seek(SeekFrom::Start(offset as u64)).unwrap(); .as_mut()
.unwrap()
.seek(SeekFrom::Start(offset as u64))
.unwrap();
let (_headers, data) = request_chunk(&self.session, self.shared.file_id, self.index).split(); let (_headers, data) = request_chunk(&self.session, self.shared.file_id, self.index).split();
self.data_rx = data; self.data_rx = data;
@ -275,13 +288,20 @@ impl Future for AudioFileFetch {
Ok(Async::Ready(Some(data))) => { Ok(Async::Ready(Some(data))) => {
progress = true; progress = true;
self.output.as_mut().unwrap() self.output
.write_all(data.as_ref()).unwrap(); .as_mut()
.unwrap()
.write_all(data.as_ref())
.unwrap();
} }
Ok(Async::Ready(None)) => { Ok(Async::Ready(None)) => {
progress = true; progress = true;
trace!("chunk {} / {} complete", self.index, self.shared.chunk_count); trace!(
"chunk {} / {} complete",
self.index,
self.shared.chunk_count
);
let full = { let full = {
let mut bitmap = self.shared.bitmap.lock().unwrap(); let mut bitmap = self.shared.bitmap.lock().unwrap();
@ -303,7 +323,7 @@ impl Future for AudioFileFetch {
Err(ChannelError) => { Err(ChannelError) => {
warn!("error from channel"); warn!("error from channel");
return Ok(Async::Ready(())); return Ok(Async::Ready(()));
}, }
} }
if !progress { if !progress {

View file

@ -2,16 +2,17 @@ extern crate lewton;
use self::lewton::inside_ogg::OggStreamReader; use self::lewton::inside_ogg::OggStreamReader;
use std::io::{Read, Seek};
use std::fmt;
use std::error; use std::error;
use std::fmt;
use std::io::{Read, Seek};
pub struct VorbisDecoder<R: Read + Seek>(OggStreamReader<R>); pub struct VorbisDecoder<R: Read + Seek>(OggStreamReader<R>);
pub struct VorbisPacket(Vec<i16>); pub struct VorbisPacket(Vec<i16>);
pub struct VorbisError(lewton::VorbisError); pub struct VorbisError(lewton::VorbisError);
impl <R> VorbisDecoder<R> impl<R> VorbisDecoder<R>
where R: Read + Seek where
R: Read + Seek,
{ {
pub fn new(input: R) -> Result<VorbisDecoder<R>, VorbisError> { pub fn new(input: R) -> Result<VorbisDecoder<R>, VorbisError> {
Ok(VorbisDecoder(OggStreamReader::new(input)?)) Ok(VorbisDecoder(OggStreamReader::new(input)?))

View file

@ -1,27 +1,29 @@
#[macro_use] extern crate log; #[macro_use]
#[macro_use] extern crate futures; extern crate futures;
#[macro_use]
extern crate log;
extern crate bit_set; extern crate bit_set;
extern crate byteorder; extern crate byteorder;
extern crate crypto; extern crate crypto;
extern crate num_traits;
extern crate num_bigint; extern crate num_bigint;
extern crate num_traits;
extern crate tempfile; extern crate tempfile;
extern crate librespot_core as core; extern crate librespot_core as core;
mod fetch;
mod decrypt; mod decrypt;
mod fetch;
#[cfg(not(any(feature = "with-tremor", feature = "with-vorbis")))] #[cfg(not(any(feature = "with-tremor", feature = "with-vorbis")))]
mod lewton_decoder; mod lewton_decoder;
#[cfg(any(feature = "with-tremor", feature = "with-vorbis"))] #[cfg(any(feature = "with-tremor", feature = "with-vorbis"))]
mod libvorbis_decoder; mod libvorbis_decoder;
pub use fetch::{AudioFile, AudioFileOpen};
pub use decrypt::AudioDecrypt; pub use decrypt::AudioDecrypt;
pub use fetch::{AudioFile, AudioFileOpen};
#[cfg(not(any(feature = "with-tremor", feature = "with-vorbis")))] #[cfg(not(any(feature = "with-tremor", feature = "with-vorbis")))]
pub use lewton_decoder::{VorbisDecoder, VorbisPacket, VorbisError}; pub use lewton_decoder::{VorbisDecoder, VorbisError, VorbisPacket};
#[cfg(any(feature = "with-tremor", feature = "with-vorbis"))] #[cfg(any(feature = "with-tremor", feature = "with-vorbis"))]
pub use libvorbis_decoder::{VorbisDecoder, VorbisPacket, VorbisError}; pub use libvorbis_decoder::{VorbisDecoder, VorbisError, VorbisPacket};

View file

@ -1,16 +1,19 @@
#[cfg(not(feature = "with-tremor"))] extern crate vorbis; #[cfg(feature = "with-tremor")]
#[cfg(feature = "with-tremor")] extern crate tremor as vorbis; extern crate tremor as vorbis;
#[cfg(not(feature = "with-tremor"))]
extern crate vorbis;
use std::io::{Read, Seek};
use std::fmt;
use std::error; use std::error;
use std::fmt;
use std::io::{Read, Seek};
pub struct VorbisDecoder<R: Read + Seek>(vorbis::Decoder<R>); pub struct VorbisDecoder<R: Read + Seek>(vorbis::Decoder<R>);
pub struct VorbisPacket(vorbis::Packet); pub struct VorbisPacket(vorbis::Packet);
pub struct VorbisError(vorbis::VorbisError); pub struct VorbisError(vorbis::VorbisError);
impl <R> VorbisDecoder<R> impl<R> VorbisDecoder<R>
where R: Read + Seek where
R: Read + Seek,
{ {
pub fn new(input: R) -> Result<VorbisDecoder<R>, VorbisError> { pub fn new(input: R) -> Result<VorbisDecoder<R>, VorbisError> {
Ok(VorbisDecoder(vorbis::Decoder::new(input)?)) Ok(VorbisDecoder(vorbis::Decoder::new(input)?))

View file

@ -6,9 +6,9 @@ use tokio_core::reactor::Core;
use librespot::core::authentication::Credentials; use librespot::core::authentication::Credentials;
use librespot::core::config::SessionConfig; use librespot::core::config::SessionConfig;
use librespot::playback::config::PlayerConfig;
use librespot::core::session::Session; use librespot::core::session::Session;
use librespot::core::spotify_id::SpotifyId; use librespot::core::spotify_id::SpotifyId;
use librespot::playback::config::PlayerConfig;
use librespot::playback::audio_backend; use librespot::playback::audio_backend;
use librespot::playback::player::Player; use librespot::playback::player::Player;
@ -20,7 +20,7 @@ fn main() {
let session_config = SessionConfig::default(); let session_config = SessionConfig::default();
let player_config = PlayerConfig::default(); let player_config = PlayerConfig::default();
let args : Vec<_> = env::args().collect(); let args: Vec<_> = env::args().collect();
if args.len() != 4 { if args.len() != 4 {
println!("Usage: {} USERNAME PASSWORD TRACK", args[0]); println!("Usage: {} USERNAME PASSWORD TRACK", args[0]);
} }
@ -33,9 +33,12 @@ fn main() {
let backend = audio_backend::find(None).unwrap(); let backend = audio_backend::find(None).unwrap();
println!("Connecting .."); println!("Connecting ..");
let session = core.run(Session::connect(session_config, credentials, None, handle)).unwrap(); let session = core.run(Session::connect(session_config, credentials, None, handle))
.unwrap();
let player = Player::new(player_config, session.clone(), None, move || (backend)(None)); let player = Player::new(player_config, session.clone(), None, move || {
(backend)(None)
});
println!("Playing..."); println!("Playing...");
core.run(player.load(track, true, 0)).unwrap(); core.run(player.load(track, true, 0)).unwrap();

View file

@ -1,11 +1,11 @@
use super::{Open, Sink}; use super::{Open, Sink};
use alsa::{Access, Format, Mode, Stream, PCM};
use std::io; use std::io;
use alsa::{PCM, Stream, Mode, Format, Access};
pub struct AlsaSink(Option<PCM>, String); pub struct AlsaSink(Option<PCM>, String);
impl Open for AlsaSink { impl Open for AlsaSink {
fn open(device: Option<String>) -> AlsaSink { fn open(device: Option<String>) -> AlsaSink {
info!("Using alsa sink"); info!("Using alsa sink");
let name = device.unwrap_or("default".to_string()); let name = device.unwrap_or("default".to_string());
@ -17,14 +17,22 @@ impl Open for AlsaSink {
impl Sink for AlsaSink { impl Sink for AlsaSink {
fn start(&mut self) -> io::Result<()> { fn start(&mut self) -> io::Result<()> {
if self.0.is_none() { if self.0.is_none() {
match PCM::open(&*self.1, match PCM::open(
Stream::Playback, Mode::Blocking, &*self.1,
Format::Signed16, Access::Interleaved, Stream::Playback,
2, 44100) { Mode::Blocking,
Format::Signed16,
Access::Interleaved,
2,
44100,
) {
Ok(f) => self.0 = Some(f), Ok(f) => self.0 = Some(f),
Err(e) => { Err(e) => {
error!("Alsa error PCM open {}", e); error!("Alsa error PCM open {}", e);
return Err(io::Error::new(io::ErrorKind::Other, "Alsa error: PCM open failed")); return Err(io::Error::new(
io::ErrorKind::Other,
"Alsa error: PCM open failed",
));
} }
} }
} }

View file

@ -1,11 +1,12 @@
use std::io;
use super::{Open, Sink}; use super::{Open, Sink};
use jack::prelude::{AudioOutPort, AudioOutSpec, Client, JackControl, ProcessScope, AsyncClient, client_options, ProcessHandler, Port }; use jack::prelude::{client_options, AsyncClient, AudioOutPort, AudioOutSpec, Client, JackControl, Port,
use std::sync::mpsc::{sync_channel, SyncSender, Receiver}; ProcessHandler, ProcessScope};
use std::io;
use std::sync::mpsc::{sync_channel, Receiver, SyncSender};
pub struct JackSink { pub struct JackSink {
send: SyncSender<i16>, send: SyncSender<i16>,
active_client: AsyncClient<(),JackData>, active_client: AsyncClient<(), JackData>,
} }
pub struct JackData { pub struct JackData {
@ -43,15 +44,26 @@ impl Open for JackSink {
let client_name = client_name.unwrap_or("librespot".to_string()); let client_name = client_name.unwrap_or("librespot".to_string());
let (client, _status) = Client::new(&client_name[..], client_options::NO_START_SERVER).unwrap(); let (client, _status) = Client::new(&client_name[..], client_options::NO_START_SERVER).unwrap();
let ch_r = client.register_port("out_0", AudioOutSpec::default()).unwrap(); let ch_r = client
let ch_l = client.register_port("out_1", AudioOutSpec::default()).unwrap(); .register_port("out_0", AudioOutSpec::default())
.unwrap();
let ch_l = client
.register_port("out_1", AudioOutSpec::default())
.unwrap();
// buffer for samples from librespot (~10ms) // buffer for samples from librespot (~10ms)
let (tx, rx) = sync_channel(2*1024*4); let (tx, rx) = sync_channel(2 * 1024 * 4);
let jack_data = JackData { rec: rx, port_l: ch_l, port_r: ch_r }; let jack_data = JackData {
rec: rx,
port_l: ch_l,
port_r: ch_r,
};
let active_client = AsyncClient::new(client, (), jack_data).unwrap(); let active_client = AsyncClient::new(client, (), jack_data).unwrap();
JackSink { send: tx, active_client: active_client } JackSink {
} send: tx,
active_client: active_client,
}
}
} }
impl Sink for JackSink { impl Sink for JackSink {

View file

@ -37,9 +37,7 @@ use self::jackaudio::JackSink;
mod pipe; mod pipe;
use self::pipe::StdoutSink; use self::pipe::StdoutSink;
pub const BACKENDS : &'static [ pub const BACKENDS: &'static [(&'static str, fn(Option<String>) -> Box<Sink>)] = &[
(&'static str, fn(Option<String>) -> Box<Sink>)
] = &[
#[cfg(feature = "alsa-backend")] #[cfg(feature = "alsa-backend")]
("alsa", mk_sink::<AlsaSink>), ("alsa", mk_sink::<AlsaSink>),
#[cfg(feature = "portaudio-backend")] #[cfg(feature = "portaudio-backend")]
@ -53,8 +51,16 @@ pub const BACKENDS : &'static [
pub fn find(name: Option<String>) -> Option<fn(Option<String>) -> Box<Sink>> { pub fn find(name: Option<String>) -> Option<fn(Option<String>) -> Box<Sink>> {
if let Some(name) = name { if let Some(name) = name {
BACKENDS.iter().find(|backend| name == backend.0).map(|backend| backend.1) BACKENDS
.iter()
.find(|backend| name == backend.0)
.map(|backend| backend.1)
} else { } else {
Some(BACKENDS.first().expect("No backends were enabled at build time").1) Some(
BACKENDS
.first()
.expect("No backends were enabled at build time")
.1,
)
} }
} }

View file

@ -28,7 +28,10 @@ impl Sink for StdoutSink {
fn write(&mut self, data: &[i16]) -> io::Result<()> { fn write(&mut self, data: &[i16]) -> io::Result<()> {
let data: &[u8] = unsafe { let data: &[u8] = unsafe {
slice::from_raw_parts(data.as_ptr() as *const u8, data.len() * mem::size_of::<i16>()) slice::from_raw_parts(
data.as_ptr() as *const u8,
data.len() * mem::size_of::<i16>(),
)
}; };
self.0.write_all(data)?; self.0.write_all(data)?;
@ -37,4 +40,3 @@ impl Sink for StdoutSink {
Ok(()) Ok(())
} }
} }

View file

@ -1,21 +1,21 @@
use super::{Open, Sink}; use super::{Open, Sink};
use portaudio_rs;
use portaudio_rs::device::{get_default_output_index, DeviceIndex, DeviceInfo};
use portaudio_rs::stream::*;
use std::io; use std::io;
use std::process::exit; use std::process::exit;
use std::time::Duration; use std::time::Duration;
use portaudio_rs;
use portaudio_rs::stream::*;
use portaudio_rs::device::{DeviceIndex, DeviceInfo, get_default_output_index};
pub struct PortAudioSink<'a>(Option<portaudio_rs::stream::Stream<'a, i16, i16>>, StreamParameters<i16>); pub struct PortAudioSink<'a>(
Option<portaudio_rs::stream::Stream<'a, i16, i16>>,
StreamParameters<i16>,
);
fn output_devices() -> Box<Iterator<Item=(DeviceIndex, DeviceInfo)>> { fn output_devices() -> Box<Iterator<Item = (DeviceIndex, DeviceInfo)>> {
let count = portaudio_rs::device::get_count().unwrap(); let count = portaudio_rs::device::get_count().unwrap();
let devices = (0..count) let devices = (0..count)
.filter_map(|idx| { .filter_map(|idx| portaudio_rs::device::get_info(idx).map(|info| (idx, info)))
portaudio_rs::device::get_info(idx).map(|info| (idx, info)) .filter(|&(_, ref info)| info.max_output_channels > 0);
}).filter(|&(_, ref info)| {
info.max_output_channels > 0
});
Box::new(devices) Box::new(devices)
} }
@ -38,9 +38,8 @@ fn find_output(device: &str) -> Option<DeviceIndex> {
.map(|(idx, _)| idx) .map(|(idx, _)| idx)
} }
impl <'a> Open for PortAudioSink<'a> { impl<'a> Open for PortAudioSink<'a> {
fn open(device: Option<String>) -> PortAudioSink<'a> { fn open(device: Option<String>) -> PortAudioSink<'a> {
debug!("Using PortAudio sink"); debug!("Using PortAudio sink");
portaudio_rs::initialize().unwrap(); portaudio_rs::initialize().unwrap();
@ -71,16 +70,19 @@ impl <'a> Open for PortAudioSink<'a> {
} }
} }
impl <'a> Sink for PortAudioSink<'a> { impl<'a> Sink for PortAudioSink<'a> {
fn start(&mut self) -> io::Result<()> { fn start(&mut self) -> io::Result<()> {
if self.0.is_none() { if self.0.is_none() {
self.0 = Some(Stream::open( self.0 = Some(
None, Some(self.1), Stream::open(
44100.0, None,
FRAMES_PER_BUFFER_UNSPECIFIED, Some(self.1),
StreamFlags::empty(), 44100.0,
None FRAMES_PER_BUFFER_UNSPECIFIED,
).unwrap());; StreamFlags::empty(),
None,
).unwrap(),
);;
} }
self.0.as_mut().unwrap().start().unwrap(); self.0.as_mut().unwrap().start().unwrap();
@ -94,8 +96,7 @@ impl <'a> Sink for PortAudioSink<'a> {
fn write(&mut self, data: &[i16]) -> io::Result<()> { fn write(&mut self, data: &[i16]) -> io::Result<()> {
match self.0.as_mut().unwrap().write(data) { match self.0.as_mut().unwrap().write(data) {
Ok(_) => (), Ok(_) => (),
Err(portaudio_rs::PaError::OutputUnderflowed) => Err(portaudio_rs::PaError::OutputUnderflowed) => error!("PortAudio write underflow"),
error!("PortAudio write underflow"),
Err(e) => panic!("PA Error {}", e), Err(e) => panic!("PA Error {}", e),
}; };
@ -103,7 +104,7 @@ impl <'a> Sink for PortAudioSink<'a> {
} }
} }
impl <'a> Drop for PortAudioSink<'a> { impl<'a> Drop for PortAudioSink<'a> {
fn drop(&mut self) { fn drop(&mut self) {
portaudio_rs::terminate().unwrap(); portaudio_rs::terminate().unwrap();
} }

View file

@ -1,20 +1,21 @@
use super::{Open, Sink}; use super::{Open, Sink};
use std::io;
use libpulse_sys::*;
use std::ptr::{null, null_mut};
use std::ffi::CString;
use std::ffi::CStr;
use std::mem;
use libc; use libc;
use libpulse_sys::*;
use std::ffi::CStr;
use std::ffi::CString;
use std::io;
use std::mem;
use std::ptr::{null, null_mut};
pub struct PulseAudioSink { pub struct PulseAudioSink {
s : *mut pa_simple, s: *mut pa_simple,
ss : pa_sample_spec, ss: pa_sample_spec,
name : CString, name: CString,
desc : CString desc: CString,
} }
fn call_pulseaudio<T, F, FailCheck>(f: F, fail_check: FailCheck, kind: io::ErrorKind) -> io::Result<T> where fn call_pulseaudio<T, F, FailCheck>(f: F, fail_check: FailCheck, kind: io::ErrorKind) -> io::Result<T>
where
T: Copy, T: Copy,
F: Fn(*mut libc::c_int) -> T, F: Fn(*mut libc::c_int) -> T,
FailCheck: Fn(T) -> bool, FailCheck: Fn(T) -> bool,
@ -23,7 +24,7 @@ fn call_pulseaudio<T, F, FailCheck>(f: F, fail_check: FailCheck, kind: io::Error
let ret = f(&mut error); let ret = f(&mut error);
if fail_check(ret) { if fail_check(ret) {
let err_cstr = unsafe { CStr::from_ptr(pa_strerror(error)) }; let err_cstr = unsafe { CStr::from_ptr(pa_strerror(error)) };
let errstr = err_cstr.to_string_lossy().into_owned(); let errstr = err_cstr.to_string_lossy().into_owned();
Err(io::Error::new(kind, errstr)) Err(io::Error::new(kind, errstr))
} else { } else {
Ok(ret) Ok(ret)
@ -48,7 +49,7 @@ impl Drop for PulseAudioSink {
} }
impl Open for PulseAudioSink { impl Open for PulseAudioSink {
fn open(device: Option<String>) -> PulseAudioSink { fn open(device: Option<String>) -> PulseAudioSink {
debug!("Using PulseAudio sink"); debug!("Using PulseAudio sink");
if device.is_some() { if device.is_some() {
@ -58,9 +59,9 @@ impl Open for PulseAudioSink {
let ss = pa_sample_spec { let ss = pa_sample_spec {
format: PA_SAMPLE_S16LE, format: PA_SAMPLE_S16LE,
channels: 2, // stereo channels: 2, // stereo
rate: 44100 rate: 44100,
}; };
let name = CString::new("librespot").unwrap(); let name = CString::new("librespot").unwrap();
let description = CString::new("Spotify endpoint").unwrap(); let description = CString::new("Spotify endpoint").unwrap();
@ -68,7 +69,7 @@ impl Open for PulseAudioSink {
s: null_mut(), s: null_mut(),
ss: ss, ss: ss,
name: name, name: name,
desc: description desc: description,
} }
} }
} }
@ -78,18 +79,21 @@ impl Sink for PulseAudioSink {
if self.s == null_mut() { if self.s == null_mut() {
self.s = call_pulseaudio( self.s = call_pulseaudio(
|err| unsafe { |err| unsafe {
pa_simple_new(null(), // Use the default server. pa_simple_new(
self.name.as_ptr(), // Our application's name. null(), // Use the default server.
PA_STREAM_PLAYBACK, self.name.as_ptr(), // Our application's name.
null(), // Use the default device. PA_STREAM_PLAYBACK,
self.desc.as_ptr(), // desc of our stream. null(), // Use the default device.
&self.ss, // Our sample format. self.desc.as_ptr(), // desc of our stream.
null(), // Use default channel map &self.ss, // Our sample format.
null(), // Use default buffering attributes. null(), // Use default channel map
err) null(), // Use default buffering attributes.
err,
)
}, },
|ptr| ptr == null_mut(), |ptr| ptr == null_mut(),
io::ErrorKind::ConnectionRefused)?; io::ErrorKind::ConnectionRefused,
)?;
} }
Ok(()) Ok(())
} }
@ -101,17 +105,18 @@ impl Sink for PulseAudioSink {
fn write(&mut self, data: &[i16]) -> io::Result<()> { fn write(&mut self, data: &[i16]) -> io::Result<()> {
if self.s == null_mut() { if self.s == null_mut() {
Err(io::Error::new(io::ErrorKind::NotConnected, "Not connected to pulseaudio")) Err(io::Error::new(
} io::ErrorKind::NotConnected,
else { "Not connected to pulseaudio",
))
} else {
let ptr = data.as_ptr() as *const libc::c_void; let ptr = data.as_ptr() as *const libc::c_void;
let len = data.len() as usize * mem::size_of::<i16>(); let len = data.len() as usize * mem::size_of::<i16>();
call_pulseaudio( call_pulseaudio(
|err| unsafe { |err| unsafe { pa_simple_write(self.s, ptr, len, err) },
pa_simple_write(self.s, ptr, len, err)
},
|ret| ret < 0, |ret| ret < 0,
io::ErrorKind::BrokenPipe)?; io::ErrorKind::BrokenPipe,
)?;
Ok(()) Ok(())
} }
} }

View file

@ -40,4 +40,4 @@ impl Default for PlayerConfig {
normalisation_pregain: 0.0, normalisation_pregain: 0.0,
} }
} }
} }

View file

@ -1,7 +1,8 @@
#[macro_use] extern crate log; #[macro_use]
extern crate log;
extern crate futures;
extern crate byteorder; extern crate byteorder;
extern crate futures;
#[cfg(feature = "alsa-backend")] #[cfg(feature = "alsa-backend")]
extern crate alsa; extern crate alsa;

View file

@ -1,5 +1,7 @@
pub trait Mixer : Send { pub trait Mixer: Send {
fn open() -> Self where Self: Sized; fn open() -> Self
where
Self: Sized;
fn start(&self); fn start(&self);
fn stop(&self); fn stop(&self);
fn set_volume(&self, volume: u16); fn set_volume(&self, volume: u16);

View file

@ -1,24 +1,22 @@
use std::sync::Arc; use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::atomic::{AtomicUsize, Ordering};
use super::Mixer;
use super::AudioFilter; use super::AudioFilter;
use super::Mixer;
#[derive(Clone)] #[derive(Clone)]
pub struct SoftMixer { pub struct SoftMixer {
volume: Arc<AtomicUsize> volume: Arc<AtomicUsize>,
} }
impl Mixer for SoftMixer { impl Mixer for SoftMixer {
fn open() -> SoftMixer { fn open() -> SoftMixer {
SoftMixer { SoftMixer {
volume: Arc::new(AtomicUsize::new(0xFFFF)) volume: Arc::new(AtomicUsize::new(0xFFFF)),
} }
} }
fn start(&self) { fn start(&self) {}
} fn stop(&self) {}
fn stop(&self) {
}
fn volume(&self) -> u16 { fn volume(&self) -> u16 {
self.volume.load(Ordering::Relaxed) as u16 self.volume.load(Ordering::Relaxed) as u16
} }
@ -26,12 +24,14 @@ impl Mixer for SoftMixer {
self.volume.store(volume as usize, Ordering::Relaxed); self.volume.store(volume as usize, Ordering::Relaxed);
} }
fn get_audio_filter(&self) -> Option<Box<AudioFilter + Send>> { fn get_audio_filter(&self) -> Option<Box<AudioFilter + Send>> {
Some(Box::new(SoftVolumeApplier { volume: self.volume.clone() })) Some(Box::new(SoftVolumeApplier {
volume: self.volume.clone(),
}))
} }
} }
struct SoftVolumeApplier { struct SoftVolumeApplier {
volume: Arc<AtomicUsize> volume: Arc<AtomicUsize>,
} }
impl AudioFilter for SoftVolumeApplier { impl AudioFilter for SoftVolumeApplier {

View file

@ -1,12 +1,12 @@
use byteorder::{LittleEndian, ReadBytesExt}; use byteorder::{LittleEndian, ReadBytesExt};
use futures::sync::oneshot;
use futures::{future, Future};
use futures; use futures;
use futures::{future, Future};
use futures::sync::oneshot;
use std; use std;
use std::borrow::Cow; use std::borrow::Cow;
use std::io::{Read, Seek, SeekFrom, Result}; use std::io::{Read, Result, Seek, SeekFrom};
use std::mem; use std::mem;
use std::sync::mpsc::{RecvError, TryRecvError, RecvTimeoutError}; use std::sync::mpsc::{RecvError, RecvTimeoutError, TryRecvError};
use std::thread; use std::thread;
use std::time::Duration; use std::time::Duration;
@ -14,10 +14,10 @@ use config::{Bitrate, PlayerConfig};
use core::session::Session; use core::session::Session;
use core::spotify_id::SpotifyId; use core::spotify_id::SpotifyId;
use audio_backend::Sink; use audio::{AudioDecrypt, AudioFile};
use audio::{AudioFile, AudioDecrypt};
use audio::{VorbisDecoder, VorbisPacket}; use audio::{VorbisDecoder, VorbisPacket};
use metadata::{FileFormat, Track, Metadata}; use audio_backend::Sink;
use metadata::{FileFormat, Metadata, Track};
use mixer::AudioFilter; use mixer::AudioFilter;
pub struct Player { pub struct Player {
@ -58,7 +58,7 @@ pub enum PlayerEvent {
Stopped { Stopped {
track_id: SpotifyId, track_id: SpotifyId,
} },
} }
type PlayerEventChannel = futures::sync::mpsc::UnboundedReceiver<PlayerEvent>; type PlayerEventChannel = futures::sync::mpsc::UnboundedReceiver<PlayerEvent>;
@ -74,7 +74,8 @@ struct NormalisationData {
impl NormalisationData { impl NormalisationData {
fn parse_from_file<T: Read + Seek>(mut file: T) -> Result<NormalisationData> { fn parse_from_file<T: Read + Seek>(mut file: T) -> Result<NormalisationData> {
const SPOTIFY_NORMALIZATION_HEADER_START_OFFSET: u64 = 144; const SPOTIFY_NORMALIZATION_HEADER_START_OFFSET: u64 = 144;
file.seek(SeekFrom::Start(SPOTIFY_NORMALIZATION_HEADER_START_OFFSET)).unwrap(); file.seek(SeekFrom::Start(SPOTIFY_NORMALIZATION_HEADER_START_OFFSET))
.unwrap();
let track_gain_db = file.read_f32::<LittleEndian>().unwrap(); let track_gain_db = file.read_f32::<LittleEndian>().unwrap();
let track_peak = file.read_f32::<LittleEndian>().unwrap(); let track_peak = file.read_f32::<LittleEndian>().unwrap();
@ -92,7 +93,10 @@ impl NormalisationData {
} }
fn get_factor(config: &PlayerConfig, data: NormalisationData) -> f32 { fn get_factor(config: &PlayerConfig, data: NormalisationData) -> f32 {
let mut normalisation_factor = f32::powf(10.0, (data.track_gain_db + config.normalisation_pregain) / 20.0); let mut normalisation_factor = f32::powf(
10.0,
(data.track_gain_db + config.normalisation_pregain) / 20.0,
);
if normalisation_factor * data.track_peak > 1.0 { if normalisation_factor * data.track_peak > 1.0 {
warn!("Reducing normalisation factor to prevent clipping. Please add negative pregain to avoid."); warn!("Reducing normalisation factor to prevent clipping. Please add negative pregain to avoid.");
@ -107,10 +111,14 @@ impl NormalisationData {
} }
impl Player { impl Player {
pub fn new<F>(config: PlayerConfig, session: Session, pub fn new<F>(
audio_filter: Option<Box<AudioFilter + Send>>, config: PlayerConfig,
sink_builder: F) -> (Player, PlayerEventChannel) session: Session,
where F: FnOnce() -> Box<Sink> + Send + 'static audio_filter: Option<Box<AudioFilter + Send>>,
sink_builder: F,
) -> (Player, PlayerEventChannel)
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 (event_sender, event_receiver) = futures::sync::mpsc::unbounded();
@ -133,17 +141,25 @@ impl Player {
internal.run(); internal.run();
}); });
(Player { commands: Some(cmd_tx), thread_handle: Some(handle) }, (
event_receiver) Player {
commands: Some(cmd_tx),
thread_handle: Some(handle),
},
event_receiver,
)
} }
fn command(&self, cmd: PlayerCommand) { fn command(&self, cmd: PlayerCommand) {
self.commands.as_ref().unwrap().send(cmd).unwrap(); self.commands.as_ref().unwrap().send(cmd).unwrap();
} }
pub fn load(&self, track: SpotifyId, start_playing: bool, position_ms: u32) pub fn load(
-> oneshot::Receiver<()> &self,
{ track: SpotifyId,
start_playing: bool,
position_ms: u32,
) -> oneshot::Receiver<()> {
let (tx, rx) = oneshot::channel(); let (tx, rx) = oneshot::channel();
self.command(PlayerCommand::Load(track, start_playing, position_ms, tx)); self.command(PlayerCommand::Load(track, start_playing, position_ms, tx));
@ -174,7 +190,7 @@ impl Drop for Player {
if let Some(handle) = self.thread_handle.take() { if let Some(handle) = self.thread_handle.take() {
match handle.join() { match handle.join() {
Ok(_) => (), Ok(_) => (),
Err(_) => error!("Player thread panicked!") Err(_) => error!("Player thread panicked!"),
} }
} }
} }
@ -195,7 +211,9 @@ enum PlayerState {
end_of_track: oneshot::Sender<()>, end_of_track: oneshot::Sender<()>,
normalisation_factor: f32, normalisation_factor: f32,
}, },
EndOfTrack { track_id: SpotifyId }, EndOfTrack {
track_id: SpotifyId,
},
Invalid, Invalid,
} }
@ -213,8 +231,12 @@ impl PlayerState {
use self::PlayerState::*; use self::PlayerState::*;
match *self { match *self {
Stopped | EndOfTrack { .. } => None, Stopped | EndOfTrack { .. } => None,
Paused { ref mut decoder, .. } | Paused {
Playing { ref mut decoder, .. } => Some(decoder), ref mut decoder, ..
}
| Playing {
ref mut decoder, ..
} => Some(decoder),
Invalid => panic!("invalid state"), Invalid => panic!("invalid state"),
} }
} }
@ -222,18 +244,27 @@ impl PlayerState {
fn playing_to_end_of_track(&mut self) { fn playing_to_end_of_track(&mut self) {
use self::PlayerState::*; use self::PlayerState::*;
match mem::replace(self, Invalid) { match mem::replace(self, Invalid) {
Playing { track_id, end_of_track, ..} => { Playing {
track_id,
end_of_track,
..
} => {
let _ = end_of_track.send(()); let _ = end_of_track.send(());
*self = EndOfTrack { track_id }; *self = EndOfTrack { track_id };
}, }
_ => panic!("Called playing_to_end_of_track in non-playing state.") _ => 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 { track_id, decoder, end_of_track, normalisation_factor } => { Paused {
track_id,
decoder,
end_of_track,
normalisation_factor,
} => {
*self = Playing { *self = Playing {
track_id: track_id, track_id: track_id,
decoder: decoder, decoder: decoder,
@ -248,7 +279,12 @@ 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 { track_id, decoder, end_of_track, normalisation_factor } => { Playing {
track_id,
decoder,
end_of_track,
normalisation_factor,
} => {
*self = Paused { *self = Paused {
track_id: track_id, track_id: track_id,
decoder: decoder, decoder: decoder,
@ -265,16 +301,13 @@ impl PlayerInternal {
fn run(mut self) { fn run(mut self) {
loop { loop {
let cmd = if self.state.is_playing() { let cmd = if self.state.is_playing() {
if self.sink_running if self.sink_running {
{
match self.commands.try_recv() { match self.commands.try_recv() {
Ok(cmd) => Some(cmd), Ok(cmd) => Some(cmd),
Err(TryRecvError::Empty) => None, Err(TryRecvError::Empty) => None,
Err(TryRecvError::Disconnected) => return, Err(TryRecvError::Disconnected) => return,
} }
} } else {
else
{
match self.commands.recv_timeout(Duration::from_secs(5)) { match self.commands.recv_timeout(Duration::from_secs(5)) {
Ok(cmd) => Some(cmd), Ok(cmd) => Some(cmd),
Err(RecvTimeoutError::Timeout) => None, Err(RecvTimeoutError::Timeout) => None,
@ -292,14 +325,19 @@ impl PlayerInternal {
self.handle_command(cmd); self.handle_command(cmd);
} }
if self.state.is_playing() && ! self.sink_running { if self.state.is_playing() && !self.sink_running {
self.start_sink(); self.start_sink();
} }
if self.sink_running { if self.sink_running {
let mut current_normalisation_factor: f32 = 1.0; let mut current_normalisation_factor: f32 = 1.0;
let packet = if let PlayerState::Playing { ref mut decoder, normalisation_factor, .. } = self.state { let packet = if let PlayerState::Playing {
ref mut decoder,
normalisation_factor,
..
} = self.state
{
current_normalisation_factor = normalisation_factor; current_normalisation_factor = normalisation_factor;
Some(decoder.next_packet().expect("Vorbis error")) Some(decoder.next_packet().expect("Vorbis error"))
} else { } else {
@ -369,12 +407,17 @@ impl PlayerInternal {
Some((decoder, normalisation_factor)) => { Some((decoder, normalisation_factor)) => {
if play { if play {
match self.state { match self.state {
PlayerState::Playing { track_id: old_track_id, ..} PlayerState::Playing {
| PlayerState::EndOfTrack { track_id: old_track_id, .. } => track_id: old_track_id,
self.send_event(PlayerEvent::Changed { ..
old_track_id: old_track_id, }
new_track_id: 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.send_event(PlayerEvent::Started { track_id }),
} }
@ -394,12 +437,17 @@ impl PlayerInternal {
normalisation_factor: normalisation_factor, normalisation_factor: normalisation_factor,
}; };
match self.state { match self.state {
PlayerState::Playing { track_id: old_track_id, ..} PlayerState::Playing {
| PlayerState::EndOfTrack { track_id: old_track_id, .. } => track_id: old_track_id,
self.send_event(PlayerEvent::Changed { ..
old_track_id: old_track_id, }
new_track_id: 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 }); self.send_event(PlayerEvent::Stopped { track_id });
@ -445,21 +493,19 @@ impl PlayerInternal {
} }
} }
PlayerCommand::Stop => { PlayerCommand::Stop => match self.state {
match self.state { PlayerState::Playing { track_id, .. }
PlayerState::Playing { track_id, .. } | PlayerState::Paused { track_id, .. }
| PlayerState::Paused { track_id, .. } | PlayerState::EndOfTrack { track_id } => {
| PlayerState::EndOfTrack { track_id } => { self.stop_sink_if_running();
self.stop_sink_if_running(); self.send_event(PlayerEvent::Stopped { track_id });
self.send_event(PlayerEvent::Stopped { track_id }); self.state = PlayerState::Stopped;
self.state = PlayerState::Stopped;
},
PlayerState::Stopped => {
warn!("Player::stop called from invalid state");
}
PlayerState::Invalid => panic!("invalid state"),
} }
} PlayerState::Stopped => {
warn!("Player::stop called from invalid state");
}
PlayerState::Invalid => panic!("invalid state"),
},
} }
} }
@ -471,14 +517,16 @@ impl PlayerInternal {
if track.available { if track.available {
Some(Cow::Borrowed(track)) Some(Cow::Borrowed(track))
} else { } else {
let alternatives = track.alternatives let alternatives = track
.alternatives
.iter() .iter()
.map(|alt_id| { .map(|alt_id| Track::get(&self.session, *alt_id));
Track::get(&self.session, *alt_id)
});
let alternatives = future::join_all(alternatives).wait().unwrap(); let alternatives = future::join_all(alternatives).wait().unwrap();
alternatives.into_iter().find(|alt| alt.available).map(Cow::Owned) alternatives
.into_iter()
.find(|alt| alt.available)
.map(Cow::Owned)
} }
} }
@ -504,12 +552,19 @@ impl PlayerInternal {
let file_id = match track.files.get(&format) { let file_id = match track.files.get(&format) {
Some(&file_id) => file_id, Some(&file_id) => file_id,
None => { None => {
warn!("Track \"{}\" is not available in format {:?}", track.name, format); warn!(
"Track \"{}\" is not available in format {:?}",
track.name, format
);
return None; return None;
} }
}; };
let key = self.session.audio_key().request(track.id, file_id).wait().unwrap(); let key = self.session
.audio_key()
.request(track.id, file_id)
.wait()
.unwrap();
let encrypted_file = AudioFile::open(&self.session, file_id).wait().unwrap(); let encrypted_file = AudioFile::open(&self.session, file_id).wait().unwrap();
@ -520,7 +575,7 @@ impl PlayerInternal {
Err(_) => { Err(_) => {
warn!("Unable to extract normalisation data, using default value."); warn!("Unable to extract normalisation data, using default value.");
1.0 as f32 1.0 as f32
}, }
}; };
let audio_file = Subfile::new(decrypted_file, 0xa7); let audio_file = Subfile::new(decrypted_file, 0xa7);
@ -547,27 +602,15 @@ impl Drop for PlayerInternal {
impl ::std::fmt::Debug for PlayerCommand { impl ::std::fmt::Debug for PlayerCommand {
fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result { fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result {
match *self { match *self {
PlayerCommand::Load(track, play, position, _) => { PlayerCommand::Load(track, play, position, _) => f.debug_tuple("Load")
f.debug_tuple("Load") .field(&track)
.field(&track) .field(&play)
.field(&play) .field(&position)
.field(&position) .finish(),
.finish() PlayerCommand::Play => f.debug_tuple("Play").finish(),
} PlayerCommand::Pause => f.debug_tuple("Pause").finish(),
PlayerCommand::Play => { PlayerCommand::Stop => f.debug_tuple("Stop").finish(),
f.debug_tuple("Play").finish() PlayerCommand::Seek(position) => f.debug_tuple("Seek").field(&position).finish(),
}
PlayerCommand::Pause => {
f.debug_tuple("Pause").finish()
}
PlayerCommand::Stop => {
f.debug_tuple("Stop").finish()
}
PlayerCommand::Seek(position) => {
f.debug_tuple("Seek")
.field(&position)
.finish()
}
} }
} }
} }

View file

@ -1,5 +1,5 @@
use std::io::prelude::*;
use std::fs::File; use std::fs::File;
use std::io::prelude::*;
mod files; mod files;
@ -7,7 +7,10 @@ fn main() {
for &(path, expected_checksum) in files::FILES { for &(path, expected_checksum) in files::FILES {
let actual = cksum_file(path).unwrap(); let actual = cksum_file(path).unwrap();
if expected_checksum != actual { if expected_checksum != actual {
panic!("Checksum for {:?} does not match. Try running build.sh", path); panic!(
"Checksum for {:?} does not match. Try running build.sh",
path
);
} }
} }
} }
@ -23,83 +26,51 @@ fn cksum_file<T: AsRef<std::path::Path>>(path: T) -> std::io::Result<u32> {
fn cksum<T: AsRef<[u8]>>(data: T) -> u32 { fn cksum<T: AsRef<[u8]>>(data: T) -> u32 {
let data = data.as_ref(); let data = data.as_ref();
let mut value = 0u32; let mut value = 0u32;
for x in data { for x in data {
value = (value << 8) ^ CRC_LOOKUP_ARRAY[(*x as u32 ^ (value >> 24)) as usize]; value = (value << 8) ^ CRC_LOOKUP_ARRAY[(*x as u32 ^ (value >> 24)) as usize];
} }
let mut n = data.len(); let mut n = data.len();
while n != 0 { while n != 0 {
value = (value << 8) ^ CRC_LOOKUP_ARRAY[((n & 0xFF) as u32 ^ (value >> 24)) as usize]; value = (value << 8) ^ CRC_LOOKUP_ARRAY[((n & 0xFF) as u32 ^ (value >> 24)) as usize];
n >>= 8; n >>= 8;
} }
!value !value
} }
static CRC_LOOKUP_ARRAY : &'static[u32] = &[ static CRC_LOOKUP_ARRAY: &'static [u32] = &[
0x00000000, 0x04c11db7, 0x09823b6e, 0x0d4326d9, 0x00000000, 0x04c11db7, 0x09823b6e, 0x0d4326d9, 0x130476dc, 0x17c56b6b, 0x1a864db2, 0x1e475005,
0x130476dc, 0x17c56b6b, 0x1a864db2, 0x1e475005, 0x2608edb8, 0x22c9f00f, 0x2f8ad6d6, 0x2b4bcb61, 0x350c9b64, 0x31cd86d3, 0x3c8ea00a, 0x384fbdbd,
0x2608edb8, 0x22c9f00f, 0x2f8ad6d6, 0x2b4bcb61, 0x4c11db70, 0x48d0c6c7, 0x4593e01e, 0x4152fda9, 0x5f15adac, 0x5bd4b01b, 0x569796c2, 0x52568b75,
0x350c9b64, 0x31cd86d3, 0x3c8ea00a, 0x384fbdbd, 0x6a1936c8, 0x6ed82b7f, 0x639b0da6, 0x675a1011, 0x791d4014, 0x7ddc5da3, 0x709f7b7a, 0x745e66cd,
0x4c11db70, 0x48d0c6c7, 0x4593e01e, 0x4152fda9, 0x9823b6e0, 0x9ce2ab57, 0x91a18d8e, 0x95609039, 0x8b27c03c, 0x8fe6dd8b, 0x82a5fb52, 0x8664e6e5,
0x5f15adac, 0x5bd4b01b, 0x569796c2, 0x52568b75, 0xbe2b5b58, 0xbaea46ef, 0xb7a96036, 0xb3687d81, 0xad2f2d84, 0xa9ee3033, 0xa4ad16ea, 0xa06c0b5d,
0x6a1936c8, 0x6ed82b7f, 0x639b0da6, 0x675a1011, 0xd4326d90, 0xd0f37027, 0xddb056fe, 0xd9714b49, 0xc7361b4c, 0xc3f706fb, 0xceb42022, 0xca753d95,
0x791d4014, 0x7ddc5da3, 0x709f7b7a, 0x745e66cd, 0xf23a8028, 0xf6fb9d9f, 0xfbb8bb46, 0xff79a6f1, 0xe13ef6f4, 0xe5ffeb43, 0xe8bccd9a, 0xec7dd02d,
0x9823b6e0, 0x9ce2ab57, 0x91a18d8e, 0x95609039, 0x34867077, 0x30476dc0, 0x3d044b19, 0x39c556ae, 0x278206ab, 0x23431b1c, 0x2e003dc5, 0x2ac12072,
0x8b27c03c, 0x8fe6dd8b, 0x82a5fb52, 0x8664e6e5, 0x128e9dcf, 0x164f8078, 0x1b0ca6a1, 0x1fcdbb16, 0x018aeb13, 0x054bf6a4, 0x0808d07d, 0x0cc9cdca,
0xbe2b5b58, 0xbaea46ef, 0xb7a96036, 0xb3687d81, 0x7897ab07, 0x7c56b6b0, 0x71159069, 0x75d48dde, 0x6b93dddb, 0x6f52c06c, 0x6211e6b5, 0x66d0fb02,
0xad2f2d84, 0xa9ee3033, 0xa4ad16ea, 0xa06c0b5d, 0x5e9f46bf, 0x5a5e5b08, 0x571d7dd1, 0x53dc6066, 0x4d9b3063, 0x495a2dd4, 0x44190b0d, 0x40d816ba,
0xd4326d90, 0xd0f37027, 0xddb056fe, 0xd9714b49, 0xaca5c697, 0xa864db20, 0xa527fdf9, 0xa1e6e04e, 0xbfa1b04b, 0xbb60adfc, 0xb6238b25, 0xb2e29692,
0xc7361b4c, 0xc3f706fb, 0xceb42022, 0xca753d95, 0x8aad2b2f, 0x8e6c3698, 0x832f1041, 0x87ee0df6, 0x99a95df3, 0x9d684044, 0x902b669d, 0x94ea7b2a,
0xf23a8028, 0xf6fb9d9f, 0xfbb8bb46, 0xff79a6f1, 0xe0b41de7, 0xe4750050, 0xe9362689, 0xedf73b3e, 0xf3b06b3b, 0xf771768c, 0xfa325055, 0xfef34de2,
0xe13ef6f4, 0xe5ffeb43, 0xe8bccd9a, 0xec7dd02d, 0xc6bcf05f, 0xc27dede8, 0xcf3ecb31, 0xcbffd686, 0xd5b88683, 0xd1799b34, 0xdc3abded, 0xd8fba05a,
0x34867077, 0x30476dc0, 0x3d044b19, 0x39c556ae, 0x690ce0ee, 0x6dcdfd59, 0x608edb80, 0x644fc637, 0x7a089632, 0x7ec98b85, 0x738aad5c, 0x774bb0eb,
0x278206ab, 0x23431b1c, 0x2e003dc5, 0x2ac12072, 0x4f040d56, 0x4bc510e1, 0x46863638, 0x42472b8f, 0x5c007b8a, 0x58c1663d, 0x558240e4, 0x51435d53,
0x128e9dcf, 0x164f8078, 0x1b0ca6a1, 0x1fcdbb16, 0x251d3b9e, 0x21dc2629, 0x2c9f00f0, 0x285e1d47, 0x36194d42, 0x32d850f5, 0x3f9b762c, 0x3b5a6b9b,
0x018aeb13, 0x054bf6a4, 0x0808d07d, 0x0cc9cdca, 0x0315d626, 0x07d4cb91, 0x0a97ed48, 0x0e56f0ff, 0x1011a0fa, 0x14d0bd4d, 0x19939b94, 0x1d528623,
0x7897ab07, 0x7c56b6b0, 0x71159069, 0x75d48dde, 0xf12f560e, 0xf5ee4bb9, 0xf8ad6d60, 0xfc6c70d7, 0xe22b20d2, 0xe6ea3d65, 0xeba91bbc, 0xef68060b,
0x6b93dddb, 0x6f52c06c, 0x6211e6b5, 0x66d0fb02, 0xd727bbb6, 0xd3e6a601, 0xdea580d8, 0xda649d6f, 0xc423cd6a, 0xc0e2d0dd, 0xcda1f604, 0xc960ebb3,
0x5e9f46bf, 0x5a5e5b08, 0x571d7dd1, 0x53dc6066, 0xbd3e8d7e, 0xb9ff90c9, 0xb4bcb610, 0xb07daba7, 0xae3afba2, 0xaafbe615, 0xa7b8c0cc, 0xa379dd7b,
0x4d9b3063, 0x495a2dd4, 0x44190b0d, 0x40d816ba, 0x9b3660c6, 0x9ff77d71, 0x92b45ba8, 0x9675461f, 0x8832161a, 0x8cf30bad, 0x81b02d74, 0x857130c3,
0xaca5c697, 0xa864db20, 0xa527fdf9, 0xa1e6e04e, 0x5d8a9099, 0x594b8d2e, 0x5408abf7, 0x50c9b640, 0x4e8ee645, 0x4a4ffbf2, 0x470cdd2b, 0x43cdc09c,
0xbfa1b04b, 0xbb60adfc, 0xb6238b25, 0xb2e29692, 0x7b827d21, 0x7f436096, 0x7200464f, 0x76c15bf8, 0x68860bfd, 0x6c47164a, 0x61043093, 0x65c52d24,
0x8aad2b2f, 0x8e6c3698, 0x832f1041, 0x87ee0df6, 0x119b4be9, 0x155a565e, 0x18197087, 0x1cd86d30, 0x029f3d35, 0x065e2082, 0x0b1d065b, 0x0fdc1bec,
0x99a95df3, 0x9d684044, 0x902b669d, 0x94ea7b2a, 0x3793a651, 0x3352bbe6, 0x3e119d3f, 0x3ad08088, 0x2497d08d, 0x2056cd3a, 0x2d15ebe3, 0x29d4f654,
0xe0b41de7, 0xe4750050, 0xe9362689, 0xedf73b3e, 0xc5a92679, 0xc1683bce, 0xcc2b1d17, 0xc8ea00a0, 0xd6ad50a5, 0xd26c4d12, 0xdf2f6bcb, 0xdbee767c,
0xf3b06b3b, 0xf771768c, 0xfa325055, 0xfef34de2, 0xe3a1cbc1, 0xe760d676, 0xea23f0af, 0xeee2ed18, 0xf0a5bd1d, 0xf464a0aa, 0xf9278673, 0xfde69bc4,
0xc6bcf05f, 0xc27dede8, 0xcf3ecb31, 0xcbffd686, 0x89b8fd09, 0x8d79e0be, 0x803ac667, 0x84fbdbd0, 0x9abc8bd5, 0x9e7d9662, 0x933eb0bb, 0x97ffad0c,
0xd5b88683, 0xd1799b34, 0xdc3abded, 0xd8fba05a, 0xafb010b1, 0xab710d06, 0xa6322bdf, 0xa2f33668, 0xbcb4666d, 0xb8757bda, 0xb5365d03, 0xb1f740b4,
0x690ce0ee, 0x6dcdfd59, 0x608edb80, 0x644fc637,
0x7a089632, 0x7ec98b85, 0x738aad5c, 0x774bb0eb,
0x4f040d56, 0x4bc510e1, 0x46863638, 0x42472b8f,
0x5c007b8a, 0x58c1663d, 0x558240e4, 0x51435d53,
0x251d3b9e, 0x21dc2629, 0x2c9f00f0, 0x285e1d47,
0x36194d42, 0x32d850f5, 0x3f9b762c, 0x3b5a6b9b,
0x0315d626, 0x07d4cb91, 0x0a97ed48, 0x0e56f0ff,
0x1011a0fa, 0x14d0bd4d, 0x19939b94, 0x1d528623,
0xf12f560e, 0xf5ee4bb9, 0xf8ad6d60, 0xfc6c70d7,
0xe22b20d2, 0xe6ea3d65, 0xeba91bbc, 0xef68060b,
0xd727bbb6, 0xd3e6a601, 0xdea580d8, 0xda649d6f,
0xc423cd6a, 0xc0e2d0dd, 0xcda1f604, 0xc960ebb3,
0xbd3e8d7e, 0xb9ff90c9, 0xb4bcb610, 0xb07daba7,
0xae3afba2, 0xaafbe615, 0xa7b8c0cc, 0xa379dd7b,
0x9b3660c6, 0x9ff77d71, 0x92b45ba8, 0x9675461f,
0x8832161a, 0x8cf30bad, 0x81b02d74, 0x857130c3,
0x5d8a9099, 0x594b8d2e, 0x5408abf7, 0x50c9b640,
0x4e8ee645, 0x4a4ffbf2, 0x470cdd2b, 0x43cdc09c,
0x7b827d21, 0x7f436096, 0x7200464f, 0x76c15bf8,
0x68860bfd, 0x6c47164a, 0x61043093, 0x65c52d24,
0x119b4be9, 0x155a565e, 0x18197087, 0x1cd86d30,
0x029f3d35, 0x065e2082, 0x0b1d065b, 0x0fdc1bec,
0x3793a651, 0x3352bbe6, 0x3e119d3f, 0x3ad08088,
0x2497d08d, 0x2056cd3a, 0x2d15ebe3, 0x29d4f654,
0xc5a92679, 0xc1683bce, 0xcc2b1d17, 0xc8ea00a0,
0xd6ad50a5, 0xd26c4d12, 0xdf2f6bcb, 0xdbee767c,
0xe3a1cbc1, 0xe760d676, 0xea23f0af, 0xeee2ed18,
0xf0a5bd1d, 0xf464a0aa, 0xf9278673, 0xfde69bc4,
0x89b8fd09, 0x8d79e0be, 0x803ac667, 0x84fbdbd0,
0x9abc8bd5, 0x9e7d9662, 0x933eb0bb, 0x97ffad0c,
0xafb010b1, 0xab710d06, 0xa6322bdf, 0xa2f33668,
0xbcb4666d, 0xb8757bda, 0xb5365d03, 0xb1f740b4
]; ];

View file

@ -1,6 +1,6 @@
// Autogenerated by build.sh // Autogenerated by build.sh
pub const FILES : &'static [(&'static str, u32)] = &[ pub const FILES: &'static [(&'static str, u32)] = &[
("proto/authentication.proto", 2098196376), ("proto/authentication.proto", 2098196376),
("proto/keyexchange.proto", 451735664), ("proto/keyexchange.proto", 451735664),
("proto/mercury.proto", 709993906), ("proto/mercury.proto", 709993906),

View file

@ -1,5 +1,4 @@
#![crate_name = "librespot"] #![crate_name = "librespot"]
#![cfg_attr(feature = "cargo-clippy", allow(unused_io_amount))] #![cfg_attr(feature = "cargo-clippy", allow(unused_io_amount))]
extern crate base64; extern crate base64;
@ -15,6 +14,6 @@ extern crate url;
pub extern crate librespot_audio as audio; pub extern crate librespot_audio as audio;
pub extern crate librespot_connect as connect; pub extern crate librespot_connect as connect;
pub extern crate librespot_core as core; pub extern crate librespot_core as core;
pub extern crate librespot_metadata as metadata;
pub extern crate librespot_playback as playback; pub extern crate librespot_playback as playback;
pub extern crate librespot_protocol as protocol; pub extern crate librespot_protocol as protocol;
pub extern crate librespot_metadata as metadata;

View file

@ -1,40 +1,41 @@
#[macro_use] extern crate log; extern crate crypto;
extern crate env_logger; extern crate env_logger;
extern crate futures; extern crate futures;
extern crate getopts; extern crate getopts;
extern crate librespot; extern crate librespot;
#[macro_use]
extern crate log;
extern crate rpassword; extern crate rpassword;
extern crate tokio_core; extern crate tokio_core;
extern crate tokio_io; extern crate tokio_io;
extern crate tokio_signal; extern crate tokio_signal;
extern crate crypto;
use crypto::digest::Digest;
use crypto::sha1::Sha1;
use env_logger::LogBuilder; use env_logger::LogBuilder;
use futures::{Future, Async, Poll, Stream}; use futures::{Async, Future, Poll, Stream};
use futures::sync::mpsc::UnboundedReceiver; 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::mem;
use std::path::PathBuf; use std::path::PathBuf;
use std::process::exit; use std::process::exit;
use std::str::FromStr; use std::str::FromStr;
use tokio_core::reactor::{Handle, Core}; use tokio_core::reactor::{Core, Handle};
use tokio_io::IoStream; use tokio_io::IoStream;
use std::mem;
use crypto::digest::Digest;
use crypto::sha1::Sha1;
use librespot::core::authentication::{get_credentials, Credentials}; use librespot::core::authentication::{get_credentials, Credentials};
use librespot::core::cache::Cache; use librespot::core::cache::Cache;
use librespot::core::config::{DeviceType, SessionConfig, ConnectConfig}; use librespot::core::config::{ConnectConfig, DeviceType, SessionConfig};
use librespot::core::session::Session; use librespot::core::session::Session;
use librespot::core::version; use librespot::core::version;
use librespot::connect::discovery::{discovery, DiscoveryStream};
use librespot::connect::spirc::{Spirc, SpircTask};
use librespot::playback::audio_backend::{self, Sink, BACKENDS}; 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::playback::mixer::{self, Mixer}; use librespot::playback::mixer::{self, Mixer};
use librespot::playback::player::{Player, PlayerEvent}; use librespot::playback::player::{Player, PlayerEvent};
use librespot::connect::spirc::{Spirc, SpircTask};
mod player_event_handler; mod player_event_handler;
use player_event_handler::run_program_on_events; use player_event_handler::run_program_on_events;
@ -102,28 +103,80 @@ struct Setup {
fn setup(args: &[String]) -> Setup { fn setup(args: &[String]) -> Setup {
let mut opts = getopts::Options::new(); let mut opts = getopts::Options::new();
opts.optopt("c", "cache", "Path to a directory where files will be cached.", "CACHE") opts.optopt(
.optflag("", "disable-audio-cache", "Disable caching of the audio data.") "c",
"cache",
"Path to a directory where files will be cached.",
"CACHE",
).optflag(
"",
"disable-audio-cache",
"Disable caching of the audio data.",
)
.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(
.optopt("", "onevent", "Run PROGRAM when playback is about to begin.", "PROGRAM") "b",
"bitrate",
"Bitrate (96, 160 or 320). Defaults to 160",
"BITRATE",
)
.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")
.optflag("", "disable-discovery", "Disable discovery mode") .optflag("", "disable-discovery", "Disable discovery mode")
.optopt("", "backend", "Audio backend to use. Use '?' to list options", "BACKEND") .optopt(
.optopt("", "device", "Audio device to use. Use '?' to list options if using portaudio", "DEVICE") "",
"backend",
"Audio backend to use. Use '?' to list options",
"BACKEND",
)
.optopt(
"",
"device",
"Audio device to use. Use '?' to list options if using portaudio",
"DEVICE",
)
.optopt("", "mixer", "Mixer to use", "MIXER") .optopt("", "mixer", "Mixer to use", "MIXER")
.optopt("", "initial-volume", "Initial volume in %, once connected (must be from 0 to 100)", "VOLUME") .optopt(
.optopt("", "zeroconf-port", "The port the internal server advertised over zeroconf uses.", "ZEROCONF_PORT") "",
.optflag("", "enable-volume-normalisation", "Play all tracks at the same volume") "initial-volume",
.optopt("", "normalisation-pregain", "Pregain (dB) applied by volume normalisation", "PREGAIN"); "Initial volume in %, once connected (must be from 0 to 100)",
"VOLUME",
)
.optopt(
"",
"zeroconf-port",
"The port the internal server advertised over zeroconf uses.",
"ZEROCONF_PORT",
)
.optflag(
"",
"enable-volume-normalisation",
"Play all tracks at the same volume",
)
.optopt(
"",
"normalisation-pregain",
"Pregain (dB) applied by volume normalisation",
"PREGAIN",
);
let matches = match opts.parse(&args[1..]) { let matches = match opts.parse(&args[1..]) {
Ok(m) => m, Ok(m) => m,
Err(f) => { Err(f) => {
writeln!(stderr(), "error: {}\n{}", f.to_string(), usage(&args[0], &opts)).unwrap(); writeln!(
stderr(),
"error: {}\n{}",
f.to_string(),
usage(&args[0], &opts)
).unwrap();
exit(1); exit(1);
} }
}; };
@ -131,11 +184,13 @@ fn setup(args: &[String]) -> Setup {
let verbose = matches.opt_present("verbose"); let verbose = matches.opt_present("verbose");
setup_logging(verbose); setup_logging(verbose);
info!("librespot {} ({}). Built on {}. Build ID: {}", info!(
version::short_sha(), "librespot {} ({}). Built on {}. Build ID: {}",
version::commit_date(), version::short_sha(),
version::short_now(), version::commit_date(),
version::build_id()); version::short_now(),
version::build_id()
);
let backend_name = matches.opt_str("backend"); let backend_name = matches.opt_str("backend");
if backend_name == Some("?".into()) { if backend_name == Some("?".into()) {
@ -143,14 +198,12 @@ fn setup(args: &[String]) -> Setup {
exit(0); exit(0);
} }
let backend = audio_backend::find(backend_name) let backend = audio_backend::find(backend_name).expect("Invalid backend");
.expect("Invalid backend");
let device = matches.opt_str("device"); let device = matches.opt_str("device");
let mixer_name = matches.opt_str("mixer"); let mixer_name = matches.opt_str("mixer");
let mixer = mixer::find(mixer_name.as_ref()) let mixer = mixer::find(mixer_name.as_ref()).expect("Invalid mixer");
.expect("Invalid mixer");
let initial_volume = matches let initial_volume = matches
.opt_str("initial-volume") .opt_str("initial-volume")
@ -163,17 +216,17 @@ fn setup(args: &[String]) -> Setup {
}) })
.unwrap_or(0x8000); .unwrap_or(0x8000);
let zeroconf_port = let zeroconf_port = matches
matches.opt_str("zeroconf-port") .opt_str("zeroconf-port")
.map(|port| port.parse::<u16>().unwrap()) .map(|port| port.parse::<u16>().unwrap())
.unwrap_or(0); .unwrap_or(0);
let name = matches.opt_str("name").unwrap(); let name = matches.opt_str("name").unwrap();
let use_audio_cache = !matches.opt_present("disable-audio-cache"); let use_audio_cache = !matches.opt_present("disable-audio-cache");
let cache = matches.opt_str("c").map(|cache_location| { let cache = matches
Cache::new(PathBuf::from(cache_location), use_audio_cache) .opt_str("c")
}); .map(|cache_location| Cache::new(PathBuf::from(cache_location), use_audio_cache));
let credentials = { let credentials = {
let cached_credentials = cache.as_ref().and_then(Cache::credentials); let cached_credentials = cache.as_ref().and_then(Cache::credentials);
@ -188,7 +241,7 @@ fn setup(args: &[String]) -> Setup {
matches.opt_str("username"), matches.opt_str("username"),
matches.opt_str("password"), matches.opt_str("password"),
cached_credentials, cached_credentials,
password password,
) )
}; };
@ -202,21 +255,26 @@ fn setup(args: &[String]) -> Setup {
}; };
let player_config = { let player_config = {
let bitrate = matches.opt_str("b").as_ref() let bitrate = matches
.opt_str("b")
.as_ref()
.map(|bitrate| Bitrate::from_str(bitrate).expect("Invalid bitrate")) .map(|bitrate| Bitrate::from_str(bitrate).expect("Invalid bitrate"))
.unwrap_or(Bitrate::default()); .unwrap_or(Bitrate::default());
PlayerConfig { PlayerConfig {
bitrate: bitrate, bitrate: bitrate,
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"))
.unwrap_or(PlayerConfig::default().normalisation_pregain), .unwrap_or(PlayerConfig::default().normalisation_pregain),
} }
}; };
let connect_config = { let connect_config = {
let device_type = matches.opt_str("device-type").as_ref() let device_type = matches
.opt_str("device-type")
.as_ref()
.map(|device_type| DeviceType::from_str(device_type).expect("Invalid device type")) .map(|device_type| DeviceType::from_str(device_type).expect("Invalid device type"))
.unwrap_or(DeviceType::default()); .unwrap_or(DeviceType::default());
@ -259,7 +317,7 @@ struct Main {
spirc: Option<Spirc>, spirc: Option<Spirc>,
spirc_task: Option<SpircTask>, spirc_task: Option<SpircTask>,
connect: Box<Future<Item=Session, Error=io::Error>>, connect: Box<Future<Item = Session, Error = io::Error>>,
shutdown: bool, shutdown: bool,
@ -345,9 +403,10 @@ 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, event_channel) = Player::new(player_config, session.clone(), audio_filter, move || { let (player, event_channel) =
(backend)(device) Player::new(player_config, session.clone(), audio_filter, move || {
}); (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);

View file

@ -1,6 +1,6 @@
use std::process::Command;
use std::collections::HashMap;
use librespot::playback::player::PlayerEvent; use librespot::playback::player::PlayerEvent;
use std::collections::HashMap;
use std::process::Command;
fn run_program(program: &str, env_vars: HashMap<&str, String>) { fn run_program(program: &str, env_vars: HashMap<&str, String>) {
let mut v: Vec<&str> = program.split_whitespace().collect(); let mut v: Vec<&str> = program.split_whitespace().collect();
@ -15,16 +15,19 @@ fn run_program(program: &str, env_vars: HashMap<&str, String>) {
pub fn run_program_on_events(event: PlayerEvent, onevent: &str) { pub fn run_program_on_events(event: PlayerEvent, onevent: &str) {
let mut env_vars = HashMap::new(); let mut env_vars = HashMap::new();
match event { match event {
PlayerEvent::Changed { old_track_id, new_track_id } => { PlayerEvent::Changed {
old_track_id,
new_track_id,
} => {
env_vars.insert("PLAYER_EVENT", "change".to_string()); env_vars.insert("PLAYER_EVENT", "change".to_string());
env_vars.insert("OLD_TRACK_ID", old_track_id.to_base16()); env_vars.insert("OLD_TRACK_ID", old_track_id.to_base16());
env_vars.insert("TRACK_ID", new_track_id.to_base16()); env_vars.insert("TRACK_ID", new_track_id.to_base16());
}, }
PlayerEvent::Started { track_id } => { PlayerEvent::Started { track_id } => {
env_vars.insert("PLAYER_EVENT", "start".to_string()); env_vars.insert("PLAYER_EVENT", "start".to_string());
env_vars.insert("TRACK_ID", track_id.to_base16()); env_vars.insert("TRACK_ID", track_id.to_base16());
} }
PlayerEvent::Stopped { track_id } => { PlayerEvent::Stopped { track_id } => {
env_vars.insert("PLAYER_EVENT", "stop".to_string()); env_vars.insert("PLAYER_EVENT", "stop".to_string());
env_vars.insert("TRACK_ID", track_id.to_base16()); env_vars.insert("TRACK_ID", track_id.to_base16());
} }