mirror of
https://github.com/librespot-org/librespot.git
synced 2024-12-18 17:11:53 +00:00
Add a main helper to make it easier to use librespot.
This commit is contained in:
parent
edb8a6a766
commit
a36325a46a
5 changed files with 173 additions and 131 deletions
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
143
src/main.rs
143
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<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
156
src/main_helper.rs
Normal 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())
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue