use anyhow::{Context, Result};
use aprender_rag::loader::LoaderRegistry;
use globset::{Glob, GlobSet, GlobSetBuilder};
use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};
pub(crate) const MEDIA_EXTENSIONS: &[&str] = &[
"mp4", "mkv", "avi", "mov", "webm", "flv", "wmv", "mp3", "wav", "flac", "ogg", "m4a", "aac", "wma", ];
pub(crate) fn build_exclude_set(patterns: &[String]) -> Result<Option<GlobSet>> {
if patterns.is_empty() {
return Ok(None);
}
let mut builder = GlobSetBuilder::new();
for pattern in patterns {
builder
.add(Glob::new(pattern).with_context(|| format!("Invalid exclude glob: {pattern}"))?);
}
Ok(Some(
builder.build().context("Failed to build exclude set")?,
))
}
pub(crate) fn is_excluded(path: &Path, exclude: &Option<GlobSet>) -> bool {
match exclude {
Some(set) => set.is_match(path),
None => false,
}
}
pub(crate) enum DirEntryAction {
AcceptFile(PathBuf),
Recurse(PathBuf),
Excluded,
Skip,
}
pub(crate) fn classify_entry(
path: PathBuf,
recursive: bool,
exclude: &Option<GlobSet>,
accept: &impl Fn(&Path) -> bool,
) -> DirEntryAction {
if is_excluded(&path, exclude) {
return DirEntryAction::Excluded;
}
if path.is_dir() && recursive {
return DirEntryAction::Recurse(path);
}
if path.is_file() && accept(&path) {
return DirEntryAction::AcceptFile(path);
}
DirEntryAction::Skip
}
pub(crate) fn walk_directory(
root: &Path,
recursive: bool,
exclude: &Option<GlobSet>,
accept: impl Fn(&Path) -> bool,
) -> Result<Vec<PathBuf>> {
let mut files = Vec::new();
let mut dirs_to_visit = vec![root.to_path_buf()];
let mut excluded_count = 0usize;
while let Some(dir) = dirs_to_visit.pop() {
let entries = fs::read_dir(&dir)
.with_context(|| format!("Failed to read directory: {}", dir.display()))?;
for entry in entries {
match classify_entry(entry?.path(), recursive, exclude, &accept) {
DirEntryAction::AcceptFile(p) => files.push(p),
DirEntryAction::Recurse(p) => dirs_to_visit.push(p),
DirEntryAction::Excluded => excluded_count += 1,
DirEntryAction::Skip => {}
}
}
}
if excluded_count > 0 {
println!("Excluded {} paths by glob pattern", excluded_count);
}
files.sort();
Ok(files)
}
pub(crate) fn discover_files(
root: &Path,
recursive: bool,
registry: &LoaderRegistry,
exclude: &Option<GlobSet>,
) -> Result<Vec<PathBuf>> {
if root.is_file() {
if registry.loader_for(root).is_some() {
return Ok(vec![root.to_path_buf()]);
}
anyhow::bail!(
"Unsupported file format: {}",
root.extension()
.and_then(|e| e.to_str())
.unwrap_or("(none)")
);
}
walk_directory(root, recursive, exclude, |p| {
registry.loader_for(p).is_some()
})
}
pub(crate) fn is_media_file(path: &Path) -> bool {
path.extension()
.and_then(|ext| ext.to_str())
.is_some_and(|ext| MEDIA_EXTENSIONS.contains(&ext.to_lowercase().as_str()))
}
pub(crate) fn discover_media_files(
root: &Path,
recursive: bool,
exclude: &Option<GlobSet>,
) -> Result<Vec<PathBuf>> {
if root.is_file() {
if is_media_file(root) {
return Ok(vec![root.to_path_buf()]);
}
anyhow::bail!("Not a media file: {}", root.display());
}
walk_directory(root, recursive, exclude, is_media_file)
}
pub(crate) fn classify_media_sidecar_status(files: &[PathBuf]) -> (Vec<PathBuf>, Vec<PathBuf>) {
let mut has_sidecar = Vec::new();
let mut needs_transcription = Vec::new();
for file in files {
if LoaderRegistry::find_sidecar(file).is_some() {
has_sidecar.push(file.clone());
} else {
needs_transcription.push(file.clone());
}
}
(has_sidecar, needs_transcription)
}
pub(crate) fn classify_files(files: &[PathBuf]) -> HashMap<String, usize> {
let mut counts: HashMap<String, usize> = HashMap::new();
for file in files {
let ext = file
.extension()
.and_then(|e| e.to_str())
.unwrap_or("other")
.to_lowercase();
*counts.entry(ext).or_insert(0) += 1;
}
counts
}
pub(crate) fn discover_and_report_media(
root: &Path,
recursive: bool,
exclude: &Option<GlobSet>,
) -> Result<Vec<PathBuf>> {
println!("Discovering media files...");
let media_files = discover_media_files(root, recursive, exclude)?;
if media_files.is_empty() {
println!("No media files found at: {}", root.display());
return Ok(media_files);
}
let ext_counts = classify_files(&media_files);
println!(
"Found {} media files{}",
media_files.len(),
if recursive { " (recursive)" } else { "" }
);
for (ext, count) in &ext_counts {
println!(" {} .{} files", count, ext);
}
Ok(media_files)
}