254 lines
7.9 KiB
Rust
254 lines
7.9 KiB
Rust
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<String, CompactMetadata>) {
|
|
let mut names = videos.keys().collect::<Vec<_>>();
|
|
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<String, Vec<(String, CompactMetadata)>>) {
|
|
let mut names = video_groups.keys().collect::<Vec<_>>();
|
|
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<String, CompactMetadata>, filter: Option<&Regex>) {
|
|
let mut names = videos.keys().collect::<Vec<_>>();
|
|
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<String>,
|
|
#[structopt(subcommand)] // Note that we mark a field as a subcommand
|
|
cmd: Command,
|
|
}
|
|
|
|
fn main() -> Result<(), Box<dyn Error>> {
|
|
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<String, Vec<(String, CompactMetadata)>> = 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(())
|
|
}
|