use crate::media::metadata::read_audio_metadata;
use crate::templates::{self, SidecarMetadata};
use indicatif::{MultiProgress, ProgressBar, ProgressStyle};
use owo_colors::OwoColorize;
use std::collections::HashSet;
use std::error::Error;
use std::fs;
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex};
pub fn handle_update(project_path: &str) -> Result<(), Box<dyn Error>> {
let project_path = Path::new(project_path);
if !project_path.exists() {
return Err(format!(
"{} Path does not exist: {}",
"Error:".red().bold(),
project_path.display()
)
.into());
}
println!(
"{} {}",
"Scanning project:".bright_black(),
project_path.display().to_string().cyan()
);
let audio_extensions: HashSet<&str> = ["wav", "flac", "aiff", "mp3", "m4a"]
.iter()
.cloned()
.collect();
let spinner = ProgressBar::new_spinner();
spinner.set_style(
ProgressStyle::default_spinner()
.template("{spinner:.cyan} {msg}")
.unwrap()
.tick_strings(&["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]),
);
spinner.set_message("Counting audio files...");
let total_files = count_audio_files(project_path, &audio_extensions)?;
spinner.finish_and_clear();
if total_files == 0 {
println!("{} No audio files found in project", "⚠".yellow());
return Ok(());
}
println!(
"{} Found {} audio files\n",
"ℹ".blue(),
total_files.to_string().cyan().bold()
);
let created_count = Arc::new(Mutex::new(0));
let skipped_count = Arc::new(Mutex::new(0));
let updated_count = Arc::new(Mutex::new(0));
let multi = MultiProgress::new();
let pb = multi.add(ProgressBar::new(total_files as u64));
pb.set_style(
ProgressStyle::default_bar()
.template("{spinner:.cyan} [{bar:40.cyan/blue}] {pos}/{len} {msg}")
.unwrap()
.progress_chars("█▓░"),
);
pb.set_message("Processing audio files...");
scan_directory(
project_path,
&audio_extensions,
&created_count,
&skipped_count,
&updated_count,
&pb,
)?;
pb.finish_with_message("Done");
let created = *created_count.lock().unwrap();
let updated = *updated_count.lock().unwrap();
let skipped = *skipped_count.lock().unwrap();
println!("\n{} {}", "✓".green().bold(), "Update complete!".bold());
println!(
" {} {} new sidecar files",
"Created:".bright_black(),
created.to_string().green().bold()
);
println!(
" {} {} existing files",
"Updated:".bright_black(),
updated.to_string().blue().bold()
);
println!(
" {} {} files {}",
"Skipped:".bright_black(),
skipped.to_string().yellow().bold(),
"(already have sidecars)".bright_black()
);
Ok(())
}
fn count_audio_files(dir: &Path, audio_exts: &HashSet<&str>) -> Result<u32, Box<dyn Error>> {
let mut count = 0;
let entries = fs::read_dir(dir)?;
for entry in entries {
let entry = entry?;
let path = entry.path();
if let Some(name) = path.file_name() {
if name.to_string_lossy().starts_with('.') {
continue;
}
}
if path.is_dir() {
let dir_name = path.file_name().unwrap().to_string_lossy();
if dir_name == "node_modules" || dir_name == ".git" || dir_name == "temp" {
continue;
}
count += count_audio_files(&path, audio_exts)?;
} else if path.is_file() {
if let Some(extension) = path.extension() {
let ext = extension.to_string_lossy().to_lowercase();
if audio_exts.contains(ext.as_str()) {
count += 1;
}
}
}
}
Ok(count)
}
fn scan_directory(
dir: &Path,
audio_exts: &HashSet<&str>,
created: &Arc<Mutex<u32>>,
skipped: &Arc<Mutex<u32>>,
updated: &Arc<Mutex<u32>>,
pb: &ProgressBar,
) -> Result<(), Box<dyn Error>> {
let entries = fs::read_dir(dir)?;
for entry in entries {
let entry = entry?;
let path = entry.path();
if let Some(name) = path.file_name() {
if name.to_string_lossy().starts_with('.') {
continue;
}
}
if path.is_dir() {
let dir_name = path.file_name().unwrap().to_string_lossy();
if dir_name == "node_modules" || dir_name == ".git" || dir_name == "temp" {
continue;
}
scan_directory(&path, audio_exts, created, skipped, updated, pb)?;
} else if path.is_file() {
if let Some(extension) = path.extension() {
let ext = extension.to_string_lossy().to_lowercase();
if audio_exts.contains(ext.as_str()) {
process_media_file(&path, created, skipped, updated, pb)?;
pb.inc(1);
}
}
}
}
Ok(())
}
fn process_media_file(
file_path: &Path,
created: &Arc<Mutex<u32>>,
skipped: &Arc<Mutex<u32>>,
_updated: &Arc<Mutex<u32>>,
pb: &ProgressBar,
) -> Result<(), Box<dyn Error>> {
let sidecar_path = get_sidecar_path(file_path);
let file_name = file_path.file_name().unwrap().to_string_lossy();
if sidecar_path.exists() {
*skipped.lock().unwrap() += 1;
pb.set_message(format!("Skipped: {}", file_name.bright_black()));
return Ok(());
}
let relative_path = file_path.strip_prefix(".").unwrap_or(file_path);
let file_metadata = std::fs::metadata(file_path)?;
let file_size = file_metadata.len();
let modified = file_metadata
.modified()
.ok()
.and_then(|time| time.duration_since(std::time::UNIX_EPOCH).ok())
.and_then(|duration| {
chrono::DateTime::<chrono::Utc>::from_timestamp(duration.as_secs() as i64, 0)
})
.map(|dt| dt.format("%Y-%m-%d %H:%M:%S UTC").to_string());
let extension = file_path
.extension()
.and_then(|e| e.to_str())
.map(|e| e.to_lowercase());
let content = match extension.as_deref() {
Some("flac") | Some("wav") => {
match read_audio_metadata(file_path) {
Ok(metadata) => {
templates::generate_audio_sidecar_with_metadata(&SidecarMetadata {
file_name: &file_name,
file_path: &relative_path.to_string_lossy(),
sample_rate: metadata.sample_rate,
channels: metadata.channels,
bits_per_sample: metadata.bits_per_sample,
duration_seconds: metadata.duration_seconds,
file_size,
modified: modified.as_deref(),
})
}
Err(e) => {
eprintln!(
" {} Could not read metadata from {}: {}",
"Warning:".yellow(),
file_name.yellow(),
e.to_string().bright_black()
);
templates::generate_minimal_sidecar_with_fs_metadata(
&file_name,
&relative_path.to_string_lossy(),
file_size,
modified.as_deref(),
)
}
}
}
_ => {
templates::generate_minimal_sidecar_with_fs_metadata(
&file_name,
&relative_path.to_string_lossy(),
file_size,
modified.as_deref(),
)
}
};
fs::write(&sidecar_path, content)?;
pb.set_message(format!("Created: {}", file_name.green()));
*created.lock().unwrap() += 1;
Ok(())
}
fn get_sidecar_path(media_path: &Path) -> PathBuf {
let mut sidecar_path = media_path.to_path_buf();
let current_name = sidecar_path.file_name().unwrap().to_string_lossy();
let new_name = format!("{current_name}.md");
sidecar_path.set_file_name(new_name);
sidecar_path
}