2023-02-26 03:17:00 +00:00
|
|
|
use crate::audio::{TrackId, TrackInfo};
|
|
|
|
use std::{
|
|
|
|
fs::{DirEntry, ReadDir},
|
|
|
|
path::PathBuf,
|
|
|
|
};
|
|
|
|
use thiserror::Error;
|
|
|
|
|
|
|
|
#[derive(Debug, Error)]
|
|
|
|
pub enum ScannerError {
|
|
|
|
#[error("Cannot scan {0}")]
|
|
|
|
CannotScan(PathBuf),
|
|
|
|
#[error("Not found {0}")]
|
|
|
|
NotFound(PathBuf),
|
|
|
|
#[error("IO error {0}")]
|
|
|
|
IO(std::io::Error),
|
|
|
|
}
|
|
|
|
|
|
|
|
impl From<std::io::Error> for ScannerError {
|
|
|
|
fn from(err: std::io::Error) -> Self {
|
|
|
|
Self::IO(err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
pub trait MusicScanner: Send {
|
|
|
|
fn scan<'a>(&'a self) -> Box<dyn Iterator<Item = Result<TrackInfo, ScannerError>> + 'a>;
|
|
|
|
}
|
|
|
|
|
|
|
|
pub struct FileScanner {
|
|
|
|
roots: Vec<PathBuf>,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl FileScanner {
|
|
|
|
pub fn new(roots: Vec<PathBuf>) -> Self {
|
|
|
|
Self { roots }
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
pub struct FileIterator {
|
|
|
|
dirs: Vec<PathBuf>,
|
|
|
|
file_iter: Option<ReadDir>,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl FileIterator {
|
|
|
|
fn scan_file(&self, path: PathBuf) -> Result<TrackInfo, ScannerError> {
|
|
|
|
Ok(TrackInfo {
|
|
|
|
id: TrackId::from(path.to_str().unwrap().to_owned()),
|
|
|
|
album: None,
|
|
|
|
artist: None,
|
2023-03-01 14:20:58 +00:00
|
|
|
name: path
|
|
|
|
.file_stem()
|
|
|
|
.and_then(|s| s.to_str())
|
|
|
|
.map(|s| s.to_owned()),
|
2023-02-26 03:17:00 +00:00
|
|
|
track_number: None,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
enum EntryInfo {
|
|
|
|
Dir(PathBuf),
|
|
|
|
File(PathBuf),
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Iterator for FileIterator {
|
|
|
|
type Item = Result<TrackInfo, ScannerError>;
|
|
|
|
|
|
|
|
fn next(&mut self) -> Option<Self::Item> {
|
|
|
|
fn process_entry(entry: DirEntry) -> Result<EntryInfo, ScannerError> {
|
|
|
|
entry
|
|
|
|
.metadata()
|
|
|
|
.map_err(ScannerError::from)
|
|
|
|
.map(|metadata| {
|
|
|
|
if metadata.is_dir() {
|
|
|
|
EntryInfo::Dir(entry.path())
|
|
|
|
} else {
|
|
|
|
EntryInfo::File(entry.path())
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
let next_file = match &mut self.file_iter {
|
|
|
|
Some(iter) => iter.next(),
|
|
|
|
None => None,
|
|
|
|
};
|
|
|
|
|
|
|
|
match next_file {
|
|
|
|
Some(Ok(entry)) => match process_entry(entry) {
|
|
|
|
Ok(EntryInfo::Dir(path)) => {
|
|
|
|
self.dirs.push(path);
|
|
|
|
self.next()
|
|
|
|
}
|
|
|
|
Ok(EntryInfo::File(path)) => Some(self.scan_file(path)),
|
|
|
|
Err(err) => Some(Err(err)),
|
|
|
|
},
|
|
|
|
Some(Err(err)) => Some(Err(ScannerError::from(err))),
|
|
|
|
None => match self.dirs.pop() {
|
|
|
|
Some(dir) => match dir.read_dir() {
|
|
|
|
Ok(entry) => {
|
|
|
|
self.file_iter = Some(entry);
|
|
|
|
self.next()
|
|
|
|
}
|
|
|
|
Err(err) => {
|
|
|
|
if err.kind() == std::io::ErrorKind::NotFound {
|
|
|
|
Some(Err(ScannerError::NotFound(dir)))
|
|
|
|
} else {
|
|
|
|
Some(Err(ScannerError::from(err)))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
None => None,
|
|
|
|
},
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl MusicScanner for FileScanner {
|
|
|
|
fn scan<'a>(&'a self) -> Box<dyn Iterator<Item = Result<TrackInfo, ScannerError>> + 'a> {
|
|
|
|
Box::new(FileIterator {
|
|
|
|
dirs: self.roots.clone(),
|
|
|
|
file_iter: None,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
pub mod factories {
|
|
|
|
use super::*;
|
|
|
|
use crate::audio::TrackId;
|
|
|
|
|
|
|
|
pub struct MockScanner {
|
|
|
|
data: Vec<TrackInfo>,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl MockScanner {
|
|
|
|
pub fn new() -> Self {
|
|
|
|
Self {
|
|
|
|
data: vec![
|
|
|
|
TrackInfo {
|
|
|
|
id: TrackId::from("/home/savanni/Track 1.mp3".to_owned()),
|
|
|
|
track_number: Some(1),
|
|
|
|
name: Some("Track 1".to_owned()),
|
|
|
|
album: Some("Savanni's Demo".to_owned()),
|
|
|
|
artist: Some("Savanni".to_owned()),
|
|
|
|
},
|
|
|
|
TrackInfo {
|
|
|
|
id: TrackId::from("/home/savanni/Track 2.mp3".to_owned()),
|
|
|
|
track_number: Some(2),
|
|
|
|
name: Some("Track 2".to_owned()),
|
|
|
|
album: Some("Savanni's Demo".to_owned()),
|
|
|
|
artist: Some("Savanni".to_owned()),
|
|
|
|
},
|
|
|
|
TrackInfo {
|
|
|
|
id: TrackId::from("/home/savanni/Track 3.mp3".to_owned()),
|
|
|
|
track_number: Some(3),
|
|
|
|
name: Some("Track 3".to_owned()),
|
|
|
|
album: Some("Savanni's Demo".to_owned()),
|
|
|
|
artist: Some("Savanni".to_owned()),
|
|
|
|
},
|
|
|
|
TrackInfo {
|
|
|
|
id: TrackId::from("/home/savanni/Track 4.mp3".to_owned()),
|
|
|
|
track_number: Some(4),
|
|
|
|
name: Some("Track 4".to_owned()),
|
|
|
|
album: Some("Savanni's Demo".to_owned()),
|
|
|
|
artist: Some("Savanni".to_owned()),
|
|
|
|
},
|
|
|
|
TrackInfo {
|
|
|
|
id: TrackId::from("/home/savanni/Track 5.mp3".to_owned()),
|
|
|
|
track_number: Some(5),
|
|
|
|
name: Some("Track 5".to_owned()),
|
|
|
|
album: Some("Savanni's Demo".to_owned()),
|
|
|
|
artist: Some("Savanni".to_owned()),
|
|
|
|
},
|
|
|
|
],
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl MusicScanner for MockScanner {
|
|
|
|
fn scan<'a>(&'a self) -> Box<dyn Iterator<Item = Result<TrackInfo, ScannerError>> + 'a> {
|
|
|
|
Box::new(self.data.iter().map(|t| Ok(t.clone())))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|