use std::env;
use std::fs;
use std::path::Path;
use std::process;
use std::time::UNIX_EPOCH;
use revelo_core::computed_fields::fill_computed_fields;
use revelo_core::multi_file::MultiFileLoader;
use revelo_core::multi_file::find_duplicate_streams;
use revelo_core::{FileAnalyze, FileLevelInfo, fill_file_level_fields};
use revelo_dispatcher::detect;
use revelo_export::{to_json, to_text, to_xml};
fn main() -> process::ExitCode {
let mut args: Vec<String> = env::args().skip(1).collect();
let mut xml_mode = false;
let mut json_mode = false;
let mut demux_level = String::from("frame");
let mut trace_level = String::from("0");
let mut multi_file = false;
args.retain(|a| {
if a == "--xml" {
xml_mode = true;
false
} else if a == "--json" {
json_mode = true;
false
} else if a.starts_with("--demux=") {
demux_level = a[8..].to_string();
false
} else if a.starts_with("--trace=") {
trace_level = a[8..].to_string();
false
} else if a == "--multi-file" {
multi_file = true;
false
} else {
true
}
});
if args.is_empty() {
eprintln!("{}", include_str!("banner.txt"));
eprintln!(
"Usage: revelo [--xml|--json] <file-path>
Options:
--xml XML output
--json JSON output
--multi-file scan companion files (BDMV M2TS, sidecar subtitles)
--demux=N demux level: frame (default), container, elementary
--trace=N trace verbosity (0-9)
"
);
return process::ExitCode::SUCCESS;
}
let path = &args[0];
let bytes = match fs::read(path) {
Ok(b) => b,
Err(e) => {
eprintln!("{path}: {e}");
return process::ExitCode::from(1);
}
};
let metadata = fs::metadata(path).ok();
let mut parsed = false;
let mut extra_data: Option<Vec<u8>> = None;
if multi_file {
let mut loader = MultiFileLoader::new();
loader.scan_references(std::path::Path::new(path), &Default::default());
if let Some((data, _count)) = loader.load_all() {
extra_data = Some(data);
}
}
let parse_buf: Vec<u8> = if let Some(ref extra) = extra_data {
let mut combined = bytes.clone();
combined.extend_from_slice(extra);
combined
} else {
bytes.clone()
};
let winner = detect(&parse_buf);
if let Some(winner) = winner {
let mut fa = FileAnalyze::new(&parse_buf);
fa.set_option("demux", &demux_level);
fa.set_option("trace_level", &trace_level);
fa.set_option("multi_file", if multi_file { "1" } else { "0" });
if let Some(ref _extra) = extra_data {
fa.reference_count = 1;
}
if winner(&mut fa) {
parsed = true;
let modified_unix_secs = metadata
.as_ref()
.and_then(|m| m.modified().ok())
.and_then(|t| t.duration_since(UNIX_EPOCH).ok())
.map(|d| d.as_secs() as i64);
let info = FileLevelInfo {
file_size: metadata.as_ref().map(|m| m.len()).unwrap_or(bytes.len() as u64),
extension: Path::new(path).extension().and_then(|s| s.to_str()),
modified_unix_secs,
local_offset_secs: local_offset_seconds(),
};
fill_file_level_fields(&mut fa, &info);
fill_computed_fields(fa.streams_mut());
fa.duplicate_indices = find_duplicate_streams(fa.streams());
let output = if json_mode {
to_json(fa.streams(), path, env!("CARGO_PKG_VERSION"))
} else if xml_mode {
to_xml(fa.streams(), path, env!("CARGO_PKG_VERSION"))
} else {
to_text(fa.streams(), path)
};
println!("{output}");
}
}
if !parsed {
eprintln!("{path}: no parser matched ({} bytes)", bytes.len());
return process::ExitCode::from(1);
}
process::ExitCode::SUCCESS
}
fn local_offset_seconds() -> i64 {
let Ok(out) = process::Command::new("date").arg("+%z").output() else {
return 0;
};
let s = String::from_utf8_lossy(&out.stdout);
let s = s.trim();
if s.len() < 5 {
return 0;
}
let sign = if s.starts_with('-') { -1 } else { 1 };
let hh: i64 = s[1..3].parse().unwrap_or(0);
let mm: i64 = s[3..5].parse().unwrap_or(0);
sign * (hh * 3600 + mm * 60)
}