From 8b809060ecbee5f59de00a9e78fd31db284f7fbb Mon Sep 17 00:00:00 2001 From: Bill Thiede Date: Sun, 3 Nov 2019 20:59:31 -0800 Subject: [PATCH] Fix `sample` command. Add largest_dimension and unitest. Use const for metadata filenames. --- src/lib.rs | 166 ++++++++++++++++++++++++++++++++++++++++------------ src/main.rs | 56 +++++++++--------- 2 files changed, 156 insertions(+), 66 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index eb70346..8ea5737 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -27,7 +27,10 @@ use serde::Deserialize; use serde::Serialize; use serde_json::Value; -#[derive(Clone, Deserialize, Debug, Serialize)] +const FULL_METADATA_FILENAME: &str = "metadata.json"; +const COMPACT_METADATA_FILENAME: &str = "metadata.compact.json"; + +#[derive(Clone, Deserialize, Debug, PartialEq, Serialize)] pub struct Resolution(usize, usize); impl Display for Resolution { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { @@ -57,7 +60,7 @@ where } #[derive(Clone, Deserialize, Debug, Serialize)] -pub struct Format { +struct Format { #[serde(deserialize_with = "from_str")] bit_rate: usize, #[serde(deserialize_with = "from_str")] @@ -83,7 +86,7 @@ impl Tags { // TODO(wathiede): make strem an enum with the tag type stored in codec_type? #[derive(Clone, Deserialize, Debug, Serialize)] #[serde(tag = "codec_type")] -pub enum Stream { +enum Stream { #[serde(rename = "video")] Video { #[serde(default, deserialize_with = "option_from_str")] @@ -127,7 +130,7 @@ impl Stream { } #[derive(Clone, Deserialize, Debug, Serialize)] -pub struct Metadata { +struct Metadata { format: Format, streams: Vec, } @@ -156,6 +159,20 @@ pub struct VideoFormat { language: Option, } +#[cfg(test)] +impl Default for VideoFormat { + fn default() -> Self { + VideoFormat { + short_name: "UNAMED_SHORT".to_string(), + long_name: "UNAMED_LONG".to_string(), + height: 0, + width: 0, + title: None, + language: None, + } + } +} + #[derive(Clone, Deserialize, Debug, Serialize)] pub struct AudioFormat { short_name: String, @@ -180,24 +197,54 @@ pub struct SubtitleFormat { #[derive(Clone, Deserialize, Debug, Serialize)] pub struct CompactMetadata { - #[serde(deserialize_with = "from_str")] bit_rate: usize, - #[serde(deserialize_with = "from_str")] - duration: f32, + pub duration: f32, filename: String, format_name: String, - #[serde(deserialize_with = "from_str")] - size: usize, + pub size: usize, video: Vec, audio: Vec, - #[serde(skip_serializing_if = "Vec::is_empty")] subtitle: Vec, } +impl CompactMetadata { + pub fn largest_dimension(&self) -> Option { + if self.video.is_empty() { + return None; + } + Some(self.video.iter().fold( + Resolution(0, 0), + |acc, VideoFormat { width, height, .. }| { + if acc.0 * acc.1 < width * height { + Resolution(*width, *height) + } else { + acc + } + }, + )) + } +} + +#[cfg(test)] +impl Default for CompactMetadata { + fn default() -> Self { + CompactMetadata { + bit_rate: 0, + duration: 0., + filename: "UNSET".to_string(), + format_name: "UNKNOWN".to_string(), + size: 0, + video: Vec::new(), + audio: Vec::new(), + subtitle: Vec::new(), + } + } +} + #[derive(Deserialize, Debug, Serialize)] pub struct MetadataFile { #[serde(flatten)] - pub metadata: HashMap, + metadata: HashMap, } pub struct MovieLibrary { @@ -241,7 +288,15 @@ impl MovieLibrary { } pub fn compact_metadata(&self) -> Result<(), Error> { - let mdf = read_metadata_from_file(Path::new(&self.root).join("metadata.json"))?; + let path = Path::new(&self.root).join(FULL_METADATA_FILENAME); + // Open the file in read-only mode with buffer. + let f = File::open(&path).context(format!("open {}", path.display()))?; + let r = BufReader::new(f); + + // Read the JSON contents of the file as an instance of `User`. + let mdf: MetadataFile = serde_json::from_reader(r) + .context(format!("serde_json::from_reader {}", path.display()))?; + info!("Read metadata, {} videos found", mdf.metadata.len()); let metadata: HashMap = mdf @@ -340,13 +395,13 @@ impl MovieLibrary { }) .collect(); - let f = File::create(Path::new(&self.root).join("metadata.compact.json"))?; + let f = File::create(Path::new(&self.root).join(COMPACT_METADATA_FILENAME))?; let f = BufWriter::new(f); Ok(serde_json::ser::to_writer_pretty(f, &metadata)?) } pub fn update_metadata(&self) -> Result, Error> { - let path = Path::new(&self.root).join("metadata.json"); + let path = Path::new(&self.root).join(FULL_METADATA_FILENAME); // Open the file in read-only mode with buffer. let f = File::open(&path).context(format!("open {}", path.display()))?; let r = BufReader::new(f); @@ -389,14 +444,14 @@ impl MovieLibrary { }) .map(|(path, json)| (path, serde_json::from_str::(&json).unwrap())) .collect(); - let new_movies = metadata.keys().cloned().collect(); + let new_videos = metadata.keys().cloned().collect(); info!("Adding {} new videos", metadata.len()); metadata.extend(old_metadata); - let f = File::create(Path::new(&self.root).join("metadata.json"))?; + let f = File::create(Path::new(&self.root).join(FULL_METADATA_FILENAME))?; let f = BufWriter::new(f); serde_json::ser::to_writer_pretty(f, &metadata)?; - Ok(new_movies) + Ok(new_videos) } fn iter_video_files(&self) -> impl Send + Iterator> { @@ -415,25 +470,16 @@ impl MovieLibrary { }) } - pub fn movies(&self, include_stale: bool) -> Result<(HashMap), Error> { - let mut movies = HashMap::new(); - for md in glob(&format!("{}/*/metadata.json", self.root))? { - let path = md?; - let mdf = read_metadata_from_file(&path)?; - for (name, md) in mdf.metadata { - if include_stale { - movies.insert(name, md); - } else { - // Filter out files that don't exist - let mut p = PathBuf::from(&self.root); - p.push(&name); - if p.is_file() { - movies.insert(name, md); - } - } - } - } - Ok(movies) + pub fn videos(&self, include_stale: bool) -> Result<(HashMap), Error> { + // TODO(wathiede): implement include_stale. + let path = Path::new(&self.root).join(COMPACT_METADATA_FILENAME); + // Open the file in read-only mode with buffer. + let f = File::open(&path).context(format!("open {}", path.display()))?; + let r = BufReader::new(f); + + // Read the JSON contents of the file as an instance of `User`. + Ok(serde_json::from_reader(r) + .context(format!("serde_json::from_reader {}", path.display()))?) } } @@ -459,10 +505,54 @@ mod tests { format!("{}/testdata", env::var("CARGO_MANIFEST_DIR").unwrap()) } + #[test] + fn largest_dimension() { + let md = CompactMetadata { + ..Default::default() + }; + assert_eq!(md.largest_dimension(), None); + + let md = CompactMetadata { + video: vec![ + VideoFormat { + height: 3, + width: 4, + ..Default::default() + }, + VideoFormat { + width: 640, + height: 480, + ..Default::default() + }, + ], + ..Default::default() + }; + assert_eq!(md.largest_dimension(), Some(Resolution(640, 480))); + + let md = CompactMetadata { + video: vec![ + VideoFormat { + width: 640, + height: 480, + ..Default::default() + }, + VideoFormat { + height: 3, + width: 4, + ..Default::default() + }, + ], + ..Default::default() + }; + assert_eq!(md.largest_dimension(), Some(Resolution(640, 480))); + } + #[test] fn test_read_full_metadata() { - let mdf = read_metadata_from_file(Path::new(&testdata_dir()).join("Movies/metadata.json")) - .expect("failed to read metadata"); + let mdf = read_metadata_from_file( + Path::new(&testdata_dir()).join(&format!("Movies/{}", FULL_METADATA_FILENAME)), + ) + .expect("failed to read metadata"); assert_eq!(mdf.metadata.len(), 1214); } diff --git a/src/main.rs b/src/main.rs index cd07120..0b427a6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -9,7 +9,7 @@ use log::info; use regex::Regex; use structopt::StructOpt; -use superdeduper::Metadata; +use superdeduper::CompactMetadata; use superdeduper::MovieLibrary; const MOVIE_DIR: &str = "/home/wathiede/Movies"; @@ -25,35 +25,35 @@ fn clean_path_parent>(path: P) -> PathBuf { PathBuf::from(path) } -fn print_movie_groups(movie_groups: &HashMap>) { - let mut names = movie_groups.keys().collect::>(); +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 = &movie_groups[name]; + let paths = &video_groups[name]; if paths.len() < 2 { continue; } - let mut file: Vec<_> = movie_groups[name].iter().collect(); + let mut file: Vec<_> = video_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(), + md.largest_dimension().unwrap(), + fmtr.format(md.size as f64), + md.duration, &p[p.rfind("/").unwrap() + 1..] ); } } } -fn print_movies(movies: &HashMap, filter: Option<&Regex>) { - let mut names = movies.keys().collect::>(); +fn print_videos(videos: &HashMap, filter: Option<&Regex>) { + let mut names = videos.keys().collect::>(); names.sort(); let mut fmtr = Formatter::new(); fmtr.with_separator(""); @@ -64,12 +64,12 @@ fn print_movies(movies: &HashMap, filter: Option<&Regex>) { continue; } } - let md = &movies[name]; + let md = &videos[name]; info!( "{:>9} {:>8} {} {}", - md.dimension().unwrap(), - fmtr.format(md.size() as f64), - md.duration(), + md.largest_dimension().unwrap(), + fmtr.format(md.size as f64), + md.duration, &name[MOVIE_DIR.len() + 1..] ); println!("mv '{}' '{}'", name, TO_BE_REMOVED_DIR); @@ -78,9 +78,9 @@ fn print_movies(movies: &HashMap, filter: Option<&Regex>) { #[derive(StructOpt)] enum Command { - #[structopt(name = "samples", about = "Print movie files deemed to be samples")] + #[structopt(name = "samples", about = "Print video files deemed to be samples")] Samples, - #[structopt(name = "groups", about = "Print movies grouped by root name")] + #[structopt(name = "groups", about = "Print videos grouped by root name")] Groups, #[structopt( name = "compact-metadata", @@ -99,7 +99,7 @@ enum Command { #[derive(StructOpt)] #[structopt( name = "superdeduper", - about = "Tool for pruning extra movies in collection" + about = "Tool for pruning extra videos in collection" )] struct SuperDeduper { #[structopt( @@ -130,23 +130,23 @@ fn main() -> Result<(), Box> { match app.cmd { Command::Samples => { let lib = MovieLibrary::new(MOVIE_DIR); - let movies = lib.movies(false)?; + let videos = lib.videos(false)?; let samples_re = Regex::new(r"(?i).*sample.*").unwrap(); - print_movies(&movies, Some(&samples_re)); + print_videos(&videos, Some(&samples_re)); } Command::Groups => { let lib = MovieLibrary::new(MOVIE_DIR); - let movies = lib.movies(false)?; + let videos = lib.videos(false)?; - let mut movie_groups: HashMap> = HashMap::new(); - for (name, md) in movies.into_iter() { + let mut video_groups: HashMap> = HashMap::new(); + for (name, md) in videos.into_iter() { let clean_name = clean_path_parent(&name); - let paths = movie_groups.entry(clean_name).or_insert(Vec::new()); + let paths = video_groups.entry(clean_name).or_insert(Vec::new()); paths.push((name.to_string(), md)); } - print_movie_groups(&movie_groups); + print_video_groups(&video_groups); } Command::CompactMetadata => { let lib = MovieLibrary::new(MOVIE_DIR); @@ -158,11 +158,11 @@ fn main() -> Result<(), Box> { } Command::UpdateAndCompactMetadata => { let lib = MovieLibrary::new(MOVIE_DIR); - let new_movies = lib.update_metadata()?; - if !new_movies.is_empty() { + let new_videos = lib.update_metadata()?; + if !new_videos.is_empty() { info!( - "{} new movies added, recompacting metadata", - new_movies.len() + "{} new videos added, recompacting metadata", + new_videos.len() ); lib.compact_metadata()?; }