From a36325a46a2f5a924216eb1af28612444772fe28 Mon Sep 17 00:00:00 2001 From: Paul Lietar Date: Sun, 24 Apr 2016 13:29:26 +0100 Subject: [PATCH] Add a main helper to make it easier to use librespot. --- src/lib.in.rs | 1 + src/lib.rs | 2 + src/main.rs | 143 ++++------------------------------------- src/main_helper.rs | 156 +++++++++++++++++++++++++++++++++++++++++++++ src/session.rs | 2 + 5 files changed, 173 insertions(+), 131 deletions(-) create mode 100644 src/main_helper.rs diff --git a/src/lib.in.rs b/src/lib.in.rs index c419b7f8..e44c9e69 100644 --- a/src/lib.in.rs +++ b/src/lib.in.rs @@ -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; diff --git a/src/lib.rs b/src/lib.rs index 76cbb190..23ed9656 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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; diff --git a/src/main.rs b/src/main.rs index 990ccba7..47857b93 100644 --- a/src/main.rs +++ b/src/main.rs @@ -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 = 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 - }).unwrap_or_else(|| Box::new(NoCache) as Box); - - 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()); diff --git a/src/main_helper.rs b/src/main_helper.rs new file mode 100644 index 00000000..e4b4a622 --- /dev/null +++ b/src/main_helper.rs @@ -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 + 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>(path: Option

) -> Vec { + 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 + }).unwrap_or_else(|| Box::new(NoCache) as Box); + + 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()) +} diff --git a/src/session.rs b/src/session.rs index d67efdb0..21b91a29 100644 --- a/src/session.rs +++ b/src/session.rs @@ -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) }