use anyhow::Result; use camino::{Utf8Path, Utf8PathBuf}; use clap::{Parser, Subcommand}; use m3u::Entry; use std::collections::HashSet; use std::sync::LazyLock; /// Check if files in a playlist actually exist. #[derive(Parser, Debug)] #[command(author, version, about, long_about = None)] struct Args { /// Check mode. #[command(subcommand)] mode: Mode, /// Playlist file name. input: String, } #[derive(Subcommand, Clone, Debug)] enum Mode { #[command()] Files, #[command()] Directories { #[arg(long, short)] base_directory: String, #[arg(long, short, default_value_t = false)] relative: bool, }, } static AUDIO_EXTENSIONS: LazyLock> = LazyLock::new(|| HashSet::from(["m4a", "mp3", "ogg", "flac", "opus"])); fn has_audio_files(path: &Utf8Path) -> Result { let any_audio_file = path .read_dir_utf8()? .map(|entry| { let entry = entry?; if entry.metadata()?.is_file() { if let Some(extension) = entry.path().extension() { Ok(AUDIO_EXTENSIONS.contains(extension)) } else { Ok(false) } } else { Ok(false) } }) .any(|res: Result| res.unwrap_or(false)); Ok(any_audio_file) } fn get_directories( base_directory: &Utf8Path, relative_to: Option, ) -> Result> { let mut result = HashSet::::new(); for entry in base_directory.read_dir_utf8()? { let entry = entry?; if entry.metadata()?.is_dir() { // Skip dot directories if entry.file_name().starts_with('.') { continue; } let p = match &relative_to { Some(base) => { if base.is_empty() { entry.file_name().to_owned() } else { format!("{base}/{dir}", dir = entry.file_name()) } } None => entry.path().to_string(), }; // The directory must have audio files to count if has_audio_files(entry.path()).unwrap_or(false) { result.insert(p.clone()); } // Recursively check let relative_to = relative_to.as_ref().map(|_| p); result.extend(get_directories(entry.path(), relative_to).unwrap_or_default()); } } Ok(result) } fn main() -> Result<()> { let args = Args::parse(); let mut reader = m3u::Reader::open_ext(&args.input)?; match args.mode { Mode::Files => { for entry in reader.entry_exts() { if let Entry::Path(p) = entry?.entry { let p = Utf8PathBuf::try_from(p)?; if std::fs::metadata(&p).is_err() { println!("{p}"); } } } } Mode::Directories { base_directory, relative, } => { let mut playlist_directories = HashSet::::new(); for entry in reader.entry_exts() { if let Entry::Path(p) = entry?.entry { let p = Utf8PathBuf::try_from(p)?; if let Some(dir) = p.parent() { playlist_directories.insert(dir.to_string()); } } } let relative_to = if relative { Some(String::new()) } else { None }; let actual_directories = get_directories(Utf8Path::new(&base_directory), relative_to)?; let mut missing_directories = actual_directories .difference(&playlist_directories) .collect::>(); missing_directories.sort_unstable(); for d in missing_directories { println!("{d}"); } } } Ok(()) }