diff --git a/Cargo.lock b/Cargo.lock index 3907b80..85e7561 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -587,6 +587,20 @@ dependencies = [ "native-tls 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "hyper-tls" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "futures 0.1.17 (registry+https://github.com/rust-lang/crates.io-index)", + "hyper 0.11.9 (registry+https://github.com/rust-lang/crates.io-index)", + "native-tls 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-core 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-io 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-service 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-tls 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "id3" version = "0.2.3" @@ -1189,6 +1203,7 @@ dependencies = [ "ring 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)", "router 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", "rusqlite 0.12.0 (registry+https://github.com/rust-lang/crates.io-index)", + "rustfm-scrobble 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)", "secure-session 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.24 (registry+https://github.com/rust-lang/crates.io-index)", "serde_derive 1.0.24 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1290,6 +1305,27 @@ dependencies = [ "url 1.6.0 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "reqwest" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "bytes 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)", + "futures 0.1.17 (registry+https://github.com/rust-lang/crates.io-index)", + "hyper 0.11.9 (registry+https://github.com/rust-lang/crates.io-index)", + "hyper-tls 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", + "libflate 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", + "native-tls 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.24 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 1.0.8 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_urlencoded 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-core 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-io 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-tls 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", + "url 1.6.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "ring" version = "0.11.0" @@ -1350,6 +1386,19 @@ name = "rustc-serialize" version = "0.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "rustfm-scrobble" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "reqwest 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)", + "rust-crypto 0.2.36 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.24 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_derive 1.0.24 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 1.0.8 (registry+https://github.com/rust-lang/crates.io-index)", + "wrapped-vec 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "safemem" version = "0.1.1" @@ -1672,6 +1721,17 @@ dependencies = [ "futures 0.1.17 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "tokio-tls" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "futures 0.1.17 (registry+https://github.com/rust-lang/crates.io-index)", + "native-tls 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-core 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-io 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "toml" version = "0.4.5" @@ -1844,6 +1904,15 @@ name = "winapi-x86_64-pc-windows-gnu" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "wrapped-vec" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "quote 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 0.11.11 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "ws2_32-sys" version = "0.2.1" @@ -1930,6 +1999,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum hyper 0.10.13 (registry+https://github.com/rust-lang/crates.io-index)" = "368cb56b2740ebf4230520e2b90ebb0461e69034d85d1945febd9b3971426db2" "checksum hyper 0.11.9 (registry+https://github.com/rust-lang/crates.io-index)" = "e0594792d2109069d0caffd176f674d770a84adf024c5bb48e686b1ee5ac7659" "checksum hyper-native-tls 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)" = "72332e4a35d3059583623b50e98e491b78f8b96c5521fcb3f428167955aa56e8" +"checksum hyper-tls 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "9c81fa95203e2a6087242c38691a0210f23e9f3f8f944350bd676522132e2985" "checksum id3 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "fe971c235b6fd4d52ac6d2e71a816511dab3983e1f7fcaac7ea7a218d72b63cb" "checksum idna 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "014b298351066f1512874135335d62a789ffe78a9974f94b43ed5621951eaf7d" "checksum image 0.15.0 (registry+https://github.com/rust-lang/crates.io-index)" = "634700d4a51fa91ceaa798001d46bf862c7b712bd691085d7ba6afd5521e21f7" @@ -2004,6 +2074,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum regex-syntax 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)" = "ad890a5eef7953f55427c50575c680c42841653abd2b028b68cd223d157f62db" "checksum relay 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "f301bafeb60867c85170031bdb2fcf24c8041f33aee09e7b116a58d4e9f781c5" "checksum reqwest 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)" = "1d56dbe269dbe19d716b76ec8c3efce8ef84e974f5b7e5527463e8c0507d4e17" +"checksum reqwest 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)" = "5866613d84e2a39c0479a960bf2d0eff1fbfc934f02cd42b5c08c1e1efc5b1fd" "checksum ring 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "1f2a6dc7fc06a05e6de183c5b97058582e9da2de0c136eafe49609769c507724" "checksum route-recognizer 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)" = "cf3255338088df8146ba63d60a9b8e3556f1146ce2973bc05a75181a42ce2256" "checksum router 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "b9b1797ff166029cb632237bb5542696e54961b4cf75a324c6f05c9cf0584e4e" @@ -2011,6 +2082,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum rust-crypto 0.2.36 (registry+https://github.com/rust-lang/crates.io-index)" = "f76d05d3993fd5f4af9434e8e436db163a12a9d40e1a58a726f27a01dfd12a2a" "checksum rustc-demangle 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "aee45432acc62f7b9a108cc054142dac51f979e69e71ddce7d6fc7adf29e817e" "checksum rustc-serialize 0.3.24 (registry+https://github.com/rust-lang/crates.io-index)" = "dcf128d1287d2ea9d80910b5f1120d0b8eede3fbf1abe91c40d39ea7d51e6fda" +"checksum rustfm-scrobble 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)" = "a186f33cd665fc22db048b72e0b52b077eff8c060d33f6d06384f43efe477734" "checksum safemem 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "725b3bf47ae40b4abcd27b5f0a9540369426a29f7b905649b3e1468e13e22009" "checksum safemem 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e27a8b19b835f7aea908818e871f5cc3a5a186550c30773be987e155e8163d8f" "checksum schannel 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)" = "4330c2e874379fbd28fa67ba43239dbe8c7fb00662ceb1078bd37474f08bf5ce" @@ -2048,6 +2120,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum tokio-io 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "514aae203178929dbf03318ad7c683126672d4d96eccb77b29603d33c9e25743" "checksum tokio-proto 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "8fbb47ae81353c63c487030659494b295f6cb6576242f907f203473b191b0389" "checksum tokio-service 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "24da22d077e0f15f55162bdbdc661228c1581892f52074fb242678d015b45162" +"checksum tokio-tls 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "772f4b04e560117fe3b0a53e490c16ddc8ba6ec437015d91fa385564996ed913" "checksum toml 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)" = "a7540f4ffc193e0d3c94121edb19b055670d369f77d5804db11ae053a45b6e7e" "checksum traitobject 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "efd1f82c56340fdf16f2a953d7bda4f8fdffba13d93b00844c25572110b26079" "checksum twoway 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "db65ddf5811ef1964163e55df0b0b8171e4afc8a53a606dcdb5df87be3dcc302" @@ -2074,5 +2147,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc" "checksum winapi-i686-pc-windows-gnu 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "ec6667f60c23eca65c561e63a13d81b44234c2e38a6b6c959025ee907ec614cc" "checksum winapi-x86_64-pc-windows-gnu 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "98f12c52b2630cd05d2c3ffd8e008f7f48252c042b4871c72aed9dc733b96668" +"checksum wrapped-vec 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "06c29bb4abe93d1c8ef79b60f270d0efcaa6c5c97aaaaaaa0d477ea72f5f9e45" "checksum ws2_32-sys 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "d59cefebd0c892fa2dd6de581e937301d8552cb44489cdff035c6187cb63fa5e" "checksum xdg 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "a66b7c2281ebde13cf4391d70d4c7e5946c3c25e72a7b859ca8f677dcd0b0c61" diff --git a/Cargo.toml b/Cargo.toml index a136733..97e2e1b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,7 @@ hyper = "0.11.2" id3 = "0.2.3" image = "0.15.0" iron = "0.5.1" +rustfm-scrobble = "0.9.1" lewton = "0.6.2" metaflac = "0.1.8" mount = "0.3.0" diff --git a/src/api.rs b/src/api.rs index ca76558..a1817b7 100644 --- a/src/api.rs +++ b/src/api.rs @@ -18,11 +18,11 @@ use typemap; use url::percent_encoding::percent_decode; use config; -use config::MiscSettings; use db::{ConnectionSource, DB}; use db::misc_settings; use errors::*; use index; +use lastfm; use playlist; use user; use serve; @@ -31,7 +31,7 @@ use utils::*; use vfs::VFSSource; const CURRENT_MAJOR_VERSION: i32 = 2; -const CURRENT_MINOR_VERSION: i32 = 1; +const CURRENT_MINOR_VERSION: i32 = 2; #[derive(Deserialize, Serialize)] @@ -50,7 +50,7 @@ fn get_auth_secret(db: &T) -> Result { use self::misc_settings::dsl::*; let connection = db.get_connection(); - let misc: MiscSettings = misc_settings.get_result(connection.deref())?; + let misc: config::MiscSettings = misc_settings.get_result(connection.deref())?; Ok(misc.auth_secret.to_owned()) } @@ -122,6 +122,22 @@ fn get_endpoints(db: Arc, index_channel: Arc>>) auth_api_mount.mount("/serve/", move |request: &mut Request| self::serve(request, db.deref())); } + { + let mut preferences_router = Router::new(); + let get_db = db.clone(); + let put_db = db.clone(); + preferences_router.get("/", + move |request: &mut Request| { + self::get_preferences(request, get_db.deref()) + }, + "get_preferences"); + preferences_router.put("/", + move |request: &mut Request| { + self::put_preferences(request, put_db.deref()) + }, + "put_preferences"); + auth_api_mount.mount("/preferences/", preferences_router); + } { let mut settings_router = Router::new(); let get_db = db.clone(); @@ -188,6 +204,18 @@ fn get_endpoints(db: Arc, index_channel: Arc>>) auth_api_mount.mount("/playlist/", playlist_router); } + { + let db = db.clone(); + auth_api_mount.mount("/lastfm/now_playing/", move |request: &mut Request| { + self::lastfm_now_playing(request, db.deref()) + }); + } + { + let db = db.clone(); + auth_api_mount.mount("/lastfm/scrobble/", move |request: &mut Request| { + self::lastfm_scrobble(request, db.deref()) + }); + } let mut auth_api_chain = Chain::new(auth_api_mount); let auth = AuthRequirement { db: db.clone() }; @@ -523,6 +551,41 @@ fn put_config(request: &mut Request, db: &DB) -> IronResult { Ok(Response::with(status::Ok)) } +fn get_preferences(request: &mut Request, db: &DB) -> IronResult { + let username = match request.extensions.get::() { + Some(s) => s.username.clone(), + None => return Err(Error::from(ErrorKind::AuthenticationRequired).into()), + }; + + let preferences = config::read_preferences(db, &username)?; + let result_json = serde_json::to_string(&preferences); + let result_json = match result_json { + Ok(j) => j, + Err(e) => return Err(IronError::new(e, status::InternalServerError)), + }; + Ok(Response::with((status::Ok, result_json))) +} + +fn put_preferences(request: &mut Request, db: &DB) -> IronResult { + let username = match request.extensions.get::() { + Some(s) => s.username.clone(), + None => return Err(Error::from(ErrorKind::AuthenticationRequired).into()), + }; + + let input = request.get_ref::().unwrap(); + let preferences = match input.find(&["preferences"]) { + Some(¶ms::Value::String(ref preferences)) => preferences, + _ => return Err(Error::from(ErrorKind::MissingPreferences).into()), + }; + let preferences = match serde_json::from_str::(preferences) { + Ok(p) => p, + Err(e) => return Err(IronError::new(e, status::InternalServerError)), + }; + + config::write_preferences(db, &username, &preferences)?; + Ok(Response::with(status::Ok)) +} + fn trigger_index(channel: &Mutex>) -> IronResult { let channel = channel.lock().unwrap(); let channel = channel.deref(); @@ -634,3 +697,37 @@ fn delete_playlist(request: &mut Request, db: &DB) -> IronResult { Ok(Response::with(status::Ok)) } + +fn lastfm_now_playing(request: &mut Request, db: &DB) -> IronResult { + let username = match request.extensions.get::() { + Some(s) => s.username.clone(), + None => return Err(Error::from(ErrorKind::AuthenticationRequired).into()), + }; + + let virtual_path = path_from_request(request); + let virtual_path = match virtual_path { + Err(e) => return Err(IronError::new(e, status::BadRequest)), + Ok(p) => p, + }; + + lastfm::now_playing(db, &username, &virtual_path)?; + + Ok(Response::with(status::Ok)) +} + +fn lastfm_scrobble(request: &mut Request, db: &DB) -> IronResult { + let username = match request.extensions.get::() { + Some(s) => s.username.clone(), + None => return Err(Error::from(ErrorKind::AuthenticationRequired).into()), + }; + + let virtual_path = path_from_request(request); + let virtual_path = match virtual_path { + Err(e) => return Err(IronError::new(e, status::BadRequest)), + Ok(p) => p, + }; + + lastfm::scrobble(db, &username, &virtual_path)?; + + Ok(Response::with(status::Ok)) +} diff --git a/src/config.rs b/src/config.rs index 69522b6..53bf4ed 100644 --- a/src/config.rs +++ b/src/config.rs @@ -25,6 +25,12 @@ pub struct MiscSettings { pub prefix_url: String, } +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct Preferences { + pub lastfm_username: Option, + pub lastfm_password: Option, +} + #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct ConfigUser { pub name: String, @@ -76,7 +82,6 @@ pub fn read(db: &T) -> Result where T: ConnectionSource { use self::misc_settings::dsl::*; - use self::mount_points::dsl::*; use self::ddns_config::dsl::*; let connection = db.get_connection(); @@ -97,21 +102,25 @@ pub fn read(db: &T) -> Result config.reindex_every_n_seconds = Some(sleep_duration); config.prefix_url = if url != "" { Some(url) } else { None }; - let mount_dirs = mount_points - .select((source, name)) - .get_results(connection.deref())?; - config.mount_dirs = Some(mount_dirs); + let mount_dirs; + { + use self::mount_points::dsl::*; + mount_dirs = mount_points + .select((source, name)) + .get_results(connection.deref())?; + config.mount_dirs = Some(mount_dirs); + } let found_users: Vec<(String, i32)> = users::table .select((users::columns::name, users::columns::admin)) .get_results(connection.deref())?; config.users = Some(found_users .into_iter() - .map(|(n, a)| { + .map(|(name, admin)| { ConfigUser { - name: n, + name: name, password: "".to_owned(), - admin: a != 0, + admin: admin != 0, } }) .collect::<_>()); @@ -166,31 +175,51 @@ pub fn amend(db: &T, new_config: &Config) -> Result<()> .get_results(connection.deref())?; // Delete users that are not in new list - // Delete users that have a new password let delete_usernames: Vec = old_usernames - .into_iter() - .filter(|old_name| match config_users.iter().find(|u| &u.name == old_name) { - None => true, - Some(new_user) => !new_user.password.is_empty(), - }) + .iter() + .cloned() + .filter(|old_name| { + config_users + .iter() + .find(|u| &u.name == old_name) + .is_none() + }) .collect::<_>(); diesel::delete(users::table.filter(users::name.eq_any(&delete_usernames))) .execute(connection.deref())?; - // Insert users that have a new password + // Insert new users let insert_users: Vec<&ConfigUser> = config_users .iter() - .filter(|u| !u.password.is_empty()) + .filter(|u| { + old_usernames + .iter() + .find(|old_name| *old_name == &u.name) + .is_none() + }) .collect::<_>(); for ref config_user in insert_users { - let new_user = User::new(&config_user.name, &config_user.password, config_user.admin); + let new_user = User::new(&config_user.name, &config_user.password); diesel::insert_into(users::table) .values(&new_user) .execute(connection.deref())?; } - // Grant admin rights + // Update users for ref user in config_users { + // Update password if provided + if !user.password.is_empty() { + let salt: Vec = users::table + .select(users::columns::password_salt) + .filter(users::name.eq(&user.name)) + .get_result(connection.deref())?; + let hash = hash_password(&salt, &user.password); + diesel::update(users::table.filter(users::name.eq(&user.name))) + .set(users::password_hash.eq(hash)) + .execute(connection.deref())?; + } + + // Update admin rights diesel::update(users::table.filter(users::name.eq(&user.name))) .set(users::admin.eq(user.admin as i32)) .execute(connection.deref())?; @@ -227,6 +256,34 @@ pub fn amend(db: &T, new_config: &Config) -> Result<()> Ok(()) } +pub fn read_preferences(db: &T, username: &str) -> Result + where T: ConnectionSource +{ + use self::users::dsl::*; + let connection = db.get_connection(); + let (read_lastfm_username, read_lastfm_password) = users + .select((lastfm_username, lastfm_password)) + .filter(name.eq(username)) + .get_result(connection.deref())?; + Ok(Preferences { + lastfm_username: read_lastfm_username, + lastfm_password: read_lastfm_password, + }) +} + +pub fn write_preferences(db: &T, username: &str, preferences: &Preferences) -> Result<()> + where T: ConnectionSource +{ + use self::users::dsl::*; + let connection = db.get_connection(); + diesel::update(users) + .set((lastfm_username.eq(&preferences.lastfm_username), + lastfm_password.eq(&preferences.lastfm_password))) + .filter(name.eq(username)) + .execute(connection.deref())?; + Ok(()) +} + fn clean_path_string(path_string: &str) -> path::PathBuf { let separator_regex = Regex::new(r"\\|/").unwrap(); let mut correct_separator = String::new(); @@ -361,7 +418,6 @@ fn test_amend_preserve_password_hashes() { assert_eq!(new_hash, initial_hash); } - #[test] fn test_toggle_admin() { use self::users::dsl::*; @@ -415,6 +471,39 @@ fn test_toggle_admin() { } } +#[test] +fn test_preferences_read_write() { + + let db = _get_test_db("preferences_read_write.sqlite"); + + let initial_config = Config { + album_art_pattern: None, + reindex_every_n_seconds: None, + prefix_url: None, + mount_dirs: None, + users: Some(vec![ConfigUser { + name: "Teddy🐻".into(), + password: "Tasty🍖".into(), + admin: false, + }]), + ydns: None, + }; + amend(&db, &initial_config).unwrap(); + + let old_preferences = read_preferences(&db, "Teddy🐻").unwrap(); + assert_eq!(old_preferences.lastfm_username, None); + assert_eq!(old_preferences.lastfm_password, None); + + let new_preferences = Preferences { + lastfm_username: Some("🐻FM".into()), + lastfm_password: Some("Secret🐻Secret".into()), + }; + write_preferences(&db, "Teddy🐻", &new_preferences).unwrap(); + + let read_preferences = read_preferences(&db, "Teddy🐻").unwrap(); + assert_eq!(new_preferences, read_preferences); +} + #[test] fn test_clean_path_string() { let mut correct_path = path::PathBuf::new(); diff --git a/src/db/migrations/20180303211100_add_last_fm_credentials/down.sql b/src/db/migrations/20180303211100_add_last_fm_credentials/down.sql new file mode 100644 index 0000000..c6fda4e --- /dev/null +++ b/src/db/migrations/20180303211100_add_last_fm_credentials/down.sql @@ -0,0 +1,13 @@ +CREATE TEMPORARY TABLE users_backup(id, name, password_salt, password_hash, admin); +INSERT INTO users_backup SELECT id, name, password_salt, password_hash, admin FROM users; +DROP TABLE users; +CREATE TABLE users ( + id INTEGER PRIMARY KEY NOT NULL, + name TEXT NOT NULL, + password_salt BLOB NOT NULL, + password_hash BLOB NOT NULL, + admin INTEGER NOT NULL, + UNIQUE(name) +); +INSERT INTO users SELECT * FROM users_backup; +DROP TABLE users_backup; diff --git a/src/db/migrations/20180303211100_add_last_fm_credentials/up.sql b/src/db/migrations/20180303211100_add_last_fm_credentials/up.sql new file mode 100644 index 0000000..3a01018 --- /dev/null +++ b/src/db/migrations/20180303211100_add_last_fm_credentials/up.sql @@ -0,0 +1,2 @@ +ALTER TABLE users ADD COLUMN lastfm_username TEXT; +ALTER TABLE users ADD COLUMN lastfm_password TEXT; diff --git a/src/db/schema.sqlite b/src/db/schema.sqlite index 02b6a9b..74c0758 100644 Binary files a/src/db/schema.sqlite and b/src/db/schema.sqlite differ diff --git a/src/errors.rs b/src/errors.rs index 92ab6e6..5fd947f 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -11,6 +11,7 @@ use iron::status::Status; use lewton; use metaflac; use regex; +use rustfm_scrobble; use serde_json; use std; use toml; @@ -32,6 +33,7 @@ error_chain! { Time(std::time::SystemTimeError); Toml(toml::de::Error); Regex(regex::Error); + Scrobbler(rustfm_scrobble::ScrobblerError); Vorbis(lewton::VorbisError); } @@ -40,6 +42,7 @@ error_chain! { AuthenticationRequired {} AdminPrivilegeRequired {} MissingConfig {} + MissingPreferences {} MissingUsername {} MissingPassword {} MissingPlaylist {} @@ -50,6 +53,7 @@ error_chain! { MissingIndexVersion {} MissingPlaylistName {} EncodingError {} + MissingLastFMCredentials {} } } @@ -67,6 +71,9 @@ impl From for IronError { } e @ Error(ErrorKind::CannotServeDirectory, _) => IronError::new(e, Status::BadRequest), e @ Error(ErrorKind::UnsupportedFileType, _) => IronError::new(e, Status::BadRequest), + e @ Error(ErrorKind::MissingLastFMCredentials, _) => { + IronError::new(e, Status::Unauthorized) + } e => IronError::new(e, Status::InternalServerError), } } diff --git a/src/index.rs b/src/index.rs index 9bc81fa..6deef01 100644 --- a/src/index.rs +++ b/src/index.rs @@ -630,6 +630,25 @@ pub fn search(db: &T, query: &str) -> Result, errors::Err Ok(output) } +pub fn get_song(db: &T, virtual_path: &Path) -> Result + where T: ConnectionSource + VFSSource +{ + let vfs = db.get_vfs()?; + let connection = db.get_connection(); + let real_path = vfs.virtual_to_real(virtual_path)?; + let real_path_string = real_path.as_path().to_string_lossy(); + + use self::songs::dsl::*; + let real_song: Song = songs + .filter(path.eq(real_path_string)) + .get_result(connection.deref())?; + + match virtualize_song(&vfs, real_song) { + Some(s) => Ok(s), + _ => bail!("Missing VFS mapping"), + } +} + #[test] fn test_populate() { let db = db::_get_test_db("populate.sqlite"); @@ -746,3 +765,18 @@ fn test_recent() { assert_eq!(results.len(), 2); assert!(results[0].date_added >= results[1].date_added); } + +#[test] +fn test_get_song() { + let db = db::_get_test_db("recent.sqlite"); + update(&db).unwrap(); + + let mut song_path = PathBuf::new(); + song_path.push("root"); + song_path.push("Khemmis"); + song_path.push("Hunted"); + song_path.push("02 - Candlelight.mp3"); + + let song = get_song(&db, &song_path).unwrap(); + assert_eq!(song.title.unwrap(), "Candlelight"); +} diff --git a/src/lastfm.rs b/src/lastfm.rs new file mode 100644 index 0000000..4a3e5e9 --- /dev/null +++ b/src/lastfm.rs @@ -0,0 +1,44 @@ +use rustfm_scrobble::{Scrobbler, Scrobble}; +use std::path::Path; + +use db::ConnectionSource; +use errors::*; +use index; +use user; +use vfs::VFSSource; + +const LASTFM_API_KEY: &str = "02b96c939a2b451c31dfd67add1f696e"; +const LASTFM_API_SECRET: &str = "0f25a80ceef4b470b5cb97d99d4b3420"; + +fn scrobble_from_path(db: &T, track: &Path) -> Result + where T: ConnectionSource + VFSSource +{ + let song = index::get_song(db, track)?; + Ok(Scrobble::new(song.artist.unwrap_or("".into()), + song.title.unwrap_or("".into()), + song.album.unwrap_or("".into()))) +} + +pub fn scrobble(db: &T, username: &str, track: &Path) -> Result<()> + where T: ConnectionSource + VFSSource +{ + let mut scrobbler = Scrobbler::new(LASTFM_API_KEY.into(), LASTFM_API_SECRET.into()); + let scrobble = scrobble_from_path(db, track)?; + let (lastfm_username, lastfm_password) = user::get_lastfm_credentials(db, username)?; + scrobbler + .authenticate_with_password(lastfm_username, lastfm_password)?; + scrobbler.scrobble(scrobble)?; + Ok(()) +} + +pub fn now_playing(db: &T, username: &str, track: &Path) -> Result<()> + where T: ConnectionSource + VFSSource +{ + let mut scrobbler = Scrobbler::new(LASTFM_API_KEY.into(), LASTFM_API_SECRET.into()); + let scrobble = scrobble_from_path(db, track)?; + let (lastfm_username, lastfm_password) = user::get_lastfm_credentials(db, username)?; + scrobbler + .authenticate_with_password(lastfm_username, lastfm_password)?; + scrobbler.now_playing(scrobble)?; + Ok(()) +} diff --git a/src/main.rs b/src/main.rs index 82e4674..5a619b5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,4 @@ -#![recursion_limit = "128"] +#![recursion_limit = "256"] extern crate ape; extern crate app_dirs; @@ -26,6 +26,7 @@ extern crate reqwest; extern crate regex; extern crate ring; extern crate router; +extern crate rustfm_scrobble; extern crate secure_session; extern crate serde; #[macro_use] @@ -63,7 +64,7 @@ use staticfile::Static; use std::path::Path; use std::sync::{Arc, Mutex}; use std::sync::mpsc::channel; -use simplelog::{Config, TermLogger, LogLevelFilter}; +use simplelog::{TermLogger, LogLevelFilter}; #[cfg(unix)] use simplelog::SimpleLogger; @@ -73,6 +74,7 @@ mod db; mod ddns; mod errors; mod index; +mod lastfm; mod metadata; mod playlist; mod ui; @@ -82,6 +84,13 @@ mod serve; mod thumbnails; mod vfs; +static LOG_CONFIG: simplelog::Config = simplelog::Config { + time: Some(simplelog::LogLevel::Error), + level: Some(simplelog::LogLevel::Error), + target: Some(simplelog::LogLevel::Error), + location: Some(simplelog::LogLevel::Error), +}; + fn main() { if let Err(ref e) = run() { println!("Error: {}", e); @@ -117,11 +126,11 @@ fn daemonize(options: &getopts::Matches) -> Result<()> { #[cfg(unix)] fn init_log(log_level: LogLevelFilter, options: &getopts::Matches) -> Result<()> { if options.opt_present("f") { - if let Err(e) = TermLogger::init(log_level, Config::default()) { + if let Err(e) = TermLogger::init(log_level, LOG_CONFIG) { bail!("Error starting terminal logger: {}", e); }; } else { - if let Err(e) = SimpleLogger::init(log_level, Config::default()) { + if let Err(e) = SimpleLogger::init(log_level, LOG_CONFIG) { bail!("Error starting simple logger: {}", e); } } @@ -130,7 +139,7 @@ fn init_log(log_level: LogLevelFilter, options: &getopts::Matches) -> Result<()> #[cfg(windows)] fn init_log(log_level: LogLevelFilter, _: &getopts::Matches) -> Result<()> { - if let Err(e) = TermLogger::init(log_level, Config::default()) { + if let Err(e) = TermLogger::init(log_level, LOG_CONFIG) { bail!("Error starting terminal logger: {}", e); }; Ok(()) diff --git a/src/user.rs b/src/user.rs index f9d031b..774125f 100644 --- a/src/user.rs +++ b/src/user.rs @@ -23,35 +23,38 @@ const HASH_ITERATIONS: u32 = 10000; type PasswordHash = [u8; CREDENTIAL_LEN]; impl User { - pub fn new(name: &str, password: &str, admin: bool) -> User { + pub fn new(name: &str, password: &str) -> User { let salt = rand::random::<[u8; 16]>().to_vec(); - let hash = User::hash_password(&salt, password); + let hash = hash_password(&salt, password); User { name: name.to_owned(), password_salt: salt, password_hash: hash, - admin: admin as i32, + admin: 0, } } +} - pub fn verify_password(&self, attempted_password: &str) -> bool { - pbkdf2::verify(DIGEST_ALG, - HASH_ITERATIONS, - &self.password_salt, - attempted_password.as_bytes(), - &self.password_hash) - .is_ok() - } +pub fn hash_password(salt: &Vec, password: &str) -> Vec { + let mut hash: PasswordHash = [0; CREDENTIAL_LEN]; + pbkdf2::derive(DIGEST_ALG, + HASH_ITERATIONS, + salt, + password.as_bytes(), + &mut hash); + hash.to_vec() +} - fn hash_password(salt: &Vec, password: &str) -> Vec { - let mut hash: PasswordHash = [0; CREDENTIAL_LEN]; - pbkdf2::derive(DIGEST_ALG, - HASH_ITERATIONS, - salt, - password.as_bytes(), - &mut hash); - hash.to_vec() - } +fn verify_password(password_hash: &Vec, + password_salt: &Vec, + attempted_password: &str) + -> bool { + pbkdf2::verify(DIGEST_ALG, + HASH_ITERATIONS, + password_salt, + attempted_password.as_bytes(), + password_hash) + .is_ok() } pub fn auth(db: &T, username: &str, password: &str) -> Result @@ -59,13 +62,12 @@ pub fn auth(db: &T, username: &str, password: &str) -> Result { use db::users::dsl::*; let connection = db.get_connection(); - let user: QueryResult = users - .select((name, password_salt, password_hash, admin)) - .filter(name.eq(username)) - .get_result(connection.deref()); - match user { + match users + .select((password_hash, password_salt)) + .filter(name.eq(username)) + .get_result(connection.deref()) { Err(diesel::result::Error::NotFound) => Ok(false), - Ok(u) => Ok(u.verify_password(password)), + Ok((hash, salt)) => Ok(verify_password(&hash, &salt, password)), Err(e) => Err(e.into()), } } @@ -90,3 +92,18 @@ pub fn is_admin(db: &T, username: &str) -> Result .get_result(connection.deref())?; Ok(is_admin != 0) } + +pub fn get_lastfm_credentials(db: &T, username: &str) -> Result<(String, String)> + where T: ConnectionSource +{ + use db::users::dsl::*; + let connection = db.get_connection(); + let credentials = users + .filter(name.eq(username)) + .select((lastfm_username, lastfm_password)) + .get_result(connection.deref())?; + match credentials { + (Some(u), Some(p)) => Ok((u, p)), + _ => bail!(ErrorKind::MissingLastFMCredentials), + } +}