use std::path::PathBuf;
use clap::Parser;
use strum::VariantArray as _;
#[derive(Parser, Debug)]
#[command(version, about)]
pub struct SacadArgs {
#[clap(flatten)]
pub query: SearchQuery,
#[clap(flatten)]
pub search_opts: SearchOptions,
pub output_filepath: PathBuf,
#[clap(flatten)]
pub image_proc: ImageProcessingArgs,
#[clap(short, long, value_enum, ignore_case = true, default_value_t = Verbosity::Info)]
pub verbosity: Verbosity,
}
#[derive(Parser, Debug)]
#[command(version, about)]
pub struct SacadRecursiveArgs {
pub lib_root_dir: PathBuf,
#[clap(flatten)]
pub search_opts: SearchOptions,
#[clap(value_parser = CoverOutput::from_arg)]
pub output: CoverOutput,
#[clap(short, long)]
pub ignore_existing: bool,
#[clap(flatten)]
pub image_proc: ImageProcessingArgs,
#[clap(short, long, value_enum, ignore_case = true, default_value_t = Verbosity::Info)]
pub verbosity: Verbosity,
}
#[derive(Debug, Copy, Clone, clap::ValueEnum)]
#[expect(missing_docs)]
pub enum Verbosity {
Error,
Warn,
Info,
Debug,
Trace,
}
impl From<Verbosity> for log::Level {
fn from(v: Verbosity) -> Self {
match v {
Verbosity::Error => Self::Error,
Verbosity::Warn => Self::Warn,
Verbosity::Info => Self::Info,
Verbosity::Debug => Self::Debug,
Verbosity::Trace => Self::Trace,
}
}
}
impl From<Verbosity> for log::LevelFilter {
fn from(v: Verbosity) -> Self {
log::Level::from(v).to_level_filter()
}
}
#[derive(Clone, Debug)]
pub enum CoverOutput {
Embed,
Pattern(CoverOutputPattern<String>),
}
impl CoverOutput {
#[expect(clippy::unnecessary_wraps)]
fn from_arg(s: &str) -> Result<Self, std::convert::Infallible> {
if s == "+" {
Ok(CoverOutput::Embed)
} else {
Ok(CoverOutput::Pattern(CoverOutputPattern(s.to_owned())))
}
}
}
#[derive(Clone, Debug)]
pub struct CoverOutputPattern<S>(pub S);
#[derive(Parser, Debug)]
pub struct SearchQuery {
#[clap(required = true)]
pub artist: Option<String>,
pub album: String,
}
#[derive(Parser, Debug)]
pub struct SearchOptions {
pub size: u32,
#[clap(short = 't', long = "size-tolerance", default_value_t = 25)]
pub size_tolerance_prct: u32,
#[clap(short = 's', long, default_values_t = SourceName::VARIANTS.to_vec())]
pub cover_sources: Vec<SourceName>,
}
impl SearchOptions {
pub(crate) fn matches_min_size(&self, size: u32) -> bool {
let min_size = self.size - self.size * self.size_tolerance_prct / 100;
size >= min_size
}
pub(crate) fn matches_max_size(&self, size: u32) -> bool {
debug_assert!(self.matches_min_size(size));
let max_size = self.size + self.size * self.size_tolerance_prct / 100;
size <= max_size
}
}
#[derive(Parser, Debug)]
pub struct ImageProcessingArgs {
#[clap(short, long)]
pub preserve_format: bool,
}
#[derive(
Debug,
Copy,
Clone,
Eq,
PartialEq,
Hash,
strum::EnumString,
strum::VariantArray,
strum::AsRefStr,
strum::Display,
)]
#[strum(serialize_all = "lowercase")]
#[expect(missing_docs)]
pub enum SourceName {
CoverArtArchive,
Deezer,
Discogs,
Itunes,
LastFm,
}
#[cfg(test)]
mod tests {
use clap::CommandFactory as _;
use super::*;
#[test]
fn parse_log_level() {
let args =
SacadArgs::parse_from(["sacad", "-v", "debug", "artist", "album", "600", "c.jpg"]);
assert!(matches!(args.verbosity, Verbosity::Debug));
assert_eq!(log::Level::from(args.verbosity), log::Level::Debug);
}
#[test]
fn parse_log_level_case_insensitive() {
for value in ["debug", "DEBUG", "Debug", "dEbUg"] {
let args =
SacadArgs::parse_from(["sacad", "-v", value, "artist", "album", "600", "c.jpg"]);
assert!(
matches!(args.verbosity, Verbosity::Debug),
"failed for {value:?}"
);
}
}
#[test]
fn default_log_level() {
let args = SacadArgs::parse_from(["sacad", "artist", "album", "600", "c.jpg"]);
assert!(matches!(args.verbosity, Verbosity::Info));
}
#[test]
fn help_lists_verbosity_values() {
let mut help = Vec::new();
SacadArgs::command().write_long_help(&mut help).unwrap();
let help = String::from_utf8(help).unwrap();
for level in ["error", "warn", "info", "debug", "trace"] {
assert!(help.contains(level));
}
}
}