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

251 lines
8.3 KiB
Rust

use crate::media::{TrackId, TrackInfo};
use id3::{Tag, TagLike};
use std::{
fs::{DirEntry, ReadDir},
path::{Path, PathBuf},
};
use thiserror::Error;
#[derive(Debug, Error)]
pub enum ScannerError {
#[error("Cannot scan file")]
CannotScan,
#[error("File not found")]
FileNotFound,
#[error("Tag not found")]
TagNotFound,
#[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)
}
}
impl From<id3::Error> for ScannerError {
fn from(err: id3::Error) -> ScannerError {
match err.kind {
id3::ErrorKind::Io(err) => ScannerError::IO(err),
id3::ErrorKind::StringDecoding(_) => ScannerError::CannotScan,
id3::ErrorKind::NoTag => ScannerError::TagNotFound,
id3::ErrorKind::Parsing => ScannerError::CannotScan,
id3::ErrorKind::InvalidInput => ScannerError::CannotScan,
id3::ErrorKind::UnsupportedFeature => ScannerError::CannotScan,
}
}
}
pub trait MusicScanner: Sync + 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> {
let mimetype = mime_guess::from_path(path.clone())
.first()
.ok_or(ScannerError::CannotScan)?;
match (mimetype.type_(), mimetype.subtype().as_str()) {
(mime::AUDIO, "mpeg") => TrackInfo::scan_id3(path, mimetype),
/*
(mime::AUDIO, "ogg") => 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,
filetype: mimetype,
}),
(mime::AUDIO, "flac") => 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,
filetype: mimetype,
}),
*/
_ => Err(ScannerError::CannotScan),
}
}
}
impl TrackInfo {
fn scan_id3(path: PathBuf, mimetype: mime::Mime) -> Result<TrackInfo, ScannerError> {
let tags = Tag::read_from_path(path.clone()).map_err(ScannerError::from)?;
Ok(TrackInfo {
id: TrackId::from(path.to_str().unwrap().to_owned()),
album: tags.album().map(|s| s.to_owned()),
artist: tags.artist().map(|s| s.to_owned()),
name: tags.title().map(|s| s.to_owned()).or_else(|| {
path.file_stem()
.and_then(|s| s.to_str())
.map(|s| s.to_owned())
}),
duration: tags.duration(),
track_number: None,
filetype: mimetype,
})
}
}
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::FileNotFound))
} 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::media::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()),
duration: Some(15),
filetype: "audio/mpeg".parse::<mime::Mime>().unwrap(),
},
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()),
duration: Some(15),
filetype: "audio/mpeg".parse::<mime::Mime>().unwrap(),
},
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()),
duration: Some(15),
filetype: "audio/mpeg".parse::<mime::Mime>().unwrap(),
},
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()),
duration: Some(15),
filetype: "audio/mpeg".parse::<mime::Mime>().unwrap(),
},
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()),
duration: Some(15),
filetype: "audio/mpeg".parse::<mime::Mime>().unwrap(),
},
],
}
}
}
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())))
}
}
}