use clap::{Parser, Subcommand};
use std::path::PathBuf;
mod commands;
mod encoding;
mod frequency;
mod output;
#[derive(Parser)]
#[command(name = "kino-cli")]
#[command(author = "Purple Squirrel Media")]
#[command(version)]
#[command(about = "Video streaming analysis and QC toolkit", long_about = None)]
struct Cli {
#[arg(short, long)]
verbose: bool,
#[arg(short, long, default_value = "text")]
format: String,
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand)]
enum Commands {
Analyze {
manifest: String,
},
Validate {
manifest: String,
#[arg(short, long, default_value = "10")]
segments: usize,
#[arg(short, long)]
all_renditions: bool,
},
Qc {
manifest: String,
#[arg(short, long)]
output: Option<PathBuf>,
#[arg(long)]
strict: bool,
},
Extract {
manifest: String,
#[arg(short, long, default_value = "all")]
what: String,
},
Compare {
manifest1: String,
manifest2: String,
},
Monitor {
manifest: String,
#[arg(short, long, default_value = "5")]
interval: u64,
#[arg(short, long, default_value = "0")]
duration: u64,
},
Encode {
input: PathBuf,
#[arg(short, long)]
output: PathBuf,
#[arg(short = 'f', long, default_value = "hls")]
format: String,
#[arg(short, long, default_value = "web")]
preset: String,
#[arg(short, long)]
segment_duration: Option<f64>,
},
Preset {
#[arg(default_value = "list")]
name: String,
},
Frequency {
input: PathBuf,
#[arg(short = 'k', long, default_value = "10")]
top_k: usize,
#[arg(long)]
json: bool,
},
Fingerprint {
input: PathBuf,
#[arg(short, long)]
output: Option<PathBuf>,
#[arg(long)]
verify: Option<String>,
},
Autotag {
input: PathBuf,
#[arg(short, long, default_value = "5")]
max_tags: usize,
#[arg(short = 'c', long, default_value = "0.3")]
min_confidence: f32,
},
Thumbnail {
input: PathBuf,
#[arg(short, long)]
output: Option<PathBuf>,
#[arg(short = 'n', long, default_value = "1")]
candidates: usize,
},
Similar {
input: PathBuf,
#[arg(short, long)]
library: PathBuf,
#[arg(short = 'n', long, default_value = "10")]
limit: usize,
},
Process {
input: PathBuf,
#[arg(short, long)]
output: PathBuf,
#[arg(long)]
skip_fingerprint: bool,
#[arg(long)]
skip_tags: bool,
#[arg(long)]
skip_thumbnail: bool,
},
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let cli = Cli::parse();
let level = if cli.verbose { "debug" } else { "info" };
tracing_subscriber::fmt()
.with_env_filter(level)
.init();
match cli.command {
Commands::Analyze { manifest } => {
commands::analyze(&manifest, &cli.format).await?;
}
Commands::Validate { manifest, segments, all_renditions } => {
commands::validate(&manifest, segments, all_renditions, &cli.format).await?;
}
Commands::Qc { manifest, output, strict } => {
commands::qc(&manifest, output, strict, &cli.format).await?;
}
Commands::Extract { manifest, what } => {
commands::extract(&manifest, &what, &cli.format).await?;
}
Commands::Compare { manifest1, manifest2 } => {
commands::compare(&manifest1, &manifest2, &cli.format).await?;
}
Commands::Monitor { manifest, interval, duration } => {
commands::monitor(&manifest, interval, duration, &cli.format).await?;
}
Commands::Encode { input, output, format, preset, segment_duration } => {
match encoding::check_ffmpeg() {
Ok(version) => println!("Using: {}", version),
Err(e) => {
eprintln!("Error: {}", e);
std::process::exit(1);
}
}
let enc_preset = encoding::EncodingPreset::from_str(&preset)
.unwrap_or_else(|| {
eprintln!("Unknown preset '{}', using 'web'", preset);
encoding::EncodingPreset::Web
});
let seg_dur = segment_duration.unwrap_or(enc_preset.segment_duration());
let output_format = encoding::OutputFormat::from_str(&format)
.unwrap_or(encoding::OutputFormat::Hls);
match output_format {
encoding::OutputFormat::Hls => {
encoding::encode_hls(&input, &output, enc_preset, seg_dur, None)?;
}
encoding::OutputFormat::Dash => {
encoding::encode_dash(&input, &output, enc_preset, seg_dur)?;
}
encoding::OutputFormat::Both => {
let hls_dir = output.join("hls");
let dash_dir = output.join("dash");
encoding::encode_hls(&input, &hls_dir, enc_preset, seg_dur, None)?;
encoding::encode_dash(&input, &dash_dir, enc_preset, seg_dur)?;
}
}
}
Commands::Preset { name } => {
if name == "list" {
encoding::list_presets();
} else {
encoding::show_preset(&name);
}
}
Commands::Frequency { input, top_k, json } => {
frequency::analyze_frequency(&input, top_k, json).await?;
}
Commands::Fingerprint { input, output, verify } => {
frequency::fingerprint(&input, output, verify).await?;
}
Commands::Autotag { input, max_tags, min_confidence } => {
frequency::autotag(&input, max_tags, min_confidence).await?;
}
Commands::Thumbnail { input, output, candidates } => {
frequency::thumbnail(&input, output, candidates).await?;
}
Commands::Similar { input, library, limit } => {
frequency::similar(&input, &library, limit).await?;
}
Commands::Process { input, output, skip_fingerprint, skip_tags, skip_thumbnail } => {
frequency::process(&input, &output, skip_fingerprint, skip_tags, skip_thumbnail).await?;
}
}
Ok(())
}