librespot/src/main.rs

474 lines
14 KiB
Rust
Raw Normal View History

2018-02-26 01:50:41 +00:00
extern crate crypto;
2017-01-10 16:31:12 +00:00
extern crate env_logger;
2017-01-18 18:41:22 +00:00
extern crate futures;
2017-02-21 23:25:04 +00:00
extern crate getopts;
extern crate librespot;
2018-02-26 01:50:41 +00:00
#[macro_use]
extern crate log;
2018-02-25 04:40:00 +00:00
extern crate rpassword;
2017-01-18 18:41:22 +00:00
extern crate tokio_core;
extern crate tokio_io;
extern crate tokio_signal;
2015-04-25 20:32:07 +00:00
2018-02-26 01:50:41 +00:00
use crypto::digest::Digest;
use crypto::sha1::Sha1;
2017-01-10 16:31:12 +00:00
use env_logger::LogBuilder;
2018-02-26 01:50:41 +00:00
use futures::{Async, Future, Poll, Stream};
use futures::sync::mpsc::UnboundedReceiver;
2017-01-10 16:31:12 +00:00
use std::env;
use std::io::{self, stderr, Write};
2018-02-26 01:50:41 +00:00
use std::mem;
2017-01-10 16:31:12 +00:00
use std::path::PathBuf;
2017-02-21 23:25:04 +00:00
use std::process::exit;
2017-01-10 16:31:12 +00:00
use std::str::FromStr;
2018-02-26 01:50:41 +00:00
use tokio_core::reactor::{Core, Handle};
use tokio_io::IoStream;
2017-08-03 18:58:44 +00:00
use librespot::core::authentication::{get_credentials, Credentials};
use librespot::core::cache::Cache;
2018-02-26 01:50:41 +00:00
use librespot::core::config::{ConnectConfig, DeviceType, SessionConfig};
2017-08-03 18:58:44 +00:00
use librespot::core::session::Session;
use librespot::core::version;
2018-02-26 01:50:41 +00:00
use librespot::connect::discovery::{discovery, DiscoveryStream};
use librespot::connect::spirc::{Spirc, SpircTask};
use librespot::playback::audio_backend::{self, Sink, BACKENDS};
use librespot::playback::config::{Bitrate, PlayerConfig};
use librespot::playback::mixer::{self, Mixer};
use librespot::playback::player::{Player, PlayerEvent};
2016-03-16 00:05:05 +00:00
mod player_event_handler;
use player_event_handler::run_program_on_events;
fn device_id(name: &str) -> String {
let mut h = Sha1::new();
h.input_str(name);
h.result_str()
}
fn usage(program: &str, opts: &getopts::Options) -> String {
let brief = format!("Usage: {} [options]", program);
2017-01-29 16:25:09 +00:00
opts.usage(&brief)
}
2017-01-10 16:31:12 +00:00
fn setup_logging(verbose: bool) {
let mut builder = LogBuilder::new();
match env::var("RUST_LOG") {
Ok(config) => {
builder.parse(&config);
builder.init().unwrap();
if verbose {
warn!("`--verbose` flag overidden by `RUST_LOG` environment variable");
}
}
Err(_) => {
if verbose {
builder.parse("mdns=info,librespot=trace");
} else {
builder.parse("mdns=info,librespot=info");
}
builder.init().unwrap();
}
}
}
fn list_backends() {
println!("Available Backends : ");
for (&(name, _), idx) in BACKENDS.iter().zip(0..) {
if idx == 0 {
println!("- {} (default)", name);
} else {
println!("- {}", name);
}
}
}
#[derive(Clone)]
2017-01-18 18:41:22 +00:00
struct Setup {
backend: fn(Option<String>) -> Box<Sink>,
device: Option<String>,
mixer: fn() -> Box<Mixer>,
cache: Option<Cache>,
player_config: PlayerConfig,
session_config: SessionConfig,
connect_config: ConnectConfig,
credentials: Option<Credentials>,
enable_discovery: bool,
2018-01-30 20:38:54 +00:00
zeroconf_port: u16,
player_event_program: Option<String>,
2017-01-18 18:41:22 +00:00
}
fn setup(args: &[String]) -> Setup {
2017-01-10 16:31:12 +00:00
let mut opts = getopts::Options::new();
2018-02-26 01:50:41 +00:00
opts.optopt(
"c",
"cache",
"Path to a directory where files will be cached.",
"CACHE",
).optflag(
"",
"disable-audio-cache",
"Disable caching of the audio data.",
)
2017-01-10 16:31:12 +00:00
.reqopt("n", "name", "Device name", "NAME")
.optopt("", "device-type", "Displayed device type", "DEVICE_TYPE")
2018-02-26 01:50:41 +00:00
.optopt(
"b",
"bitrate",
"Bitrate (96, 160 or 320). Defaults to 160",
"BITRATE",
)
.optopt(
"",
"onevent",
"Run PROGRAM when playback is about to begin.",
"PROGRAM",
)
2017-01-10 16:31:12 +00:00
.optflag("v", "verbose", "Enable verbose output")
.optopt("u", "username", "Username to sign in with", "USERNAME")
.optopt("p", "password", "Password", "PASSWORD")
.optflag("", "disable-discovery", "Disable discovery mode")
2018-02-26 01:50:41 +00:00
.optopt(
"",
"backend",
"Audio backend to use. Use '?' to list options",
"BACKEND",
)
.optopt(
"",
"device",
"Audio device to use. Use '?' to list options if using portaudio",
"DEVICE",
)
2017-12-06 13:37:34 +00:00
.optopt("", "mixer", "Mixer to use", "MIXER")
2018-02-26 01:50:41 +00:00
.optopt(
"",
"initial-volume",
"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",
2018-03-11 10:27:28 +00:00
)
.optflag(
"",
"linear-volume",
"increase volume linear instead of logarithmic.",
2018-02-26 01:50:41 +00:00
);
let matches = match opts.parse(&args[1..]) {
2016-01-02 15:19:39 +00:00
Ok(m) => m,
Err(f) => {
2018-02-26 01:50:41 +00:00
writeln!(
stderr(),
"error: {}\n{}",
f.to_string(),
usage(&args[0], &opts)
).unwrap();
exit(1);
2016-04-24 11:15:53 +00:00
}
};
2017-01-10 16:31:12 +00:00
let verbose = matches.opt_present("verbose");
setup_logging(verbose);
2018-02-26 01:50:41 +00:00
info!(
"librespot {} ({}). Built on {}. Build ID: {}",
version::short_sha(),
version::commit_date(),
version::short_now(),
version::build_id()
);
2017-01-10 16:31:12 +00:00
let backend_name = matches.opt_str("backend");
if backend_name == Some("?".into()) {
list_backends();
exit(0);
}
2018-02-26 01:50:41 +00:00
let backend = audio_backend::find(backend_name).expect("Invalid backend");
2017-01-10 16:31:12 +00:00
let device = matches.opt_str("device");
let mixer_name = matches.opt_str("mixer");
2018-02-26 01:50:41 +00:00
let mixer = mixer::find(mixer_name.as_ref()).expect("Invalid mixer");
2018-01-30 23:05:54 +00:00
let initial_volume = matches
.opt_str("initial-volume")
.map(|volume| {
let volume = volume.parse::<i32>().unwrap();
if volume < 0 || volume > 100 {
panic!("Initial volume must be in the range 0-100");
}
volume * 0xFFFF / 100
})
.unwrap_or(0x8000);
2018-02-26 01:50:41 +00:00
let zeroconf_port = matches
.opt_str("zeroconf-port")
.map(|port| port.parse::<u16>().unwrap())
.unwrap_or(0);
2018-01-30 20:38:54 +00:00
2017-01-18 18:41:22 +00:00
let name = matches.opt_str("name").unwrap();
let use_audio_cache = !matches.opt_present("disable-audio-cache");
2017-01-18 17:07:20 +00:00
2018-02-26 01:50:41 +00:00
let cache = matches
.opt_str("c")
.map(|cache_location| Cache::new(PathBuf::from(cache_location), use_audio_cache));
2017-01-18 17:07:20 +00:00
let credentials = {
let cached_credentials = cache.as_ref().and_then(Cache::credentials);
2017-01-18 17:07:20 +00:00
2018-02-25 04:40:00 +00:00
let password = |username: &String| -> String {
write!(stderr(), "Password for {}: ", username).unwrap();
stderr().flush().unwrap();
rpassword::read_password().unwrap()
};
get_credentials(
matches.opt_str("username"),
matches.opt_str("password"),
2018-02-25 04:40:00 +00:00
cached_credentials,
2018-02-26 01:50:41 +00:00
password,
)
};
2017-01-18 17:07:20 +00:00
let session_config = {
let device_id = device_id(&name);
SessionConfig {
user_agent: version::version_string(),
device_id: device_id,
}
2017-01-10 16:31:12 +00:00
};
let player_config = {
2018-02-26 01:50:41 +00:00
let bitrate = matches
.opt_str("b")
.as_ref()
.map(|bitrate| Bitrate::from_str(bitrate).expect("Invalid bitrate"))
.unwrap_or(Bitrate::default());
PlayerConfig {
bitrate: bitrate,
2018-02-23 19:08:20 +00:00
normalisation: matches.opt_present("enable-volume-normalisation"),
2018-02-26 01:50:41 +00:00
normalisation_pregain: matches
.opt_str("normalisation-pregain")
2018-02-23 19:08:20 +00:00
.map(|pregain| pregain.parse::<f32>().expect("Invalid pregain float value"))
.unwrap_or(PlayerConfig::default().normalisation_pregain),
}
};
let connect_config = {
2018-02-26 01:50:41 +00:00
let device_type = matches
.opt_str("device-type")
.as_ref()
.map(|device_type| DeviceType::from_str(device_type).expect("Invalid device type"))
.unwrap_or(DeviceType::default());
ConnectConfig {
name: name,
device_type: device_type,
2017-12-06 13:37:34 +00:00
volume: initial_volume,
2018-03-11 10:27:28 +00:00
linear_volume: matches.opt_present("linear-volume")
}
};
let enable_discovery = !matches.opt_present("disable-discovery");
2017-01-18 18:41:22 +00:00
Setup {
backend: backend,
cache: cache,
session_config: session_config,
player_config: player_config,
connect_config: connect_config,
2017-01-18 18:41:22 +00:00
credentials: credentials,
device: device,
enable_discovery: enable_discovery,
2018-01-30 20:38:54 +00:00
zeroconf_port: zeroconf_port,
mixer: mixer,
player_event_program: matches.opt_str("onevent"),
2017-01-18 18:41:22 +00:00
}
2017-01-10 16:31:12 +00:00
}
struct Main {
cache: Option<Cache>,
player_config: PlayerConfig,
session_config: SessionConfig,
connect_config: ConnectConfig,
backend: fn(Option<String>) -> Box<Sink>,
device: Option<String>,
mixer: fn() -> Box<Mixer>,
handle: Handle,
2017-01-18 18:41:22 +00:00
discovery: Option<DiscoveryStream>,
signal: IoStream<()>,
spirc: Option<Spirc>,
spirc_task: Option<SpircTask>,
2018-02-26 01:50:41 +00:00
connect: Box<Future<Item = Session, Error = io::Error>>,
shutdown: bool,
player_event_channel: Option<UnboundedReceiver<PlayerEvent>>,
player_event_program: Option<String>,
}
2017-01-18 18:41:22 +00:00
impl Main {
fn new(handle: Handle, setup: Setup) -> Main {
let mut task = Main {
handle: handle.clone(),
cache: setup.cache,
session_config: setup.session_config,
player_config: setup.player_config,
connect_config: setup.connect_config,
backend: setup.backend,
device: setup.device,
mixer: setup.mixer,
connect: Box::new(futures::future::empty()),
discovery: None,
spirc: None,
spirc_task: None,
shutdown: false,
signal: Box::new(tokio_signal::ctrl_c(&handle).flatten_stream()),
player_event_channel: None,
player_event_program: setup.player_event_program,
};
if setup.enable_discovery {
let config = task.connect_config.clone();
let device_id = task.session_config.device_id.clone();
2018-01-30 20:38:54 +00:00
task.discovery = Some(discovery(&handle, config, device_id, setup.zeroconf_port).unwrap());
}
2017-01-18 18:41:22 +00:00
if let Some(credentials) = setup.credentials {
task.credentials(credentials);
}
2017-04-28 22:24:55 +00:00
task
}
2017-01-18 18:41:22 +00:00
fn credentials(&mut self, credentials: Credentials) {
let config = self.session_config.clone();
let handle = self.handle.clone();
2017-02-22 14:28:09 +00:00
let connection = Session::connect(config, credentials, self.cache.clone(), handle);
self.connect = connection;
self.spirc = None;
let task = mem::replace(&mut self.spirc_task, None);
if let Some(task) = task {
self.handle.spawn(task);
}
}
}
impl Future for Main {
type Item = ();
type Error = ();
fn poll(&mut self) -> Poll<(), ()> {
loop {
let mut progress = false;
if let Some(Async::Ready(Some(creds))) = self.discovery.as_mut().map(|d| d.poll().unwrap()) {
if let Some(ref spirc) = self.spirc {
spirc.shutdown();
}
self.credentials(creds);
progress = true;
2017-02-21 23:25:04 +00:00
}
2017-01-18 18:41:22 +00:00
if let Async::Ready(session) = self.connect.poll().unwrap() {
self.connect = Box::new(futures::future::empty());
let device = self.device.clone();
let mixer = (self.mixer)();
let player_config = self.player_config.clone();
let connect_config = self.connect_config.clone();
let audio_filter = mixer.get_audio_filter();
let backend = self.backend;
2018-02-26 01:50:41 +00:00
let (player, event_channel) =
Player::new(player_config, session.clone(), audio_filter, move || {
(backend)(device)
});
let (spirc, spirc_task) = Spirc::new(connect_config, session, player, mixer);
self.spirc = Some(spirc);
self.spirc_task = Some(spirc_task);
self.player_event_channel = Some(event_channel);
progress = true;
}
if let Async::Ready(Some(())) = self.signal.poll().unwrap() {
if !self.shutdown {
if let Some(ref spirc) = self.spirc {
spirc.shutdown();
}
self.shutdown = true;
} else {
return Ok(Async::Ready(()));
}
progress = true;
}
if let Some(ref mut spirc_task) = self.spirc_task {
if let Async::Ready(()) = spirc_task.poll().unwrap() {
if self.shutdown {
return Ok(Async::Ready(()));
} else {
panic!("Spirc shut down unexpectedly");
}
}
}
if let Some(ref mut player_event_channel) = self.player_event_channel {
if let Async::Ready(Some(event)) = player_event_channel.poll().unwrap() {
if let Some(ref program) = self.player_event_program {
run_program_on_events(event, program);
}
}
}
if !progress {
return Ok(Async::NotReady);
}
}
}
}
fn main() {
2018-02-13 15:46:10 +00:00
if env::var("RUST_BACKTRACE").is_err() {
env::set_var("RUST_BACKTRACE", "full")
}
let mut core = Core::new().unwrap();
let handle = core.handle();
let args: Vec<String> = std::env::args().collect();
core.run(Main::new(handle, setup(&args))).unwrap()
2015-07-01 17:49:03 +00:00
}