Implemented /browse

This commit is contained in:
Antoine Gersant 2016-10-30 17:00:09 -07:00
parent eae6e10ecd
commit 6bf65ac45a
3 changed files with 165 additions and 205 deletions

View file

@ -85,7 +85,7 @@ pub fn get_api_handler(collection: Arc<Collection>) -> Mount {
}
fn path_from_request(request: &Request) -> Result<PathBuf, Utf8Error> {
let path_string = request.url.path().join("/");
let path_string = request.url.path().join("\\");
let decoded_path = try!(percent_decode(path_string.as_bytes()).decode_utf8());
Ok(PathBuf::from(decoded_path.deref()))
}

View file

@ -1,31 +1,13 @@
use std::fs;
use core::ops::Deref;
use std::path::Path;
use std::path::PathBuf;
use regex::Regex;
use std::sync::Arc;
use config::Config;
use index::Index;
use index::*;
use vfs::*;
use error::*;
use utils::*;
#[derive(Debug, RustcEncodable)]
pub struct Album {
title: Option<String>,
year: Option<i32>,
album_art: Option<String>,
artist: Option<String>,
}
#[derive(Debug, RustcEncodable)]
pub struct Song {
path: String,
album: Album,
track_number: Option<u32>,
title: Option<String>,
artist: Option<String>,
}
#[derive(Clone, Debug)]
pub struct User {
@ -42,83 +24,9 @@ impl User {
}
}
impl Album {
fn read(collection: &Collection, real_path: &Path) -> Result<Album, PError> {
let album_art = collection.get_album_art(real_path).unwrap_or(None);
let album_art = match album_art {
Some(p) => Some(try!(collection.vfs.real_to_virtual(p.as_path()))),
None => None,
};
let album_art = match album_art {
None => None,
Some(a) => a.to_str().map(|p| p.to_string()),
};
Ok(Album {
album_art: album_art,
title: None,
year: None,
artist: None,
})
}
}
impl Song {
fn read(collection: &Collection, path: &Path) -> Result<Song, PError> {
let virtual_path = try!(collection.vfs.real_to_virtual(path));
let path_string = try!(virtual_path.to_str().ok_or(PError::PathDecoding));
let album = try!(Album::read(collection, path));
Ok(Song {
path: path_string.to_string(),
album: album,
artist: None,
title: None,
track_number: None,
})
}
}
#[derive(Debug, RustcEncodable)]
pub struct Directory {
path: String,
name: String,
album: Album,
}
impl Directory {
fn read(collection: &Collection, path: &Path) -> Result<Directory, PError> {
let virtual_path = try!(collection.vfs.real_to_virtual(path));
let path_string = try!(virtual_path.to_str().ok_or(PError::PathDecoding));
let name = virtual_path.iter().last().unwrap();
let name = name.to_str().unwrap();
let name = name.to_string();
let album = try!(Album::read(collection, path));
Ok(Directory {
path: path_string.to_string(),
name: name,
album: album,
})
}
}
#[derive(Debug, RustcEncodable)]
pub enum CollectionFile {
Directory(Directory),
Song(Song),
}
pub struct Collection {
vfs: Arc<Vfs>,
users: Vec<User>,
album_art_pattern: Option<Regex>,
index: Arc<Index>,
}
@ -127,13 +35,11 @@ impl Collection {
Collection {
vfs: vfs,
users: Vec::new(),
album_art_pattern: None,
index: index,
}
}
pub fn load_config(&mut self, config: &Config) -> Result<(), PError> {
self.album_art_pattern = config.album_art_pattern.clone();
self.users = config.users.to_vec();
Ok(())
}
@ -142,98 +48,15 @@ impl Collection {
self.users.iter().any(|u| u.name == username && u.password == password)
}
pub fn browse(&self, path: &Path) -> Result<Vec<CollectionFile>, PError> {
let mut out = vec![];
if path.components().count() == 0 {
let mount_points = self.vfs.get_mount_points();
for (_, target) in mount_points {
let directory = try!(Directory::read(self, target.as_path()));
out.push(CollectionFile::Directory(directory));
}
} else {
let full_path = try!(self.vfs.virtual_to_real(path));
for file in try!(fs::read_dir(full_path)) {
let file = try!(file);
let file_meta = try!(file.metadata());
let file_path = file.path();
let file_path = file_path.as_path();
if file_meta.is_file() {
if is_song(file_path) {
let song = try!(Song::read(self, file_path));
out.push(CollectionFile::Song(song));
}
} else if file_meta.is_dir() {
let directory = try!(Directory::read(self, file_path));
out.push(CollectionFile::Directory(directory));
}
}
}
Ok(out)
}
fn flatten_internal(&self, path: &Path) -> Result<Vec<Song>, PError> {
let files = try!(fs::read_dir(path));
files.fold(Ok(vec![]), |acc, file| {
let mut acc = try!(acc);
let file: fs::DirEntry = try!(file);
let file_meta = try!(file.metadata());
let file_path = file.path();
let file_path = file_path.as_path();
if file_meta.is_file() {
if is_song(file_path) {
let song = try!(Song::read(self, file_path));
acc.push(song);
}
} else {
let mut explore_content = try!(self.flatten_internal(file_path));
acc.append(&mut explore_content);
}
Ok(acc)
})
pub fn browse(&self, virtual_path: &Path) -> Result<Vec<CollectionFile>, PError> {
self.index.deref().browse(virtual_path)
}
pub fn flatten(&self, path: &Path) -> Result<Vec<Song>, PError> {
let real_path = try!(self.vfs.virtual_to_real(path));
self.flatten_internal(real_path.as_path())
Err(PError::Unauthorized)
}
pub fn locate(&self, virtual_path: &Path) -> Result<PathBuf, PError> {
self.vfs.virtual_to_real(virtual_path)
}
fn get_album_art(&self, real_path: &Path) -> Result<Option<PathBuf>, PError> {
let pattern = match self.album_art_pattern {
Some(ref p) => p,
None => return Ok(None),
};
let mut real_dir = real_path;
if real_dir.is_file() {
real_dir = try!(real_dir.parent().ok_or(PError::AlbumArtSearchError));
}
assert!(real_dir.is_dir());
let mut files = try!(fs::read_dir(real_dir));
let album_art = files.find(|dir_entry| {
let file = match *dir_entry {
Err(_) => return false,
Ok(ref r) => r,
};
let file_name = file.file_name();
let file_name = match file_name.to_str() {
None => return false,
Some(r) => r,
};
pattern.is_match(file_name)
});
match album_art {
Some(Err(_)) => Err(PError::AlbumArtSearchError),
Some(Ok(a)) => Ok(Some(a.path())),
None => Ok(None),
}
}
}

