mirror of
https://github.com/agersant/polaris
synced 2025-02-23 00:58:28 +00:00
Formatting
This commit is contained in:
parent
6044fbb029
commit
f9a27895b4
14 changed files with 1457 additions and 1454 deletions
2
.rustfmt.toml
Normal file
2
.rustfmt.toml
Normal file
|
@ -0,0 +1,2 @@
|
|||
write_mode = "Overwrite"
|
||||
hard_tabs = true
|
290
src/api.rs
290
src/api.rs
|
@ -23,197 +23,197 @@ const CURRENT_MINOR_VERSION: i32 = 1;
|
|||
|
||||
#[derive(RustcEncodable)]
|
||||
struct Version {
|
||||
major: i32,
|
||||
minor: i32,
|
||||
major: i32,
|
||||
minor: i32,
|
||||
}
|
||||
|
||||
impl Version {
|
||||
fn new(major: i32, minor: i32) -> Version {
|
||||
Version {
|
||||
major: major,
|
||||
minor: minor,
|
||||
}
|
||||
}
|
||||
fn new(major: i32, minor: i32) -> Version {
|
||||
Version {
|
||||
major: major,
|
||||
minor: minor,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_api_handler(collection: Arc<Collection>) -> Mount {
|
||||
let mut api_handler = Mount::new();
|
||||
let mut api_handler = Mount::new();
|
||||
|
||||
{
|
||||
let collection = collection.clone();
|
||||
api_handler.mount("/version/", self::version);
|
||||
api_handler.mount("/auth/",
|
||||
move |request: &mut Request| self::auth(request, collection.deref()));
|
||||
}
|
||||
{
|
||||
let collection = collection.clone();
|
||||
api_handler.mount("/version/", self::version);
|
||||
api_handler.mount("/auth/",
|
||||
move |request: &mut Request| self::auth(request, collection.deref()));
|
||||
}
|
||||
|
||||
{
|
||||
let mut auth_api_mount = Mount::new();
|
||||
{
|
||||
let collection = collection.clone();
|
||||
auth_api_mount.mount("/browse/", move |request: &mut Request| {
|
||||
self::browse(request, collection.deref())
|
||||
});
|
||||
}
|
||||
{
|
||||
let collection = collection.clone();
|
||||
auth_api_mount.mount("/flatten/", move |request: &mut Request| {
|
||||
self::flatten(request, collection.deref())
|
||||
});
|
||||
}
|
||||
{
|
||||
let collection = collection.clone();
|
||||
auth_api_mount.mount("/random/", move |request: &mut Request| {
|
||||
self::random(request, collection.deref())
|
||||
});
|
||||
}
|
||||
{
|
||||
let collection = collection.clone();
|
||||
auth_api_mount.mount("/serve/", move |request: &mut Request| {
|
||||
self::serve(request, collection.deref())
|
||||
});
|
||||
}
|
||||
{
|
||||
let mut auth_api_mount = Mount::new();
|
||||
{
|
||||
let collection = collection.clone();
|
||||
auth_api_mount.mount("/browse/", move |request: &mut Request| {
|
||||
self::browse(request, collection.deref())
|
||||
});
|
||||
}
|
||||
{
|
||||
let collection = collection.clone();
|
||||
auth_api_mount.mount("/flatten/", move |request: &mut Request| {
|
||||
self::flatten(request, collection.deref())
|
||||
});
|
||||
}
|
||||
{
|
||||
let collection = collection.clone();
|
||||
auth_api_mount.mount("/random/", move |request: &mut Request| {
|
||||
self::random(request, collection.deref())
|
||||
});
|
||||
}
|
||||
{
|
||||
let collection = collection.clone();
|
||||
auth_api_mount.mount("/serve/", move |request: &mut Request| {
|
||||
self::serve(request, collection.deref())
|
||||
});
|
||||
}
|
||||
|
||||
let mut auth_api_chain = Chain::new(auth_api_mount);
|
||||
auth_api_chain.link_before(AuthRequirement);
|
||||
let mut auth_api_chain = Chain::new(auth_api_mount);
|
||||
auth_api_chain.link_before(AuthRequirement);
|
||||
|
||||
api_handler.mount("/", auth_api_chain);
|
||||
}
|
||||
api_handler
|
||||
api_handler.mount("/", auth_api_chain);
|
||||
}
|
||||
api_handler
|
||||
}
|
||||
|
||||
fn path_from_request(request: &Request) -> Result<PathBuf> {
|
||||
let path_string = request.url.path().join("\\");
|
||||
let decoded_path = percent_decode(path_string.as_bytes()).decode_utf8()?;
|
||||
Ok(PathBuf::from(decoded_path.deref()))
|
||||
let path_string = request.url.path().join("\\");
|
||||
let decoded_path = percent_decode(path_string.as_bytes()).decode_utf8()?;
|
||||
Ok(PathBuf::from(decoded_path.deref()))
|
||||
}
|
||||
|
||||
struct AuthRequirement;
|
||||
impl BeforeMiddleware for AuthRequirement {
|
||||
fn before(&self, req: &mut Request) -> IronResult<()> {
|
||||
match req.get_cookie("username") {
|
||||
Some(_) => Ok(()),
|
||||
None => Err(Error::from(ErrorKind::AuthenticationRequired).into()),
|
||||
}
|
||||
}
|
||||
fn before(&self, req: &mut Request) -> IronResult<()> {
|
||||
match req.get_cookie("username") {
|
||||
Some(_) => Ok(()),
|
||||
None => Err(Error::from(ErrorKind::AuthenticationRequired).into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn version(_: &mut Request) -> IronResult<Response> {
|
||||
let current_version = Version::new(CURRENT_MAJOR_VERSION, CURRENT_MINOR_VERSION);
|
||||
match json::encode(¤t_version) {
|
||||
Ok(result_json) => Ok(Response::with((status::Ok, result_json))),
|
||||
Err(e) => Err(IronError::new(e, status::InternalServerError)),
|
||||
}
|
||||
let current_version = Version::new(CURRENT_MAJOR_VERSION, CURRENT_MINOR_VERSION);
|
||||
match json::encode(¤t_version) {
|
||||
Ok(result_json) => Ok(Response::with((status::Ok, result_json))),
|
||||
Err(e) => Err(IronError::new(e, status::InternalServerError)),
|
||||
}
|
||||
}
|
||||
|
||||
fn auth(request: &mut Request, collection: &Collection) -> IronResult<Response> {
|
||||
let input = request.get_ref::<params::Params>().unwrap();
|
||||
let username = match input.find(&["username"]) {
|
||||
Some(¶ms::Value::String(ref username)) => username,
|
||||
_ => return Err(Error::from(ErrorKind::MissingUsername).into()),
|
||||
};
|
||||
let password = match input.find(&["password"]) {
|
||||
Some(¶ms::Value::String(ref password)) => password,
|
||||
_ => return Err(Error::from(ErrorKind::MissingPassword).into()),
|
||||
};
|
||||
if collection.auth(username.as_str(), password.as_str()) {
|
||||
let mut response = Response::with((status::Ok, ""));
|
||||
let mut username_cookie = CookiePair::new("username".to_string(), username.clone());
|
||||
username_cookie.path = Some("/".to_owned());
|
||||
response.set_cookie(username_cookie);
|
||||
Ok(response)
|
||||
} else {
|
||||
Err(Error::from(ErrorKind::IncorrectCredentials).into())
|
||||
}
|
||||
let input = request.get_ref::<params::Params>().unwrap();
|
||||
let username = match input.find(&["username"]) {
|
||||
Some(¶ms::Value::String(ref username)) => username,
|
||||
_ => return Err(Error::from(ErrorKind::MissingUsername).into()),
|
||||
};
|
||||
let password = match input.find(&["password"]) {
|
||||
Some(¶ms::Value::String(ref password)) => password,
|
||||
_ => return Err(Error::from(ErrorKind::MissingPassword).into()),
|
||||
};
|
||||
if collection.auth(username.as_str(), password.as_str()) {
|
||||
let mut response = Response::with((status::Ok, ""));
|
||||
let mut username_cookie = CookiePair::new("username".to_string(), username.clone());
|
||||
username_cookie.path = Some("/".to_owned());
|
||||
response.set_cookie(username_cookie);
|
||||
Ok(response)
|
||||
} else {
|
||||
Err(Error::from(ErrorKind::IncorrectCredentials).into())
|
||||
}
|
||||
}
|
||||
|
||||
fn browse(request: &mut Request, collection: &Collection) -> IronResult<Response> {
|
||||
let path = path_from_request(request);
|
||||
let path = match path {
|
||||
Err(e) => return Err(IronError::new(e, status::BadRequest)),
|
||||
Ok(p) => p,
|
||||
};
|
||||
let browse_result = collection.browse(&path)?;
|
||||
let path = path_from_request(request);
|
||||
let path = match path {
|
||||
Err(e) => return Err(IronError::new(e, status::BadRequest)),
|
||||
Ok(p) => p,
|
||||
};
|
||||
let browse_result = collection.browse(&path)?;
|
||||
|
||||
let result_json = json::encode(&browse_result);
|
||||
let result_json = match result_json {
|
||||
Ok(j) => j,
|
||||
Err(e) => return Err(IronError::new(e, status::InternalServerError)),
|
||||
};
|
||||
let result_json = json::encode(&browse_result);
|
||||
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)))
|
||||
Ok(Response::with((status::Ok, result_json)))
|
||||
}
|
||||
|
||||
fn flatten(request: &mut Request, collection: &Collection) -> IronResult<Response> {
|
||||
let path = path_from_request(request);
|
||||
let path = match path {
|
||||
Err(e) => return Err(IronError::new(e, status::BadRequest)),
|
||||
Ok(p) => p,
|
||||
};
|
||||
let flatten_result = collection.flatten(&path)?;
|
||||
let path = path_from_request(request);
|
||||
let path = match path {
|
||||
Err(e) => return Err(IronError::new(e, status::BadRequest)),
|
||||
Ok(p) => p,
|
||||
};
|
||||
let flatten_result = collection.flatten(&path)?;
|
||||
|
||||
let result_json = json::encode(&flatten_result);
|
||||
let result_json = match result_json {
|
||||
Ok(j) => j,
|
||||
Err(e) => return Err(IronError::new(e, status::InternalServerError)),
|
||||
};
|
||||
let result_json = json::encode(&flatten_result);
|
||||
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)))
|
||||
Ok(Response::with((status::Ok, result_json)))
|
||||
}
|
||||
|
||||
fn random(_: &mut Request, collection: &Collection) -> IronResult<Response> {
|
||||
let random_result = collection.get_random_albums(20)?;
|
||||
let result_json = json::encode(&random_result);
|
||||
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)))
|
||||
let random_result = collection.get_random_albums(20)?;
|
||||
let result_json = json::encode(&random_result);
|
||||
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 serve(request: &mut Request, collection: &Collection) -> IronResult<Response> {
|
||||
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,
|
||||
};
|
||||
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,
|
||||
};
|
||||
|
||||
let real_path = collection.locate(virtual_path.as_path());
|
||||
let real_path = match real_path {
|
||||
Err(e) => return Err(IronError::new(e, status::NotFound)),
|
||||
Ok(p) => p,
|
||||
};
|
||||
let real_path = collection.locate(virtual_path.as_path());
|
||||
let real_path = match real_path {
|
||||
Err(e) => return Err(IronError::new(e, status::NotFound)),
|
||||
Ok(p) => p,
|
||||
};
|
||||
|
||||
let metadata = match fs::metadata(real_path.as_path()) {
|
||||
Ok(meta) => meta,
|
||||
Err(e) => {
|
||||
let status = match e.kind() {
|
||||
io::ErrorKind::NotFound => status::NotFound,
|
||||
io::ErrorKind::PermissionDenied => status::Forbidden,
|
||||
_ => status::InternalServerError,
|
||||
};
|
||||
return Err(IronError::new(e, status));
|
||||
}
|
||||
};
|
||||
let metadata = match fs::metadata(real_path.as_path()) {
|
||||
Ok(meta) => meta,
|
||||
Err(e) => {
|
||||
let status = match e.kind() {
|
||||
io::ErrorKind::NotFound => status::NotFound,
|
||||
io::ErrorKind::PermissionDenied => status::Forbidden,
|
||||
_ => status::InternalServerError,
|
||||
};
|
||||
return Err(IronError::new(e, status));
|
||||
}
|
||||
};
|
||||
|
||||
if !metadata.is_file() {
|
||||
return Err(Error::from(ErrorKind::CannotServeDirectory).into());
|
||||
}
|
||||
if !metadata.is_file() {
|
||||
return Err(Error::from(ErrorKind::CannotServeDirectory).into());
|
||||
}
|
||||
|
||||
if is_song(real_path.as_path()) {
|
||||
return Ok(Response::with((status::Ok, real_path)));
|
||||
}
|
||||
if is_song(real_path.as_path()) {
|
||||
return Ok(Response::with((status::Ok, real_path)));
|
||||
}
|
||||
|
||||
if is_image(real_path.as_path()) {
|
||||
return art(request, real_path.as_path());
|
||||
}
|
||||
if is_image(real_path.as_path()) {
|
||||
return art(request, real_path.as_path());
|
||||
}
|
||||
|
||||
Err(Error::from(ErrorKind::UnsupportedFileType).into())
|
||||
Err(Error::from(ErrorKind::UnsupportedFileType).into())
|
||||
}
|
||||
|
||||
fn art(_: &mut Request, real_path: &Path) -> IronResult<Response> {
|
||||
let thumb = get_thumbnail(real_path, 400);
|
||||
match thumb {
|
||||
Ok(path) => Ok(Response::with((status::Ok, path))),
|
||||
Err(e) => Err(IronError::from(e)),
|
||||
}
|
||||
let thumb = get_thumbnail(real_path, 400);
|
||||
match thumb {
|
||||
Ok(path) => Ok(Response::with((status::Ok, path))),
|
||||
Err(e) => Err(IronError::from(e)),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,56 +11,56 @@ use vfs::*;
|
|||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct User {
|
||||
name: String,
|
||||
password: String,
|
||||
name: String,
|
||||
password: String,
|
||||
}
|
||||
|
||||
impl User {
|
||||
pub fn new(name: String, password: String) -> User {
|
||||
User {
|
||||
name: name,
|
||||
password: password,
|
||||
}
|
||||
}
|
||||
pub fn new(name: String, password: String) -> User {
|
||||
User {
|
||||
name: name,
|
||||
password: password,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Collection {
|
||||
vfs: Arc<Vfs>,
|
||||
users: Vec<User>,
|
||||
index: Arc<Index>,
|
||||
vfs: Arc<Vfs>,
|
||||
users: Vec<User>,
|
||||
index: Arc<Index>,
|
||||
}
|
||||
|
||||
impl Collection {
|
||||
pub fn new(vfs: Arc<Vfs>, index: Arc<Index>) -> Collection {
|
||||
Collection {
|
||||
vfs: vfs,
|
||||
users: Vec::new(),
|
||||
index: index,
|
||||
}
|
||||
}
|
||||
pub fn new(vfs: Arc<Vfs>, index: Arc<Index>) -> Collection {
|
||||
Collection {
|
||||
vfs: vfs,
|
||||
users: Vec::new(),
|
||||
index: index,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn load_config(&mut self, config: &Config) -> Result<()> {
|
||||
self.users = config.users.to_vec();
|
||||
Ok(())
|
||||
}
|
||||
pub fn load_config(&mut self, config: &Config) -> Result<()> {
|
||||
self.users = config.users.to_vec();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn auth(&self, username: &str, password: &str) -> bool {
|
||||
self.users.iter().any(|u| u.name == username && u.password == password)
|
||||
}
|
||||
pub fn auth(&self, username: &str, password: &str) -> bool {
|
||||
self.users.iter().any(|u| u.name == username && u.password == password)
|
||||
}
|
||||
|
||||
pub fn browse(&self, virtual_path: &Path) -> Result<Vec<CollectionFile>> {
|
||||
self.index.deref().browse(virtual_path)
|
||||
}
|
||||
pub fn browse(&self, virtual_path: &Path) -> Result<Vec<CollectionFile>> {
|
||||
self.index.deref().browse(virtual_path)
|
||||
}
|
||||
|
||||
pub fn flatten(&self, virtual_path: &Path) -> Result<Vec<Song>> {
|
||||
self.index.deref().flatten(virtual_path)
|
||||
}
|
||||
pub fn flatten(&self, virtual_path: &Path) -> Result<Vec<Song>> {
|
||||
self.index.deref().flatten(virtual_path)
|
||||
}
|
||||
|
||||
pub fn get_random_albums(&self, count: u32) -> Result<Vec<Directory>> {
|
||||
self.index.deref().get_random_albums(count)
|
||||
}
|
||||
pub fn get_random_albums(&self, count: u32) -> Result<Vec<Directory>> {
|
||||
self.index.deref().get_random_albums(count)
|
||||
}
|
||||
|
||||
pub fn locate(&self, virtual_path: &Path) -> Result<PathBuf> {
|
||||
self.vfs.virtual_to_real(virtual_path)
|
||||
}
|
||||
pub fn locate(&self, virtual_path: &Path) -> Result<PathBuf> {
|
||||
self.vfs.virtual_to_real(virtual_path)
|
||||
}
|
||||
}
|
||||
|
|
322
src/config.rs
322
src/config.rs
|
@ -28,195 +28,195 @@ const CONFIG_DDNS_USERNAME: &'static str = "username";
|
|||
const CONFIG_DDNS_PASSWORD: &'static str = "password";
|
||||
|
||||
pub struct Config {
|
||||
pub secret: String,
|
||||
pub vfs: VfsConfig,
|
||||
pub users: Vec<User>,
|
||||
pub index: IndexConfig,
|
||||
pub ddns: Option<DDNSConfig>,
|
||||
pub secret: String,
|
||||
pub vfs: VfsConfig,
|
||||
pub users: Vec<User>,
|
||||
pub index: IndexConfig,
|
||||
pub ddns: Option<DDNSConfig>,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn parse(custom_path: Option<path::PathBuf>) -> Result<Config> {
|
||||
pub fn parse(custom_path: Option<path::PathBuf>) -> Result<Config> {
|
||||
|
||||
let config_path = match custom_path {
|
||||
Some(p) => p,
|
||||
None => {
|
||||
let mut root = utils::get_config_root()?;
|
||||
root.push(DEFAULT_CONFIG_FILE_NAME);
|
||||
root
|
||||
}
|
||||
};
|
||||
println!("Config file path: {}", config_path.to_string_lossy());
|
||||
let config_path = match custom_path {
|
||||
Some(p) => p,
|
||||
None => {
|
||||
let mut root = utils::get_config_root()?;
|
||||
root.push(DEFAULT_CONFIG_FILE_NAME);
|
||||
root
|
||||
}
|
||||
};
|
||||
println!("Config file path: {}", config_path.to_string_lossy());
|
||||
|
||||
let mut config_file = fs::File::open(config_path)?;
|
||||
let mut config_file_content = String::new();
|
||||
config_file.read_to_string(&mut config_file_content)?;
|
||||
let parsed_config = toml::Parser::new(config_file_content.as_str()).parse();
|
||||
let parsed_config = parsed_config.ok_or("Could not parse config as valid TOML")?;
|
||||
let mut config_file = fs::File::open(config_path)?;
|
||||
let mut config_file_content = String::new();
|
||||
config_file.read_to_string(&mut config_file_content)?;
|
||||
let parsed_config = toml::Parser::new(config_file_content.as_str()).parse();
|
||||
let parsed_config = parsed_config.ok_or("Could not parse config as valid TOML")?;
|
||||
|
||||
let mut config = Config {
|
||||
secret: String::new(),
|
||||
vfs: VfsConfig::new(),
|
||||
users: Vec::new(),
|
||||
index: IndexConfig::new(),
|
||||
ddns: None,
|
||||
};
|
||||
let mut config = Config {
|
||||
secret: String::new(),
|
||||
vfs: VfsConfig::new(),
|
||||
users: Vec::new(),
|
||||
index: IndexConfig::new(),
|
||||
ddns: None,
|
||||
};
|
||||
|
||||
config.parse_secret(&parsed_config)?;
|
||||
config.parse_index_sleep_duration(&parsed_config)?;
|
||||
config.parse_mount_points(&parsed_config)?;
|
||||
config.parse_users(&parsed_config)?;
|
||||
config.parse_album_art_pattern(&parsed_config)?;
|
||||
config.parse_ddns(&parsed_config)?;
|
||||
config.parse_secret(&parsed_config)?;
|
||||
config.parse_index_sleep_duration(&parsed_config)?;
|
||||
config.parse_mount_points(&parsed_config)?;
|
||||
config.parse_users(&parsed_config)?;
|
||||
config.parse_album_art_pattern(&parsed_config)?;
|
||||
config.parse_ddns(&parsed_config)?;
|
||||
|
||||
let mut index_path = utils::get_cache_root()?;
|
||||
index_path.push(INDEX_FILE_NAME);
|
||||
config.index.path = index_path;
|
||||
let mut index_path = utils::get_cache_root()?;
|
||||
index_path.push(INDEX_FILE_NAME);
|
||||
config.index.path = index_path;
|
||||
|
||||
Ok(config)
|
||||
}
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
fn parse_secret(&mut self, source: &toml::Table) -> Result<()> {
|
||||
self.secret = source.get(CONFIG_SECRET)
|
||||
.and_then(|s| s.as_str())
|
||||
.map(|s| s.to_owned())
|
||||
.ok_or("Could not parse config secret")?;
|
||||
Ok(())
|
||||
}
|
||||
fn parse_secret(&mut self, source: &toml::Table) -> Result<()> {
|
||||
self.secret = source.get(CONFIG_SECRET)
|
||||
.and_then(|s| s.as_str())
|
||||
.map(|s| s.to_owned())
|
||||
.ok_or("Could not parse config secret")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn parse_index_sleep_duration(&mut self, source: &toml::Table) -> Result<()> {
|
||||
let sleep_duration = match source.get(CONFIG_INDEX_SLEEP_DURATION) {
|
||||
Some(s) => s,
|
||||
None => return Ok(()),
|
||||
};
|
||||
let sleep_duration = match sleep_duration {
|
||||
&toml::Value::Integer(s) => s as u64,
|
||||
_ => bail!("Could not parse index sleep duration"),
|
||||
};
|
||||
self.index.sleep_duration = sleep_duration;
|
||||
Ok(())
|
||||
}
|
||||
fn parse_index_sleep_duration(&mut self, source: &toml::Table) -> Result<()> {
|
||||
let sleep_duration = match source.get(CONFIG_INDEX_SLEEP_DURATION) {
|
||||
Some(s) => s,
|
||||
None => return Ok(()),
|
||||
};
|
||||
let sleep_duration = match sleep_duration {
|
||||
&toml::Value::Integer(s) => s as u64,
|
||||
_ => bail!("Could not parse index sleep duration"),
|
||||
};
|
||||
self.index.sleep_duration = sleep_duration;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn parse_album_art_pattern(&mut self, source: &toml::Table) -> Result<()> {
|
||||
let pattern = match source.get(CONFIG_ALBUM_ART_PATTERN) {
|
||||
Some(s) => s,
|
||||
None => return Ok(()),
|
||||
};
|
||||
let pattern = match pattern {
|
||||
&toml::Value::String(ref s) => s,
|
||||
_ => bail!("Could not parse album art pattern"),
|
||||
};
|
||||
self.index.album_art_pattern = Some(regex::Regex::new(pattern)?);
|
||||
Ok(())
|
||||
}
|
||||
fn parse_album_art_pattern(&mut self, source: &toml::Table) -> Result<()> {
|
||||
let pattern = match source.get(CONFIG_ALBUM_ART_PATTERN) {
|
||||
Some(s) => s,
|
||||
None => return Ok(()),
|
||||
};
|
||||
let pattern = match pattern {
|
||||
&toml::Value::String(ref s) => s,
|
||||
_ => bail!("Could not parse album art pattern"),
|
||||
};
|
||||
self.index.album_art_pattern = Some(regex::Regex::new(pattern)?);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn parse_users(&mut self, source: &toml::Table) -> Result<()> {
|
||||
let users = match source.get(CONFIG_USERS) {
|
||||
Some(s) => s,
|
||||
None => return Ok(()),
|
||||
};
|
||||
fn parse_users(&mut self, source: &toml::Table) -> Result<()> {
|
||||
let users = match source.get(CONFIG_USERS) {
|
||||
Some(s) => s,
|
||||
None => return Ok(()),
|
||||
};
|
||||
|
||||
let users = match users {
|
||||
&toml::Value::Array(ref a) => a,
|
||||
_ => bail!("Could not parse users array"),
|
||||
};
|
||||
let users = match users {
|
||||
&toml::Value::Array(ref a) => a,
|
||||
_ => bail!("Could not parse users array"),
|
||||
};
|
||||
|
||||
for user in users {
|
||||
let name = user.lookup(CONFIG_USER_NAME)
|
||||
.and_then(|n| n.as_str())
|
||||
.ok_or("Could not parse username")?;
|
||||
let password = user.lookup(CONFIG_USER_PASSWORD)
|
||||
.and_then(|n| n.as_str())
|
||||
.ok_or("Could not parse user password")?;
|
||||
let user = User::new(name.to_owned(), password.to_owned());
|
||||
self.users.push(user);
|
||||
}
|
||||
for user in users {
|
||||
let name = user.lookup(CONFIG_USER_NAME)
|
||||
.and_then(|n| n.as_str())
|
||||
.ok_or("Could not parse username")?;
|
||||
let password = user.lookup(CONFIG_USER_PASSWORD)
|
||||
.and_then(|n| n.as_str())
|
||||
.ok_or("Could not parse user password")?;
|
||||
let user = User::new(name.to_owned(), password.to_owned());
|
||||
self.users.push(user);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn parse_mount_points(&mut self, source: &toml::Table) -> Result<()> {
|
||||
let mount_dirs = match source.get(CONFIG_MOUNT_DIRS) {
|
||||
Some(s) => s,
|
||||
None => return Ok(()),
|
||||
};
|
||||
fn parse_mount_points(&mut self, source: &toml::Table) -> Result<()> {
|
||||
let mount_dirs = match source.get(CONFIG_MOUNT_DIRS) {
|
||||
Some(s) => s,
|
||||
None => return Ok(()),
|
||||
};
|
||||
|
||||
let mount_dirs = match mount_dirs {
|
||||
&toml::Value::Array(ref a) => a,
|
||||
_ => bail!("Could not parse mount directories array"),
|
||||
};
|
||||
let mount_dirs = match mount_dirs {
|
||||
&toml::Value::Array(ref a) => a,
|
||||
_ => bail!("Could not parse mount directories array"),
|
||||
};
|
||||
|
||||
for dir in mount_dirs {
|
||||
let name = dir.lookup(CONFIG_MOUNT_DIR_NAME)
|
||||
.and_then(|n| n.as_str())
|
||||
.ok_or("Could not parse mount directory name")?;
|
||||
let source = dir.lookup(CONFIG_MOUNT_DIR_SOURCE)
|
||||
.and_then(|n| n.as_str())
|
||||
.ok_or("Could not parse mount directory source")?;
|
||||
let source = clean_path_string(source);
|
||||
if self.vfs.mount_points.contains_key(name) {
|
||||
bail!("Conflicting mount directories");
|
||||
}
|
||||
self.vfs.mount_points.insert(name.to_owned(), source);
|
||||
}
|
||||
for dir in mount_dirs {
|
||||
let name = dir.lookup(CONFIG_MOUNT_DIR_NAME)
|
||||
.and_then(|n| n.as_str())
|
||||
.ok_or("Could not parse mount directory name")?;
|
||||
let source = dir.lookup(CONFIG_MOUNT_DIR_SOURCE)
|
||||
.and_then(|n| n.as_str())
|
||||
.ok_or("Could not parse mount directory source")?;
|
||||
let source = clean_path_string(source);
|
||||
if self.vfs.mount_points.contains_key(name) {
|
||||
bail!("Conflicting mount directories");
|
||||
}
|
||||
self.vfs.mount_points.insert(name.to_owned(), source);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn parse_ddns(&mut self, source: &toml::Table) -> Result<()> {
|
||||
let ddns = match source.get(CONFIG_DDNS) {
|
||||
Some(s) => s,
|
||||
None => return Ok(()),
|
||||
};
|
||||
let ddns = match ddns {
|
||||
&toml::Value::Table(ref a) => a,
|
||||
_ => bail!("Could not parse DDNS settings table"),
|
||||
};
|
||||
fn parse_ddns(&mut self, source: &toml::Table) -> Result<()> {
|
||||
let ddns = match source.get(CONFIG_DDNS) {
|
||||
Some(s) => s,
|
||||
None => return Ok(()),
|
||||
};
|
||||
let ddns = match ddns {
|
||||
&toml::Value::Table(ref a) => a,
|
||||
_ => bail!("Could not parse DDNS settings table"),
|
||||
};
|
||||
|
||||
let host =
|
||||
ddns.get(CONFIG_DDNS_HOST).and_then(|n| n.as_str()).ok_or("Could not parse DDNS host")?;
|
||||
let username = ddns.get(CONFIG_DDNS_USERNAME)
|
||||
.and_then(|n| n.as_str())
|
||||
.ok_or("Could not parse DDNS username")?;
|
||||
let password = ddns.get(CONFIG_DDNS_PASSWORD)
|
||||
.and_then(|n| n.as_str())
|
||||
.ok_or("Could not parse DDNS password")?;
|
||||
let host =
|
||||
ddns.get(CONFIG_DDNS_HOST).and_then(|n| n.as_str()).ok_or("Could not parse DDNS host")?;
|
||||
let username = ddns.get(CONFIG_DDNS_USERNAME)
|
||||
.and_then(|n| n.as_str())
|
||||
.ok_or("Could not parse DDNS username")?;
|
||||
let password = ddns.get(CONFIG_DDNS_PASSWORD)
|
||||
.and_then(|n| n.as_str())
|
||||
.ok_or("Could not parse DDNS password")?;
|
||||
|
||||
self.ddns = Some(DDNSConfig {
|
||||
host: host.to_owned(),
|
||||
username: username.to_owned(),
|
||||
password: password.to_owned(),
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
self.ddns = Some(DDNSConfig {
|
||||
host: host.to_owned(),
|
||||
username: username.to_owned(),
|
||||
password: password.to_owned(),
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn clean_path_string(path_string: &str) -> path::PathBuf {
|
||||
let separator_regex = regex::Regex::new(r"\\|/").unwrap();
|
||||
let mut correct_separator = String::new();
|
||||
correct_separator.push(path::MAIN_SEPARATOR);
|
||||
let path_string = separator_regex.replace_all(path_string, correct_separator.as_str());
|
||||
path::PathBuf::from(path_string)
|
||||
let separator_regex = regex::Regex::new(r"\\|/").unwrap();
|
||||
let mut correct_separator = String::new();
|
||||
correct_separator.push(path::MAIN_SEPARATOR);
|
||||
let path_string = separator_regex.replace_all(path_string, correct_separator.as_str());
|
||||
path::PathBuf::from(path_string)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_clean_path_string() {
|
||||
let mut correct_path = path::PathBuf::new();
|
||||
if cfg!(target_os = "windows") {
|
||||
correct_path.push("C:\\");
|
||||
} else {
|
||||
correct_path.push("/usr");
|
||||
}
|
||||
correct_path.push("some");
|
||||
correct_path.push("path");
|
||||
if cfg!(target_os = "windows") {
|
||||
assert_eq!(correct_path, clean_path_string(r#"C:/some/path"#));
|
||||
assert_eq!(correct_path, clean_path_string(r#"C:\some\path"#));
|
||||
assert_eq!(correct_path, clean_path_string(r#"C:\some\path\"#));
|
||||
} else {
|
||||
assert_eq!(correct_path, clean_path_string(r#"/usr/some/path"#));
|
||||
assert_eq!(correct_path, clean_path_string(r#"/usr\some\path"#));
|
||||
assert_eq!(correct_path, clean_path_string(r#"/usr\some\path\"#));
|
||||
}
|
||||
|
||||
let mut correct_path = path::PathBuf::new();
|
||||
if cfg!(target_os = "windows") {
|
||||
correct_path.push("C:\\");
|
||||
} else {
|
||||
correct_path.push("/usr");
|
||||
}
|
||||
correct_path.push("some");
|
||||
correct_path.push("path");
|
||||
if cfg!(target_os = "windows") {
|
||||
assert_eq!(correct_path, clean_path_string(r#"C:/some/path"#));
|
||||
assert_eq!(correct_path, clean_path_string(r#"C:\some\path"#));
|
||||
assert_eq!(correct_path, clean_path_string(r#"C:\some\path\"#));
|
||||
} else {
|
||||
assert_eq!(correct_path, clean_path_string(r#"/usr/some/path"#));
|
||||
assert_eq!(correct_path, clean_path_string(r#"/usr\some\path"#));
|
||||
assert_eq!(correct_path, clean_path_string(r#"/usr\some\path\"#));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
84
src/ddns.rs
84
src/ddns.rs
|
@ -8,69 +8,69 @@ use std::time;
|
|||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DDNSConfig {
|
||||
pub host: String,
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
pub host: String,
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum DDNSError {
|
||||
IoError(io::Error),
|
||||
HyperError(hyper::Error),
|
||||
UpdateError(hyper::status::StatusCode),
|
||||
IoError(io::Error),
|
||||
HyperError(hyper::Error),
|
||||
UpdateError(hyper::status::StatusCode),
|
||||
}
|
||||
|
||||
impl From<io::Error> for DDNSError {
|
||||
fn from(err: io::Error) -> DDNSError {
|
||||
DDNSError::IoError(err)
|
||||
}
|
||||
fn from(err: io::Error) -> DDNSError {
|
||||
DDNSError::IoError(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<hyper::Error> for DDNSError {
|
||||
fn from(err: hyper::Error) -> DDNSError {
|
||||
DDNSError::HyperError(err)
|
||||
}
|
||||
fn from(err: hyper::Error) -> DDNSError {
|
||||
DDNSError::HyperError(err)
|
||||
}
|
||||
}
|
||||
|
||||
const MY_IP_API_URL: &'static str = "http://api.ipify.org";
|
||||
const DDNS_UPDATE_URL: &'static str = "http://ydns.io/api/v1/update/";
|
||||
|
||||
fn get_my_ip() -> Result<String, DDNSError> {
|
||||
let client = Client::new();
|
||||
let mut res = client.get(MY_IP_API_URL).send()?;
|
||||
let mut buf = String::new();
|
||||
res.read_to_string(&mut buf)?;
|
||||
Ok(buf)
|
||||
let client = Client::new();
|
||||
let mut res = client.get(MY_IP_API_URL).send()?;
|
||||
let mut buf = String::new();
|
||||
res.read_to_string(&mut buf)?;
|
||||
Ok(buf)
|
||||
}
|
||||
|
||||
fn update_my_ip(ip: &String, config: &DDNSConfig) -> Result<(), DDNSError> {
|
||||
let client = Client::new();
|
||||
let url = DDNS_UPDATE_URL;
|
||||
let host = &config.host;
|
||||
let full_url = format!("{}?host={}&ip={}", url, host, ip);
|
||||
let auth_header = Authorization(Basic {
|
||||
username: config.username.clone(),
|
||||
password: Some(config.password.to_owned()),
|
||||
});
|
||||
let client = Client::new();
|
||||
let url = DDNS_UPDATE_URL;
|
||||
let host = &config.host;
|
||||
let full_url = format!("{}?host={}&ip={}", url, host, ip);
|
||||
let auth_header = Authorization(Basic {
|
||||
username: config.username.clone(),
|
||||
password: Some(config.password.to_owned()),
|
||||
});
|
||||
|
||||
let res = client.get(full_url.as_str()).header(auth_header).send()?;
|
||||
match res.status {
|
||||
hyper::status::StatusCode::Ok => Ok(()),
|
||||
s => Err(DDNSError::UpdateError(s)),
|
||||
}
|
||||
let res = client.get(full_url.as_str()).header(auth_header).send()?;
|
||||
match res.status {
|
||||
hyper::status::StatusCode::Ok => Ok(()),
|
||||
s => Err(DDNSError::UpdateError(s)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run(config: DDNSConfig) {
|
||||
loop {
|
||||
let my_ip_res = get_my_ip();
|
||||
if let Ok(my_ip) = my_ip_res {
|
||||
match update_my_ip(&my_ip, &config) {
|
||||
Err(e) => println!("Dynamic DNS Error: {:?}", e),
|
||||
Ok(_) => (),
|
||||
};
|
||||
} else {
|
||||
println!("Dynamic DNS Error: could not retrieve our own IP address");
|
||||
}
|
||||
thread::sleep(time::Duration::from_secs(60 * 30));
|
||||
}
|
||||
loop {
|
||||
let my_ip_res = get_my_ip();
|
||||
if let Ok(my_ip) = my_ip_res {
|
||||
match update_my_ip(&my_ip, &config) {
|
||||
Err(e) => println!("Dynamic DNS Error: {:?}", e),
|
||||
Ok(_) => (),
|
||||
};
|
||||
} else {
|
||||
println!("Dynamic DNS Error: could not retrieve our own IP address");
|
||||
}
|
||||
thread::sleep(time::Duration::from_secs(60 * 30));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -36,17 +36,17 @@ error_chain! {
|
|||
}
|
||||
|
||||
impl From<Error> for IronError {
|
||||
fn from(err: Error) -> IronError {
|
||||
match err {
|
||||
e @ Error(ErrorKind::AuthenticationRequired, _) => {
|
||||
IronError::new(e, Status::Unauthorized)
|
||||
}
|
||||
e @ Error(ErrorKind::MissingUsername, _) => IronError::new(e, Status::BadRequest),
|
||||
e @ Error(ErrorKind::MissingPassword, _) => IronError::new(e, Status::BadRequest),
|
||||
e @ Error(ErrorKind::IncorrectCredentials, _) => IronError::new(e, Status::BadRequest),
|
||||
e @ Error(ErrorKind::CannotServeDirectory, _) => IronError::new(e, Status::BadRequest),
|
||||
e @ Error(ErrorKind::UnsupportedFileType, _) => IronError::new(e, Status::BadRequest),
|
||||
e => IronError::new(e, Status::InternalServerError),
|
||||
}
|
||||
}
|
||||
fn from(err: Error) -> IronError {
|
||||
match err {
|
||||
e @ Error(ErrorKind::AuthenticationRequired, _) => {
|
||||
IronError::new(e, Status::Unauthorized)
|
||||
}
|
||||
e @ Error(ErrorKind::MissingUsername, _) => IronError::new(e, Status::BadRequest),
|
||||
e @ Error(ErrorKind::MissingPassword, _) => IronError::new(e, Status::BadRequest),
|
||||
e @ Error(ErrorKind::IncorrectCredentials, _) => IronError::new(e, Status::BadRequest),
|
||||
e @ Error(ErrorKind::CannotServeDirectory, _) => IronError::new(e, Status::BadRequest),
|
||||
e @ Error(ErrorKind::UnsupportedFileType, _) => IronError::new(e, Status::BadRequest),
|
||||
e => IronError::new(e, Status::InternalServerError),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
1027
src/index.rs
1027
src/index.rs
File diff suppressed because it is too large
Load diff
128
src/main.rs
128
src/main.rs
|
@ -55,83 +55,83 @@ mod thumbnails;
|
|||
mod vfs;
|
||||
|
||||
fn main() {
|
||||
if let Err(ref e) = run() {
|
||||
println!("Error: {}", e);
|
||||
if let Err(ref e) = run() {
|
||||
println!("Error: {}", e);
|
||||
|
||||
for e in e.iter().skip(1) {
|
||||
println!("caused by: {}", e);
|
||||
}
|
||||
if let Some(backtrace) = e.backtrace() {
|
||||
println!("backtrace: {:?}", backtrace);
|
||||
}
|
||||
::std::process::exit(1);
|
||||
}
|
||||
for e in e.iter().skip(1) {
|
||||
println!("caused by: {}", e);
|
||||
}
|
||||
if let Some(backtrace) = e.backtrace() {
|
||||
println!("backtrace: {:?}", backtrace);
|
||||
}
|
||||
::std::process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
fn run() -> Result<()> {
|
||||
|
||||
// Parse CLI options
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
let mut options = Options::new();
|
||||
options.optopt("c", "config", "set the configuration file", "FILE");
|
||||
let matches = match options.parse(&args[1..]) {
|
||||
Ok(m) => m,
|
||||
Err(f) => panic!(f.to_string()),
|
||||
};
|
||||
let config_file_name = matches.opt_str("c");
|
||||
let config_file_path = config_file_name.map(|n| Path::new(n.as_str()).to_path_buf());
|
||||
// Parse CLI options
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
let mut options = Options::new();
|
||||
options.optopt("c", "config", "set the configuration file", "FILE");
|
||||
let matches = match options.parse(&args[1..]) {
|
||||
Ok(m) => m,
|
||||
Err(f) => panic!(f.to_string()),
|
||||
};
|
||||
let config_file_name = matches.opt_str("c");
|
||||
let config_file_path = config_file_name.map(|n| Path::new(n.as_str()).to_path_buf());
|
||||
|
||||
// Parse config
|
||||
let config = config::Config::parse(config_file_path)?;
|
||||
// Parse config
|
||||
let config = config::Config::parse(config_file_path)?;
|
||||
|
||||
// Init VFS
|
||||
let vfs = Arc::new(vfs::Vfs::new(config.vfs.clone()));
|
||||
// Init VFS
|
||||
let vfs = Arc::new(vfs::Vfs::new(config.vfs.clone()));
|
||||
|
||||
// Init index
|
||||
println!("Starting up index");
|
||||
let index = Arc::new(index::Index::new(vfs.clone(), &config.index)?);
|
||||
let index_ref = index.clone();
|
||||
std::thread::spawn(move || index_ref.run());
|
||||
// Init index
|
||||
println!("Starting up index");
|
||||
let index = Arc::new(index::Index::new(vfs.clone(), &config.index)?);
|
||||
let index_ref = index.clone();
|
||||
std::thread::spawn(move || index_ref.run());
|
||||
|
||||
// Start server
|
||||
println!("Starting up server");
|
||||
let mut api_chain;
|
||||
{
|
||||
let api_handler;
|
||||
{
|
||||
let mut collection = collection::Collection::new(vfs, index);
|
||||
collection.load_config(&config)?;
|
||||
let collection = Arc::new(collection);
|
||||
api_handler = api::get_api_handler(collection);
|
||||
}
|
||||
api_chain = Chain::new(api_handler);
|
||||
// Start server
|
||||
println!("Starting up server");
|
||||
let mut api_chain;
|
||||
{
|
||||
let api_handler;
|
||||
{
|
||||
let mut collection = collection::Collection::new(vfs, index);
|
||||
collection.load_config(&config)?;
|
||||
let collection = Arc::new(collection);
|
||||
api_handler = api::get_api_handler(collection);
|
||||
}
|
||||
api_chain = Chain::new(api_handler);
|
||||
|
||||
let auth_secret = config.secret.to_owned();
|
||||
let cookie_middleware = oven::new(auth_secret.into_bytes());
|
||||
api_chain.link(cookie_middleware);
|
||||
}
|
||||
let auth_secret = config.secret.to_owned();
|
||||
let cookie_middleware = oven::new(auth_secret.into_bytes());
|
||||
api_chain.link(cookie_middleware);
|
||||
}
|
||||
|
||||
let mut mount = Mount::new();
|
||||
mount.mount("/api/", api_chain);
|
||||
mount.mount("/", Static::new(Path::new("web")));
|
||||
let mut server = Iron::new(mount).http(("0.0.0.0", 5050))?;
|
||||
let mut mount = Mount::new();
|
||||
mount.mount("/api/", api_chain);
|
||||
mount.mount("/", Static::new(Path::new("web")));
|
||||
let mut server = Iron::new(mount).http(("0.0.0.0", 5050))?;
|
||||
|
||||
// Start DDNS updates
|
||||
match config.ddns {
|
||||
Some(ref ddns_config) => {
|
||||
let ddns_config = ddns_config.clone();
|
||||
std::thread::spawn(|| {
|
||||
ddns::run(ddns_config);
|
||||
});
|
||||
}
|
||||
None => (),
|
||||
};
|
||||
// Start DDNS updates
|
||||
match config.ddns {
|
||||
Some(ref ddns_config) => {
|
||||
let ddns_config = ddns_config.clone();
|
||||
std::thread::spawn(|| {
|
||||
ddns::run(ddns_config);
|
||||
});
|
||||
}
|
||||
None => (),
|
||||
};
|
||||
|
||||
// Run UI
|
||||
ui::run();
|
||||
// Run UI
|
||||
ui::run();
|
||||
|
||||
println!("Shutting down server");
|
||||
server.close()?;
|
||||
println!("Shutting down server");
|
||||
server.close()?;
|
||||
|
||||
Ok(())
|
||||
Ok(())
|
||||
}
|
||||
|
|
238
src/metadata.rs
238
src/metadata.rs
|
@ -13,157 +13,157 @@ use utils::AudioFormat;
|
|||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub struct SongTags {
|
||||
pub disc_number: Option<u32>,
|
||||
pub track_number: Option<u32>,
|
||||
pub title: Option<String>,
|
||||
pub artist: Option<String>,
|
||||
pub album_artist: Option<String>,
|
||||
pub album: Option<String>,
|
||||
pub year: Option<i32>,
|
||||
pub disc_number: Option<u32>,
|
||||
pub track_number: Option<u32>,
|
||||
pub title: Option<String>,
|
||||
pub artist: Option<String>,
|
||||
pub album_artist: Option<String>,
|
||||
pub album: Option<String>,
|
||||
pub year: Option<i32>,
|
||||
}
|
||||
|
||||
pub fn read(path: &Path) -> Result<SongTags> {
|
||||
match utils::get_audio_format(path) {
|
||||
Some(AudioFormat::FLAC) => read_flac(path),
|
||||
Some(AudioFormat::MP3) => read_id3(path),
|
||||
Some(AudioFormat::MPC) => read_ape(path),
|
||||
Some(AudioFormat::OGG) => read_vorbis(path),
|
||||
_ => bail!("Unsupported file format for reading metadata"),
|
||||
}
|
||||
match utils::get_audio_format(path) {
|
||||
Some(AudioFormat::FLAC) => read_flac(path),
|
||||
Some(AudioFormat::MP3) => read_id3(path),
|
||||
Some(AudioFormat::MPC) => read_ape(path),
|
||||
Some(AudioFormat::OGG) => read_vorbis(path),
|
||||
_ => bail!("Unsupported file format for reading metadata"),
|
||||
}
|
||||
}
|
||||
|
||||
fn read_id3(path: &Path) -> Result<SongTags> {
|
||||
let tag = id3::Tag::read_from_path(path)?;
|
||||
let tag = id3::Tag::read_from_path(path)?;
|
||||
|
||||
let artist = tag.artist().map(|s| s.to_string());
|
||||
let album_artist = tag.album_artist().map(|s| s.to_string());
|
||||
let album = tag.album().map(|s| s.to_string());
|
||||
let title = tag.title().map(|s| s.to_string());
|
||||
let disc_number = tag.disc();
|
||||
let track_number = tag.track();
|
||||
let year = tag.year()
|
||||
.map(|y| y as i32)
|
||||
.or(tag.date_released().and_then(|d| d.year))
|
||||
.or(tag.date_recorded().and_then(|d| d.year));
|
||||
let artist = tag.artist().map(|s| s.to_string());
|
||||
let album_artist = tag.album_artist().map(|s| s.to_string());
|
||||
let album = tag.album().map(|s| s.to_string());
|
||||
let title = tag.title().map(|s| s.to_string());
|
||||
let disc_number = tag.disc();
|
||||
let track_number = tag.track();
|
||||
let year = tag.year()
|
||||
.map(|y| y as i32)
|
||||
.or(tag.date_released().and_then(|d| d.year))
|
||||
.or(tag.date_recorded().and_then(|d| d.year));
|
||||
|
||||
Ok(SongTags {
|
||||
artist: artist,
|
||||
album_artist: album_artist,
|
||||
album: album,
|
||||
title: title,
|
||||
disc_number: disc_number,
|
||||
track_number: track_number,
|
||||
year: year,
|
||||
})
|
||||
Ok(SongTags {
|
||||
artist: artist,
|
||||
album_artist: album_artist,
|
||||
album: album,
|
||||
title: title,
|
||||
disc_number: disc_number,
|
||||
track_number: track_number,
|
||||
year: year,
|
||||
})
|
||||
}
|
||||
|
||||
fn read_ape_string(item: &ape::Item) -> Option<String> {
|
||||
match item.value {
|
||||
ape::ItemValue::Text(ref s) => Some(s.clone()),
|
||||
_ => None,
|
||||
}
|
||||
match item.value {
|
||||
ape::ItemValue::Text(ref s) => Some(s.clone()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn read_ape_i32(item: &ape::Item) -> Option<i32> {
|
||||
match item.value {
|
||||
ape::ItemValue::Text(ref s) => s.parse::<i32>().ok(),
|
||||
_ => None,
|
||||
}
|
||||
match item.value {
|
||||
ape::ItemValue::Text(ref s) => s.parse::<i32>().ok(),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn read_ape_x_of_y(item: &ape::Item) -> Option<u32> {
|
||||
match item.value {
|
||||
ape::ItemValue::Text(ref s) => {
|
||||
let format = Regex::new(r#"^\d+"#).unwrap();
|
||||
if let Some((start, end)) = format.find(s) {
|
||||
s[start..end].parse().ok()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
match item.value {
|
||||
ape::ItemValue::Text(ref s) => {
|
||||
let format = Regex::new(r#"^\d+"#).unwrap();
|
||||
if let Some((start, end)) = format.find(s) {
|
||||
s[start..end].parse().ok()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn read_ape(path: &Path) -> Result<SongTags> {
|
||||
let tag = ape::read(path)?;
|
||||
let artist = tag.item("Artist").and_then(read_ape_string);
|
||||
let album = tag.item("Album").and_then(read_ape_string);
|
||||
let album_artist = tag.item("Album artist").and_then(read_ape_string);
|
||||
let title = tag.item("Title").and_then(read_ape_string);
|
||||
let year = tag.item("Year").and_then(read_ape_i32);
|
||||
let disc_number = tag.item("Disc").and_then(read_ape_x_of_y);
|
||||
let track_number = tag.item("Track").and_then(read_ape_x_of_y);
|
||||
Ok(SongTags {
|
||||
artist: artist,
|
||||
album_artist: album_artist,
|
||||
album: album,
|
||||
title: title,
|
||||
disc_number: disc_number,
|
||||
track_number: track_number,
|
||||
year: year,
|
||||
})
|
||||
let tag = ape::read(path)?;
|
||||
let artist = tag.item("Artist").and_then(read_ape_string);
|
||||
let album = tag.item("Album").and_then(read_ape_string);
|
||||
let album_artist = tag.item("Album artist").and_then(read_ape_string);
|
||||
let title = tag.item("Title").and_then(read_ape_string);
|
||||
let year = tag.item("Year").and_then(read_ape_i32);
|
||||
let disc_number = tag.item("Disc").and_then(read_ape_x_of_y);
|
||||
let track_number = tag.item("Track").and_then(read_ape_x_of_y);
|
||||
Ok(SongTags {
|
||||
artist: artist,
|
||||
album_artist: album_artist,
|
||||
album: album,
|
||||
title: title,
|
||||
disc_number: disc_number,
|
||||
track_number: track_number,
|
||||
year: year,
|
||||
})
|
||||
}
|
||||
|
||||
fn read_vorbis(path: &Path) -> Result<SongTags> {
|
||||
|
||||
let file = fs::File::open(path)?;
|
||||
let source = OggStreamReader::new(PacketReader::new(file))?;
|
||||
let file = fs::File::open(path)?;
|
||||
let source = OggStreamReader::new(PacketReader::new(file))?;
|
||||
|
||||
let mut tags = SongTags {
|
||||
artist: None,
|
||||
album_artist: None,
|
||||
album: None,
|
||||
title: None,
|
||||
disc_number: None,
|
||||
track_number: None,
|
||||
year: None,
|
||||
};
|
||||
let mut tags = SongTags {
|
||||
artist: None,
|
||||
album_artist: None,
|
||||
album: None,
|
||||
title: None,
|
||||
disc_number: None,
|
||||
track_number: None,
|
||||
year: None,
|
||||
};
|
||||
|
||||
for (key, value) in source.comment_hdr.comment_list {
|
||||
match key.as_str() {
|
||||
"TITLE" => tags.title = Some(value),
|
||||
"ALBUM" => tags.album = Some(value),
|
||||
"ARTIST" => tags.artist = Some(value),
|
||||
"ALBUMARTIST" => tags.album_artist = Some(value),
|
||||
"TRACKNUMBER" => tags.track_number = value.parse::<u32>().ok(),
|
||||
"DISCNUMBER" => tags.disc_number = value.parse::<u32>().ok(),
|
||||
"DATE" => tags.year = value.parse::<i32>().ok(),
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
for (key, value) in source.comment_hdr.comment_list {
|
||||
match key.as_str() {
|
||||
"TITLE" => tags.title = Some(value),
|
||||
"ALBUM" => tags.album = Some(value),
|
||||
"ARTIST" => tags.artist = Some(value),
|
||||
"ALBUMARTIST" => tags.album_artist = Some(value),
|
||||
"TRACKNUMBER" => tags.track_number = value.parse::<u32>().ok(),
|
||||
"DISCNUMBER" => tags.disc_number = value.parse::<u32>().ok(),
|
||||
"DATE" => tags.year = value.parse::<i32>().ok(),
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(tags)
|
||||
Ok(tags)
|
||||
}
|
||||
|
||||
fn read_flac(path: &Path) -> Result<SongTags> {
|
||||
let tag = metaflac::Tag::read_from_path(path)?;
|
||||
let vorbis = tag.vorbis_comments().ok_or("Missing Vorbis comments")?;
|
||||
let disc_number = vorbis.get("DISCNUMBER").and_then(|d| d[0].parse::<u32>().ok());
|
||||
let year = vorbis.get("DATE").and_then(|d| d[0].parse::<i32>().ok());
|
||||
Ok(SongTags {
|
||||
artist: vorbis.artist().map(|v| v[0].clone()),
|
||||
album_artist: vorbis.album_artist().map(|v| v[0].clone()),
|
||||
album: vorbis.album().map(|v| v[0].clone()),
|
||||
title: vorbis.title().map(|v| v[0].clone()),
|
||||
disc_number: disc_number,
|
||||
track_number: vorbis.track(),
|
||||
year: year,
|
||||
})
|
||||
let tag = metaflac::Tag::read_from_path(path)?;
|
||||
let vorbis = tag.vorbis_comments().ok_or("Missing Vorbis comments")?;
|
||||
let disc_number = vorbis.get("DISCNUMBER").and_then(|d| d[0].parse::<u32>().ok());
|
||||
let year = vorbis.get("DATE").and_then(|d| d[0].parse::<i32>().ok());
|
||||
Ok(SongTags {
|
||||
artist: vorbis.artist().map(|v| v[0].clone()),
|
||||
album_artist: vorbis.album_artist().map(|v| v[0].clone()),
|
||||
album: vorbis.album().map(|v| v[0].clone()),
|
||||
title: vorbis.title().map(|v| v[0].clone()),
|
||||
disc_number: disc_number,
|
||||
track_number: vorbis.track(),
|
||||
year: year,
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_read_metadata() {
|
||||
let sample_tags = SongTags {
|
||||
disc_number: Some(3),
|
||||
track_number: Some(1),
|
||||
title: Some("TEST TITLE".into()),
|
||||
artist: Some("TEST ARTIST".into()),
|
||||
album_artist: Some("TEST ALBUM ARTIST".into()),
|
||||
album: Some("TEST ALBUM".into()),
|
||||
year: Some(2016),
|
||||
};
|
||||
assert_eq!(read(Path::new("test/sample.mp3")).unwrap(), sample_tags);
|
||||
assert_eq!(read(Path::new("test/sample.ogg")).unwrap(), sample_tags);
|
||||
assert_eq!(read(Path::new("test/sample.flac")).unwrap(), sample_tags);
|
||||
let sample_tags = SongTags {
|
||||
disc_number: Some(3),
|
||||
track_number: Some(1),
|
||||
title: Some("TEST TITLE".into()),
|
||||
artist: Some("TEST ARTIST".into()),
|
||||
album_artist: Some("TEST ALBUM ARTIST".into()),
|
||||
album: Some("TEST ALBUM".into()),
|
||||
year: Some(2016),
|
||||
};
|
||||
assert_eq!(read(Path::new("test/sample.mp3")).unwrap(), sample_tags);
|
||||
assert_eq!(read(Path::new("test/sample.ogg")).unwrap(), sample_tags);
|
||||
assert_eq!(read(Path::new("test/sample.flac")).unwrap(), sample_tags);
|
||||
}
|
||||
|
|
|
@ -15,50 +15,50 @@ use utils;
|
|||
const THUMBNAILS_PATH: &'static str = "thumbnails";
|
||||
|
||||
fn hash(path: &Path, dimension: u32) -> u64 {
|
||||
let path_string = path.to_string_lossy();
|
||||
let hash_input = format!("{}:{}", path_string, dimension.to_string());
|
||||
let mut hasher = DefaultHasher::new();
|
||||
hash_input.hash(&mut hasher);
|
||||
hasher.finish()
|
||||
let path_string = path.to_string_lossy();
|
||||
let hash_input = format!("{}:{}", path_string, dimension.to_string());
|
||||
let mut hasher = DefaultHasher::new();
|
||||
hash_input.hash(&mut hasher);
|
||||
hasher.finish()
|
||||
}
|
||||
|
||||
pub fn get_thumbnail(real_path: &Path, max_dimension: u32) -> Result<PathBuf> {
|
||||
|
||||
let mut out_path = utils::get_cache_root()?;
|
||||
out_path.push(THUMBNAILS_PATH);
|
||||
let mut out_path = utils::get_cache_root()?;
|
||||
out_path.push(THUMBNAILS_PATH);
|
||||
|
||||
let mut dir_builder = DirBuilder::new();
|
||||
dir_builder.recursive(true);
|
||||
dir_builder.create(out_path.as_path())?;
|
||||
let mut dir_builder = DirBuilder::new();
|
||||
dir_builder.recursive(true);
|
||||
dir_builder.create(out_path.as_path())?;
|
||||
|
||||
let source_image = image::open(real_path)?;
|
||||
let (source_width, source_height) = source_image.dimensions();
|
||||
let cropped_dimension = cmp::max(source_width, source_height);
|
||||
let out_dimension = cmp::min(max_dimension, cropped_dimension);
|
||||
let source_image = image::open(real_path)?;
|
||||
let (source_width, source_height) = source_image.dimensions();
|
||||
let cropped_dimension = cmp::max(source_width, source_height);
|
||||
let out_dimension = cmp::min(max_dimension, cropped_dimension);
|
||||
|
||||
let hash = hash(real_path, out_dimension);
|
||||
out_path.push(format!("{}.png", hash.to_string()));
|
||||
let hash = hash(real_path, out_dimension);
|
||||
out_path.push(format!("{}.png", hash.to_string()));
|
||||
|
||||
if !out_path.exists() {
|
||||
let source_aspect_ratio: f32 = source_width as f32 / source_height as f32;
|
||||
if source_aspect_ratio < 0.8 || source_aspect_ratio > 1.2 {
|
||||
let mut cropped_image = ImageBuffer::new(cropped_dimension, cropped_dimension);
|
||||
cropped_image.copy_from(&source_image,
|
||||
(cropped_dimension - source_width) / 2,
|
||||
(cropped_dimension - source_height) / 2);
|
||||
let out_image = resize(&cropped_image,
|
||||
out_dimension,
|
||||
out_dimension,
|
||||
FilterType::Lanczos3);
|
||||
out_image.save(out_path.as_path())?;
|
||||
} else {
|
||||
let out_image = resize(&source_image,
|
||||
out_dimension,
|
||||
out_dimension,
|
||||
FilterType::Lanczos3);
|
||||
out_image.save(out_path.as_path())?;
|
||||
}
|
||||
}
|
||||
if !out_path.exists() {
|
||||
let source_aspect_ratio: f32 = source_width as f32 / source_height as f32;
|
||||
if source_aspect_ratio < 0.8 || source_aspect_ratio > 1.2 {
|
||||
let mut cropped_image = ImageBuffer::new(cropped_dimension, cropped_dimension);
|
||||
cropped_image.copy_from(&source_image,
|
||||
(cropped_dimension - source_width) / 2,
|
||||
(cropped_dimension - source_height) / 2);
|
||||
let out_image = resize(&cropped_image,
|
||||
out_dimension,
|
||||
out_dimension,
|
||||
FilterType::Lanczos3);
|
||||
out_image.save(out_path.as_path())?;
|
||||
} else {
|
||||
let out_image = resize(&source_image,
|
||||
out_dimension,
|
||||
out_dimension,
|
||||
FilterType::Lanczos3);
|
||||
out_image.save(out_path.as_path())?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(out_path)
|
||||
Ok(out_path)
|
||||
}
|
||||
|
|
|
@ -2,8 +2,8 @@ use std::time;
|
|||
use std::thread;
|
||||
|
||||
pub fn run() {
|
||||
println!("Starting up UI (headless)");
|
||||
loop {
|
||||
thread::sleep(time::Duration::from_secs(10));
|
||||
}
|
||||
println!("Starting up UI (headless)");
|
||||
loop {
|
||||
thread::sleep(time::Duration::from_secs(10));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,220 +13,220 @@ const MESSAGE_NOTIFICATION_ICON: u32 = winapi::WM_USER + 1;
|
|||
const MESSAGE_NOTIFICATION_ICON_QUIT: u32 = winapi::WM_USER + 2;
|
||||
|
||||
pub trait ToWin {
|
||||
type Out;
|
||||
fn to_win(&self) -> Self::Out;
|
||||
type Out;
|
||||
fn to_win(&self) -> Self::Out;
|
||||
}
|
||||
|
||||
impl<'a> ToWin for &'a str {
|
||||
type Out = Vec<u16>;
|
||||
type Out = Vec<u16>;
|
||||
|
||||
fn to_win(&self) -> Self::Out {
|
||||
OsStr::new(self)
|
||||
.encode_wide()
|
||||
.chain(std::iter::once(0))
|
||||
.collect()
|
||||
}
|
||||
fn to_win(&self) -> Self::Out {
|
||||
OsStr::new(self)
|
||||
.encode_wide()
|
||||
.chain(std::iter::once(0))
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
impl ToWin for uuid::Uuid {
|
||||
type Out = winapi::GUID;
|
||||
type Out = winapi::GUID;
|
||||
|
||||
fn to_win(&self) -> Self::Out {
|
||||
let bytes = self.as_bytes();
|
||||
let end = [bytes[8], bytes[9], bytes[10], bytes[11], bytes[12], bytes[13], bytes[14],
|
||||
bytes[15]];
|
||||
fn to_win(&self) -> Self::Out {
|
||||
let bytes = self.as_bytes();
|
||||
let end = [bytes[8], bytes[9], bytes[10], bytes[11], bytes[12], bytes[13], bytes[14],
|
||||
bytes[15]];
|
||||
|
||||
winapi::GUID {
|
||||
Data1: ((bytes[0] as u32) << 24 | (bytes[1] as u32) << 16 | (bytes[2] as u32) << 8 |
|
||||
(bytes[3] as u32)),
|
||||
Data2: ((bytes[4] as u16) << 8 | (bytes[5] as u16)),
|
||||
Data3: ((bytes[6] as u16) << 8 | (bytes[7] as u16)),
|
||||
Data4: end,
|
||||
}
|
||||
}
|
||||
winapi::GUID {
|
||||
Data1: ((bytes[0] as u32) << 24 | (bytes[1] as u32) << 16 | (bytes[2] as u32) << 8 |
|
||||
(bytes[3] as u32)),
|
||||
Data2: ((bytes[4] as u16) << 8 | (bytes[5] as u16)),
|
||||
Data3: ((bytes[6] as u16) << 8 | (bytes[7] as u16)),
|
||||
Data4: end,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait Constructible {
|
||||
type Out;
|
||||
fn new() -> Self::Out;
|
||||
type Out;
|
||||
fn new() -> Self::Out;
|
||||
}
|
||||
|
||||
impl Constructible for winapi::NOTIFYICONDATAW {
|
||||
type Out = winapi::NOTIFYICONDATAW;
|
||||
type Out = winapi::NOTIFYICONDATAW;
|
||||
|
||||
fn new() -> Self::Out {
|
||||
winapi::NOTIFYICONDATAW {
|
||||
cbSize: std::mem::size_of::<winapi::NOTIFYICONDATAW>() as u32,
|
||||
hWnd: std::ptr::null_mut(),
|
||||
uFlags: 0,
|
||||
guidItem: uuid::Uuid::nil().to_win(),
|
||||
hIcon: std::ptr::null_mut(),
|
||||
uID: 0,
|
||||
uCallbackMessage: 0,
|
||||
szTip: [0; 128],
|
||||
dwState: 0,
|
||||
dwStateMask: 0,
|
||||
szInfo: [0; 256],
|
||||
uTimeout: winapi::NOTIFYICON_VERSION_4,
|
||||
szInfoTitle: [0; 64],
|
||||
dwInfoFlags: 0,
|
||||
hBalloonIcon: std::ptr::null_mut(),
|
||||
}
|
||||
}
|
||||
fn new() -> Self::Out {
|
||||
winapi::NOTIFYICONDATAW {
|
||||
cbSize: std::mem::size_of::<winapi::NOTIFYICONDATAW>() as u32,
|
||||
hWnd: std::ptr::null_mut(),
|
||||
uFlags: 0,
|
||||
guidItem: uuid::Uuid::nil().to_win(),
|
||||
hIcon: std::ptr::null_mut(),
|
||||
uID: 0,
|
||||
uCallbackMessage: 0,
|
||||
szTip: [0; 128],
|
||||
dwState: 0,
|
||||
dwStateMask: 0,
|
||||
szInfo: [0; 256],
|
||||
uTimeout: winapi::NOTIFYICON_VERSION_4,
|
||||
szInfoTitle: [0; 64],
|
||||
dwInfoFlags: 0,
|
||||
hBalloonIcon: std::ptr::null_mut(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn create_window() -> Option<winapi::HWND> {
|
||||
|
||||
let class_name = "Polaris-class".to_win();
|
||||
let window_name = "Polaris-window".to_win();
|
||||
let class_name = "Polaris-class".to_win();
|
||||
let window_name = "Polaris-window".to_win();
|
||||
|
||||
unsafe {
|
||||
let module_handle = kernel32::GetModuleHandleW(std::ptr::null());
|
||||
let wnd = winapi::WNDCLASSW {
|
||||
style: 0,
|
||||
lpfnWndProc: Some(window_proc),
|
||||
hInstance: module_handle,
|
||||
hIcon: std::ptr::null_mut(),
|
||||
hCursor: std::ptr::null_mut(),
|
||||
lpszClassName: class_name.as_ptr(),
|
||||
hbrBackground: winapi::COLOR_WINDOW as winapi::HBRUSH,
|
||||
lpszMenuName: std::ptr::null_mut(),
|
||||
cbClsExtra: 0,
|
||||
cbWndExtra: 0,
|
||||
};
|
||||
unsafe {
|
||||
let module_handle = kernel32::GetModuleHandleW(std::ptr::null());
|
||||
let wnd = winapi::WNDCLASSW {
|
||||
style: 0,
|
||||
lpfnWndProc: Some(window_proc),
|
||||
hInstance: module_handle,
|
||||
hIcon: std::ptr::null_mut(),
|
||||
hCursor: std::ptr::null_mut(),
|
||||
lpszClassName: class_name.as_ptr(),
|
||||
hbrBackground: winapi::COLOR_WINDOW as winapi::HBRUSH,
|
||||
lpszMenuName: std::ptr::null_mut(),
|
||||
cbClsExtra: 0,
|
||||
cbWndExtra: 0,
|
||||
};
|
||||
|
||||
let atom = user32::RegisterClassW(&wnd);
|
||||
if atom == 0 {
|
||||
return None;
|
||||
}
|
||||
let atom = user32::RegisterClassW(&wnd);
|
||||
if atom == 0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let window_handle = user32::CreateWindowExW(0,
|
||||
atom as winapi::LPCWSTR,
|
||||
window_name.as_ptr(),
|
||||
winapi::WS_DISABLED,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
user32::GetDesktopWindow(),
|
||||
std::ptr::null_mut(),
|
||||
std::ptr::null_mut(),
|
||||
std::ptr::null_mut());
|
||||
let window_handle = user32::CreateWindowExW(0,
|
||||
atom as winapi::LPCWSTR,
|
||||
window_name.as_ptr(),
|
||||
winapi::WS_DISABLED,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
user32::GetDesktopWindow(),
|
||||
std::ptr::null_mut(),
|
||||
std::ptr::null_mut(),
|
||||
std::ptr::null_mut());
|
||||
|
||||
if window_handle.is_null() {
|
||||
return None;
|
||||
}
|
||||
if window_handle.is_null() {
|
||||
return None;
|
||||
}
|
||||
|
||||
return Some(window_handle);
|
||||
}
|
||||
return Some(window_handle);
|
||||
}
|
||||
}
|
||||
|
||||
fn add_notification_icon(window: winapi::HWND) {
|
||||
|
||||
let mut tooltip = [0 as winapi::WCHAR; 128];
|
||||
for (&x, p) in "Polaris".to_win().iter().zip(tooltip.iter_mut()) {
|
||||
*p = x;
|
||||
}
|
||||
let mut tooltip = [0 as winapi::WCHAR; 128];
|
||||
for (&x, p) in "Polaris".to_win().iter().zip(tooltip.iter_mut()) {
|
||||
*p = x;
|
||||
}
|
||||
|
||||
unsafe {
|
||||
let module = kernel32::GetModuleHandleW(std::ptr::null());
|
||||
let icon = user32::LoadIconW(module, std::mem::transmute(IDI_POLARIS_TRAY));
|
||||
let mut flags = winapi::NIF_MESSAGE | winapi::NIF_TIP;
|
||||
if !icon.is_null() {
|
||||
flags |= winapi::NIF_ICON;
|
||||
}
|
||||
unsafe {
|
||||
let module = kernel32::GetModuleHandleW(std::ptr::null());
|
||||
let icon = user32::LoadIconW(module, std::mem::transmute(IDI_POLARIS_TRAY));
|
||||
let mut flags = winapi::NIF_MESSAGE | winapi::NIF_TIP;
|
||||
if !icon.is_null() {
|
||||
flags |= winapi::NIF_ICON;
|
||||
}
|
||||
|
||||
let mut icon_data = winapi::NOTIFYICONDATAW::new();
|
||||
icon_data.hWnd = window;
|
||||
icon_data.uID = UID_NOTIFICATION_ICON;
|
||||
icon_data.uFlags = flags;
|
||||
icon_data.hIcon = icon;
|
||||
icon_data.uCallbackMessage = MESSAGE_NOTIFICATION_ICON;
|
||||
icon_data.szTip = tooltip;
|
||||
let mut icon_data = winapi::NOTIFYICONDATAW::new();
|
||||
icon_data.hWnd = window;
|
||||
icon_data.uID = UID_NOTIFICATION_ICON;
|
||||
icon_data.uFlags = flags;
|
||||
icon_data.hIcon = icon;
|
||||
icon_data.uCallbackMessage = MESSAGE_NOTIFICATION_ICON;
|
||||
icon_data.szTip = tooltip;
|
||||
|
||||
shell32::Shell_NotifyIconW(winapi::NIM_ADD, &mut icon_data);
|
||||
}
|
||||
shell32::Shell_NotifyIconW(winapi::NIM_ADD, &mut icon_data);
|
||||
}
|
||||
}
|
||||
|
||||
fn remove_notification_icon(window: winapi::HWND) {
|
||||
let mut icon_data = winapi::NOTIFYICONDATAW::new();
|
||||
icon_data.hWnd = window;
|
||||
icon_data.uID = UID_NOTIFICATION_ICON;
|
||||
unsafe {
|
||||
shell32::Shell_NotifyIconW(winapi::NIM_DELETE, &mut icon_data);
|
||||
}
|
||||
let mut icon_data = winapi::NOTIFYICONDATAW::new();
|
||||
icon_data.hWnd = window;
|
||||
icon_data.uID = UID_NOTIFICATION_ICON;
|
||||
unsafe {
|
||||
shell32::Shell_NotifyIconW(winapi::NIM_DELETE, &mut icon_data);
|
||||
}
|
||||
}
|
||||
|
||||
fn open_notification_context_menu(window: winapi::HWND) {
|
||||
println!("Opening notification icon context menu");
|
||||
let quit_string = "Quit Polaris".to_win();
|
||||
println!("Opening notification icon context menu");
|
||||
let quit_string = "Quit Polaris".to_win();
|
||||
|
||||
unsafe {
|
||||
let context_menu = user32::CreatePopupMenu();
|
||||
if context_menu.is_null() {
|
||||
return;
|
||||
}
|
||||
user32::InsertMenuW(context_menu,
|
||||
0,
|
||||
winapi::winuser::MF_STRING,
|
||||
MESSAGE_NOTIFICATION_ICON_QUIT as u64,
|
||||
quit_string.as_ptr());
|
||||
unsafe {
|
||||
let context_menu = user32::CreatePopupMenu();
|
||||
if context_menu.is_null() {
|
||||
return;
|
||||
}
|
||||
user32::InsertMenuW(context_menu,
|
||||
0,
|
||||
winapi::winuser::MF_STRING,
|
||||
MESSAGE_NOTIFICATION_ICON_QUIT as u64,
|
||||
quit_string.as_ptr());
|
||||
|
||||
let mut cursor_position = winapi::POINT { x: 0, y: 0 };
|
||||
user32::GetCursorPos(&mut cursor_position);
|
||||
let mut cursor_position = winapi::POINT { x: 0, y: 0 };
|
||||
user32::GetCursorPos(&mut cursor_position);
|
||||
|
||||
user32::SetForegroundWindow(window);
|
||||
let flags = winapi::winuser::TPM_RIGHTALIGN | winapi::winuser::TPM_BOTTOMALIGN |
|
||||
winapi::winuser::TPM_RIGHTBUTTON;
|
||||
user32::TrackPopupMenu(context_menu,
|
||||
flags,
|
||||
cursor_position.x,
|
||||
cursor_position.y,
|
||||
0,
|
||||
window,
|
||||
std::ptr::null_mut());
|
||||
user32::PostMessageW(window, 0, 0, 0);
|
||||
user32::SetForegroundWindow(window);
|
||||
let flags = winapi::winuser::TPM_RIGHTALIGN | winapi::winuser::TPM_BOTTOMALIGN |
|
||||
winapi::winuser::TPM_RIGHTBUTTON;
|
||||
user32::TrackPopupMenu(context_menu,
|
||||
flags,
|
||||
cursor_position.x,
|
||||
cursor_position.y,
|
||||
0,
|
||||
window,
|
||||
std::ptr::null_mut());
|
||||
user32::PostMessageW(window, 0, 0, 0);
|
||||
|
||||
println!("Closing notification context menu");
|
||||
user32::DestroyMenu(context_menu);
|
||||
}
|
||||
println!("Closing notification context menu");
|
||||
user32::DestroyMenu(context_menu);
|
||||
}
|
||||
}
|
||||
|
||||
fn quit(window: winapi::HWND) {
|
||||
println!("Shutting down UI");
|
||||
unsafe {
|
||||
user32::PostMessageW(window, winapi::winuser::WM_CLOSE, 0, 0);
|
||||
}
|
||||
println!("Shutting down UI");
|
||||
unsafe {
|
||||
user32::PostMessageW(window, winapi::winuser::WM_CLOSE, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run() {
|
||||
println!("Starting up UI (Windows)");
|
||||
println!("Starting up UI (Windows)");
|
||||
|
||||
create_window().expect("Could not initialize window");
|
||||
create_window().expect("Could not initialize window");
|
||||
|
||||
let mut message = winapi::MSG {
|
||||
hwnd: std::ptr::null_mut(),
|
||||
message: 0,
|
||||
wParam: 0,
|
||||
lParam: 0,
|
||||
time: 0,
|
||||
pt: winapi::POINT { x: 0, y: 0 },
|
||||
};
|
||||
let mut message = winapi::MSG {
|
||||
hwnd: std::ptr::null_mut(),
|
||||
message: 0,
|
||||
wParam: 0,
|
||||
lParam: 0,
|
||||
time: 0,
|
||||
pt: winapi::POINT { x: 0, y: 0 },
|
||||
};
|
||||
|
||||
loop {
|
||||
let status: i32;
|
||||
unsafe {
|
||||
status = user32::GetMessageW(&mut message, std::ptr::null_mut(), 0, 0);
|
||||
if status == -1 {
|
||||
panic!("GetMessageW error: {}", kernel32::GetLastError());
|
||||
}
|
||||
if status == 0 {
|
||||
break;
|
||||
}
|
||||
user32::TranslateMessage(&message);
|
||||
user32::DispatchMessageW(&message);
|
||||
}
|
||||
}
|
||||
loop {
|
||||
let status: i32;
|
||||
unsafe {
|
||||
status = user32::GetMessageW(&mut message, std::ptr::null_mut(), 0, 0);
|
||||
if status == -1 {
|
||||
panic!("GetMessageW error: {}", kernel32::GetLastError());
|
||||
}
|
||||
if status == 0 {
|
||||
break;
|
||||
}
|
||||
user32::TranslateMessage(&message);
|
||||
user32::DispatchMessageW(&message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub unsafe extern "system" fn window_proc(window: winapi::HWND,
|
||||
|
@ -234,37 +234,37 @@ pub unsafe extern "system" fn window_proc(window: winapi::HWND,
|
|||
w_param: winapi::WPARAM,
|
||||
l_param: winapi::LPARAM)
|
||||
-> winapi::LRESULT {
|
||||
match msg {
|
||||
match msg {
|
||||
|
||||
winapi::winuser::WM_CREATE => {
|
||||
add_notification_icon(window);
|
||||
}
|
||||
winapi::winuser::WM_CREATE => {
|
||||
add_notification_icon(window);
|
||||
}
|
||||
|
||||
MESSAGE_NOTIFICATION_ICON => {
|
||||
match winapi::LOWORD(l_param as winapi::DWORD) as u32 {
|
||||
winapi::winuser::WM_RBUTTONUP => {
|
||||
open_notification_context_menu(window);
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
MESSAGE_NOTIFICATION_ICON => {
|
||||
match winapi::LOWORD(l_param as winapi::DWORD) as u32 {
|
||||
winapi::winuser::WM_RBUTTONUP => {
|
||||
open_notification_context_menu(window);
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
winapi::winuser::WM_COMMAND => {
|
||||
match winapi::LOWORD(w_param as winapi::DWORD) as u32 {
|
||||
MESSAGE_NOTIFICATION_ICON_QUIT => {
|
||||
quit(window);
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
winapi::winuser::WM_COMMAND => {
|
||||
match winapi::LOWORD(w_param as winapi::DWORD) as u32 {
|
||||
MESSAGE_NOTIFICATION_ICON_QUIT => {
|
||||
quit(window);
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
winapi::winuser::WM_DESTROY => {
|
||||
remove_notification_icon(window);
|
||||
user32::PostQuitMessage(0);
|
||||
}
|
||||
winapi::winuser::WM_DESTROY => {
|
||||
remove_notification_icon(window);
|
||||
user32::PostQuitMessage(0);
|
||||
}
|
||||
|
||||
_ => (),
|
||||
};
|
||||
_ => (),
|
||||
};
|
||||
|
||||
return user32::DefWindowProcW(window, msg, w_param, l_param);
|
||||
return user32::DefWindowProcW(window, msg, w_param, l_param);
|
||||
}
|
||||
|
|
116
src/utils.rs
116
src/utils.rs
|
@ -5,90 +5,90 @@ use std::fs;
|
|||
use errors::*;
|
||||
|
||||
pub fn get_config_root() -> Result<PathBuf> {
|
||||
if let Ok(mut root) = data_root(AppDataType::SharedConfig) {
|
||||
root.push("Polaris");
|
||||
fs::create_dir_all(&root)?;
|
||||
return Ok(root);
|
||||
}
|
||||
bail!("Could not retrieve config directory root");
|
||||
if let Ok(mut root) = data_root(AppDataType::SharedConfig) {
|
||||
root.push("Polaris");
|
||||
fs::create_dir_all(&root)?;
|
||||
return Ok(root);
|
||||
}
|
||||
bail!("Could not retrieve config directory root");
|
||||
}
|
||||
|
||||
pub fn get_cache_root() -> Result<PathBuf> {
|
||||
if let Ok(mut root) = data_root(AppDataType::SharedData) {
|
||||
root.push("Polaris");
|
||||
fs::create_dir_all(&root)?;
|
||||
return Ok(root);
|
||||
}
|
||||
bail!("Could not retrieve cache directory root");
|
||||
if let Ok(mut root) = data_root(AppDataType::SharedData) {
|
||||
root.push("Polaris");
|
||||
fs::create_dir_all(&root)?;
|
||||
return Ok(root);
|
||||
}
|
||||
bail!("Could not retrieve cache directory root");
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum AudioFormat {
|
||||
FLAC,
|
||||
MP3,
|
||||
MP4,
|
||||
MPC,
|
||||
OGG,
|
||||
FLAC,
|
||||
MP3,
|
||||
MP4,
|
||||
MPC,
|
||||
OGG,
|
||||
}
|
||||
|
||||
pub fn get_audio_format(path: &Path) -> Option<AudioFormat> {
|
||||
let extension = match path.extension() {
|
||||
Some(e) => e,
|
||||
_ => return None,
|
||||
};
|
||||
let extension = match extension.to_str() {
|
||||
Some(e) => e,
|
||||
_ => return None,
|
||||
};
|
||||
match extension.to_lowercase().as_str() {
|
||||
"flac" => Some(AudioFormat::FLAC),
|
||||
"mp3" => Some(AudioFormat::MP3),
|
||||
"m4a" => Some(AudioFormat::MP4),
|
||||
"mpc" => Some(AudioFormat::MPC),
|
||||
"ogg" => Some(AudioFormat::OGG),
|
||||
_ => None,
|
||||
}
|
||||
let extension = match path.extension() {
|
||||
Some(e) => e,
|
||||
_ => return None,
|
||||
};
|
||||
let extension = match extension.to_str() {
|
||||
Some(e) => e,
|
||||
_ => return None,
|
||||
};
|
||||
match extension.to_lowercase().as_str() {
|
||||
"flac" => Some(AudioFormat::FLAC),
|
||||
"mp3" => Some(AudioFormat::MP3),
|
||||
"m4a" => Some(AudioFormat::MP4),
|
||||
"mpc" => Some(AudioFormat::MPC),
|
||||
"ogg" => Some(AudioFormat::OGG),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_audio_format() {
|
||||
assert_eq!(get_audio_format(Path::new("animals/🐷/my🐖file.jpg")),
|
||||
None);
|
||||
assert_eq!(get_audio_format(Path::new("animals/🐷/my🐖file.flac")),
|
||||
Some(AudioFormat::FLAC));
|
||||
assert_eq!(get_audio_format(Path::new("animals/🐷/my🐖file.jpg")),
|
||||
None);
|
||||
assert_eq!(get_audio_format(Path::new("animals/🐷/my🐖file.flac")),
|
||||
Some(AudioFormat::FLAC));
|
||||
}
|
||||
|
||||
pub fn is_song(path: &Path) -> bool {
|
||||
get_audio_format(path).is_some()
|
||||
get_audio_format(path).is_some()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_song() {
|
||||
assert!(is_song(Path::new("animals/🐷/my🐖file.mp3")));
|
||||
assert!(!is_song(Path::new("animals/🐷/my🐖file.jpg")));
|
||||
assert!(is_song(Path::new("animals/🐷/my🐖file.mp3")));
|
||||
assert!(!is_song(Path::new("animals/🐷/my🐖file.jpg")));
|
||||
}
|
||||
|
||||
pub fn is_image(path: &Path) -> bool {
|
||||
let extension = match path.extension() {
|
||||
Some(e) => e,
|
||||
_ => return false,
|
||||
};
|
||||
let extension = match extension.to_str() {
|
||||
Some(e) => e,
|
||||
_ => return false,
|
||||
};
|
||||
match extension.to_lowercase().as_str() {
|
||||
"png" => true,
|
||||
"gif" => true,
|
||||
"jpg" => true,
|
||||
"jpeg" => true,
|
||||
"bmp" => true,
|
||||
_ => false,
|
||||
}
|
||||
let extension = match path.extension() {
|
||||
Some(e) => e,
|
||||
_ => return false,
|
||||
};
|
||||
let extension = match extension.to_str() {
|
||||
Some(e) => e,
|
||||
_ => return false,
|
||||
};
|
||||
match extension.to_lowercase().as_str() {
|
||||
"png" => true,
|
||||
"gif" => true,
|
||||
"jpg" => true,
|
||||
"jpeg" => true,
|
||||
"bmp" => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_image() {
|
||||
assert!(!is_image(Path::new("animals/🐷/my🐖file.mp3")));
|
||||
assert!(is_image(Path::new("animals/🐷/my🐖file.jpg")));
|
||||
assert!(!is_image(Path::new("animals/🐷/my🐖file.mp3")));
|
||||
assert!(is_image(Path::new("animals/🐷/my🐖file.jpg")));
|
||||
}
|
||||
|
|
142
src/vfs.rs
142
src/vfs.rs
|
@ -6,105 +6,105 @@ use errors::*;
|
|||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct VfsConfig {
|
||||
pub mount_points: HashMap<String, PathBuf>,
|
||||
pub mount_points: HashMap<String, PathBuf>,
|
||||
}
|
||||
|
||||
impl VfsConfig {
|
||||
pub fn new() -> VfsConfig {
|
||||
VfsConfig { mount_points: HashMap::new() }
|
||||
}
|
||||
pub fn new() -> VfsConfig {
|
||||
VfsConfig { mount_points: HashMap::new() }
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Vfs {
|
||||
mount_points: HashMap<String, PathBuf>,
|
||||
mount_points: HashMap<String, PathBuf>,
|
||||
}
|
||||
|
||||
impl Vfs {
|
||||
pub fn new(config: VfsConfig) -> Vfs {
|
||||
Vfs { mount_points: config.mount_points }
|
||||
}
|
||||
pub fn new(config: VfsConfig) -> Vfs {
|
||||
Vfs { mount_points: config.mount_points }
|
||||
}
|
||||
|
||||
pub fn real_to_virtual(&self, real_path: &Path) -> Result<PathBuf> {
|
||||
for (name, target) in &self.mount_points {
|
||||
match real_path.strip_prefix(target) {
|
||||
Ok(p) => {
|
||||
let mount_path = Path::new(&name);
|
||||
return Ok(mount_path.join(p));
|
||||
}
|
||||
Err(_) => (),
|
||||
}
|
||||
}
|
||||
bail!("Real path has no match in VFS")
|
||||
}
|
||||
pub fn real_to_virtual(&self, real_path: &Path) -> Result<PathBuf> {
|
||||
for (name, target) in &self.mount_points {
|
||||
match real_path.strip_prefix(target) {
|
||||
Ok(p) => {
|
||||
let mount_path = Path::new(&name);
|
||||
return Ok(mount_path.join(p));
|
||||
}
|
||||
Err(_) => (),
|
||||
}
|
||||
}
|
||||
bail!("Real path has no match in VFS")
|
||||
}
|
||||
|
||||
pub fn virtual_to_real(&self, virtual_path: &Path) -> Result<PathBuf> {
|
||||
for (name, target) in &self.mount_points {
|
||||
let mount_path = Path::new(&name);
|
||||
match virtual_path.strip_prefix(mount_path) {
|
||||
Ok(p) => {
|
||||
return if p.components().count() == 0 {
|
||||
Ok(target.clone())
|
||||
} else {
|
||||
Ok(target.join(p))
|
||||
}
|
||||
}
|
||||
Err(_) => (),
|
||||
}
|
||||
}
|
||||
bail!("Virtual path has no match in VFS")
|
||||
}
|
||||
pub fn virtual_to_real(&self, virtual_path: &Path) -> Result<PathBuf> {
|
||||
for (name, target) in &self.mount_points {
|
||||
let mount_path = Path::new(&name);
|
||||
match virtual_path.strip_prefix(mount_path) {
|
||||
Ok(p) => {
|
||||
return if p.components().count() == 0 {
|
||||
Ok(target.clone())
|
||||
} else {
|
||||
Ok(target.join(p))
|
||||
}
|
||||
}
|
||||
Err(_) => (),
|
||||
}
|
||||
}
|
||||
bail!("Virtual path has no match in VFS")
|
||||
}
|
||||
|
||||
pub fn get_mount_points(&self) -> &HashMap<String, PathBuf> {
|
||||
return &self.mount_points;
|
||||
}
|
||||
pub fn get_mount_points(&self) -> &HashMap<String, PathBuf> {
|
||||
return &self.mount_points;
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_virtual_to_real() {
|
||||
let mut config = VfsConfig::new();
|
||||
config.mount_points.insert("root".to_owned(), Path::new("test_dir").to_path_buf());
|
||||
let vfs = Vfs::new(config);
|
||||
let mut config = VfsConfig::new();
|
||||
config.mount_points.insert("root".to_owned(), Path::new("test_dir").to_path_buf());
|
||||
let vfs = Vfs::new(config);
|
||||
|
||||
let mut correct_path = PathBuf::new();
|
||||
correct_path.push("test_dir");
|
||||
correct_path.push("somewhere");
|
||||
correct_path.push("something.png");
|
||||
let mut correct_path = PathBuf::new();
|
||||
correct_path.push("test_dir");
|
||||
correct_path.push("somewhere");
|
||||
correct_path.push("something.png");
|
||||
|
||||
let mut virtual_path = PathBuf::new();
|
||||
virtual_path.push("root");
|
||||
virtual_path.push("somewhere");
|
||||
virtual_path.push("something.png");
|
||||
let mut virtual_path = PathBuf::new();
|
||||
virtual_path.push("root");
|
||||
virtual_path.push("somewhere");
|
||||
virtual_path.push("something.png");
|
||||
|
||||
let found_path = vfs.virtual_to_real(virtual_path.as_path()).unwrap();
|
||||
assert!(found_path.to_str() == correct_path.to_str());
|
||||
let found_path = vfs.virtual_to_real(virtual_path.as_path()).unwrap();
|
||||
assert!(found_path.to_str() == correct_path.to_str());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_virtual_to_real_no_trail() {
|
||||
let mut config = VfsConfig::new();
|
||||
config.mount_points.insert("root".to_owned(), Path::new("test_dir").to_path_buf());
|
||||
let vfs = Vfs::new(config);
|
||||
let correct_path = Path::new("test_dir");
|
||||
let found_path = vfs.virtual_to_real(Path::new("root")).unwrap();
|
||||
assert!(found_path.to_str() == correct_path.to_str());
|
||||
let mut config = VfsConfig::new();
|
||||
config.mount_points.insert("root".to_owned(), Path::new("test_dir").to_path_buf());
|
||||
let vfs = Vfs::new(config);
|
||||
let correct_path = Path::new("test_dir");
|
||||
let found_path = vfs.virtual_to_real(Path::new("root")).unwrap();
|
||||
assert!(found_path.to_str() == correct_path.to_str());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_real_to_virtual() {
|
||||
let mut config = VfsConfig::new();
|
||||
config.mount_points.insert("root".to_owned(), Path::new("test_dir").to_path_buf());
|
||||
let vfs = Vfs::new(config);
|
||||
let mut config = VfsConfig::new();
|
||||
config.mount_points.insert("root".to_owned(), Path::new("test_dir").to_path_buf());
|
||||
let vfs = Vfs::new(config);
|
||||
|
||||
let mut correct_path = PathBuf::new();
|
||||
correct_path.push("root");
|
||||
correct_path.push("somewhere");
|
||||
correct_path.push("something.png");
|
||||
let mut correct_path = PathBuf::new();
|
||||
correct_path.push("root");
|
||||
correct_path.push("somewhere");
|
||||
correct_path.push("something.png");
|
||||
|
||||
let mut real_path = PathBuf::new();
|
||||
real_path.push("test_dir");
|
||||
real_path.push("somewhere");
|
||||
real_path.push("something.png");
|
||||
let mut real_path = PathBuf::new();
|
||||
real_path.push("test_dir");
|
||||
real_path.push("somewhere");
|
||||
real_path.push("something.png");
|
||||
|
||||
let found_path = vfs.real_to_virtual(real_path.as_path()).unwrap();
|
||||
assert!(found_path == correct_path);
|
||||
let found_path = vfs.real_to_virtual(real_path.as_path()).unwrap();
|
||||
assert!(found_path == correct_path);
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue