committed by
Henrik Friedrichsen
parent
58f34b9288
commit
d78e71871a
@@ -15,6 +15,7 @@ maintenance = { status = "experimental" }
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
clap = "2.32.0"
|
clap = "2.32.0"
|
||||||
chrono = "0.4"
|
chrono = "0.4"
|
||||||
|
reqwest = "0.9.11"
|
||||||
crossbeam-channel = "0.3.8"
|
crossbeam-channel = "0.3.8"
|
||||||
directories = "1.0"
|
directories = "1.0"
|
||||||
failure = "0.1.3"
|
failure = "0.1.3"
|
||||||
@@ -34,13 +35,16 @@ rand = "0.6.5"
|
|||||||
|
|
||||||
[dependencies.librespot]
|
[dependencies.librespot]
|
||||||
git = "https://github.com/librespot-org/librespot.git"
|
git = "https://github.com/librespot-org/librespot.git"
|
||||||
rev = "2fb901a743906a5b49b3148dbfa85074964dd745"
|
rev = "14721f4"
|
||||||
default-features = false
|
default-features = false
|
||||||
|
|
||||||
[dependencies.cursive]
|
[dependencies.cursive]
|
||||||
version = "0.11.1"
|
version = "0.11.1"
|
||||||
default-features = false
|
default-features = false
|
||||||
|
|
||||||
|
[dependencies.winapi]
|
||||||
|
git = "https://github.com/overdrivenpotato/winapi-rs"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
pulseaudio_backend = ["librespot/pulseaudio-backend"]
|
pulseaudio_backend = ["librespot/pulseaudio-backend"]
|
||||||
rodio_backend = ["librespot/rodio-backend"]
|
rodio_backend = ["librespot/rodio-backend"]
|
||||||
|
|||||||
169
src/authentication.rs
Normal file
169
src/authentication.rs
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
use cursive::view::Identifiable;
|
||||||
|
use cursive::views::*;
|
||||||
|
use cursive::{CbSink, Cursive};
|
||||||
|
|
||||||
|
use librespot::core::authentication::Credentials as RespotCredentials;
|
||||||
|
use librespot::protocol::authentication::AuthenticationType;
|
||||||
|
|
||||||
|
pub fn create_credentials(path: &Path) -> Result<RespotCredentials, String> {
|
||||||
|
let mut login_cursive = Cursive::default();
|
||||||
|
let mut info_buf = TextContent::new("Failed to authenticate\n");
|
||||||
|
info_buf.append(format!(
|
||||||
|
"Cannot read config file from {}\n",
|
||||||
|
path.to_str().unwrap()
|
||||||
|
));
|
||||||
|
let info_view = Dialog::around(TextView::new_with_content(info_buf.clone()))
|
||||||
|
.button("Login", move |s| {
|
||||||
|
let login_view = Dialog::new()
|
||||||
|
.title("Login with Spotify username and password")
|
||||||
|
.content(
|
||||||
|
ListView::new()
|
||||||
|
.child("username", EditView::new().with_id("spotify_user"))
|
||||||
|
.child(
|
||||||
|
"password",
|
||||||
|
EditView::new().secret().with_id("spotify_password"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.button("Login", |s| {
|
||||||
|
let username = s
|
||||||
|
.call_on_id("spotify_user", |view: &mut EditView| view.get_content())
|
||||||
|
.unwrap()
|
||||||
|
.to_string();
|
||||||
|
let auth_data = s
|
||||||
|
.call_on_id("spotify_password", |view: &mut EditView| view.get_content())
|
||||||
|
.unwrap()
|
||||||
|
.to_string()
|
||||||
|
.as_bytes()
|
||||||
|
.to_vec();
|
||||||
|
s.set_user_data::<Result<RespotCredentials, String>>(Ok(RespotCredentials {
|
||||||
|
username,
|
||||||
|
auth_type: AuthenticationType::AUTHENTICATION_USER_PASS,
|
||||||
|
auth_data,
|
||||||
|
}));
|
||||||
|
s.quit();
|
||||||
|
})
|
||||||
|
.button("Quit", |s| s.quit());
|
||||||
|
s.pop_layer();
|
||||||
|
s.add_layer(login_view);
|
||||||
|
})
|
||||||
|
.button("Login with Facebook", |s| {
|
||||||
|
let urls: std::collections::HashMap<String, String> =
|
||||||
|
reqwest::get("https://login2.spotify.com/v1/config")
|
||||||
|
.expect("didn't connect")
|
||||||
|
.json()
|
||||||
|
.expect("didn't parse");
|
||||||
|
// not a dialog to let people copy & paste the URL
|
||||||
|
let url_notice = TextView::new(format!("Browse to {}", urls.get("login_url").unwrap()));
|
||||||
|
|
||||||
|
let controls = Button::new("Quit", |s| s.quit());
|
||||||
|
|
||||||
|
let login_view = LinearLayout::new(cursive::direction::Orientation::Vertical)
|
||||||
|
.child(url_notice)
|
||||||
|
.child(controls);
|
||||||
|
url_open(urls.get("login_url").unwrap().to_string());
|
||||||
|
crappy_poller(urls.get("credentials_url").unwrap(), &s.cb_sink());
|
||||||
|
s.pop_layer();
|
||||||
|
s.add_layer(login_view)
|
||||||
|
})
|
||||||
|
.button("Quit", |s| s.quit());
|
||||||
|
|
||||||
|
login_cursive.add_layer(info_view);
|
||||||
|
login_cursive.run();
|
||||||
|
|
||||||
|
login_cursive
|
||||||
|
.user_data()
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or(Err("Didn't obtain any credentials".to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: better with futures?
|
||||||
|
fn crappy_poller(url: &str, app_sink: &CbSink) {
|
||||||
|
let app_sink = app_sink.clone();
|
||||||
|
let url = url.to_string();
|
||||||
|
std::thread::spawn(move || {
|
||||||
|
let timeout = std::time::Duration::from_secs(5 * 60);
|
||||||
|
let start_time = std::time::SystemTime::now();
|
||||||
|
while std::time::SystemTime::now()
|
||||||
|
.duration_since(start_time)
|
||||||
|
.unwrap_or(timeout)
|
||||||
|
< timeout
|
||||||
|
{
|
||||||
|
if let Ok(mut response) = reqwest::get(&url) {
|
||||||
|
if response.status() != reqwest::StatusCode::ACCEPTED {
|
||||||
|
let result = match response.status() {
|
||||||
|
reqwest::StatusCode::OK => {
|
||||||
|
let creds = response
|
||||||
|
.json::<AuthResponse>()
|
||||||
|
.expect("Unable to parse")
|
||||||
|
.credentials;
|
||||||
|
Ok(creds)
|
||||||
|
}
|
||||||
|
|
||||||
|
_ => Err(format!(
|
||||||
|
"Facebook auth failed with code {}: {}",
|
||||||
|
response.status(),
|
||||||
|
response.text().unwrap()
|
||||||
|
)),
|
||||||
|
};
|
||||||
|
app_sink
|
||||||
|
.send(Box::new(|s: &mut Cursive| {
|
||||||
|
s.set_user_data(result);
|
||||||
|
s.quit();
|
||||||
|
}))
|
||||||
|
.unwrap();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
std::thread::sleep(std::time::Duration::from_millis(1000));
|
||||||
|
}
|
||||||
|
|
||||||
|
app_sink
|
||||||
|
.send(Box::new(|s: &mut Cursive| {
|
||||||
|
s.set_user_data::<Result<RespotCredentials, String>>(Err(
|
||||||
|
"Timed out authenticating".to_string(),
|
||||||
|
));
|
||||||
|
s.quit();
|
||||||
|
}))
|
||||||
|
.unwrap();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
pub struct AuthResponse {
|
||||||
|
pub credentials: RespotCredentials,
|
||||||
|
pub error: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Thanks to Marko Mijalkovic for this, but I don't want the url crate
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
pub fn url_open(url: String) {
|
||||||
|
extern crate shell32;
|
||||||
|
extern crate winapi;
|
||||||
|
|
||||||
|
use std::ffi::CString;
|
||||||
|
use std::ptr;
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
shell32::ShellExecuteA(
|
||||||
|
ptr::null_mut(),
|
||||||
|
CString::new("open").unwrap().as_ptr(),
|
||||||
|
CString::new(url.replace("\n", "%0A")).unwrap().as_ptr(),
|
||||||
|
ptr::null(),
|
||||||
|
ptr::null(),
|
||||||
|
winapi::SW_SHOWNORMAL,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
pub fn url_open(url: String) {
|
||||||
|
let _ = std::process::Command::new("open").arg(url).output();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
pub fn url_open(url: String) {
|
||||||
|
let _ = std::process::Command::new("xdg-open").arg(url).output();
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::PathBuf;
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
use directories::ProjectDirs;
|
use directories::ProjectDirs;
|
||||||
|
|
||||||
@@ -8,8 +8,6 @@ pub const CLIENT_ID: &str = "d420a117a32841c2b3474932e49fb54b";
|
|||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Default)]
|
#[derive(Serialize, Deserialize, Debug, Default)]
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
pub username: String,
|
|
||||||
pub password: String,
|
|
||||||
pub keybindings: Option<HashMap<String, String>>,
|
pub keybindings: Option<HashMap<String, String>>,
|
||||||
pub theme: Option<ConfigTheme>,
|
pub theme: Option<ConfigTheme>,
|
||||||
pub use_nerdfont: Option<bool>,
|
pub use_nerdfont: Option<bool>,
|
||||||
@@ -42,7 +40,7 @@ pub fn config_path(file: &str) -> PathBuf {
|
|||||||
let proj_dirs = proj_dirs();
|
let proj_dirs = proj_dirs();
|
||||||
let cfg_dir = proj_dirs.config_dir();
|
let cfg_dir = proj_dirs.config_dir();
|
||||||
trace!("{:?}", cfg_dir);
|
trace!("{:?}", cfg_dir);
|
||||||
if !cfg_dir.is_dir() {
|
if cfg_dir.exists() && !cfg_dir.is_dir() {
|
||||||
fs::remove_file(cfg_dir).expect("unable to remove old config file");
|
fs::remove_file(cfg_dir).expect("unable to remove old config file");
|
||||||
}
|
}
|
||||||
if !cfg_dir.exists() {
|
if !cfg_dir.exists() {
|
||||||
@@ -63,3 +61,54 @@ pub fn cache_path(file: &str) -> PathBuf {
|
|||||||
pb.push(file);
|
pb.push(file);
|
||||||
pb
|
pb
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Configuration and credential file helper
|
||||||
|
/// Creates a default configuration if none exist, otherwise will optionally overwrite
|
||||||
|
/// the file if it fails to parse
|
||||||
|
pub fn load_or_generate_default<
|
||||||
|
P: AsRef<Path>,
|
||||||
|
T: serde::Serialize + serde::de::DeserializeOwned,
|
||||||
|
F: Fn(&Path) -> Result<T, String>,
|
||||||
|
>(
|
||||||
|
path: P,
|
||||||
|
default: F,
|
||||||
|
default_on_parse_failure: bool,
|
||||||
|
) -> Result<T, String> {
|
||||||
|
let path = path.as_ref();
|
||||||
|
// Nothing exists so just write the default and return it
|
||||||
|
if !path.exists() {
|
||||||
|
let value = default(&path)?;
|
||||||
|
return write_content_helper(&path, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// load the serialized content. Always report this failure
|
||||||
|
let contents = std::fs::read_to_string(&path)
|
||||||
|
.map_err(|e| format!("Unable to read {}: {}", path.to_string_lossy(), e))?;
|
||||||
|
|
||||||
|
// Deserialize the content, optionally fall back to default if it fails
|
||||||
|
let result = toml::from_str(&contents);
|
||||||
|
if default_on_parse_failure {
|
||||||
|
if let Err(_) = result {
|
||||||
|
let value = default(&path)?;
|
||||||
|
return write_content_helper(&path, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result.map_err(|e| format!("Unable to parse {}: {}", path.to_string_lossy(), e))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_content_helper<P: AsRef<Path>, T: serde::Serialize>(
|
||||||
|
path: P,
|
||||||
|
value: T,
|
||||||
|
) -> Result<T, String> {
|
||||||
|
let content =
|
||||||
|
toml::to_string_pretty(&value).map_err(|e| format!("Failed serializing value: {}", e))?;
|
||||||
|
fs::write(path.as_ref(), content)
|
||||||
|
.map(|_| value)
|
||||||
|
.map_err(|e| {
|
||||||
|
format!(
|
||||||
|
"Failed writing content to {}: {}",
|
||||||
|
path.as_ref().display(),
|
||||||
|
e
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
38
src/main.rs
38
src/main.rs
@@ -35,6 +35,9 @@ use clap::{App, Arg};
|
|||||||
use cursive::traits::Identifiable;
|
use cursive::traits::Identifiable;
|
||||||
use cursive::Cursive;
|
use cursive::Cursive;
|
||||||
|
|
||||||
|
use librespot::core::authentication::Credentials;
|
||||||
|
|
||||||
|
mod authentication;
|
||||||
mod commands;
|
mod commands;
|
||||||
mod config;
|
mod config;
|
||||||
mod events;
|
mod events;
|
||||||
@@ -98,21 +101,22 @@ fn main() {
|
|||||||
|
|
||||||
// Things here may cause the process to abort; we must do them before creating curses windows
|
// Things here may cause the process to abort; we must do them before creating curses windows
|
||||||
// otherwise the error message will not be seen by a user
|
// otherwise the error message will not be seen by a user
|
||||||
let path = config::config_path("config.toml");
|
let cfg: ::config::Config = {
|
||||||
|
let path = config::config_path("config.toml");
|
||||||
|
::config::load_or_generate_default(path, |_| Ok(::config::Config::default()), false)
|
||||||
|
.unwrap_or_else(|e| {
|
||||||
|
eprintln!("{}", e);
|
||||||
|
process::exit(1);
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
let cfg: config::Config = {
|
let credentials: Credentials = {
|
||||||
let contents = std::fs::read_to_string(&path).unwrap_or_else(|_| {
|
let path = config::config_path("credentials.toml");
|
||||||
eprintln!("Cannot read config file from {}", path.to_str().unwrap());
|
::config::load_or_generate_default(path, authentication::create_credentials, true)
|
||||||
eprintln!(
|
.unwrap_or_else(|e| {
|
||||||
"Expected a config file with this format:\n{}",
|
eprintln!("{}", e);
|
||||||
toml::to_string_pretty(&config::Config::default()).unwrap()
|
process::exit(1);
|
||||||
);
|
})
|
||||||
process::exit(1)
|
|
||||||
});
|
|
||||||
toml::from_str(&contents).unwrap_or_else(|e| {
|
|
||||||
eprintln!("{}", e);
|
|
||||||
process::exit(1)
|
|
||||||
})
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let theme = theme::load(&cfg);
|
let theme = theme::load(&cfg);
|
||||||
@@ -122,11 +126,7 @@ fn main() {
|
|||||||
|
|
||||||
let event_manager = EventManager::new(cursive.cb_sink().clone());
|
let event_manager = EventManager::new(cursive.cb_sink().clone());
|
||||||
|
|
||||||
let spotify = Arc::new(spotify::Spotify::new(
|
let spotify = Arc::new(spotify::Spotify::new(event_manager.clone(), credentials));
|
||||||
event_manager.clone(),
|
|
||||||
cfg.username.clone(),
|
|
||||||
cfg.password.clone(),
|
|
||||||
));
|
|
||||||
|
|
||||||
let queue = Arc::new(queue::Queue::new(spotify.clone()));
|
let queue = Arc::new(queue::Queue::new(spotify.clone()));
|
||||||
|
|
||||||
|
|||||||
@@ -184,13 +184,13 @@ impl futures::Future for Worker {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Spotify {
|
impl Spotify {
|
||||||
pub fn new(events: EventManager, user: String, password: String) -> Spotify {
|
pub fn new(events: EventManager, credentials: Credentials) -> Spotify {
|
||||||
let player_config = PlayerConfig {
|
let player_config = PlayerConfig {
|
||||||
bitrate: Bitrate::Bitrate320,
|
bitrate: Bitrate::Bitrate320,
|
||||||
normalisation: false,
|
normalisation: false,
|
||||||
normalisation_pregain: 0.0,
|
normalisation_pregain: 0.0,
|
||||||
};
|
};
|
||||||
let credentials = Credentials::with_password(user.clone(), password.clone());
|
let user = credentials.username.clone();
|
||||||
|
|
||||||
let (tx, rx) = mpsc::unbounded();
|
let (tx, rx) = mpsc::unbounded();
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user