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 for ScannerError { fn from(err: std::io::Error) -> Self { Self::IO(err) } } pub trait MusicScanner: Send { fn scan<'a>(&'a self) -> Box> + 'a>; } pub struct FileScanner { roots: Vec, } impl FileScanner { pub fn new(roots: Vec) -> Self { Self { roots } } } pub struct FileIterator { dirs: Vec, file_iter: Option, } impl FileIterator { fn scan_file(&self, path: PathBuf) -> Result { Ok(TrackInfo { id: TrackId::from(path.to_str().unwrap().to_owned()), album: None, artist: None, name: path .file_stem() .and_then(|s| s.to_str()) .map(|s| s.to_owned()), track_number: None, }) } } enum EntryInfo { Dir(PathBuf), File(PathBuf), } impl Iterator for FileIterator { type Item = Result; fn next(&mut self) -> Option { fn process_entry(entry: DirEntry) -> Result { 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> + '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, } 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> + 'a> { Box::new(self.data.iter().map(|t| Ok(t.clone()))) } } }