use std::io::{self, BufRead, Write};
use std::process::ExitCode;
use anitomy_ng::{Element, Options};
use clap::Parser;
#[derive(Parser)]
#[command(
name = "anitomy",
version,
about = "Parse anime video filenames into their elements.",
long_about = "Parse anime video filenames into their elements.\n\n\
Provide filenames as arguments, or pipe them to stdin (one per line):\n\
\n \
anitomy '[Group] Title - 01 [1080p].mkv'\n \
ls *.mkv | anitomy --json"
)]
struct Cli {
#[arg(value_name = "FILENAME")]
filenames: Vec<String>,
#[arg(short, long)]
json: bool,
#[arg(long, help_heading = "Parsing toggles")]
no_episode: bool,
#[arg(long, help_heading = "Parsing toggles")]
no_episode_title: bool,
#[arg(long, help_heading = "Parsing toggles")]
no_file_checksum: bool,
#[arg(long, help_heading = "Parsing toggles")]
no_file_extension: bool,
#[arg(long, help_heading = "Parsing toggles")]
no_part: bool,
#[arg(long, help_heading = "Parsing toggles")]
no_release_group: bool,
#[arg(long, help_heading = "Parsing toggles")]
no_season: bool,
#[arg(long, help_heading = "Parsing toggles")]
no_title: bool,
#[arg(long, help_heading = "Parsing toggles")]
no_video_resolution: bool,
#[arg(long, help_heading = "Parsing toggles")]
no_year: bool,
}
impl Cli {
fn options(&self) -> Options {
Options {
parse_episode: !self.no_episode,
parse_episode_title: !self.no_episode_title,
parse_file_checksum: !self.no_file_checksum,
parse_file_extension: !self.no_file_extension,
parse_part: !self.no_part,
parse_release_group: !self.no_release_group,
parse_season: !self.no_season,
parse_title: !self.no_title,
parse_video_resolution: !self.no_video_resolution,
parse_year: !self.no_year,
}
}
}
fn main() -> ExitCode {
let cli = Cli::parse();
let options = cli.options();
let inputs: Vec<String> = if cli.filenames.is_empty() {
match read_stdin_lines() {
Ok(lines) => lines,
Err(err) => {
eprintln!("anitomy: error reading stdin: {err}");
return ExitCode::FAILURE;
}
}
} else {
cli.filenames.clone()
};
let results: Vec<(String, Vec<Element>)> = inputs
.into_iter()
.map(|name| {
let elements = anitomy_ng::parse(&name, options);
(name, elements)
})
.collect();
let stdout = io::stdout();
let mut out = stdout.lock();
let render = if cli.json {
write_json(&mut out, &results)
} else {
write_table(&mut out, &results)
};
match render {
Ok(()) => ExitCode::SUCCESS,
Err(err) if err.kind() == io::ErrorKind::BrokenPipe => ExitCode::SUCCESS,
Err(err) => {
eprintln!("anitomy: error writing output: {err}");
ExitCode::FAILURE
}
}
}
fn read_stdin_lines() -> io::Result<Vec<String>> {
let mut lines = Vec::new();
for line in io::stdin().lock().lines() {
let line = line?;
if !line.trim().is_empty() {
lines.push(line);
}
}
Ok(lines)
}
fn write_table(out: &mut impl Write, results: &[(String, Vec<Element>)]) -> io::Result<()> {
for (i, (filename, elements)) in results.iter().enumerate() {
if i > 0 {
writeln!(out)?;
}
writeln!(out, "{filename}")?;
if elements.is_empty() {
writeln!(out, " (no elements)")?;
continue;
}
let width = elements
.iter()
.map(|e| e.kind.as_str().len())
.max()
.unwrap_or(0);
for element in elements {
writeln!(
out,
" {:<width$} {}",
element.kind.as_str(),
element.value,
width = width
)?;
}
}
Ok(())
}
fn write_json(out: &mut impl Write, results: &[(String, Vec<Element>)]) -> io::Result<()> {
let json: Vec<serde_json::Value> = results
.iter()
.map(|(filename, elements)| {
let elements: Vec<serde_json::Value> = elements
.iter()
.map(|e| {
serde_json::json!({
"kind": e.kind.as_str(),
"value": e.value,
"position": e.position,
})
})
.collect();
serde_json::json!({ "filename": filename, "elements": elements })
})
.collect();
writeln!(out, "{}", serde_json::Value::Array(json))
}