Added support for lastfm scrobbling

- Added user preferences
- Added time and location to log entries
This commit is contained in:
Antoine Gersant 2018-03-06 21:36:10 -08:00
parent 42d1bfb882
commit c52ec3d30c
12 changed files with 440 additions and 53 deletions

74
Cargo.lock generated
View file

@ -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"

View file

@ -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"

View file

@ -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<T>(db: &T) -> Result<String>
{
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<DB>, index_channel: Arc<Mutex<Sender<index::Command>>>)
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<DB>, index_channel: Arc<Mutex<Sender<index::Command>>>)
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<Response> {
Ok(Response::with(status::Ok))
}
fn get_preferences(request: &mut Request, db: &DB) -> IronResult<Response> {
let username = match request.extensions.get::<SessionKey>() {
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<Response> {
let username = match request.extensions.get::<SessionKey>() {
Some(s) => s.username.clone(),
None => return Err(Error::from(ErrorKind::AuthenticationRequired).into()),
};
let input = request.get_ref::<params::Params>().unwrap();
let preferences = match input.find(&["preferences"]) {
Some(&params::Value::String(ref preferences)) => preferences,
_ => return Err(Error::from(ErrorKind::MissingPreferences).into()),
};
let preferences = match serde_json::from_str::<config::Preferences>(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<Sender<index::Command>>) -> IronResult<Response> {
let channel = channel.lock().unwrap();
let channel = channel.deref();
@ -634,3 +697,37 @@ fn delete_playlist(request: &mut Request, db: &DB) -> IronResult<Response> {
Ok(Response::with(status::Ok))
}
fn lastfm_now_playing(request: &mut Request, db: &DB) -> IronResult<Response> {
let username = match request.extensions.get::<SessionKey>() {
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<Response> {
let username = match request.extensions.get::<SessionKey>() {
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))
}

View file

@ -25,6 +25,12 @@ pub struct MiscSettings {
pub prefix_url: String,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct Preferences {
pub lastfm_username: Option<String>,
pub lastfm_password: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct ConfigUser {
pub name: String,
@ -76,7 +82,6 @@ pub fn read<T>(db: &T) -> Result<Config>
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<T>(db: &T) -> Result<Config>
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<T>(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<String> = 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<u8> = 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<T>(db: &T, new_config: &Config) -> Result<()>
Ok(())
}
pub fn read_preferences<T>(db: &T, username: &str) -> Result<Preferences>
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<T>(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();

View file

@ -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;

View file

@ -0,0 +1,2 @@
ALTER TABLE users ADD COLUMN lastfm_username TEXT;
ALTER TABLE users ADD COLUMN lastfm_password TEXT;

Binary file not shown.

View file

@ -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<Error> 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),
}
}

View file

@ -630,6 +630,25 @@ pub fn search<T>(db: &T, query: &str) -> Result<Vec<CollectionFile>, errors::Err
Ok(output)
}
pub fn get_song<T>(db: &T, virtual_path: &Path) -> Result<Song, errors::Error>
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");
}

44
src/lastfm.rs Normal file
View file

@ -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<T>(db: &T, track: &Path) -> Result<Scrobble>
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<T>(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<T>(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(())
}

View file

@ -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(())

View file

@ -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<u8>, password: &str) -> Vec<u8> {
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<u8>, password: &str) -> Vec<u8> {
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<u8>,
password_salt: &Vec<u8>,
attempted_password: &str)
-> bool {
pbkdf2::verify(DIGEST_ALG,
HASH_ITERATIONS,
password_salt,
attempted_password.as_bytes(),
password_hash)
.is_ok()
}
pub fn auth<T>(db: &T, username: &str, password: &str) -> Result<bool>
@ -59,13 +62,12 @@ pub fn auth<T>(db: &T, username: &str, password: &str) -> Result<bool>
{
use db::users::dsl::*;
let connection = db.get_connection();
let user: QueryResult<User> = 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<T>(db: &T, username: &str) -> Result<bool>
.get_result(connection.deref())?;
Ok(is_admin != 0)
}
pub fn get_lastfm_credentials<T>(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),
}
}