pub mod file_utils;
pub mod tui_app;
pub mod config;
pub mod file_cache;
pub mod media_dedup;
pub mod audio_fingerprint;
pub mod video_fingerprint;
use clap::Parser;
use std::path::PathBuf;
use std::str::FromStr;
use crate::config::DedupConfig;
use crate::file_utils::{SortCriterion, SortOrder};
use crate::media_dedup::MediaDedupOptions;
#[derive(Parser, Debug, Clone)]
#[clap(author, version, about, long_about = None)]
pub struct Cli {
#[clap(required_unless_present = "interactive")]
pub directories: Vec<PathBuf>,
#[clap(long)]
pub target: Option<PathBuf>,
#[clap(long, help = "Deduplicate between source and target directories")]
pub deduplicate: bool,
#[clap(
short,
long,
help = "Delete duplicate files automatically based on selection strategy"
)]
pub delete: bool,
#[clap(
short = 'M',
long,
help = "Move duplicate files to a specified directory"
)]
pub move_to: Option<PathBuf>,
#[clap(short, long, help = "Enable logging to a file (default: dedups.log)")]
pub log: bool,
#[clap(long, value_name = "PATH", help = "Specify a custom log file path")]
pub log_file: Option<PathBuf>,
#[clap(
short,
long,
help = "Output duplicate sets to a file (e.g., duplicates.json)"
)]
pub output: Option<PathBuf>,
#[clap(short, long, value_parser = clap::builder::PossibleValuesParser::new(["json", "toml"]), default_value = "json", help = "Format for the output file [json|toml]")]
pub format: String,
#[clap(short, long, value_parser = clap::builder::PossibleValuesParser::new(["md5", "sha1", "sha256", "blake3", "xxhash", "gxhash", "fnv1a", "crc32"]), default_value = "xxhash", help = "Hashing algorithm [md5|sha1|sha256|blake3|xxhash|gxhash|fnv1a|crc32]")]
pub algorithm: String,
#[clap(
short,
long,
help = "Number of parallel threads for hashing (default: auto)"
)]
pub parallel: Option<usize>,
#[clap(
long,
default_value = "newest_modified",
help = "Selection strategy for delete/move [newest_modified|oldest_modified|shortest_path|longest_path]"
)]
pub mode: String,
#[clap(short, long, help = "Run in interactive TUI mode")]
pub interactive: bool,
#[clap(short, long, action = clap::ArgAction::Count, help = "Verbosity level (-v, -vv, -vvv)")]
pub verbose: u8,
#[clap(long, help = "Include specific file patterns (glob)")]
pub include: Vec<String>,
#[clap(long, help = "Exclude specific file patterns (glob)")]
pub exclude: Vec<String>,
#[clap(
long,
help = "Load filter rules from a file (one pattern per line, # for comments)"
)]
pub filter_from: Option<PathBuf>,
#[clap(
long,
help = "Show progress bar for CLI scan (TUI has its own progress display)"
)]
pub progress: bool,
#[clap(
long,
help = "Show progress during TUI scan (enabled by default for TUI mode)"
)]
pub progress_tui: bool,
#[clap(long, value_parser = SortCriterion::from_str, default_value_t = SortCriterion::ModifiedAt, help = "Sort files by criterion [name|size|created|modified|path]")]
pub sort_by: SortCriterion,
#[clap(long, value_parser = SortOrder::from_str, default_value_t = SortOrder::Descending, help = "Sort order [asc|desc]")]
pub sort_order: SortOrder,
#[clap(
long,
help = "Display file sizes in raw bytes instead of human-readable format"
)]
pub raw_sizes: bool,
#[clap(
long,
help = "Path to a custom config file (overrides the default ~/.deduprc for dedups)"
)]
pub config_file: Option<PathBuf>,
#[clap(long, help = "Perform a dry run without making any actual changes")]
pub dry_run: bool,
#[clap(long, help = "Directory to store file hash cache for faster rescans")]
pub cache_location: Option<PathBuf>,
#[clap(
long,
help = "Use cached file hashes when available (requires cache-location)"
)]
pub fast_mode: bool,
#[clap(
long,
help = "Enable media deduplication for similar images/videos/audio"
)]
pub media_mode: bool,
#[clap(long, default_value = "highest", value_parser = ["highest", "lowest"], help = "Preferred resolution for media files [highest|lowest|WIDTHxHEIGHT]")]
pub media_resolution: String,
#[clap(
long,
value_delimiter = ',',
help = "Preferred formats for media files (comma-separated, e.g., 'raw,png,jpg')"
)]
pub media_formats: Vec<String>,
#[clap(
long,
default_value = "90",
help = "Similarity threshold percentage for media files (0-100)"
)]
pub media_similarity: u32,
#[clap(skip)]
pub media_dedup_options: MediaDedupOptions,
}
impl Cli {
pub fn with_config() -> anyhow::Result<Self> {
let mut cli = Self::parse();
cli.media_dedup_options = MediaDedupOptions::default();
let config = if let Some(config_path) = &cli.config_file {
DedupConfig::load_from_path(config_path)?
} else {
DedupConfig::load()?
};
cli.apply_config(config);
if cli.media_mode {
crate::media_dedup::add_media_options_to_cli(
&mut cli.media_dedup_options,
cli.media_mode,
&cli.media_resolution,
&cli.media_formats,
cli.media_similarity,
);
}
if cli.config_file.is_none() {
let _ = DedupConfig::create_default_if_not_exists();
}
Ok(cli)
}
fn apply_config(&mut self, config: DedupConfig) {
if self.algorithm.is_empty() {
self.algorithm = config.algorithm;
}
if self.parallel.is_none() {
self.parallel = config.parallel;
}
if self.mode.is_empty() {
self.mode = config.mode;
}
if self.format.is_empty() {
self.format = config.format;
}
if !self.progress && config.progress {
self.progress = config.progress;
}
if self.include.is_empty() && !config.include.is_empty() {
self.include = config.include;
}
if self.exclude.is_empty() && !config.exclude.is_empty() {
self.exclude = config.exclude;
}
if self.sort_by == SortCriterion::ModifiedAt && !config.sort_by.is_empty() {
if let Ok(sort_by) = SortCriterion::from_str(&config.sort_by) {
self.sort_by = sort_by;
}
}
if self.sort_order == SortOrder::Descending && !config.sort_order.is_empty() {
if let Ok(sort_order) = SortOrder::from_str(&config.sort_order) {
self.sort_order = sort_order;
}
}
if self.cache_location.is_none() {
self.cache_location = config.cache_location;
}
if !self.fast_mode && config.fast_mode {
self.fast_mode = config.fast_mode;
}
if self.fast_mode && self.cache_location.is_none() {
log::warn!(
"Fast mode enabled but no cache location specified. Fast mode will be disabled."
);
self.fast_mode = false;
}
if !self.media_mode && config.media_dedup.enabled {
self.media_mode = config.media_dedup.enabled;
self.media_dedup_options = config.media_dedup;
}
if self.algorithm.is_empty() {
self.algorithm = "xxhash".to_string();
}
if self.format.is_empty() {
self.format = "json".to_string();
}
if self.mode.is_empty() {
self.mode = "newest_modified".to_string();
}
}
}