251 lines
8.3 KiB
Rust
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())))
|
|
}
|
|
}
|
|
}
|