monorepo/music-player/server/src/music_scanner.rs

183 lines
5.5 KiB
Rust
Raw Normal View History

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,
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<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())))
}
}
}