use crate::constants::SKIP_DIRECTORIES;
use crate::zimignore::ZimIgnore;
use rayon::prelude::*;
use std::collections::HashSet;
use std::error::Error;
use std::fs;
use std::path::{Path, PathBuf};
pub fn is_hidden_file(path: &Path) -> bool {
path.file_name()
.map(|name| name.to_string_lossy().starts_with('.'))
.unwrap_or(false)
}
pub fn should_skip_directory(name: &str) -> bool {
SKIP_DIRECTORIES.contains(&name)
}
pub fn collect_audio_files(
dir: &Path,
audio_exts: &HashSet<&str>,
zimignore: &ZimIgnore,
) -> Result<Vec<PathBuf>, Box<dyn Error>> {
let mut files = Vec::new();
scan_directory_parallel(dir, audio_exts, zimignore, &mut files)?;
Ok(files)
}
fn scan_directory_parallel(
dir: &Path,
audio_exts: &HashSet<&str>,
zimignore: &ZimIgnore,
files: &mut Vec<PathBuf>,
) -> Result<(), Box<dyn Error>> {
let entries = fs::read_dir(dir)?;
let entries: Vec<_> = entries.collect::<Result<_, _>>()?;
let mut local_files = Vec::new();
let mut directories = Vec::new();
for entry in entries {
let path = entry.path();
if is_hidden_file(&path) {
continue;
}
if zimignore.is_ignored(&path, path.is_dir()) {
continue;
}
if path.is_dir() {
let dir_name = match path.file_name() {
Some(name) => name.to_string_lossy(),
None => continue, };
if !should_skip_directory(&dir_name) {
directories.push(path);
}
} else if path.is_file()
&& let Some(extension) = path.extension()
{
let ext = extension.to_string_lossy().to_lowercase();
if audio_exts.contains(ext.as_str()) {
local_files.push(path);
}
}
}
files.extend(local_files);
if directories.len() > 1 {
let nested_files: Vec<Vec<PathBuf>> = directories
.par_iter()
.filter_map(
|subdir| match collect_audio_files(subdir, audio_exts, zimignore) {
Ok(files) => Some(files),
Err(e) => {
eprintln!(
"Warning: Failed to scan directory '{}': {}",
subdir.display(),
e
);
None
}
},
)
.collect();
for nested in nested_files {
files.extend(nested);
}
} else {
for subdir in directories {
if let Err(e) = scan_directory_parallel(&subdir, audio_exts, zimignore, files) {
eprintln!(
"Warning: Failed to scan directory '{}': {}",
subdir.display(),
e
);
}
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::TempDir;
#[test]
fn test_is_hidden_file() {
assert!(is_hidden_file(Path::new(".hidden")));
assert!(is_hidden_file(Path::new("/path/.hidden")));
assert!(!is_hidden_file(Path::new("visible")));
}
#[test]
fn test_should_skip_directory() {
assert!(should_skip_directory("node_modules"));
assert!(should_skip_directory(".git"));
assert!(!should_skip_directory("src"));
}
#[test]
fn test_collect_audio_files_empty() {
let temp_dir = TempDir::new().unwrap();
let audio_exts: HashSet<&str> = ["wav", "flac"].iter().cloned().collect();
let zimignore = ZimIgnore::new();
let files = collect_audio_files(temp_dir.path(), &audio_exts, &zimignore).unwrap();
assert_eq!(files.len(), 0);
}
#[test]
fn test_collect_audio_files_with_audio() {
let temp_dir = TempDir::new().unwrap();
let audio_exts: HashSet<&str> = ["wav", "flac"].iter().cloned().collect();
fs::write(temp_dir.path().join("test1.wav"), b"fake").unwrap();
fs::write(temp_dir.path().join("test2.flac"), b"fake").unwrap();
fs::write(temp_dir.path().join("readme.txt"), b"fake").unwrap();
let zimignore = ZimIgnore::new();
let files = collect_audio_files(temp_dir.path(), &audio_exts, &zimignore).unwrap();
assert_eq!(files.len(), 2);
}
#[test]
fn test_collect_audio_files_nested() {
let temp_dir = TempDir::new().unwrap();
let audio_exts: HashSet<&str> = ["wav"].iter().cloned().collect();
let subdir = temp_dir.path().join("music");
fs::create_dir(&subdir).unwrap();
fs::write(temp_dir.path().join("root.wav"), b"fake").unwrap();
fs::write(subdir.join("nested.wav"), b"fake").unwrap();
let zimignore = ZimIgnore::new();
let files = collect_audio_files(temp_dir.path(), &audio_exts, &zimignore).unwrap();
assert_eq!(files.len(), 2);
}
#[test]
fn test_collect_audio_files_skip_hidden() {
let temp_dir = TempDir::new().unwrap();
let audio_exts: HashSet<&str> = ["wav"].iter().cloned().collect();
fs::write(temp_dir.path().join("visible.wav"), b"fake").unwrap();
fs::write(temp_dir.path().join(".hidden.wav"), b"fake").unwrap();
let zimignore = ZimIgnore::new();
let files = collect_audio_files(temp_dir.path(), &audio_exts, &zimignore).unwrap();
assert_eq!(files.len(), 1);
}
#[test]
fn test_collect_audio_files_skip_directories() {
let temp_dir = TempDir::new().unwrap();
let audio_exts: HashSet<&str> = ["wav"].iter().cloned().collect();
let normal_dir = temp_dir.path().join("music");
fs::create_dir(&normal_dir).unwrap();
fs::write(normal_dir.join("test.wav"), b"fake").unwrap();
let skip_dir = temp_dir.path().join("node_modules");
fs::create_dir(&skip_dir).unwrap();
fs::write(skip_dir.join("test.wav"), b"fake").unwrap();
let zimignore = ZimIgnore::new();
let files = collect_audio_files(temp_dir.path(), &audio_exts, &zimignore).unwrap();
assert_eq!(files.len(), 1);
}
}