Add a main helper to make it easier to use librespot.

This commit is contained in:
Paul Lietar 2016-04-24 13:29:26 +01:00
parent edb8a6a766
commit a36325a46a
5 changed files with 173 additions and 131 deletions

View file

@ -16,6 +16,7 @@ pub mod session;
pub mod spirc;
pub mod link;
pub mod stream;
pub mod main_helper;
#[cfg(feature = "facebook")]
pub mod spotilocal;

View file

@ -14,12 +14,14 @@ extern crate bit_set;
extern crate byteorder;
extern crate crypto;
extern crate eventual;
extern crate getopts;
extern crate hyper;
extern crate lmdb_rs;
extern crate num;
extern crate protobuf;
extern crate shannon;
extern crate rand;
extern crate rpassword;
extern crate rustc_serialize;
extern crate time;
extern crate tempfile;

View file

@ -1,166 +1,47 @@
extern crate getopts;
extern crate librespot;
extern crate rpassword;
extern crate env_logger;
#[macro_use]
extern crate log;
use rpassword::read_password;
use std::clone::Clone;
use std::fs::File;
use std::io::{stdout, Read, Write};
use std::path::PathBuf;
use std::process::exit;
use std::thread;
use std::env;
use librespot::audio_backend::BACKENDS;
use librespot::authentication::{Credentials, facebook_login, discovery_login};
use librespot::cache::{Cache, DefaultCache, NoCache};
use librespot::player::Player;
use librespot::session::{Bitrate, Config, Session};
use librespot::spirc::SpircManager;
use librespot::version;
use librespot::main_helper;
fn usage(program: &str, opts: &getopts::Options) -> String {
let brief = format!("Usage: {} [options]", program);
format!("{}", opts.usage(&brief))
}
#[cfg(feature = "static-appkey")]
static APPKEY: Option<&'static [u8]> = Some(include_bytes!(concat!(env!("CARGO_MANIFEST_DIR"), "/spotify_appkey.key")));
#[cfg(not(feature = "static-appkey"))]
static APPKEY: Option<&'static [u8]> = None;
fn main() {
if env::var("RUST_LOG").is_err() {
env::set_var("RUST_LOG", "info,librespot=trace")
}
env_logger::init().unwrap();
info!("librespot {} ({}). Built on {}.",
version::short_sha(),
version::commit_date(),
version::short_now());
let mut opts = getopts::Options::new();
main_helper::add_session_arguments(&mut opts);
main_helper::add_authentication_arguments(&mut opts);
main_helper::add_player_arguments(&mut opts);
let args: Vec<String> = std::env::args().collect();
let program = args[0].clone();
let mut opts = getopts::Options::new();
opts.optopt("u", "username", "Username to sign in with", "USERNAME")
.optopt("p", "password", "Password", "PASSWORD")
.optopt("c", "cache", "Path to a directory where files will be cached.", "CACHE")
.reqopt("n", "name", "Device name", "NAME")
.optopt("b", "bitrate", "Bitrate (96, 160 or 320). Defaults to 160", "BITRATE")
.optopt("", "backend", "Audio backend to use. Use '?' to list options", "BACKEND")
.optflag("", "facebook", "Login with a Facebook account");
if APPKEY.is_none() {
opts.reqopt("a", "appkey", "Path to a spotify appkey", "APPKEY");
} else {
opts.optopt("a", "appkey", "Path to a spotify appkey", "APPKEY");
};
let matches = match opts.parse(&args[1..]) {
Ok(m) => m,
Err(f) => {
error!("Error: {}\n{}", f.to_string(), usage(&*program, &opts));
return;
error!("Error: {}\n{}", f.to_string(), usage(&args[0], &opts));
exit(1)
}
};
let make_backend = match matches.opt_str("backend").as_ref().map(AsRef::as_ref) {
Some("?") => {
println!("Available Backends : ");
for (&(name, _), idx) in BACKENDS.iter().zip(0..) {
if idx == 0 {
println!("- {} (default)", name);
} else {
println!("- {}", name);
}
}
let session = main_helper::create_session(&matches);
let credentials = main_helper::get_credentials(&session, &matches);
session.login(credentials).unwrap();
return;
},
Some(name) => {
BACKENDS.iter().find(|backend| name == backend.0).expect("Unknown backend").1
},
None => {
BACKENDS.first().expect("No backends were enabled at build time").1
}
};
let appkey = matches.opt_str("a").map(|appkey_path| {
let mut file = File::open(appkey_path)
.expect("Could not open app key.");
let mut data = Vec::new();
file.read_to_end(&mut data).unwrap();
data
}).or_else(|| APPKEY.map(ToOwned::to_owned)).unwrap();
let username = matches.opt_str("u");
let name = matches.opt_str("n").unwrap();
let cache = matches.opt_str("c").map(|cache_location| {
Box::new(DefaultCache::new(PathBuf::from(cache_location)).unwrap()) as Box<Cache + Send + Sync>
}).unwrap_or_else(|| Box::new(NoCache) as Box<Cache + Send + Sync>);
let bitrate = match matches.opt_str("b").as_ref().map(String::as_ref) {
None => Bitrate::Bitrate160, // default value
Some("96") => Bitrate::Bitrate96,
Some("160") => Bitrate::Bitrate160,
Some("320") => Bitrate::Bitrate320,
Some(b) => panic!("Invalid bitrate {}", b),
};
let config = Config {
application_key: appkey,
user_agent: version::version_string(),
device_name: name,
bitrate: bitrate,
};
let stored_credentials = cache.get_credentials();
let session = Session::new(config, cache);
let credentials = match (username, matches.opt_str("p"), stored_credentials) {
(Some(username), Some(password), _)
=> Credentials::with_password(username, password),
(Some(ref username), _, Some(ref credentials)) if *username == credentials.username
=> credentials.clone(),
(Some(username), None, _) => {
print!("Password for {}: ", username);
stdout().flush().unwrap();
let password = read_password().unwrap();
Credentials::with_password(username.clone(), password)
}
(None, _, _) if matches.opt_present("facebook")
=> facebook_login().unwrap(),
(None, _, Some(credentials))
=> credentials,
(None, _, None) if cfg!(feature = "discovery") => {
info!("No username provided and no stored credentials, starting discovery ...");
discovery_login(&session.config().device_name, session.device_id()).unwrap()
}
(None, _, None) => {
error!("No credentials provided");
return
}
};
let reusable_credentials = session.login(credentials).unwrap();
session.cache().put_credentials(&reusable_credentials);
let player = Player::new(session.clone(), move || make_backend());
let player = main_helper::create_player(&session, &matches);
let spirc = SpircManager::new(session.clone(), player);
thread::spawn(move || spirc.run());

156
src/main_helper.rs Normal file
View file

@ -0,0 +1,156 @@
use getopts;
use rpassword;
use std::fs::File;
use std::io::{stdout, Read, Write};
use std::path::PathBuf;
use std::path::Path;
use std::process::exit;
use audio_backend::{BACKENDS, Sink};
use authentication::{Credentials, facebook_login, discovery_login};
use cache::{Cache, DefaultCache, NoCache};
use player::Player;
use session::{Bitrate, Config, Session};
use version;
#[cfg(feature = "static-appkey")]
static APPKEY: Option<&'static [u8]> = Some(include_bytes!(concat!(env!("CARGO_MANIFEST_DIR"), "/spotify_appkey.key")));
#[cfg(not(feature = "static-appkey"))]
static APPKEY: Option<&'static [u8]> = None;
pub fn find_backend(name: Option<&str>) -> &'static (Fn() -> Box<Sink> + Send + Sync) {
match name {
Some("?") => {
println!("Available Backends : ");
for (&(name, _), idx) in BACKENDS.iter().zip(0..) {
if idx == 0 {
println!("- {} (default)", name);
} else {
println!("- {}", name);
}
}
exit(0);
},
Some(name) => {
BACKENDS.iter().find(|backend| name == backend.0).expect("Unknown backend").1
},
None => {
BACKENDS.first().expect("No backends were enabled at build time").1
}
}
}
pub fn load_appkey<P: AsRef<Path>>(path: Option<P>) -> Vec<u8> {
path.map(|path| {
let mut file = File::open(path).expect("Could not open app key.");
let mut data = Vec::new();
file.read_to_end(&mut data).unwrap();
data
}).or_else(|| APPKEY.map(ToOwned::to_owned)).unwrap()
}
pub fn add_session_arguments(opts: &mut getopts::Options) {
opts.optopt("c", "cache", "Path to a directory where files will be cached.", "CACHE")
.reqopt("n", "name", "Device name", "NAME")
.optopt("b", "bitrate", "Bitrate (96, 160 or 320). Defaults to 160", "BITRATE");
if APPKEY.is_none() {
opts.reqopt("a", "appkey", "Path to a spotify appkey", "APPKEY");
} else {
opts.optopt("a", "appkey", "Path to a spotify appkey", "APPKEY");
};
}
pub fn add_authentication_arguments(opts: &mut getopts::Options) {
opts.optopt("u", "username", "Username to sign in with", "USERNAME")
.optopt("p", "password", "Password", "PASSWORD");
if cfg!(feature = "facebook") {
opts.optflag("", "facebook", "Login with a Facebook account");
}
}
pub fn add_player_arguments(opts: &mut getopts::Options) {
opts.optopt("", "backend", "Audio backend to use. Use '?' to list options", "BACKEND");
}
pub fn create_session(matches: &getopts::Matches) -> Session {
info!("librespot {} ({}). Built on {}.",
version::short_sha(),
version::commit_date(),
version::short_now());
let appkey = load_appkey(matches.opt_str("a"));
let name = matches.opt_str("n").unwrap();
let bitrate = match matches.opt_str("b").as_ref().map(String::as_ref) {
None => Bitrate::Bitrate160, // default value
Some("96") => Bitrate::Bitrate96,
Some("160") => Bitrate::Bitrate160,
Some("320") => Bitrate::Bitrate320,
Some(b) => {
error!("Invalid bitrate {}", b);
exit(1)
}
};
let cache = matches.opt_str("c").map(|cache_location| {
Box::new(DefaultCache::new(PathBuf::from(cache_location)).unwrap()) as Box<Cache + Send + Sync>
}).unwrap_or_else(|| Box::new(NoCache) as Box<Cache + Send + Sync>);
let config = Config {
application_key: appkey,
user_agent: version::version_string(),
device_name: name,
bitrate: bitrate,
};
Session::new(config, cache)
}
pub fn get_credentials(session: &Session, matches: &getopts::Matches) -> Credentials {
let credentials = session.cache().get_credentials();
match (matches.opt_str("username"),
matches.opt_str("password"),
credentials) {
(Some(username), Some(password), _)
=> Credentials::with_password(username, password),
(Some(ref username), _, Some(ref credentials)) if *username == credentials.username
=> credentials.clone(),
(Some(username), None, _) => {
print!("Password for {}: ", username);
stdout().flush().unwrap();
let password = rpassword::read_password().unwrap();
Credentials::with_password(username.clone(), password)
}
(None, _, _) if cfg!(feature = "facebook") && matches.opt_present("facebook")
=> facebook_login().unwrap(),
(None, _, Some(credentials))
=> credentials,
(None, _, None) if cfg!(feature = "discovery") => {
info!("No username provided and no stored credentials, starting discovery ...");
discovery_login(&session.config().device_name, session.device_id()).unwrap()
}
(None, _, None) => {
error!("No credentials provided");
exit(1)
}
}
}
pub fn create_player(session: &Session, matches: &getopts::Matches) -> Player {
let make_backend = find_backend(matches.opt_str("backend").as_ref().map(AsRef::as_ref));
Player::new(session.clone(), move || make_backend())
}

View file

@ -225,6 +225,8 @@ impl Session {
auth_data: welcome_data.get_reusable_auth_credentials().to_owned(),
};
self.0.cache.put_credentials(&reusable_credentials);
Ok(reusable_credentials)
}