use anyhow::{anyhow, Result}; use chrono::Local; use std::fs; use std::path::PathBuf; pub fn get_voice_notes_dir() -> Result { let dir = std::env::var("VOICE_NOTES") .map_err(|_| anyhow!("$VOICE_NOTES environment variable is not set.\nSet it with: export VOICE_NOTES=/path/to/notes"))?; Ok(PathBuf::from(dir)) } pub fn get_todays_dir() -> Result { let base = get_voice_notes_dir()?; let today = Local::now().format("%Y-%m-%d").to_string(); let dir = base.join(today); fs::create_dir_all(&dir)?; Ok(dir) } /// Determines the next recording path: $VOICE_NOTES/YYYY-MM-DD/HH-MM-SS_note-N.mp3 pub fn next_recording_path() -> Result { let dir = get_todays_dir()?; let count = fs::read_dir(&dir)? .filter_map(|e| e.ok()) .filter(|e| { e.path() .extension() .and_then(|ext| ext.to_str()) .map(|ext| ext.eq_ignore_ascii_case("mp3")) .unwrap_or(false) }) .count(); let filename = format!("practice-{}.mp3", count + 1); Ok(dir.join(filename)) } /// Returns (date_label, sorted_mp3_paths) pairs, newest dates first. pub fn list_recordings() -> Result)>> { let base = get_voice_notes_dir()?; if !base.exists() { return Ok(vec![]); } let mut entries: Vec<_> = fs::read_dir(&base)? .filter_map(|e| e.ok()) .filter(|e| e.file_type().map(|t| t.is_dir()).unwrap_or(false)) .collect(); // Newest dates first (reverse lexicographic on YYYY-MM-DD) entries.sort_by(|a, b| b.file_name().cmp(&a.file_name())); let mut result = Vec::new(); for entry in entries { let date = entry.file_name().to_string_lossy().to_string(); let mut files: Vec = fs::read_dir(entry.path())? .filter_map(|e| e.ok()) .filter(|e| { e.path() .extension() .and_then(|ext| ext.to_str()) .map(|ext| ext.eq_ignore_ascii_case("mp3")) .unwrap_or(false) }) .map(|e| e.path()) .collect(); files.sort_by(|a, b| { let mt = |p: &std::path::PathBuf| { std::fs::metadata(p).and_then(|m| m.modified()).ok() }; mt(b).cmp(&mt(a)) // newest modified time first }); if !files.is_empty() { result.push((date, files)); } } Ok(result) }