mirror of
https://github.com/agersant/polaris
synced 2024-11-10 10:14:12 +00:00
Added support for lastfm scrobbling
- Added user preferences - Added time and location to log entries
This commit is contained in:
parent
42d1bfb882
commit
c52ec3d30c
12 changed files with 440 additions and 53 deletions
74
Cargo.lock
generated
74
Cargo.lock
generated
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
103
src/api.rs
103
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<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(¶ms::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))
|
||||
}
|
||||
|
|
127
src/config.rs
127
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<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();
|
||||
|
|
|
@ -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;
|
|
@ -0,0 +1,2 @@
|
|||
ALTER TABLE users ADD COLUMN lastfm_username TEXT;
|
||||
ALTER TABLE users ADD COLUMN lastfm_password TEXT;
|
Binary file not shown.
|
@ -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),
|
||||
}
|
||||
}
|
||||
|
|
34
src/index.rs
34
src/index.rs
|
@ -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
44
src/lastfm.rs
Normal 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(())
|
||||
}
|
19
src/main.rs
19
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(())
|
||||
|
|
69
src/user.rs
69
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<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),
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue