use std::collections::HashMap; use std::error::Error; use std::io::Write; use std::time::Duration; use human_format::Formatter; use human_format::Scales; use humantime; use lazy_static::lazy_static; use log::info; use regex::Regex; use structopt::StructOpt; use tabwriter::TabWriter; use superdeduper::clean_path_parent; use superdeduper::CompactMetadata; use superdeduper::MovieLibrary; const MOVIE_DIR: &str = "/home/wathiede/Movies"; const TO_BE_REMOVED_DIR: &str = "/storage/media/to-be-deleted/"; lazy_static! { static ref CLEAN_TITLE_CHARS: Regex = Regex::new("[^ 0-9[:alpha:]]").unwrap(); } fn normalize(path: &str) -> String { CLEAN_TITLE_CHARS .replace_all(&path, "") .to_ascii_lowercase() } lazy_static! { static ref YEAR_SUFFIX: Regex = Regex::new(r" \d{4}$").unwrap(); } fn print_dupes(lib: &MovieLibrary) { let videos = lib.movies().expect("couldn't get videos from library"); let mut fmtr = Formatter::new(); fmtr.with_separator(""); fmtr.with_scales(Scales::Binary()); for (keep, deletes) in videos.duplicate_candidates() { let p = &keep.files.first().unwrap().0; println!("{}", &p[..p.find("/").unwrap()]); println!(" Keeping:"); for (p, md) in &keep.files { println!( " {:>9} {:>9} {} {}", md.largest_dimension().unwrap(), fmtr.format(md.size as f64), humantime::Duration::from(Duration::from_secs(md.duration as u64)), &p[p.rfind("/").unwrap() + 1..] ); } println!(" Need to remove:"); for delete in &deletes { for (p, md) in &delete.files { println!( " {:>9} {:>9} {} {}", md.largest_dimension().unwrap(), fmtr.format(md.size as f64), humantime::Duration::from(Duration::from_secs(md.duration as u64)), &p[p.rfind("/").unwrap() + 1..] ); } } println!(); } } fn print_all(videos: HashMap) { let mut names = videos.keys().collect::>(); names.sort(); let mut fmtr = Formatter::new(); fmtr.with_separator(""); fmtr.with_scales(Scales::Binary()); let mut tw = TabWriter::new(vec![]); for name in names { let md = &videos[name]; write!( &mut tw, "{}B/s\t{}\t{}\t{}\t{}\n", fmtr.format(md.bit_rate as f64), md.largest_dimension().unwrap(), fmtr.format(md.size as f64), humantime::Duration::from(Duration::from_secs(md.duration as u64)), name, ) .unwrap(); //&p[p.rfind("/").unwrap() + 1..] } println!("{}", String::from_utf8(tw.into_inner().unwrap()).unwrap()); } fn print_video_groups(video_groups: &HashMap>) { let mut names = video_groups.keys().collect::>(); names.sort(); let mut fmtr = Formatter::new(); fmtr.with_separator(""); fmtr.with_scales(Scales::Binary()); for name in names { let paths = &video_groups[name]; if paths.len() < 2 { continue; } let mut file: Vec<_> = video_groups[name].iter().collect(); file.sort_by(|(n1, _), (n2, _)| n1.partial_cmp(n2).unwrap()); println!("{}:", name); for (p, md) in file { println!( " {:>9} {:>9} {} {}", md.largest_dimension().unwrap(), fmtr.format(md.size as f64), humantime::Duration::from(Duration::from_secs(md.duration as u64)), &p[p.rfind("/").unwrap() + 1..] ); } } } fn print_videos(videos: &HashMap, filter: Option<&Regex>) { let mut names = videos.keys().collect::>(); names.sort(); let mut fmtr = Formatter::new(); fmtr.with_separator(""); fmtr.with_scales(Scales::Binary()); for name in names { if let Some(re) = filter { if !re.is_match(name) { continue; } } let md = &videos[name]; info!( "{:>9} {:>8} {} {}", md.largest_dimension().unwrap(), fmtr.format(md.size as f64), humantime::Duration::from(Duration::from_secs(md.duration as u64)), &name[MOVIE_DIR.len() + 1..] ); println!("mv '{}' '{}'", name, TO_BE_REMOVED_DIR); } } #[derive(StructOpt)] enum Command { #[structopt(name = "samples", about = "Print video files deemed to be samples")] Samples, #[structopt(name = "groups", about = "Print videos grouped by root name")] Groups, #[structopt( name = "compact-metadata", about = "Read full metadata file and write compact file." )] CompactMetadata, #[structopt(name = "print-all", about = "Print useful metadata about all files")] PrintAll, #[structopt(name = "print-dupes", about = "Print duplicate movies")] PrintDupes, #[structopt(name = "update-metadata", about = "Write full metadata files")] UpdateMetadata, #[structopt( name = "update-compact-metadata", about = "Write full metadata files and update compact file on changes" )] UpdateAndCompactMetadata, } #[derive(StructOpt)] #[structopt( name = "superdeduper", about = "Tool for pruning extra videos in collection" )] struct SuperDeduper { #[structopt( short = "v", help = "Sets the level of verbosity", parse(from_occurrences) )] verbose: usize, #[structopt(long = "module", help = "Additional log target to enable")] module: Option, #[structopt(subcommand)] // Note that we mark a field as a subcommand cmd: Command, } fn main() -> Result<(), Box> { let app = SuperDeduper::from_args(); let mut modules = vec![module_path!().to_string()]; if let Some(module) = app.module { modules.push(module); } stderrlog::new() .verbosity(app.verbose) .timestamp(stderrlog::Timestamp::Millisecond) .modules(modules) .init() .unwrap(); match app.cmd { Command::Samples => { let lib = MovieLibrary::new(MOVIE_DIR); let videos = lib.videos()?; let samples_re = Regex::new(r"(?i).*sample.*").unwrap(); print_videos(&videos, Some(&samples_re)); } Command::Groups => { let lib = MovieLibrary::new(MOVIE_DIR); let videos = lib.videos()?; let mut video_groups: HashMap> = HashMap::new(); for (name, md) in videos.into_iter() { let clean_name = normalize(clean_path_parent(&name).to_str().unwrap()); let paths = video_groups.entry(clean_name).or_insert(Vec::new()); paths.push((name.to_string(), md)); } print_video_groups(&video_groups); } Command::CompactMetadata => { let lib = MovieLibrary::new(MOVIE_DIR); lib.compact_metadata()?; } Command::PrintDupes => { let lib = MovieLibrary::new(MOVIE_DIR); print_dupes(&lib); } Command::PrintAll => { let lib = MovieLibrary::new(MOVIE_DIR); let videos = lib.videos()?; print_all(videos); } Command::UpdateMetadata => { let lib = MovieLibrary::new(MOVIE_DIR); lib.update_metadata()?; } Command::UpdateAndCompactMetadata => { let lib = MovieLibrary::new(MOVIE_DIR); let new_videos = lib.update_metadata()?; if !new_videos.is_empty() { info!( "{} new videos added, recompacting metadata", new_videos.len() ); lib.compact_metadata()?; } } } Ok(()) }