rustfmt
This commit is contained in:
parent
f21e4e055f
commit
2150241ae1
8 changed files with 635 additions and 588 deletions
11
src/api.rs
11
src/api.rs
|
@ -52,9 +52,8 @@ pub fn get_api_handler(collection: Arc<Collection>) -> Mount {
|
||||||
|
|
||||||
{
|
{
|
||||||
let collection = collection.clone();
|
let collection = collection.clone();
|
||||||
api_handler.mount("/auth/", move |request: &mut Request| {
|
api_handler.mount("/auth/",
|
||||||
self::auth(request, collection.deref())
|
move |request: &mut Request| self::auth(request, collection.deref()));
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
|
@ -189,7 +188,7 @@ fn serve(request: &mut Request, collection: &Collection) -> IronResult<Response>
|
||||||
}
|
}
|
||||||
|
|
||||||
if is_song(real_path.as_path()) {
|
if is_song(real_path.as_path()) {
|
||||||
return Ok(Response::with((status::Ok, real_path)))
|
return Ok(Response::with((status::Ok, real_path)));
|
||||||
}
|
}
|
||||||
|
|
||||||
if is_image(real_path.as_path()) {
|
if is_image(real_path.as_path()) {
|
||||||
|
@ -203,6 +202,6 @@ fn art(_: &mut Request, real_path: &Path) -> IronResult<Response> {
|
||||||
let thumb = get_thumbnail(real_path, 400);
|
let thumb = get_thumbnail(real_path, 400);
|
||||||
match thumb {
|
match thumb {
|
||||||
Ok(path) => Ok(Response::with((status::Ok, path))),
|
Ok(path) => Ok(Response::with((status::Ok, path))),
|
||||||
Err(e) => Err(IronError::from(e))
|
Err(e) => Err(IronError::from(e)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
110
src/config.rs
110
src/config.rs
|
@ -24,14 +24,14 @@ const CONFIG_DDNS_PASSWORD: &'static str = "password";
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum ConfigError {
|
pub enum ConfigError {
|
||||||
IoError(io::Error),
|
IoError(io::Error),
|
||||||
TOMLParseError,
|
TOMLParseError,
|
||||||
RegexError(regex::Error),
|
RegexError(regex::Error),
|
||||||
SecretParseError,
|
SecretParseError,
|
||||||
AlbumArtPatternParseError,
|
AlbumArtPatternParseError,
|
||||||
UsersParseError,
|
UsersParseError,
|
||||||
MountDirsParseError,
|
MountDirsParseError,
|
||||||
DDNSParseError,
|
DDNSParseError,
|
||||||
ConflictingMounts,
|
ConflictingMounts,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -48,46 +48,46 @@ impl From<regex::Error> for ConfigError {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
pub secret: String,
|
pub secret: String,
|
||||||
pub vfs: VfsConfig,
|
pub vfs: VfsConfig,
|
||||||
pub users: Vec<User>,
|
pub users: Vec<User>,
|
||||||
pub album_art_pattern: Option<regex::Regex>,
|
pub album_art_pattern: Option<regex::Regex>,
|
||||||
pub ddns: Option<DDNSConfig>,
|
pub ddns: Option<DDNSConfig>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Config {
|
impl Config {
|
||||||
pub fn parse(config_path: &path::Path) -> Result<Config, ConfigError> {
|
pub fn parse(config_path: &path::Path) -> Result<Config, ConfigError> {
|
||||||
let mut config_file = try!(fs::File::open(config_path));
|
let mut config_file = try!(fs::File::open(config_path));
|
||||||
let mut config_file_content = String::new();
|
let mut config_file_content = String::new();
|
||||||
try!(config_file.read_to_string(&mut config_file_content));
|
try!(config_file.read_to_string(&mut config_file_content));
|
||||||
let parsed_config = toml::Parser::new(config_file_content.as_str()).parse();
|
let parsed_config = toml::Parser::new(config_file_content.as_str()).parse();
|
||||||
let parsed_config = try!(parsed_config.ok_or(ConfigError::TOMLParseError));
|
let parsed_config = try!(parsed_config.ok_or(ConfigError::TOMLParseError));
|
||||||
|
|
||||||
let mut config = Config {
|
let mut config = Config {
|
||||||
secret: String::new(),
|
secret: String::new(),
|
||||||
vfs: VfsConfig::new(),
|
vfs: VfsConfig::new(),
|
||||||
users: Vec::new(),
|
users: Vec::new(),
|
||||||
album_art_pattern: None,
|
album_art_pattern: None,
|
||||||
ddns: None,
|
ddns: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
try!(config.parse_secret(&parsed_config));
|
try!(config.parse_secret(&parsed_config));
|
||||||
try!(config.parse_mount_points(&parsed_config));
|
try!(config.parse_mount_points(&parsed_config));
|
||||||
try!(config.parse_users(&parsed_config));
|
try!(config.parse_users(&parsed_config));
|
||||||
try!(config.parse_album_art_pattern(&parsed_config));
|
try!(config.parse_album_art_pattern(&parsed_config));
|
||||||
try!(config.parse_ddns(&parsed_config));
|
try!(config.parse_ddns(&parsed_config));
|
||||||
|
|
||||||
Ok(config)
|
Ok(config)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_secret(&mut self, source: &toml::Table) -> Result<(), ConfigError> {
|
fn parse_secret(&mut self, source: &toml::Table) -> Result<(), ConfigError> {
|
||||||
let secret = try!(source.get(CONFIG_SECRET).ok_or(ConfigError::SecretParseError));
|
let secret = try!(source.get(CONFIG_SECRET).ok_or(ConfigError::SecretParseError));
|
||||||
let secret = try!(secret.as_str().ok_or(ConfigError::SecretParseError));
|
let secret = try!(secret.as_str().ok_or(ConfigError::SecretParseError));
|
||||||
self.secret = secret.to_owned();
|
self.secret = secret.to_owned();
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_album_art_pattern(&mut self, source: &toml::Table) -> Result<(), ConfigError> {
|
fn parse_album_art_pattern(&mut self, source: &toml::Table) -> Result<(), ConfigError> {
|
||||||
let pattern = match source.get(CONFIG_ALBUM_ART_PATTERN) {
|
let pattern = match source.get(CONFIG_ALBUM_ART_PATTERN) {
|
||||||
Some(s) => s,
|
Some(s) => s,
|
||||||
None => return Ok(()),
|
None => return Ok(()),
|
||||||
|
@ -100,7 +100,7 @@ impl Config {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_users(&mut self, source: &toml::Table) -> Result<(), ConfigError> {
|
fn parse_users(&mut self, source: &toml::Table) -> Result<(), ConfigError> {
|
||||||
let users = match source.get(CONFIG_USERS) {
|
let users = match source.get(CONFIG_USERS) {
|
||||||
Some(s) => s,
|
Some(s) => s,
|
||||||
None => return Ok(()),
|
None => return Ok(()),
|
||||||
|
@ -137,7 +137,7 @@ impl Config {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_mount_points(&mut self, source: &toml::Table) -> Result<(), ConfigError> {
|
fn parse_mount_points(&mut self, source: &toml::Table) -> Result<(), ConfigError> {
|
||||||
let mount_dirs = match source.get(CONFIG_MOUNT_DIRS) {
|
let mount_dirs = match source.get(CONFIG_MOUNT_DIRS) {
|
||||||
Some(s) => s,
|
Some(s) => s,
|
||||||
None => return Ok(()),
|
None => return Ok(()),
|
||||||
|
@ -177,30 +177,32 @@ impl Config {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_ddns(&mut self, source: &toml::Table) -> Result<(), ConfigError> {
|
fn parse_ddns(&mut self, source: &toml::Table) -> Result<(), ConfigError> {
|
||||||
let ddns = match source.get(CONFIG_DDNS) {
|
let ddns = match source.get(CONFIG_DDNS) {
|
||||||
Some(s) => s,
|
Some(s) => s,
|
||||||
None => return Ok(()),
|
None => return Ok(()),
|
||||||
};
|
};
|
||||||
let ddns = match ddns {
|
let ddns = match ddns {
|
||||||
&toml::Value::Table(ref a) => a,
|
&toml::Value::Table(ref a) => a,
|
||||||
_ => return Err(ConfigError::DDNSParseError),
|
_ => return Err(ConfigError::DDNSParseError),
|
||||||
};
|
};
|
||||||
|
|
||||||
let host = try!(ddns.get(CONFIG_DDNS_HOST).ok_or(ConfigError::DDNSParseError)).as_str();
|
let host = try!(ddns.get(CONFIG_DDNS_HOST).ok_or(ConfigError::DDNSParseError)).as_str();
|
||||||
let username = try!(ddns.get(CONFIG_DDNS_USERNAME).ok_or(ConfigError::DDNSParseError)).as_str();
|
let username = try!(ddns.get(CONFIG_DDNS_USERNAME).ok_or(ConfigError::DDNSParseError))
|
||||||
let password = try!(ddns.get(CONFIG_DDNS_PASSWORD).ok_or(ConfigError::DDNSParseError)).as_str();
|
.as_str();
|
||||||
|
let password = try!(ddns.get(CONFIG_DDNS_PASSWORD).ok_or(ConfigError::DDNSParseError))
|
||||||
|
.as_str();
|
||||||
|
|
||||||
let host = try!(host.ok_or(ConfigError::DDNSParseError));
|
let host = try!(host.ok_or(ConfigError::DDNSParseError));
|
||||||
let username = try!(username.ok_or(ConfigError::DDNSParseError));
|
let username = try!(username.ok_or(ConfigError::DDNSParseError));
|
||||||
let password = try!(password.ok_or(ConfigError::DDNSParseError));
|
let password = try!(password.ok_or(ConfigError::DDNSParseError));
|
||||||
|
|
||||||
self.ddns = Some(DDNSConfig {
|
self.ddns = Some(DDNSConfig {
|
||||||
host: host.to_owned(),
|
host: host.to_owned(),
|
||||||
username: username.to_owned(),
|
username: username.to_owned(),
|
||||||
password: password.to_owned(),
|
password: password.to_owned(),
|
||||||
});
|
});
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -217,10 +219,10 @@ fn clean_path_string(path_string: &str) -> path::PathBuf {
|
||||||
#[test]
|
#[test]
|
||||||
fn test_clean_path_string() {
|
fn test_clean_path_string() {
|
||||||
let mut correct_path = path::PathBuf::new();
|
let mut correct_path = path::PathBuf::new();
|
||||||
correct_path.push("C:");
|
correct_path.push("C:");
|
||||||
correct_path.push("some");
|
correct_path.push("some");
|
||||||
correct_path.push("path");
|
correct_path.push("path");
|
||||||
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"#));
|
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\"#));
|
||||||
}
|
}
|
||||||
|
|
78
src/ddns.rs
78
src/ddns.rs
|
@ -1,6 +1,6 @@
|
||||||
use hyper;
|
use hyper;
|
||||||
use hyper::client::Client;
|
use hyper::client::Client;
|
||||||
use hyper::header::{Authorization, Basic };
|
use hyper::header::{Authorization, Basic};
|
||||||
use std::io;
|
use std::io;
|
||||||
use std::io::Read;
|
use std::io::Read;
|
||||||
use std::thread;
|
use std::thread;
|
||||||
|
@ -8,16 +8,16 @@ use std::time;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct DDNSConfig {
|
pub struct DDNSConfig {
|
||||||
pub host: String,
|
pub host: String,
|
||||||
pub username: String,
|
pub username: String,
|
||||||
pub password: String,
|
pub password: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
enum DDNSError {
|
enum DDNSError {
|
||||||
IoError(io::Error),
|
IoError(io::Error),
|
||||||
HyperError(hyper::Error),
|
HyperError(hyper::Error),
|
||||||
UpdateError(hyper::status::StatusCode),
|
UpdateError(hyper::status::StatusCode),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<io::Error> for DDNSError {
|
impl From<io::Error> for DDNSError {
|
||||||
|
@ -36,43 +36,41 @@ const MY_IP_API_URL: &'static str = "http://api.ipify.org";
|
||||||
const DDNS_UPDATE_URL: &'static str = "http://ydns.io/api/v1/update/";
|
const DDNS_UPDATE_URL: &'static str = "http://ydns.io/api/v1/update/";
|
||||||
|
|
||||||
fn get_my_ip() -> Result<String, DDNSError> {
|
fn get_my_ip() -> Result<String, DDNSError> {
|
||||||
let client = Client::new();
|
let client = Client::new();
|
||||||
let mut res = try!(client.get(MY_IP_API_URL).send());
|
let mut res = try!(client.get(MY_IP_API_URL).send());
|
||||||
let mut buf = String::new();
|
let mut buf = String::new();
|
||||||
try!(res.read_to_string(&mut buf));
|
try!(res.read_to_string(&mut buf));
|
||||||
Ok(buf)
|
Ok(buf)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update_my_ip(ip: &String, config: &DDNSConfig) -> Result<(), DDNSError> {
|
fn update_my_ip(ip: &String, config: &DDNSConfig) -> Result<(), DDNSError> {
|
||||||
let client = Client::new();
|
let client = Client::new();
|
||||||
let url = DDNS_UPDATE_URL;
|
let url = DDNS_UPDATE_URL;
|
||||||
let host = &config.host;
|
let host = &config.host;
|
||||||
let full_url = format!("{}?host={}&ip={}", url, host, ip);
|
let full_url = format!("{}?host={}&ip={}", url, host, ip);
|
||||||
let auth_header = Authorization(Basic {
|
let auth_header = Authorization(Basic {
|
||||||
username: config.username.clone(),
|
username: config.username.clone(),
|
||||||
password: Some(config.password.to_owned())
|
password: Some(config.password.to_owned()),
|
||||||
});
|
});
|
||||||
|
|
||||||
let res = try!(client.get(full_url.as_str()).header(auth_header).send());
|
let res = try!(client.get(full_url.as_str()).header(auth_header).send());
|
||||||
match res.status {
|
match res.status {
|
||||||
hyper::status::StatusCode::Ok => Ok(()),
|
hyper::status::StatusCode::Ok => Ok(()),
|
||||||
s => Err(DDNSError::UpdateError(s)),
|
s => Err(DDNSError::UpdateError(s)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn run(config: DDNSConfig) {
|
pub fn run(config: DDNSConfig) {
|
||||||
loop {
|
loop {
|
||||||
let my_ip_res = get_my_ip();
|
let my_ip_res = get_my_ip();
|
||||||
if let Ok(my_ip) = my_ip_res {
|
if let Ok(my_ip) = my_ip_res {
|
||||||
match update_my_ip(&my_ip, &config) {
|
match update_my_ip(&my_ip, &config) {
|
||||||
Err(e) => println!("Dynamic DNS Error: {:?}", e),
|
Err(e) => println!("Dynamic DNS Error: {:?}", e),
|
||||||
Ok(_) => (),
|
Ok(_) => (),
|
||||||
};
|
};
|
||||||
}
|
} else {
|
||||||
else
|
println!("Dynamic DNS Error: could not retrieve our own IP address");
|
||||||
{
|
}
|
||||||
println!("Dynamic DNS Error: could not retrieve our own IP address");
|
thread::sleep(time::Duration::from_secs(60 * 30));
|
||||||
}
|
}
|
||||||
thread::sleep(time::Duration::from_secs(60 * 30));
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
858
src/index.rs
858
src/index.rs
|
@ -19,9 +19,9 @@ const INDEX_BUILDING_INSERT_BUFFER_SIZE: usize = 500; // Put 500 insertions in e
|
||||||
const INDEX_LOCK_TIMEOUT: usize = 100; // In milliseconds
|
const INDEX_LOCK_TIMEOUT: usize = 100; // In milliseconds
|
||||||
|
|
||||||
pub struct Index {
|
pub struct Index {
|
||||||
path: String,
|
path: String,
|
||||||
vfs: Arc<Vfs>,
|
vfs: Arc<Vfs>,
|
||||||
album_art_pattern: Option<Regex>,
|
album_art_pattern: Option<Regex>,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct SongTags {
|
struct SongTags {
|
||||||
|
@ -35,15 +35,15 @@ struct SongTags {
|
||||||
|
|
||||||
impl SongTags {
|
impl SongTags {
|
||||||
fn read(path: &Path) -> Result<SongTags, PError> {
|
fn read(path: &Path) -> Result<SongTags, PError> {
|
||||||
match utils::get_audio_format(path) {
|
match utils::get_audio_format(path) {
|
||||||
Some(AudioFormat::MP3) => SongTags::read_id3(path),
|
Some(AudioFormat::MP3) => SongTags::read_id3(path),
|
||||||
Some(AudioFormat::MPC) => SongTags::read_ape(path),
|
Some(AudioFormat::MPC) => SongTags::read_ape(path),
|
||||||
_ => Err(PError::UnsupportedMetadataFormat),
|
_ => Err(PError::UnsupportedMetadataFormat),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn read_id3(path: &Path) -> Result<SongTags, PError> {
|
fn read_id3(path: &Path) -> Result<SongTags, PError> {
|
||||||
let tag = try!(Tag::read_from_path(path));
|
let tag = try!(Tag::read_from_path(path));
|
||||||
|
|
||||||
let artist = tag.artist().map(|s| s.to_string());
|
let artist = tag.artist().map(|s| s.to_string());
|
||||||
let album_artist = tag.album_artist().map(|s| s.to_string());
|
let album_artist = tag.album_artist().map(|s| s.to_string());
|
||||||
|
@ -54,7 +54,7 @@ impl SongTags {
|
||||||
.map(|y| y as i32)
|
.map(|y| y as i32)
|
||||||
.or(tag.date_released().and_then(|d| d.year))
|
.or(tag.date_released().and_then(|d| d.year))
|
||||||
.or(tag.date_recorded().and_then(|d| d.year));
|
.or(tag.date_recorded().and_then(|d| d.year));
|
||||||
|
|
||||||
Ok(SongTags {
|
Ok(SongTags {
|
||||||
artist: artist,
|
artist: artist,
|
||||||
album_artist: album_artist,
|
album_artist: album_artist,
|
||||||
|
@ -63,45 +63,45 @@ impl SongTags {
|
||||||
track_number: track_number,
|
track_number: track_number,
|
||||||
year: year,
|
year: year,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn read_ape_string(item: &ape::Item) -> Option<String> {
|
fn read_ape_string(item: &ape::Item) -> Option<String> {
|
||||||
match item.value {
|
match item.value {
|
||||||
ape::ItemValue::Text(ref s) => Some(s.clone()),
|
ape::ItemValue::Text(ref s) => Some(s.clone()),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn read_ape_i32(item: &ape::Item) -> Option<i32> {
|
fn read_ape_i32(item: &ape::Item) -> Option<i32> {
|
||||||
match item.value {
|
match item.value {
|
||||||
ape::ItemValue::Text(ref s) => s.parse::<i32>().ok(),
|
ape::ItemValue::Text(ref s) => s.parse::<i32>().ok(),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn read_ape_track_number(item: &ape::Item) -> Option<u32> {
|
fn read_ape_track_number(item: &ape::Item) -> Option<u32> {
|
||||||
match item.value {
|
match item.value {
|
||||||
ape::ItemValue::Text(ref s) => {
|
ape::ItemValue::Text(ref s) => {
|
||||||
let format = Regex::new(r#"^\d+"#).unwrap();
|
let format = Regex::new(r#"^\d+"#).unwrap();
|
||||||
if let Some((start, end)) = format.find(s) {
|
if let Some((start, end)) = format.find(s) {
|
||||||
s[start..end].parse().ok()
|
s[start..end].parse().ok()
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn read_ape(path: &Path) -> Result<SongTags, PError> {
|
fn read_ape(path: &Path) -> Result<SongTags, PError> {
|
||||||
let tag = try!(ape::read(path));
|
let tag = try!(ape::read(path));
|
||||||
let artist = tag.item("Artist").and_then(SongTags::read_ape_string);
|
let artist = tag.item("Artist").and_then(SongTags::read_ape_string);
|
||||||
let album = tag.item("Album").and_then(SongTags::read_ape_string);
|
let album = tag.item("Album").and_then(SongTags::read_ape_string);
|
||||||
let album_artist = tag.item("Album artist").and_then(SongTags::read_ape_string);
|
let album_artist = tag.item("Album artist").and_then(SongTags::read_ape_string);
|
||||||
let title = tag.item("Title").and_then(SongTags::read_ape_string);
|
let title = tag.item("Title").and_then(SongTags::read_ape_string);
|
||||||
let year = tag.item("Year").and_then(SongTags::read_ape_i32);
|
let year = tag.item("Year").and_then(SongTags::read_ape_i32);
|
||||||
let track_number = tag.item("Track").and_then(SongTags::read_ape_track_number);
|
let track_number = tag.item("Track").and_then(SongTags::read_ape_track_number);
|
||||||
Ok(SongTags {
|
Ok(SongTags {
|
||||||
artist: artist,
|
artist: artist,
|
||||||
album_artist: album_artist,
|
album_artist: album_artist,
|
||||||
album: album,
|
album: album,
|
||||||
|
@ -109,7 +109,7 @@ impl SongTags {
|
||||||
track_number: track_number,
|
track_number: track_number,
|
||||||
year: year,
|
year: year,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, RustcEncodable)]
|
#[derive(Debug, RustcEncodable)]
|
||||||
|
@ -140,464 +140,504 @@ pub enum CollectionFile {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn string_option_to_value(input: Option<String>) -> Value {
|
fn string_option_to_value(input: Option<String>) -> Value {
|
||||||
match input {
|
match input {
|
||||||
Some(s) => Value::String(s),
|
Some(s) => Value::String(s),
|
||||||
None => Value::Null,
|
None => Value::Null,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn i32_option_to_value(input: Option<i32>) -> Value {
|
fn i32_option_to_value(input: Option<i32>) -> Value {
|
||||||
match input {
|
match input {
|
||||||
Some(s) => Value::Integer(s as i64),
|
Some(s) => Value::Integer(s as i64),
|
||||||
None => Value::Null,
|
None => Value::Null,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn u32_option_to_value(input: Option<u32>) -> Value {
|
fn u32_option_to_value(input: Option<u32>) -> Value {
|
||||||
match input {
|
match input {
|
||||||
Some(s) => Value::Integer(s as i64),
|
Some(s) => Value::Integer(s as i64),
|
||||||
None => Value::Null,
|
None => Value::Null,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct IndexBuilder<'db> {
|
struct IndexBuilder<'db> {
|
||||||
queue: Vec<CollectionFile>,
|
queue: Vec<CollectionFile>,
|
||||||
db: &'db Connection,
|
db: &'db Connection,
|
||||||
insert_directory: Statement<'db>,
|
insert_directory: Statement<'db>,
|
||||||
insert_song: Statement<'db>,
|
insert_song: Statement<'db>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'db> IndexBuilder<'db> {
|
impl<'db> IndexBuilder<'db> {
|
||||||
fn new(db: &Connection) -> IndexBuilder {
|
fn new(db: &Connection) -> IndexBuilder {
|
||||||
let mut queue = Vec::new();
|
let mut queue = Vec::new();
|
||||||
queue.reserve_exact(INDEX_BUILDING_INSERT_BUFFER_SIZE);
|
queue.reserve_exact(INDEX_BUILDING_INSERT_BUFFER_SIZE);
|
||||||
IndexBuilder {
|
IndexBuilder {
|
||||||
queue: queue,
|
queue: queue,
|
||||||
db: db,
|
db: db,
|
||||||
insert_directory: db.prepare("INSERT OR REPLACE INTO directories (path, parent, artwork, year, artist, album) VALUES (?, ?, ?, ?, ?, ?)").unwrap(),
|
insert_directory:
|
||||||
insert_song: db.prepare("INSERT OR REPLACE INTO songs (path, parent, track_number, title, year, album_artist, artist, album, artwork) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)").unwrap(),
|
db.prepare("INSERT OR REPLACE INTO directories (path, parent, artwork, year, \
|
||||||
}
|
artist, album) VALUES (?, ?, ?, ?, ?, ?)")
|
||||||
}
|
.unwrap(),
|
||||||
|
insert_song:
|
||||||
|
db.prepare("INSERT OR REPLACE INTO songs (path, parent, track_number, title, year, \
|
||||||
|
album_artist, artist, album, artwork) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)")
|
||||||
|
.unwrap(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn get_parent(path: &str) -> Option<String> {
|
fn get_parent(path: &str) -> Option<String> {
|
||||||
let parent_path = Path::new(path);
|
let parent_path = Path::new(path);
|
||||||
if let Some(parent_dir) = parent_path.parent() {
|
if let Some(parent_dir) = parent_path.parent() {
|
||||||
if let Some(parent_path) = parent_dir.to_str() {
|
if let Some(parent_path) = parent_dir.to_str() {
|
||||||
return Some(parent_path.to_owned());
|
return Some(parent_path.to_owned());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
fn flush(&mut self) {
|
fn flush(&mut self) {
|
||||||
self.db.execute("BEGIN TRANSACTION").ok();
|
self.db.execute("BEGIN TRANSACTION").ok();
|
||||||
while let Some(file) = self.queue.pop() {
|
while let Some(file) = self.queue.pop() {
|
||||||
match file {
|
match file {
|
||||||
|
|
||||||
// Insert directory
|
// Insert directory
|
||||||
CollectionFile::Directory(directory) => {
|
CollectionFile::Directory(directory) => {
|
||||||
let parent = IndexBuilder::get_parent(directory.path.as_str());
|
let parent = IndexBuilder::get_parent(directory.path.as_str());
|
||||||
self.insert_directory.reset().ok();
|
self.insert_directory.reset().ok();
|
||||||
self.insert_directory.bind(1, &Value::String(directory.path)).unwrap();
|
self.insert_directory.bind(1, &Value::String(directory.path)).unwrap();
|
||||||
self.insert_directory.bind(2, &string_option_to_value(parent)).unwrap();
|
self.insert_directory.bind(2, &string_option_to_value(parent)).unwrap();
|
||||||
self.insert_directory.bind(3, &string_option_to_value(directory.artwork)).unwrap();
|
self.insert_directory
|
||||||
self.insert_directory.bind(4, &i32_option_to_value(directory.year)).unwrap();
|
.bind(3, &string_option_to_value(directory.artwork))
|
||||||
self.insert_directory.bind(5, &string_option_to_value(directory.artist)).unwrap();
|
.unwrap();
|
||||||
self.insert_directory.bind(6, &string_option_to_value(directory.album)).unwrap();
|
self.insert_directory.bind(4, &i32_option_to_value(directory.year)).unwrap();
|
||||||
self.insert_directory.next().ok();
|
self.insert_directory
|
||||||
},
|
.bind(5, &string_option_to_value(directory.artist))
|
||||||
|
.unwrap();
|
||||||
|
self.insert_directory
|
||||||
|
.bind(6, &string_option_to_value(directory.album))
|
||||||
|
.unwrap();
|
||||||
|
self.insert_directory.next().ok();
|
||||||
|
}
|
||||||
|
|
||||||
// Insert song
|
// Insert song
|
||||||
CollectionFile::Song(song) => {
|
CollectionFile::Song(song) => {
|
||||||
let parent = IndexBuilder::get_parent(song.path.as_str());
|
let parent = IndexBuilder::get_parent(song.path.as_str());
|
||||||
self.insert_song.reset().ok();
|
self.insert_song.reset().ok();
|
||||||
self.insert_song.bind(1, &Value::String(song.path)).unwrap();
|
self.insert_song.bind(1, &Value::String(song.path)).unwrap();
|
||||||
self.insert_song.bind(2, &string_option_to_value(parent)).unwrap();
|
self.insert_song.bind(2, &string_option_to_value(parent)).unwrap();
|
||||||
self.insert_song.bind(3, &u32_option_to_value(song.track_number)).unwrap();
|
self.insert_song.bind(3, &u32_option_to_value(song.track_number)).unwrap();
|
||||||
self.insert_song.bind(4, &string_option_to_value(song.title)).unwrap();
|
self.insert_song.bind(4, &string_option_to_value(song.title)).unwrap();
|
||||||
self.insert_song.bind(5, &i32_option_to_value(song.year)).unwrap();
|
self.insert_song.bind(5, &i32_option_to_value(song.year)).unwrap();
|
||||||
self.insert_song.bind(6, &string_option_to_value(song.album_artist)).unwrap();
|
self.insert_song.bind(6, &string_option_to_value(song.album_artist)).unwrap();
|
||||||
self.insert_song.bind(7, &string_option_to_value(song.artist)).unwrap();
|
self.insert_song.bind(7, &string_option_to_value(song.artist)).unwrap();
|
||||||
self.insert_song.bind(8, &string_option_to_value(song.album)).unwrap();
|
self.insert_song.bind(8, &string_option_to_value(song.album)).unwrap();
|
||||||
self.insert_song.bind(9, &string_option_to_value(song.artwork)).unwrap();
|
self.insert_song.bind(9, &string_option_to_value(song.artwork)).unwrap();
|
||||||
self.insert_song.next().ok();
|
self.insert_song.next().ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.db.execute("END TRANSACTION").ok();
|
self.db.execute("END TRANSACTION").ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn push(&mut self, file: CollectionFile) {
|
fn push(&mut self, file: CollectionFile) {
|
||||||
if self.queue.len() == self.queue.capacity() {
|
if self.queue.len() == self.queue.capacity() {
|
||||||
self.flush();
|
self.flush();
|
||||||
}
|
}
|
||||||
self.queue.push(file);
|
self.queue.push(file);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'db> Drop for IndexBuilder<'db> {
|
impl<'db> Drop for IndexBuilder<'db> {
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
self.flush();
|
self.flush();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Index {
|
impl Index {
|
||||||
|
pub fn new(path: &Path,
|
||||||
|
vfs: Arc<Vfs>,
|
||||||
|
album_art_pattern: &Option<Regex>)
|
||||||
|
-> Result<Index, PError> {
|
||||||
|
|
||||||
pub fn new(path: &Path, vfs: Arc<Vfs>, album_art_pattern: &Option<Regex>) -> Result<Index, PError> {
|
let index = Index {
|
||||||
|
path: path.to_string_lossy().deref().to_string(),
|
||||||
|
vfs: vfs,
|
||||||
|
album_art_pattern: album_art_pattern.clone(),
|
||||||
|
};
|
||||||
|
|
||||||
let index = Index {
|
if path.exists() {
|
||||||
path: path.to_string_lossy().deref().to_string(),
|
// Migration
|
||||||
vfs: vfs,
|
} else {
|
||||||
album_art_pattern: album_art_pattern.clone(),
|
index.init();
|
||||||
};
|
}
|
||||||
|
|
||||||
if path.exists() {
|
Ok(index)
|
||||||
// Migration
|
}
|
||||||
} else {
|
|
||||||
index.init();
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(index)
|
fn init(&self) {
|
||||||
}
|
|
||||||
|
|
||||||
fn init(&self) {
|
println!("Initializing index database");
|
||||||
|
|
||||||
println!("Initializing index database");
|
let db = self.connect();
|
||||||
|
db.execute("PRAGMA synchronous = NORMAL").unwrap();
|
||||||
let db = self.connect();
|
db.execute("
|
||||||
db.execute("PRAGMA synchronous = NORMAL").unwrap();
|
|
||||||
db.execute("
|
|
||||||
|
|
||||||
CREATE TABLE version
|
CREATE TABLE version
|
||||||
( id INTEGER PRIMARY KEY NOT NULL
|
( id INTEGER PRIMARY KEY NOT NULL
|
||||||
, number INTEGER NULL
|
, number \
|
||||||
|
INTEGER NULL
|
||||||
);
|
);
|
||||||
INSERT INTO version (number) VALUES(1);
|
INSERT INTO version (number) VALUES(1);
|
||||||
|
|
||||||
CREATE TABLE directories
|
CREATE \
|
||||||
|
TABLE directories
|
||||||
( id INTEGER PRIMARY KEY NOT NULL
|
( id INTEGER PRIMARY KEY NOT NULL
|
||||||
, path TEXT NOT NULL
|
, path TEXT NOT \
|
||||||
|
NULL
|
||||||
, parent TEXT
|
, parent TEXT
|
||||||
, artist TEXT
|
, artist TEXT
|
||||||
, year INTEGER
|
, year INTEGER
|
||||||
, album TEXT
|
, album TEXT
|
||||||
, artwork TEXT
|
\
|
||||||
|
, artwork TEXT
|
||||||
, UNIQUE(path)
|
, UNIQUE(path)
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE songs
|
CREATE TABLE songs
|
||||||
( id INTEGER PRIMARY KEY NOT NULL
|
( id \
|
||||||
|
INTEGER PRIMARY KEY NOT NULL
|
||||||
, path TEXT NOT NULL
|
, path TEXT NOT NULL
|
||||||
, parent TEXT NOT NULL
|
, parent TEXT NOT \
|
||||||
|
NULL
|
||||||
, track_number INTEGER
|
, track_number INTEGER
|
||||||
, title TEXT
|
, title TEXT
|
||||||
, artist TEXT
|
, artist TEXT
|
||||||
, album_artist TEXT
|
, \
|
||||||
|
album_artist TEXT
|
||||||
, year INTEGER
|
, year INTEGER
|
||||||
, album TEXT
|
, album TEXT
|
||||||
, artwork TEXT
|
, artwork TEXT
|
||||||
, UNIQUE(path)
|
, \
|
||||||
|
UNIQUE(path)
|
||||||
);
|
);
|
||||||
|
|
||||||
").unwrap();
|
")
|
||||||
}
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
fn connect(&self) -> Connection {
|
fn connect(&self) -> Connection {
|
||||||
let mut db = sqlite::open(self.path.clone()).unwrap();
|
let mut db = sqlite::open(self.path.clone()).unwrap();
|
||||||
db.set_busy_timeout(INDEX_LOCK_TIMEOUT).ok();
|
db.set_busy_timeout(INDEX_LOCK_TIMEOUT).ok();
|
||||||
db
|
db
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update_index(&self, db: &Connection) {
|
fn update_index(&self, db: &Connection) {
|
||||||
let start = time::Instant::now();
|
let start = time::Instant::now();
|
||||||
println!("Beginning library index update");
|
println!("Beginning library index update");
|
||||||
self.clean(db);
|
self.clean(db);
|
||||||
self.populate(db);
|
self.populate(db);
|
||||||
println!("Library index update took {} seconds", start.elapsed().as_secs());
|
println!("Library index update took {} seconds",
|
||||||
}
|
start.elapsed().as_secs());
|
||||||
|
}
|
||||||
|
|
||||||
fn clean(&self, db: &Connection) {
|
fn clean(&self, db: &Connection) {
|
||||||
{
|
{
|
||||||
let mut select = db.prepare("SELECT path FROM songs").unwrap();
|
let mut select = db.prepare("SELECT path FROM songs").unwrap();
|
||||||
let mut delete = db.prepare("DELETE FROM songs WHERE path = ?").unwrap();
|
let mut delete = db.prepare("DELETE FROM songs WHERE path = ?").unwrap();
|
||||||
while let State::Row = select.next().unwrap() {
|
while let State::Row = select.next().unwrap() {
|
||||||
let path_string : String = select.read(0).unwrap();
|
let path_string: String = select.read(0).unwrap();
|
||||||
let path = Path::new(path_string.as_str());
|
let path = Path::new(path_string.as_str());
|
||||||
if !path.exists() || self.vfs.real_to_virtual(path).is_err() {
|
if !path.exists() || self.vfs.real_to_virtual(path).is_err() {
|
||||||
delete.reset().ok();
|
delete.reset().ok();
|
||||||
delete.bind(1, &Value::String(path_string.to_owned())).ok();
|
delete.bind(1, &Value::String(path_string.to_owned())).ok();
|
||||||
delete.next().ok();
|
delete.next().ok();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
let mut select = db.prepare("SELECT path FROM directories").unwrap();
|
let mut select = db.prepare("SELECT path FROM directories").unwrap();
|
||||||
let mut delete = db.prepare("DELETE FROM directories WHERE path = ?").unwrap();
|
let mut delete = db.prepare("DELETE FROM directories WHERE path = ?").unwrap();
|
||||||
while let State::Row = select.next().unwrap() {
|
while let State::Row = select.next().unwrap() {
|
||||||
let path_string : String = select.read(0).unwrap();
|
let path_string: String = select.read(0).unwrap();
|
||||||
let path = Path::new(path_string.as_str());
|
let path = Path::new(path_string.as_str());
|
||||||
if !path.exists() || self.vfs.real_to_virtual(path).is_err() {
|
if !path.exists() || self.vfs.real_to_virtual(path).is_err() {
|
||||||
delete.reset().ok();
|
delete.reset().ok();
|
||||||
delete.bind(1, &Value::String(path_string.to_owned())).ok();
|
delete.bind(1, &Value::String(path_string.to_owned())).ok();
|
||||||
delete.next().ok();
|
delete.next().ok();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn populate(&self, db: &Connection) {
|
fn populate(&self, db: &Connection) {
|
||||||
let vfs = self.vfs.deref();
|
let vfs = self.vfs.deref();
|
||||||
let mount_points = vfs.get_mount_points();
|
let mount_points = vfs.get_mount_points();
|
||||||
let mut builder = IndexBuilder::new(db);
|
let mut builder = IndexBuilder::new(db);
|
||||||
for (_, target) in mount_points {
|
for (_, target) in mount_points {
|
||||||
self.populate_directory(&mut builder, target.as_path());
|
self.populate_directory(&mut builder, target.as_path());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_artwork(&self, dir: &Path) -> Option<String> {
|
fn get_artwork(&self, dir: &Path) -> Option<String> {
|
||||||
let pattern = match self.album_art_pattern {
|
let pattern = match self.album_art_pattern {
|
||||||
Some(ref p) => p,
|
Some(ref p) => p,
|
||||||
_ => return None,
|
_ => return None,
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Ok(dir_content) = fs::read_dir(dir) {
|
if let Ok(dir_content) = fs::read_dir(dir) {
|
||||||
for file in dir_content {
|
for file in dir_content {
|
||||||
if let Ok(file) = file {
|
if let Ok(file) = file {
|
||||||
if let Some(name_string) = file.file_name().to_str() {
|
if let Some(name_string) = file.file_name().to_str() {
|
||||||
if pattern.is_match(name_string) {
|
if pattern.is_match(name_string) {
|
||||||
return file.path().to_str().map(|p| p.to_owned());
|
return file.path().to_str().map(|p| p.to_owned());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
fn populate_directory(&self, builder: &mut IndexBuilder, path: &Path) {
|
fn populate_directory(&self, builder: &mut IndexBuilder, path: &Path) {
|
||||||
|
|
||||||
// Find artwork
|
|
||||||
let artwork = self.get_artwork(path);
|
|
||||||
|
|
||||||
let path_string = match path.to_str() {
|
// Find artwork
|
||||||
Some(p) => p,
|
let artwork = self.get_artwork(path);
|
||||||
_ => return,
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut directory_album = None;
|
|
||||||
let mut directory_year = None;
|
|
||||||
let mut directory_artist = None;
|
|
||||||
let mut inconsistent_directory_album = false;
|
|
||||||
let mut inconsistent_directory_year = false;
|
|
||||||
let mut inconsistent_directory_artist = false;
|
|
||||||
|
|
||||||
// Insert content
|
let path_string = match path.to_str() {
|
||||||
if let Ok(dir_content) = fs::read_dir(path) {
|
Some(p) => p,
|
||||||
for file in dir_content {
|
_ => return,
|
||||||
let file_path = match file {
|
};
|
||||||
Ok(f) => f.path(),
|
|
||||||
_ => continue,
|
|
||||||
};
|
|
||||||
|
|
||||||
if file_path.is_dir() {
|
let mut directory_album = None;
|
||||||
self.populate_directory(builder, file_path.as_path());
|
let mut directory_year = None;
|
||||||
} else {
|
let mut directory_artist = None;
|
||||||
if let Some(file_path_string) = file_path.to_str() {
|
let mut inconsistent_directory_album = false;
|
||||||
if let Ok(tags) = SongTags::read(file_path.as_path()) {
|
let mut inconsistent_directory_year = false;
|
||||||
if tags.year.is_some() {
|
let mut inconsistent_directory_artist = false;
|
||||||
inconsistent_directory_year |= directory_year.is_some() && directory_year != tags.year;
|
|
||||||
directory_year = tags.year;
|
|
||||||
}
|
|
||||||
|
|
||||||
if tags.album.is_some() {
|
// Insert content
|
||||||
inconsistent_directory_album |= directory_album.is_some() && directory_album != tags.album;
|
if let Ok(dir_content) = fs::read_dir(path) {
|
||||||
directory_album = Some(tags.album.as_ref().unwrap().clone());
|
for file in dir_content {
|
||||||
}
|
let file_path = match file {
|
||||||
|
Ok(f) => f.path(),
|
||||||
|
_ => continue,
|
||||||
|
};
|
||||||
|
|
||||||
if tags.album_artist.is_some() {
|
if file_path.is_dir() {
|
||||||
inconsistent_directory_artist |= directory_artist.is_some() && directory_artist != tags.album_artist;
|
self.populate_directory(builder, file_path.as_path());
|
||||||
directory_artist = Some(tags.album_artist.as_ref().unwrap().clone());
|
} else {
|
||||||
} else if tags.artist.is_some() {
|
if let Some(file_path_string) = file_path.to_str() {
|
||||||
inconsistent_directory_artist |= directory_artist.is_some() && directory_artist != tags.artist;
|
if let Ok(tags) = SongTags::read(file_path.as_path()) {
|
||||||
directory_artist = Some(tags.artist.as_ref().unwrap().clone());
|
if tags.year.is_some() {
|
||||||
}
|
inconsistent_directory_year |= directory_year.is_some() &&
|
||||||
|
directory_year != tags.year;
|
||||||
|
directory_year = tags.year;
|
||||||
|
}
|
||||||
|
|
||||||
let song = Song {
|
if tags.album.is_some() {
|
||||||
path: file_path_string.to_owned(),
|
inconsistent_directory_album |= directory_album.is_some() &&
|
||||||
track_number: tags.track_number,
|
directory_album != tags.album;
|
||||||
title: tags.title,
|
directory_album = Some(tags.album.as_ref().unwrap().clone());
|
||||||
artist: tags.artist,
|
}
|
||||||
album_artist: tags.album_artist,
|
|
||||||
album: tags.album,
|
|
||||||
year: tags.year,
|
|
||||||
artwork: artwork.as_ref().map(|s| s.to_owned()),
|
|
||||||
};
|
|
||||||
|
|
||||||
builder.push(CollectionFile::Song(song));
|
if tags.album_artist.is_some() {
|
||||||
}
|
inconsistent_directory_artist |= directory_artist.is_some() &&
|
||||||
}
|
directory_artist !=
|
||||||
}
|
tags.album_artist;
|
||||||
}
|
directory_artist =
|
||||||
}
|
Some(tags.album_artist.as_ref().unwrap().clone());
|
||||||
|
} else if tags.artist.is_some() {
|
||||||
|
inconsistent_directory_artist |= directory_artist.is_some() &&
|
||||||
|
directory_artist != tags.artist;
|
||||||
|
directory_artist = Some(tags.artist.as_ref().unwrap().clone());
|
||||||
|
}
|
||||||
|
|
||||||
// Insert directory
|
let song = Song {
|
||||||
if inconsistent_directory_year {
|
path: file_path_string.to_owned(),
|
||||||
directory_year = None;
|
track_number: tags.track_number,
|
||||||
}
|
title: tags.title,
|
||||||
if inconsistent_directory_album {
|
artist: tags.artist,
|
||||||
directory_album = None;
|
album_artist: tags.album_artist,
|
||||||
}
|
album: tags.album,
|
||||||
if inconsistent_directory_artist {
|
year: tags.year,
|
||||||
directory_artist = None;
|
artwork: artwork.as_ref().map(|s| s.to_owned()),
|
||||||
}
|
};
|
||||||
|
|
||||||
let directory = Directory {
|
builder.push(CollectionFile::Song(song));
|
||||||
path: path_string.to_owned(),
|
}
|
||||||
artwork: artwork,
|
}
|
||||||
album: directory_album,
|
}
|
||||||
artist: directory_artist,
|
}
|
||||||
year: directory_year,
|
}
|
||||||
};
|
|
||||||
builder.push(CollectionFile::Directory(directory));
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn run(&self)
|
// Insert directory
|
||||||
{
|
if inconsistent_directory_year {
|
||||||
loop {
|
directory_year = None;
|
||||||
{
|
}
|
||||||
let db = self.connect();
|
if inconsistent_directory_album {
|
||||||
self.update_index(&db);
|
directory_album = None;
|
||||||
}
|
}
|
||||||
thread::sleep(time::Duration::from_secs(60 * 20)); // TODO expose in configuration
|
if inconsistent_directory_artist {
|
||||||
}
|
directory_artist = None;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn select_songs(&self, select: &mut Statement) -> Vec<Song> {
|
let directory = Directory {
|
||||||
|
path: path_string.to_owned(),
|
||||||
|
artwork: artwork,
|
||||||
|
album: directory_album,
|
||||||
|
artist: directory_artist,
|
||||||
|
year: directory_year,
|
||||||
|
};
|
||||||
|
builder.push(CollectionFile::Directory(directory));
|
||||||
|
}
|
||||||
|
|
||||||
let mut output = Vec::new();
|
pub fn run(&self) {
|
||||||
|
loop {
|
||||||
|
{
|
||||||
|
let db = self.connect();
|
||||||
|
self.update_index(&db);
|
||||||
|
}
|
||||||
|
thread::sleep(time::Duration::from_secs(60 * 20)); // TODO expose in configuration
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
while let State::Row = select.next().unwrap() {
|
fn select_songs(&self, select: &mut Statement) -> Vec<Song> {
|
||||||
|
|
||||||
let song_path : String = select.read(0).unwrap();
|
let mut output = Vec::new();
|
||||||
let track_number : Value = select.read(1).unwrap();
|
|
||||||
let title : Value = select.read(2).unwrap();
|
|
||||||
let year : Value = select.read(3).unwrap();
|
|
||||||
let album_artist : Value = select.read(4).unwrap();
|
|
||||||
let artist : Value = select.read(5).unwrap();
|
|
||||||
let album : Value = select.read(6).unwrap();
|
|
||||||
let artwork : Value = select.read(7).unwrap();
|
|
||||||
|
|
||||||
let song_path = Path::new(song_path.as_str());
|
while let State::Row = select.next().unwrap() {
|
||||||
let song_path = match self.vfs.real_to_virtual(song_path) {
|
|
||||||
Ok(p) => p,
|
|
||||||
_ => continue,
|
|
||||||
};
|
|
||||||
|
|
||||||
let artwork = artwork.as_string().map(|p| Path::new(p)).and_then(|p| self.vfs.real_to_virtual(p).ok());
|
let song_path: String = select.read(0).unwrap();
|
||||||
|
let track_number: Value = select.read(1).unwrap();
|
||||||
|
let title: Value = select.read(2).unwrap();
|
||||||
|
let year: Value = select.read(3).unwrap();
|
||||||
|
let album_artist: Value = select.read(4).unwrap();
|
||||||
|
let artist: Value = select.read(5).unwrap();
|
||||||
|
let album: Value = select.read(6).unwrap();
|
||||||
|
let artwork: Value = select.read(7).unwrap();
|
||||||
|
|
||||||
let song = Song {
|
let song_path = Path::new(song_path.as_str());
|
||||||
path: song_path.to_str().unwrap().to_owned(),
|
let song_path = match self.vfs.real_to_virtual(song_path) {
|
||||||
track_number: track_number.as_integer().map(|n| n as u32),
|
Ok(p) => p,
|
||||||
title: title.as_string().map(|s| s.to_owned()),
|
_ => continue,
|
||||||
year: year.as_integer().map(|n| n as i32),
|
};
|
||||||
album_artist: album_artist.as_string().map(|s| s.to_owned()),
|
|
||||||
artist: artist.as_string().map(|s| s.to_owned()),
|
|
||||||
album: album.as_string().map(|s| s.to_owned()),
|
|
||||||
artwork: artwork.map(|p| p.to_str().unwrap().to_owned() ),
|
|
||||||
};
|
|
||||||
output.push(song);
|
|
||||||
}
|
|
||||||
|
|
||||||
output
|
let artwork = artwork.as_string()
|
||||||
}
|
.map(|p| Path::new(p))
|
||||||
|
.and_then(|p| self.vfs.real_to_virtual(p).ok());
|
||||||
|
|
||||||
// List sub-directories within a directory
|
let song = Song {
|
||||||
fn browse_directories(&self, real_path: &Path) -> Vec<CollectionFile> {
|
path: song_path.to_str().unwrap().to_owned(),
|
||||||
let db = self.connect();
|
track_number: track_number.as_integer().map(|n| n as u32),
|
||||||
let mut output = Vec::new();
|
title: title.as_string().map(|s| s.to_owned()),
|
||||||
|
year: year.as_integer().map(|n| n as i32),
|
||||||
|
album_artist: album_artist.as_string().map(|s| s.to_owned()),
|
||||||
|
artist: artist.as_string().map(|s| s.to_owned()),
|
||||||
|
album: album.as_string().map(|s| s.to_owned()),
|
||||||
|
artwork: artwork.map(|p| p.to_str().unwrap().to_owned()),
|
||||||
|
};
|
||||||
|
output.push(song);
|
||||||
|
}
|
||||||
|
|
||||||
let path_string = real_path.to_string_lossy();
|
output
|
||||||
let mut select = db.prepare("SELECT path, artwork, year, artist, album FROM directories WHERE parent = ? ORDER BY path COLLATE NOCASE ASC").unwrap();
|
}
|
||||||
select.bind(1, &Value::String(path_string.deref().to_owned())).unwrap();
|
|
||||||
|
|
||||||
while let State::Row = select.next().unwrap() {
|
// List sub-directories within a directory
|
||||||
|
fn browse_directories(&self, real_path: &Path) -> Vec<CollectionFile> {
|
||||||
|
let db = self.connect();
|
||||||
|
let mut output = Vec::new();
|
||||||
|
|
||||||
let directory_value : String = select.read(0).unwrap();
|
let path_string = real_path.to_string_lossy();
|
||||||
let artwork_path : Value = select.read(1).unwrap();
|
let mut select =
|
||||||
let year : Value = select.read(2).unwrap();
|
db.prepare("SELECT path, artwork, year, artist, album FROM directories WHERE \
|
||||||
let artist : Value = select.read(3).unwrap();
|
parent = ? ORDER BY path COLLATE NOCASE ASC")
|
||||||
let album : Value = select.read(4).unwrap();
|
.unwrap();
|
||||||
|
select.bind(1, &Value::String(path_string.deref().to_owned())).unwrap();
|
||||||
|
|
||||||
let directory_path = Path::new(directory_value.as_str());
|
while let State::Row = select.next().unwrap() {
|
||||||
let directory_path = match self.vfs.real_to_virtual(directory_path) {
|
|
||||||
Ok(p) => p,
|
|
||||||
_ => continue,
|
|
||||||
};
|
|
||||||
|
|
||||||
let artwork_path = artwork_path.as_string()
|
let directory_value: String = select.read(0).unwrap();
|
||||||
.map(|p| Path::new(p))
|
let artwork_path: Value = select.read(1).unwrap();
|
||||||
.and_then(|p| self.vfs.real_to_virtual(p).ok());
|
let year: Value = select.read(2).unwrap();
|
||||||
|
let artist: Value = select.read(3).unwrap();
|
||||||
|
let album: Value = select.read(4).unwrap();
|
||||||
|
|
||||||
let directory = Directory {
|
let directory_path = Path::new(directory_value.as_str());
|
||||||
path: directory_path.to_str().unwrap().to_owned(),
|
let directory_path = match self.vfs.real_to_virtual(directory_path) {
|
||||||
artwork: artwork_path.map(|p| p.to_str().unwrap().to_owned() ),
|
Ok(p) => p,
|
||||||
year: year.as_integer().map(|n| n as i32),
|
_ => continue,
|
||||||
artist: artist.as_string().map(|s| s.to_owned()),
|
};
|
||||||
album: album.as_string().map(|s| s.to_owned()),
|
|
||||||
};
|
|
||||||
output.push(CollectionFile::Directory(directory));
|
|
||||||
}
|
|
||||||
|
|
||||||
output
|
let artwork_path = artwork_path.as_string()
|
||||||
}
|
.map(|p| Path::new(p))
|
||||||
|
.and_then(|p| self.vfs.real_to_virtual(p).ok());
|
||||||
|
|
||||||
// List songs within a directory
|
let directory = Directory {
|
||||||
fn browse_songs(&self, real_path: &Path) -> Vec<CollectionFile> {
|
path: directory_path.to_str().unwrap().to_owned(),
|
||||||
let db = self.connect();
|
artwork: artwork_path.map(|p| p.to_str().unwrap().to_owned()),
|
||||||
let path_string = real_path.to_string_lossy();
|
year: year.as_integer().map(|n| n as i32),
|
||||||
let mut select = db.prepare("SELECT path, track_number, title, year, album_artist, artist, album, artwork FROM songs WHERE parent = ? ORDER BY path COLLATE NOCASE ASC").unwrap();
|
artist: artist.as_string().map(|s| s.to_owned()),
|
||||||
select.bind(1, &Value::String(path_string.deref().to_owned())).unwrap();
|
album: album.as_string().map(|s| s.to_owned()),
|
||||||
self.select_songs(&mut select).into_iter().map(|s| CollectionFile::Song(s)).collect()
|
};
|
||||||
}
|
output.push(CollectionFile::Directory(directory));
|
||||||
|
}
|
||||||
|
|
||||||
pub fn browse(&self, virtual_path: &Path) -> Result<Vec<CollectionFile>, PError> {
|
output
|
||||||
|
}
|
||||||
|
|
||||||
let mut output = Vec::new();
|
// List songs within a directory
|
||||||
|
fn browse_songs(&self, real_path: &Path) -> Vec<CollectionFile> {
|
||||||
|
let db = self.connect();
|
||||||
|
let path_string = real_path.to_string_lossy();
|
||||||
|
let mut select =
|
||||||
|
db.prepare("SELECT path, track_number, title, year, album_artist, artist, album, \
|
||||||
|
artwork FROM songs WHERE parent = ? ORDER BY path COLLATE NOCASE ASC")
|
||||||
|
.unwrap();
|
||||||
|
select.bind(1, &Value::String(path_string.deref().to_owned())).unwrap();
|
||||||
|
self.select_songs(&mut select).into_iter().map(|s| CollectionFile::Song(s)).collect()
|
||||||
|
}
|
||||||
|
|
||||||
// Browse top-level
|
pub fn browse(&self, virtual_path: &Path) -> Result<Vec<CollectionFile>, PError> {
|
||||||
if virtual_path.components().count() == 0 {
|
|
||||||
for (n, _) in self.vfs.get_mount_points() {
|
|
||||||
let directory = Directory {
|
|
||||||
path: n.to_owned(),
|
|
||||||
artwork: None,
|
|
||||||
year: None,
|
|
||||||
artist: None,
|
|
||||||
album: None,
|
|
||||||
};
|
|
||||||
output.push(CollectionFile::Directory(directory));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Browse sub-directory
|
let mut output = Vec::new();
|
||||||
} else {
|
|
||||||
let real_path = try!(self.vfs.virtual_to_real(virtual_path));
|
|
||||||
let directories = self.browse_directories(real_path.as_path());
|
|
||||||
let songs = self.browse_songs(real_path.as_path());
|
|
||||||
output.extend(directories);
|
|
||||||
output.extend(songs);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(output)
|
// Browse top-level
|
||||||
}
|
if virtual_path.components().count() == 0 {
|
||||||
|
for (n, _) in self.vfs.get_mount_points() {
|
||||||
|
let directory = Directory {
|
||||||
|
path: n.to_owned(),
|
||||||
|
artwork: None,
|
||||||
|
year: None,
|
||||||
|
artist: None,
|
||||||
|
album: None,
|
||||||
|
};
|
||||||
|
output.push(CollectionFile::Directory(directory));
|
||||||
|
}
|
||||||
|
|
||||||
pub fn flatten(&self, virtual_path: &Path) -> Result<Vec<Song>, PError> {
|
// Browse sub-directory
|
||||||
let db = self.connect();
|
} else {
|
||||||
let real_path = try!(self.vfs.virtual_to_real(virtual_path));
|
let real_path = try!(self.vfs.virtual_to_real(virtual_path));
|
||||||
let path_string = real_path.to_string_lossy().into_owned() + "%";
|
let directories = self.browse_directories(real_path.as_path());
|
||||||
let mut select = db.prepare("SELECT path, track_number, title, year, album_artist, artist, album, artwork FROM songs WHERE path LIKE ? ORDER BY path COLLATE NOCASE ASC").unwrap();
|
let songs = self.browse_songs(real_path.as_path());
|
||||||
select.bind(1, &Value::String(path_string.deref().to_owned())).unwrap();
|
output.extend(directories);
|
||||||
Ok(self.select_songs(&mut select))
|
output.extend(songs);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Ok(output)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn flatten(&self, virtual_path: &Path) -> Result<Vec<Song>, PError> {
|
||||||
|
let db = self.connect();
|
||||||
|
let real_path = try!(self.vfs.virtual_to_real(virtual_path));
|
||||||
|
let path_string = real_path.to_string_lossy().into_owned() + "%";
|
||||||
|
let mut select =
|
||||||
|
db.prepare("SELECT path, track_number, title, year, album_artist, artist, album, \
|
||||||
|
artwork FROM songs WHERE path LIKE ? ORDER BY path COLLATE NOCASE ASC")
|
||||||
|
.unwrap();
|
||||||
|
select.bind(1, &Value::String(path_string.deref().to_owned())).unwrap();
|
||||||
|
Ok(self.select_songs(&mut select))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -70,7 +70,8 @@ fn main() {
|
||||||
// Init index
|
// Init index
|
||||||
println!("Starting up index");
|
println!("Starting up index");
|
||||||
let index_path = path::Path::new(INDEX_FILE_NAME);
|
let index_path = path::Path::new(INDEX_FILE_NAME);
|
||||||
let index = Arc::new(index::Index::new(&index_path, vfs.clone(), &config.album_art_pattern).unwrap());
|
let index = Arc::new(index::Index::new(&index_path, vfs.clone(), &config.album_art_pattern)
|
||||||
|
.unwrap());
|
||||||
let index_ref = index.clone();
|
let index_ref = index.clone();
|
||||||
std::thread::spawn(move || index_ref.run());
|
std::thread::spawn(move || index_ref.run());
|
||||||
|
|
||||||
|
@ -104,8 +105,8 @@ fn main() {
|
||||||
std::thread::spawn(|| {
|
std::thread::spawn(|| {
|
||||||
ddns::run(ddns_config);
|
ddns::run(ddns_config);
|
||||||
});
|
});
|
||||||
},
|
}
|
||||||
None => (),
|
None => (),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Run UI
|
// Run UI
|
||||||
|
|
|
@ -14,42 +14,49 @@ use error::*;
|
||||||
const THUMBNAILS_PATH: &'static str = "tmp/thumbnails";
|
const THUMBNAILS_PATH: &'static str = "tmp/thumbnails";
|
||||||
|
|
||||||
fn hash(path: &Path, dimension: u32) -> u64 {
|
fn hash(path: &Path, dimension: u32) -> u64 {
|
||||||
let path_string = path.to_string_lossy();
|
let path_string = path.to_string_lossy();
|
||||||
let hash_input = format!("{}:{}", path_string, dimension.to_string());
|
let hash_input = format!("{}:{}", path_string, dimension.to_string());
|
||||||
let mut hasher = DefaultHasher::new();
|
let mut hasher = DefaultHasher::new();
|
||||||
hash_input.hash(&mut hasher);
|
hash_input.hash(&mut hasher);
|
||||||
hasher.finish()
|
hasher.finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_thumbnail(real_path: &Path, max_dimension: u32) -> Result<PathBuf, PError> {
|
pub fn get_thumbnail(real_path: &Path, max_dimension: u32) -> Result<PathBuf, PError> {
|
||||||
|
|
||||||
let mut out_path = PathBuf::new();
|
let mut out_path = PathBuf::new();
|
||||||
out_path.push(THUMBNAILS_PATH);
|
out_path.push(THUMBNAILS_PATH);
|
||||||
let mut dir_builder = DirBuilder::new();
|
let mut dir_builder = DirBuilder::new();
|
||||||
dir_builder.recursive(true);
|
dir_builder.recursive(true);
|
||||||
try!(dir_builder.create(out_path.as_path()));
|
try!(dir_builder.create(out_path.as_path()));
|
||||||
|
|
||||||
let source_image = try!(image::open(real_path));
|
let source_image = try!(image::open(real_path));
|
||||||
let (source_width, source_height) = source_image.dimensions();
|
let (source_width, source_height) = source_image.dimensions();
|
||||||
let cropped_dimension = cmp::max(source_width, source_height);
|
let cropped_dimension = cmp::max(source_width, source_height);
|
||||||
let out_dimension = cmp::min(max_dimension, cropped_dimension);
|
let out_dimension = cmp::min(max_dimension, cropped_dimension);
|
||||||
|
|
||||||
let hash = hash(real_path, out_dimension);
|
let hash = hash(real_path, out_dimension);
|
||||||
out_path.push(format!("{}.png", hash.to_string()));
|
out_path.push(format!("{}.png", hash.to_string()));
|
||||||
|
|
||||||
if !out_path.exists() {
|
if !out_path.exists() {
|
||||||
let source_aspect_ratio : f32 = source_width as f32 / source_height as f32;
|
let source_aspect_ratio: f32 = source_width as f32 / source_height as f32;
|
||||||
if source_aspect_ratio < 0.8 || source_aspect_ratio > 1.2 {
|
if source_aspect_ratio < 0.8 || source_aspect_ratio > 1.2 {
|
||||||
let mut cropped_image = ImageBuffer::new(cropped_dimension, cropped_dimension);
|
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);
|
cropped_image.copy_from(&source_image,
|
||||||
let out_image = resize(&cropped_image, out_dimension, out_dimension, FilterType::Lanczos3);
|
(cropped_dimension - source_width) / 2,
|
||||||
try!(out_image.save(out_path.as_path()));
|
(cropped_dimension - source_height) / 2);
|
||||||
} else {
|
let out_image = resize(&cropped_image,
|
||||||
let out_image = resize(&source_image, out_dimension, out_dimension, FilterType::Lanczos3);
|
out_dimension,
|
||||||
try!(out_image.save(out_path.as_path()));
|
out_dimension,
|
||||||
}
|
FilterType::Lanczos3);
|
||||||
}
|
try!(out_image.save(out_path.as_path()));
|
||||||
|
} else {
|
||||||
|
let out_image = resize(&source_image,
|
||||||
|
out_dimension,
|
||||||
|
out_dimension,
|
||||||
|
FilterType::Lanczos3);
|
||||||
|
try!(out_image.save(out_path.as_path()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Ok(out_path)
|
Ok(out_path)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
78
src/utils.rs
78
src/utils.rs
|
@ -1,51 +1,51 @@
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
pub enum AudioFormat {
|
pub enum AudioFormat {
|
||||||
FLAC,
|
FLAC,
|
||||||
MP3,
|
MP3,
|
||||||
MP4,
|
MP4,
|
||||||
MPC,
|
MPC,
|
||||||
OGG,
|
OGG,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_audio_format(path: &Path) -> Option<AudioFormat> {
|
pub fn get_audio_format(path: &Path) -> Option<AudioFormat> {
|
||||||
let extension = match path.extension() {
|
let extension = match path.extension() {
|
||||||
Some(e) => e,
|
Some(e) => e,
|
||||||
_ => return None,
|
_ => return None,
|
||||||
};
|
};
|
||||||
let extension = match extension.to_str() {
|
let extension = match extension.to_str() {
|
||||||
Some(e) => e,
|
Some(e) => e,
|
||||||
_ => return None,
|
_ => return None,
|
||||||
};
|
};
|
||||||
match extension.to_lowercase().as_str() {
|
match extension.to_lowercase().as_str() {
|
||||||
"flac" => Some(AudioFormat::FLAC),
|
"flac" => Some(AudioFormat::FLAC),
|
||||||
"mp3" => Some(AudioFormat::MP3),
|
"mp3" => Some(AudioFormat::MP3),
|
||||||
"m4a" => Some(AudioFormat::MP4),
|
"m4a" => Some(AudioFormat::MP4),
|
||||||
"mpc" => Some(AudioFormat::MPC),
|
"mpc" => Some(AudioFormat::MPC),
|
||||||
"ogg" => Some(AudioFormat::OGG),
|
"ogg" => Some(AudioFormat::OGG),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_song(path: &Path) -> bool {
|
pub fn is_song(path: &Path) -> bool {
|
||||||
get_audio_format(path).is_some()
|
get_audio_format(path).is_some()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_image(path: &Path) -> bool {
|
pub fn is_image(path: &Path) -> bool {
|
||||||
let extension = match path.extension() {
|
let extension = match path.extension() {
|
||||||
Some(e) => e,
|
Some(e) => e,
|
||||||
_ => return false,
|
_ => return false,
|
||||||
};
|
};
|
||||||
let extension = match extension.to_str() {
|
let extension = match extension.to_str() {
|
||||||
Some(e) => e,
|
Some(e) => e,
|
||||||
_ => return false,
|
_ => return false,
|
||||||
};
|
};
|
||||||
match extension.to_lowercase().as_str() {
|
match extension.to_lowercase().as_str() {
|
||||||
"png" => true,
|
"png" => true,
|
||||||
"gif" => true,
|
"gif" => true,
|
||||||
"jpg" => true,
|
"jpg" => true,
|
||||||
"jpeg" => true,
|
"jpeg" => true,
|
||||||
"bmp" => true,
|
"bmp" => true,
|
||||||
_ => false,
|
_ => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
18
src/vfs.rs
18
src/vfs.rs
|
@ -6,14 +6,12 @@ use error::*;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct VfsConfig {
|
pub struct VfsConfig {
|
||||||
pub mount_points: HashMap<String, PathBuf>,
|
pub mount_points: HashMap<String, PathBuf>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl VfsConfig {
|
impl VfsConfig {
|
||||||
pub fn new() -> VfsConfig {
|
pub fn new() -> VfsConfig {
|
||||||
VfsConfig {
|
VfsConfig { mount_points: HashMap::new() }
|
||||||
mount_points: HashMap::new(),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -43,11 +41,13 @@ impl Vfs {
|
||||||
for (name, target) in &self.mount_points {
|
for (name, target) in &self.mount_points {
|
||||||
let mount_path = Path::new(&name);
|
let mount_path = Path::new(&name);
|
||||||
match virtual_path.strip_prefix(mount_path) {
|
match virtual_path.strip_prefix(mount_path) {
|
||||||
Ok(p) => return if p.components().count() == 0 {
|
Ok(p) => {
|
||||||
Ok(target.clone())
|
return if p.components().count() == 0 {
|
||||||
} else {
|
Ok(target.clone())
|
||||||
Ok(target.join(p))
|
} else {
|
||||||
},
|
Ok(target.join(p))
|
||||||
|
}
|
||||||
|
}
|
||||||
Err(_) => (),
|
Err(_) => (),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue