playlist-check/src/main.rs

128 lines
3.9 KiB
Rust
Raw Normal View History

use anyhow::Result;
2023-09-19 15:42:46 -03:00
use camino::{Utf8Path, Utf8PathBuf};
use clap::{Parser, Subcommand};
use m3u::Entry;
use once_cell::sync::Lazy;
use std::collections::HashSet;
/// 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: Lazy<HashSet<&str>> =
Lazy::new(|| HashSet::from(["m4a", "mp3", "ogg", "flac", "opus"]));
fn has_audio_files(path: &Utf8Path) -> Result<bool> {
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<bool>| res.unwrap_or(false));
Ok(any_audio_file)
}
fn get_directories(
base_directory: &Utf8Path,
relative_to: Option<String>,
) -> Result<HashSet<String>> {
let mut result = HashSet::<String>::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 {
2023-09-19 15:42:46 -03:00
let p = Utf8PathBuf::try_from(p)?;
if std::fs::metadata(&p).is_err() {
2023-09-19 15:42:46 -03:00
println!("{p}");
}
}
}
}
Mode::Directories {
base_directory,
relative,
} => {
let mut playlist_directories = HashSet::<String>::new();
for entry in reader.entry_exts() {
if let Entry::Path(p) = entry?.entry {
2023-09-19 15:42:46 -03:00
let p = Utf8PathBuf::try_from(p)?;
if let Some(dir) = p.parent() {
2023-09-19 15:42:46 -03:00
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::<Vec<_>>();
missing_directories.sort_unstable();
for d in missing_directories {
println!("{d}");
}
}
}
Ok(())
}