superdeduper/src/main.rs
Bill Thiede bf9d5b7c11 Use ffprobe to load per-file metadata.
Build single metadata.json for whole library.
Have basic load into struct, a subset of useful fields fromt metadata.json.
2019-11-02 22:09:50 -07:00

157 lines
4.6 KiB
Rust

use std::collections::HashMap;
use std::error::Error;
use std::path::Path;
use std::path::PathBuf;
use human_format::Formatter;
use human_format::Scales;
use log::info;
use regex::Regex;
use structopt::StructOpt;
use superdeduper::Metadata;
use superdeduper::MovieLibrary;
const MOVIE_DIR: &str = "/home/wathiede/Movies";
const TO_BE_REMOVED_DIR: &str = "/storage/media/to-be-deleted/";
fn clean_path_parent<P: AsRef<Path>>(path: P) -> PathBuf {
let path = path.as_ref();
let path = path.parent().unwrap();
let mut path = path.to_str().unwrap();
if path.ends_with(')') {
path = &path[..path.len() - 7];
}
PathBuf::from(path)
}
fn print_movie_groups(movie_groups: &HashMap<PathBuf, Vec<(String, Metadata)>>) {
let mut names = movie_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 = &movie_groups[name];
if paths.len() < 2 {
continue;
}
let mut file: Vec<_> = movie_groups[name].iter().collect();
file.sort_by(|(n1, _), (n2, _)| n1.partial_cmp(n2).unwrap());
println!("{}:", name.display());
for (p, md) in file {
println!(
" {:>9} {:>9} {} {}",
md.dimension().unwrap(),
fmtr.format(md.size() as f64),
md.duration(),
&p[p.rfind("/").unwrap() + 1..]
);
}
}
}
fn print_movies(movies: &HashMap<String, Metadata>, filter: Option<&Regex>) {
let mut names = movies.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 = &movies[name];
info!(
"{:>9} {:>8} {} {}",
md.dimension().unwrap(),
fmtr.format(md.size() as f64),
md.duration(),
&name[MOVIE_DIR.len() + 1..]
);
println!("mv '{}' '{}'", name, TO_BE_REMOVED_DIR);
}
}
#[derive(StructOpt)]
enum Command {
#[structopt(name = "samples", about = "Print movie files deemed to be samples")]
Samples,
#[structopt(name = "groups", about = "Print movies grouped by root name")]
Groups,
#[structopt(
name = "compact-metadata",
about = "Read full metadata file and write compact file."
)]
CompactMetadata,
#[structopt(name = "update-metadata", about = "Write metadata files")]
UpdateMetadata,
}
#[derive(StructOpt)]
#[structopt(
name = "superdeduper",
about = "Tool for pruning extra movies 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 movies = lib.movies(false)?;
let samples_re = Regex::new(r"(?i).*sample.*").unwrap();
print_movies(&movies, Some(&samples_re));
}
Command::Groups => {
let lib = MovieLibrary::new(MOVIE_DIR);
let movies = lib.movies(false)?;
let mut movie_groups: HashMap<PathBuf, Vec<(String, Metadata)>> = HashMap::new();
for (name, md) in movies.into_iter() {
let clean_name = clean_path_parent(&name);
let paths = movie_groups.entry(clean_name).or_insert(Vec::new());
paths.push((name.to_string(), md));
}
print_movie_groups(&movie_groups);
}
Command::CompactMetadata => {
let lib = MovieLibrary::new(MOVIE_DIR);
lib.compact_metadata()?;
}
Command::UpdateMetadata => {
let lib = MovieLibrary::new(MOVIE_DIR);
lib.update_metadata()?;
}
}
Ok(())
}