use std::{ error::Error, fs::{self, File}, io::{self}, path::Path, process::ExitCode, }; use clap::Parser; use nom_exif::{ExifIter, MediaParser, MediaSource, TrackInfo}; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, Registry}; #[derive(Parser, Debug)] #[command(author, version, about, long_about = None)] struct Cli { file: String, #[arg(short, long)] json: bool, #[arg(long)] debug: bool, } #[cfg(feature = "json_dump")] const FEATURE_JSON_DUMP_ON: bool = true; #[cfg(not(feature = "json_dump"))] const FEATURE_JSON_DUMP_ON: bool = false; fn main() -> ExitCode { let cli = Cli::parse(); tracing_run(&cli) } #[tracing::instrument] fn tracing_run(cli: &Cli) -> ExitCode { if cli.debug { init_tracing().expect("init tracing failed"); } if let Err(err) = run(cli) { tracing::error!(?err); eprintln!("{err}"); return ExitCode::FAILURE; } ExitCode::SUCCESS } fn run(cli: &Cli) -> Result<(), Box> { if cli.json && !FEATURE_JSON_DUMP_ON { let msg = "-j/--json option requires the feature `json_dump`."; eprintln!("{msg}"); return Err(msg.into()); } let mut parser = MediaParser::new(); let path = Path::new(&cli.file); if path.is_file() { let _ = parse_file(&mut parser, path, cli.json); } else if path.is_dir() { parse_dir(path, parser, cli)?; } Ok(()) } fn parse_dir(path: &Path, mut parser: MediaParser, cli: &Cli) -> Result<(), Box> { let mut first = true; for entry in fs::read_dir(path)? { if first { first = false; } else { println!(); } match entry { Ok(entry) => { let Ok(ft) = entry.file_type() else { continue; }; if !ft.is_file() { continue; } println!("File: {:?}", entry.path().as_os_str()); println!("------------------------------------------------"); let _ = parse_file(&mut parser, entry.path(), cli.json); } Err(e) => { eprintln!("Read dir entry failed: {e}"); continue; } } } Ok(()) } fn parse_file>( parser: &mut MediaParser, path: P, json: bool, ) -> Result<(), nom_exif::Error> { let ms = MediaSource::file_path(path).inspect_err(handle_parsing_error)?; let values = if ms.has_exif() { let iter: ExifIter = parser.parse(ms).inspect_err(handle_parsing_error)?; iter.into_iter() .filter_map(|mut x| { let res = x.take_result(); match res { Ok(v) => Some(( x.tag() .map(|x| x.to_string()) .unwrap_or_else(|| format!("Unknown(0x{:04x})", x.tag_code())), v, )), Err(e) => { tracing::warn!(?e); None } } }) .collect::>() } else { let info: TrackInfo = parser.parse(ms)?; info.into_iter() .map(|x| (x.0.to_string(), x.1)) .collect::>() }; if json { #[cfg(feature = "json_dump")] use std::collections::HashMap; #[cfg(feature = "json_dump")] match serde_json::to_string_pretty( &values .into_iter() .map(|x| (x.0.to_string(), x.1)) .collect::>(), ) { Ok(s) => { println!("{}", s); } Err(e) => eprintln!("Error: {e}"), } } else { values.iter().for_each(|x| { println!("{:<32}=> {}", x.0, x.1); }); }; Ok(()) } fn handle_parsing_error(e: &nom_exif::Error) { match e { nom_exif::Error::UnrecognizedFileFormat => { eprintln!("Unrecognized file format, consider filing a bug @ https://github.com/mindeng/nom-exif."); } nom_exif::Error::ParseFailed(_) | nom_exif::Error::IOError(_) => { eprintln!("Error: {e}"); } } } fn init_tracing() -> io::Result<()> { let stdout_log = tracing_subscriber::fmt::layer().pretty(); let subscriber = Registry::default().with(stdout_log); let file = File::create("debug.log")?; let debug_log = tracing_subscriber::fmt::layer() .with_ansi(false) .with_writer(file); let subscriber = subscriber.with(debug_log); subscriber.init(); Ok(()) }