View file

@ -51,6 +51,33 @@ impl SongTags {
}
}
#[derive(Debug, RustcEncodable)]
pub struct Song {
path: String,
track_number: Option<u32>,
title: Option<String>,
artist: Option<String>,
album_artist: Option<String>,
album: Option<String>,
year: Option<i32>,
artwork: Option<String>,
}
#[derive(Debug, RustcEncodable)]
pub struct Directory {
path: String,
artist: Option<String>,
album: Option<String>,
year: Option<i32>,
artwork: Option<String>,
}
#[derive(Debug, RustcEncodable)]
pub enum CollectionFile {
Directory(Directory),
Song(Song),
}
impl Index {
pub fn new(path: &Path, vfs: Arc<Vfs>, album_art_pattern: &Option<Regex>) -> Result<Index, PError> {
@ -76,7 +103,6 @@ impl Index {
let db = self.connect();
db.execute("PRAGMA synchronous = NORMAL").unwrap();
db.execute("PRAGMA journal_mode = WAL").unwrap();
db.execute("
CREATE TABLE version
@ -87,7 +113,8 @@ impl Index {
CREATE TABLE directories
( id INTEGER PRIMARY KEY NOT NULL
, path NOT NULL
, path TEXT NOT NULL
, parent TEXT
, artist TEXT
, year INTEGER
, album TEXT
@ -97,7 +124,8 @@ impl Index {
CREATE TABLE songs
( id INTEGER PRIMARY KEY NOT NULL
, path NOT NULL
, path TEXT NOT NULL
, parent TEXT NOT NULL
, track_number INTEGER
, title TEXT
, artist TEXT
@ -187,6 +215,11 @@ impl Index {
// Find artwork
let artwork = self.get_artwork(path).map_or(sqlite::Value::Null, |t| sqlite::Value::String(t));
let path_string = match path.to_str() {
Some(p) => p,
_ => return,
};
let mut directory_album = None;
let mut directory_year = None;
@ -197,13 +230,13 @@ impl Index {
// Prepare statements
let mut insert_directory = db.prepare("
INSERT OR REPLACE INTO directories (path, artwork, year, artist, album)
VALUES (?, ?, ?, ?, ?)
INSERT OR REPLACE INTO directories (path, parent, artwork, year, artist, album)
VALUES (?, ?, ?, ?, ?, ?)
").unwrap();
let mut insert_song = db.prepare("
INSERT OR REPLACE INTO songs (path, track_number, title, year, album_artist, artist, album, artwork)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
INSERT OR REPLACE INTO songs (path, parent, track_number, title, year, album_artist, artist, album, artwork)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
").unwrap();
// Insert content
@ -239,13 +272,14 @@ impl Index {
insert_song.reset().ok();
insert_song.bind(1, &sqlite::Value::String(file_path_string.to_owned())).unwrap();
insert_song.bind(2, &tags.track_number.map_or(sqlite::Value::Null, |t| sqlite::Value::Integer(t as i64))).unwrap();
insert_song.bind(3, &tags.title.map_or(sqlite::Value::Null, |t| sqlite::Value::String(t))).unwrap();
insert_song.bind(4, &tags.year.map_or(sqlite::Value::Null, |t| sqlite::Value::Integer(t as i64))).unwrap();
insert_song.bind(5, &tags.album_artist.map_or(sqlite::Value::Null, |t| sqlite::Value::String(t))).unwrap();
insert_song.bind(6, &tags.artist.map_or(sqlite::Value::Null, |t| sqlite::Value::String(t))).unwrap();
insert_song.bind(7, &tags.album.map_or(sqlite::Value::Null, |t| sqlite::Value::String(t))).unwrap();
insert_song.bind(8, &artwork).unwrap();
insert_song.bind(2, &sqlite::Value::String(path_string.to_owned())).unwrap();
insert_song.bind(3, &tags.track_number.map_or(sqlite::Value::Null, |t| sqlite::Value::Integer(t as i64))).unwrap();
insert_song.bind(4, &tags.title.map_or(sqlite::Value::Null, |t| sqlite::Value::String(t))).unwrap();
insert_song.bind(5, &tags.year.map_or(sqlite::Value::Null, |t| sqlite::Value::Integer(t as i64))).unwrap();
insert_song.bind(6, &tags.album_artist.map_or(sqlite::Value::Null, |t| sqlite::Value::String(t))).unwrap();
insert_song.bind(7, &tags.artist.map_or(sqlite::Value::Null, |t| sqlite::Value::String(t))).unwrap();
insert_song.bind(8, &tags.album.map_or(sqlite::Value::Null, |t| sqlite::Value::String(t))).unwrap();
insert_song.bind(9, &artwork).unwrap();
insert_song.next().ok();
}
}
@ -263,20 +297,28 @@ impl Index {
if inconsistent_directory_artist {
directory_artist = None;
}
if let Some(path_string) = path.to_str() {
insert_directory.reset().ok();
insert_directory.bind(1, &sqlite::Value::String(path_string.to_owned())).unwrap();
insert_directory.bind(2, &artwork).unwrap();
insert_directory.bind(3, &directory_year.map_or(sqlite::Value::Null, |t| sqlite::Value::Integer(t as i64))).unwrap();
insert_directory.bind(4, &directory_artist.map_or(sqlite::Value::Null, |t| sqlite::Value::String(t))).unwrap();
insert_directory.bind(5, &directory_album.map_or(sqlite::Value::Null, |t| sqlite::Value::String(t))).unwrap();
insert_directory.next().ok();
let mut parent : Option<String> = None;
if let Some(parent_dir) = path.parent() {
if let Some(parent_path) = parent_dir.to_str() {
parent = Some(parent_path.to_owned());
}
}
insert_directory.reset().ok();
insert_directory.bind(1, &sqlite::Value::String(path_string.to_owned())).unwrap();
insert_directory.bind(2, &parent.map_or(sqlite::Value::Null, |t| sqlite::Value::String(t.to_owned()))).unwrap();
insert_directory.bind(3, &artwork).unwrap();
insert_directory.bind(4, &directory_year.map_or(sqlite::Value::Null, |t| sqlite::Value::Integer(t as i64))).unwrap();
insert_directory.bind(5, &directory_artist.map_or(sqlite::Value::Null, |t| sqlite::Value::String(t))).unwrap();
insert_directory.bind(6, &directory_album.map_or(sqlite::Value::Null, |t| sqlite::Value::String(t))).unwrap();
insert_directory.next().ok();
}
pub fn run(&self)
{
loop {
// TODO fix uber-lock
let db = self.connect();
if let Err(e) = db.execute("BEGIN TRANSACTION") {
print!("Error while beginning transaction for index update: {}", e);
@ -289,4 +331,99 @@ impl Index {
thread::sleep(time::Duration::from_secs(60 * 20)); // TODO expose in configuration
}
}
// 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 path_string = real_path.to_string_lossy();
let mut cursor = db.prepare("SELECT path, artwork, year, artist, album FROM directories WHERE parent = ?").unwrap().cursor();
cursor.bind(&[sqlite::Value::String(path_string.deref().to_owned())]).unwrap();
while let Ok(Some(row)) = cursor.next() {
let directory_path = Path::new(row[0].as_string().unwrap());
let directory_path = match self.vfs.real_to_virtual(directory_path) {
Ok(p) => p,
_ => continue,
};
let artwork_path = row[1].as_string()
.map(|p| Path::new(p))
.and_then(|p| self.vfs.real_to_virtual(p).ok());
let directory = Directory {
path: directory_path.to_str().unwrap().to_owned(),
artwork: artwork_path.map(|p| p.to_str().unwrap().to_owned() ),
year: row[2].as_integer().map(|n| n as i32),
artist: row[3].as_string().map(|s| s.to_owned()),
album: row[4].as_string().map(|s| s.to_owned()),
};
output.push(CollectionFile::Directory(directory));
}
output
}
// List songs within a directory
fn browse_songs(&self, real_path: &Path) -> Vec<CollectionFile> {
let db = self.connect();
let mut output = Vec::new();
let path_string = real_path.to_string_lossy();
let mut cursor = db.prepare("SELECT path, track_number, title, year, album_artist, artist, album, artwork FROM songs WHERE parent = ?").unwrap().cursor();
cursor.bind(&[sqlite::Value::String(path_string.deref().to_owned())]).unwrap();
while let Some(row) = cursor.next().unwrap() {
let song_path = Path::new(row[0].as_string().unwrap());
let song_path = match self.vfs.real_to_virtual(song_path) {
Ok(p) => p,
_ => continue,
};
let artwork_path = row[7].as_string().map(|p| Path::new(p)).and_then(|p| self.vfs.real_to_virtual(p).ok());
let song = Song {
path: song_path.to_str().unwrap().to_owned(),
track_number: row[1].as_integer().map(|n| n as u32),
title: row[2].as_string().map(|s| s.to_owned()),
year: row[3].as_integer().map(|n| n as i32),
album_artist: row[4].as_string().map(|s| s.to_owned()),
artist: row[5].as_string().map(|s| s.to_owned()),
album: row[6].as_string().map(|s| s.to_owned()),
artwork: artwork_path.map(|p| p.to_str().unwrap().to_owned() ),
};
output.push(CollectionFile::Song(song));
}
output
}
pub fn browse(&self, virtual_path: &Path) -> Result<Vec<CollectionFile>, PError> {
let mut output = Vec::new();
// 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));
}
// Browse sub-directory
} 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)
}
}