mirror of
https://github.com/agersant/polaris
synced 2024-12-03 10:09:09 +00:00
Implemented /browse
This commit is contained in:
parent
eae6e10ecd
commit
6bf65ac45a
3 changed files with 165 additions and 205 deletions
|
@ -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()))
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
181
src/index.rs
181
src/index.rs
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue