use std::{
collections::HashSet,
fs,
path::{Path, PathBuf},
sync::{
Arc, LazyLock,
atomic::{AtomicUsize, Ordering},
},
};
#[derive(Default)]
pub struct Stats {
pub audio_files: AtomicUsize,
pub audio_dirs: AtomicUsize,
pub missing_covers: AtomicUsize,
pub done: AtomicUsize,
pub no_result_found: AtomicUsize,
pub errors: AtomicUsize,
}
pub struct AudioFileIterator {
dirs: Vec<PathBuf>,
stats: Arc<Stats>,
}
impl AudioFileIterator {
pub fn new(dir: &Path, stats: Arc<Stats>) -> Self {
Self {
dirs: vec![dir.to_owned()],
stats,
}
}
fn is_audio_file(path: &Path) -> bool {
static AUDIO_EXTENSIONS: LazyLock<HashSet<&str>> = LazyLock::new(|| {
[
"aac", "ape", "flac", "m4a", "mp3", "mp4", "mpc", "ogg", "oga", "opus", "tta", "wv",
]
.into_iter()
.collect()
});
path.extension()
.and_then(|ext| ext.to_str())
.is_some_and(|ext| AUDIO_EXTENSIONS.contains(ext.to_lowercase().as_str()))
}
}
impl Iterator for AudioFileIterator {
type Item = Vec<PathBuf>;
fn next(&mut self) -> Option<Self::Item> {
while let Some(dir) = self.dirs.pop() {
let mut audio_files = Vec::new();
let dir_it = match fs::read_dir(&dir) {
Ok(dir_it) => dir_it,
Err(err) => {
self.stats.errors.fetch_add(1, Ordering::Relaxed);
log::warn!("Failed to read dir {dir:?}: {err}");
continue;
}
};
for entry_res in dir_it {
let entry = match entry_res {
Ok(entry) => entry,
Err(err) => {
self.stats.errors.fetch_add(1, Ordering::Relaxed);
log::warn!("Failed to read dir {dir:?} entry: {err}");
continue;
}
};
let ftype = match entry.file_type() {
Ok(ftype) => ftype,
Err(err) => {
self.stats.errors.fetch_add(1, Ordering::Relaxed);
log::warn!("Failed to read dir {dir:?} entry: {err}");
continue;
}
};
let path = entry.path();
if ftype.is_dir() {
self.dirs.push(path);
} else if ftype.is_file() && Self::is_audio_file(&path) {
audio_files.push(path);
}
}
if !audio_files.is_empty() {
self.stats.audio_dirs.fetch_add(1, Ordering::Relaxed);
self.stats
.audio_files
.fetch_add(audio_files.len(), Ordering::Relaxed);
return Some(audio_files);
}
}
None
}
}