Files
deduper/src/main.rs
admin bb04871383 feat: add --review flag with browser-based side-by-side image review
- Launches local HTTP server with dark-themed review UI
- Side-by-side image comparison per duplicate group
- Checkbox selection + delete confirmation
- Shows file size and path per image
- Exact/similar badges per group
- Shutdown endpoint for clean exit
- Magic byte format detection (fixes misnamed screenshots)
- 23 tests passing
2026-04-28 00:08:33 +00:00

80 lines
2.0 KiB
Rust

use deduper::{find_duplicate_groups, scan_images, DuplicateKind};
use std::env;
use std::path::Path;
mod review;
struct Config {
root: String,
threshold: u32,
review: bool,
}
fn parse_args() -> Option<Config> {
let args: Vec<String> = env::args().collect();
let mut root = None;
let mut threshold = 8u32;
let mut review = false;
let mut i = 1;
while i < args.len() {
match args[i].as_str() {
"--review" => review = true,
arg if arg.starts_with('-') => {
eprintln!("unknown flag: {arg}");
return None;
}
_ => {
if root.is_none() {
root = Some(args[i].clone());
} else {
threshold = args[i].parse().unwrap_or(8);
}
}
}
i += 1;
}
root.map(|r| Config { root: r, threshold, review })
}
fn main() {
let config = match parse_args() {
Some(c) => c,
None => {
eprintln!("usage: deduper <folder> [hamming-threshold] [--review]");
std::process::exit(1);
}
};
let root = Path::new(&config.root);
let entries = match scan_images(root) {
Ok(v) => v,
Err(e) => {
eprintln!("scan error: {e}");
std::process::exit(1);
}
};
let groups = find_duplicate_groups(&entries, config.threshold);
if groups.is_empty() {
println!("no image duplicates found");
return;
}
if config.review {
review::launch_review(&groups);
} else {
for (idx, group) in groups.iter().enumerate() {
let kind = match group.kind {
DuplicateKind::Exact => "exact",
DuplicateKind::Similar => "similar",
};
println!("group {} [{}]", idx + 1, kind);
for path in &group.paths {
println!(" {}", path.display());
}
}
}
}