2017-04-29 11:56:15 +00:00
|
|
|
// TODO: many items from tokio-core::io have been deprecated in favour of tokio-io
|
|
|
|
#![allow(deprecated)]
|
|
|
|
|
2017-01-10 16:31:12 +00:00
|
|
|
#[macro_use] extern crate log;
|
|
|
|
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;
|
2017-01-18 18:41:22 +00:00
|
|
|
extern crate tokio_core;
|
2017-02-22 04:17:04 +00:00
|
|
|
extern crate tokio_signal;
|
2015-04-25 20:32:07 +00:00
|
|
|
|
2017-01-10 16:31:12 +00:00
|
|
|
use env_logger::LogBuilder;
|
2017-02-22 04:17:04 +00:00
|
|
|
use futures::{Future, Async, Poll, Stream};
|
2017-01-10 16:31:12 +00:00
|
|
|
use std::env;
|
2017-02-22 04:17:04 +00:00
|
|
|
use std::io::{self, stderr, Write};
|
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;
|
2017-02-22 04:17:04 +00:00
|
|
|
use tokio_core::reactor::{Handle, Core};
|
|
|
|
use tokio_core::io::IoStream;
|
|
|
|
use std::mem;
|
2015-07-19 20:36:14 +00:00
|
|
|
|
2017-08-03 18:58:44 +00:00
|
|
|
use librespot::core::authentication::{get_credentials, Credentials};
|
|
|
|
use librespot::core::cache::Cache;
|
|
|
|
use librespot::core::config::{Bitrate, DeviceType, PlayerConfig, SessionConfig, ConnectConfig};
|
|
|
|
use librespot::core::session::Session;
|
|
|
|
use librespot::core::version;
|
|
|
|
|
2017-01-18 18:41:22 +00:00
|
|
|
use librespot::audio_backend::{self, Sink, BACKENDS};
|
2017-08-03 18:58:44 +00:00
|
|
|
use librespot::discovery::{discovery, DiscoveryStream};
|
2017-01-25 21:49:18 +00:00
|
|
|
use librespot::mixer::{self, Mixer};
|
2017-08-03 18:58:44 +00:00
|
|
|
use librespot::player::Player;
|
|
|
|
use librespot::spirc::{Spirc, SpircTask};
|
2016-03-16 00:05:05 +00:00
|
|
|
|
2016-03-14 23:41:51 +00:00
|
|
|
fn usage(program: &str, opts: &getopts::Options) -> String {
|
2015-07-19 20:36:14 +00:00
|
|
|
let brief = format!("Usage: {} [options]", program);
|
2017-01-29 16:25:09 +00:00
|
|
|
opts.usage(&brief)
|
2015-07-19 20:36:14 +00:00
|
|
|
}
|
|
|
|
|
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);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-02-22 04:17:04 +00:00
|
|
|
#[derive(Clone)]
|
2017-01-18 18:41:22 +00:00
|
|
|
struct Setup {
|
2017-02-21 22:46:19 +00:00
|
|
|
backend: fn(Option<String>) -> Box<Sink>,
|
2017-02-22 04:17:04 +00:00
|
|
|
device: Option<String>,
|
|
|
|
|
|
|
|
mixer: fn() -> Box<Mixer>,
|
|
|
|
|
2017-01-29 15:36:39 +00:00
|
|
|
cache: Option<Cache>,
|
2017-08-03 18:31:15 +00:00
|
|
|
player_config: PlayerConfig,
|
|
|
|
session_config: SessionConfig,
|
|
|
|
connect_config: ConnectConfig,
|
2017-02-22 04:17:04 +00:00
|
|
|
credentials: Option<Credentials>,
|
|
|
|
enable_discovery: bool,
|
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();
|
|
|
|
opts.optopt("c", "cache", "Path to a directory where files will be cached.", "CACHE")
|
2017-06-30 06:43:11 +00:00
|
|
|
.optflag("", "disable-audio-cache", "Disable caching of the audio data.")
|
2017-01-10 16:31:12 +00:00
|
|
|
.reqopt("n", "name", "Device name", "NAME")
|
2017-08-03 18:31:15 +00:00
|
|
|
.optopt("", "device-type", "Displayed device type", "DEVICE_TYPE")
|
2017-01-10 16:31:12 +00:00
|
|
|
.optopt("b", "bitrate", "Bitrate (96, 160 or 320). Defaults to 160", "BITRATE")
|
|
|
|
.optopt("", "onstart", "Run PROGRAM when playback is about to begin.", "PROGRAM")
|
|
|
|
.optopt("", "onstop", "Run PROGRAM when playback has ended.", "PROGRAM")
|
|
|
|
.optflag("v", "verbose", "Enable verbose output")
|
|
|
|
.optopt("u", "username", "Username to sign in with", "USERNAME")
|
|
|
|
.optopt("p", "password", "Password", "PASSWORD")
|
2017-02-22 04:17:04 +00:00
|
|
|
.optflag("", "disable-discovery", "Disable discovery mode")
|
2017-01-10 16:31:12 +00:00
|
|
|
.optopt("", "backend", "Audio backend to use. Use '?' to list options", "BACKEND")
|
2017-01-25 21:49:18 +00:00
|
|
|
.optopt("", "device", "Audio device to use. Use '?' to list options", "DEVICE")
|
2017-12-06 13:37:34 +00:00
|
|
|
.optopt("", "mixer", "Mixer to use", "MIXER")
|
2017-12-06 13:47:46 +00:00
|
|
|
.optopt("", "initial-volume", "Initial volume in %, once connected (must be from 0 to 100)", "VOLUME");
|
2016-03-14 23:41:51 +00:00
|
|
|
|
2015-07-19 20:36:14 +00:00
|
|
|
let matches = match opts.parse(&args[1..]) {
|
2016-01-02 15:19:39 +00:00
|
|
|
Ok(m) => m,
|
|
|
|
Err(f) => {
|
2017-01-05 13:25:14 +00:00
|
|
|
writeln!(stderr(), "error: {}\n{}", f.to_string(), usage(&args[0], &opts)).unwrap();
|
|
|
|
exit(1);
|
2016-04-24 11:15:53 +00:00
|
|
|
}
|
|
|
|
};
|
2016-03-13 20:45:31 +00:00
|
|
|
|
2017-01-10 16:31:12 +00:00
|
|
|
let verbose = matches.opt_present("verbose");
|
|
|
|
setup_logging(verbose);
|
|
|
|
|
2017-07-17 12:31:06 +00:00
|
|
|
info!("librespot {} ({}). Built on {}. Build ID: {}",
|
2017-01-10 16:31:12 +00:00
|
|
|
version::short_sha(),
|
|
|
|
version::commit_date(),
|
2017-07-17 12:31:06 +00:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
|
2017-04-28 22:24:55 +00:00
|
|
|
let backend = audio_backend::find(backend_name)
|
2017-01-10 16:31:12 +00:00
|
|
|
.expect("Invalid backend");
|
|
|
|
|
2017-08-03 18:31:15 +00:00
|
|
|
let device = matches.opt_str("device");
|
|
|
|
|
2017-02-21 22:46:19 +00:00
|
|
|
let mixer_name = matches.opt_str("mixer");
|
|
|
|
let mixer = mixer::find(mixer_name.as_ref())
|
|
|
|
.expect("Invalid mixer");
|
2017-12-06 13:37:34 +00:00
|
|
|
let initial_volume;
|
|
|
|
if matches.opt_present("initial-volume"){
|
2017-12-06 13:47:46 +00:00
|
|
|
if matches.opt_str("initial-volume").unwrap().parse::<i32>().is_ok(){
|
2017-12-06 14:22:28 +00:00
|
|
|
if matches.opt_str("initial-volume").unwrap().parse::<i32>().unwrap() < 0 {
|
|
|
|
initial_volume = 0 as i32;
|
2017-12-06 13:47:46 +00:00
|
|
|
}
|
2017-12-06 14:22:28 +00:00
|
|
|
else if matches.opt_str("initial-volume").unwrap().parse::<i32>().unwrap() > 100{
|
|
|
|
initial_volume = 0xFFFF as i32;
|
|
|
|
}
|
|
|
|
else{
|
|
|
|
initial_volume = matches.opt_str("initial-volume").unwrap().parse::<i32>().unwrap()* 0xFFFF as i32 / 100 ;
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
2017-12-06 13:47:46 +00:00
|
|
|
else {
|
|
|
|
initial_volume = 0x8000 as i32;
|
2017-12-06 13:37:34 +00:00
|
|
|
}
|
2017-12-06 14:22:28 +00:00
|
|
|
}
|
2017-12-06 13:37:34 +00:00
|
|
|
else{
|
|
|
|
initial_volume = 0x8000 as i32;
|
|
|
|
}
|
|
|
|
info!("Volume \"{}\" !", initial_volume);
|
2017-02-21 22:46:19 +00:00
|
|
|
|
2017-01-18 18:41:22 +00:00
|
|
|
let name = matches.opt_str("name").unwrap();
|
2017-06-30 06:43:11 +00:00
|
|
|
let use_audio_cache = !matches.opt_present("disable-audio-cache");
|
2017-01-18 17:07:20 +00:00
|
|
|
|
|
|
|
let cache = matches.opt_str("c").map(|cache_location| {
|
2017-06-30 06:43:11 +00:00
|
|
|
Cache::new(PathBuf::from(cache_location), use_audio_cache)
|
2017-01-29 15:36:39 +00:00
|
|
|
});
|
2017-01-18 17:07:20 +00:00
|
|
|
|
2017-08-03 18:31:15 +00:00
|
|
|
let credentials = {
|
|
|
|
let cached_credentials = cache.as_ref().and_then(Cache::credentials);
|
2017-01-18 17:07:20 +00:00
|
|
|
|
2017-08-03 18:31:15 +00:00
|
|
|
get_credentials(
|
|
|
|
matches.opt_str("username"),
|
|
|
|
matches.opt_str("password"),
|
|
|
|
cached_credentials
|
|
|
|
)
|
|
|
|
};
|
2017-01-18 17:07:20 +00:00
|
|
|
|
2017-08-03 18:31:15 +00:00
|
|
|
let session_config = {
|
2017-08-03 18:58:44 +00:00
|
|
|
let device_id = librespot::core::session::device_id(&name);
|
2017-02-22 04:17:04 +00:00
|
|
|
|
2017-08-03 18:31:15 +00:00
|
|
|
SessionConfig {
|
|
|
|
user_agent: version::version_string(),
|
|
|
|
device_id: device_id,
|
|
|
|
}
|
2017-01-10 16:31:12 +00:00
|
|
|
};
|
|
|
|
|
2017-08-03 18:31:15 +00:00
|
|
|
let player_config = {
|
|
|
|
let bitrate = matches.opt_str("b").as_ref()
|
|
|
|
.map(|bitrate| Bitrate::from_str(bitrate).expect("Invalid bitrate"))
|
|
|
|
.unwrap_or(Bitrate::default());
|
|
|
|
|
|
|
|
PlayerConfig {
|
|
|
|
bitrate: bitrate,
|
|
|
|
onstart: matches.opt_str("onstart"),
|
|
|
|
onstop: matches.opt_str("onstop"),
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
let connect_config = {
|
|
|
|
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,
|
2017-08-03 18:31:15 +00:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
let enable_discovery = !matches.opt_present("disable-discovery");
|
2017-01-05 13:25:14 +00:00
|
|
|
|
2017-01-18 18:41:22 +00:00
|
|
|
Setup {
|
|
|
|
backend: backend,
|
|
|
|
cache: cache,
|
2017-08-03 18:31:15 +00:00
|
|
|
session_config: session_config,
|
|
|
|
player_config: player_config,
|
|
|
|
connect_config: connect_config,
|
2017-01-18 18:41:22 +00:00
|
|
|
credentials: credentials,
|
|
|
|
device: device,
|
2017-02-22 04:17:04 +00:00
|
|
|
enable_discovery: enable_discovery,
|
|
|
|
mixer: mixer,
|
2017-01-18 18:41:22 +00:00
|
|
|
}
|
2017-01-10 16:31:12 +00:00
|
|
|
}
|
|
|
|
|
2017-02-22 04:17:04 +00:00
|
|
|
struct Main {
|
|
|
|
cache: Option<Cache>,
|
2017-08-03 18:31:15 +00:00
|
|
|
player_config: PlayerConfig,
|
|
|
|
session_config: SessionConfig,
|
|
|
|
connect_config: ConnectConfig,
|
2017-02-22 04:17:04 +00:00
|
|
|
backend: fn(Option<String>) -> Box<Sink>,
|
|
|
|
device: Option<String>,
|
|
|
|
mixer: fn() -> Box<Mixer>,
|
|
|
|
handle: Handle,
|
2017-01-18 18:41:22 +00:00
|
|
|
|
2017-02-22 04:17:04 +00:00
|
|
|
discovery: Option<DiscoveryStream>,
|
|
|
|
signal: IoStream<()>,
|
2016-03-20 16:16:11 +00:00
|
|
|
|
2017-02-22 04:17:04 +00:00
|
|
|
spirc: Option<Spirc>,
|
|
|
|
spirc_task: Option<SpircTask>,
|
|
|
|
connect: Box<Future<Item=Session, Error=io::Error>>,
|
2016-12-31 11:51:44 +00:00
|
|
|
|
2017-02-22 04:17:04 +00:00
|
|
|
shutdown: bool,
|
|
|
|
}
|
2017-01-18 18:41:22 +00:00
|
|
|
|
2017-02-22 04:17:04 +00:00
|
|
|
impl Main {
|
2017-08-03 18:31:15 +00:00
|
|
|
fn new(handle: Handle, setup: Setup) -> Main {
|
|
|
|
let mut task = Main {
|
2017-02-22 04:17:04 +00:00
|
|
|
handle: handle.clone(),
|
2017-08-03 18:31:15 +00:00
|
|
|
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,
|
2017-02-22 04:17:04 +00:00
|
|
|
|
|
|
|
connect: Box::new(futures::future::empty()),
|
|
|
|
discovery: None,
|
|
|
|
spirc: None,
|
|
|
|
spirc_task: None,
|
|
|
|
shutdown: false,
|
|
|
|
signal: tokio_signal::ctrl_c(&handle).flatten_stream().boxed(),
|
2017-08-03 18:31:15 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
if setup.enable_discovery {
|
|
|
|
let config = task.connect_config.clone();
|
|
|
|
let device_id = task.session_config.device_id.clone();
|
|
|
|
|
|
|
|
task.discovery = Some(discovery(&handle, config, device_id).unwrap());
|
2017-02-22 04:17:04 +00:00
|
|
|
}
|
2017-01-18 18:41:22 +00:00
|
|
|
|
2017-08-03 18:31:15 +00:00
|
|
|
if let Some(credentials) = setup.credentials {
|
|
|
|
task.credentials(credentials);
|
|
|
|
}
|
2017-04-28 22:24:55 +00:00
|
|
|
|
2017-08-03 18:31:15 +00:00
|
|
|
task
|
2017-02-22 04:17:04 +00:00
|
|
|
}
|
2017-01-18 18:41:22 +00:00
|
|
|
|
2017-02-22 04:17:04 +00:00
|
|
|
fn credentials(&mut self, credentials: Credentials) {
|
2017-08-03 18:31:15 +00:00
|
|
|
let config = self.session_config.clone();
|
2017-02-22 04:17:04 +00:00
|
|
|
let handle = self.handle.clone();
|
2017-02-22 14:28:09 +00:00
|
|
|
|
2017-02-22 04:17:04 +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
|
|
|
|
2017-02-22 04:17:04 +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)();
|
2017-08-03 18:31:15 +00:00
|
|
|
let player_config = self.player_config.clone();
|
|
|
|
let connect_config = self.connect_config.clone();
|
2017-02-22 04:17:04 +00:00
|
|
|
|
|
|
|
let audio_filter = mixer.get_audio_filter();
|
|
|
|
let backend = self.backend;
|
2017-08-03 18:31:15 +00:00
|
|
|
let player = Player::new(player_config, session.clone(), audio_filter, move || {
|
2017-02-22 04:17:04 +00:00
|
|
|
(backend)(device)
|
|
|
|
});
|
|
|
|
|
2017-08-03 18:31:15 +00:00
|
|
|
let (spirc, spirc_task) = Spirc::new(connect_config, session, player, mixer);
|
2017-02-22 04:17:04 +00:00
|
|
|
self.spirc = Some(spirc);
|
|
|
|
self.spirc_task = Some(spirc_task);
|
|
|
|
|
|
|
|
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 !progress {
|
|
|
|
return Ok(Async::NotReady);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn main() {
|
|
|
|
let mut core = Core::new().unwrap();
|
|
|
|
let handle = core.handle();
|
|
|
|
|
|
|
|
let args: Vec<String> = std::env::args().collect();
|
2015-07-02 17:24:25 +00:00
|
|
|
|
2017-08-03 18:31:15 +00:00
|
|
|
core.run(Main::new(handle, setup(&args))).unwrap()
|
2015-07-01 17:49:03 +00:00
|
|
|
}
|