use clap::{Parser, Subcommand, ValueEnum};
#[derive(Debug, Clone, ValueEnum)]
pub enum Format {
#[cfg(feature = "srt")]
Srt,
#[cfg(feature = "vtt")]
Vtt,
#[cfg(feature = "ass")]
Ass,
#[cfg(feature = "ssa")]
Ssa,
#[cfg(feature = "microdvd")]
#[value(name = "microdvd")]
MicroDvd,
#[cfg(feature = "subviewer")]
#[value(name = "subviewer")]
SubViewer,
#[cfg(feature = "ttml")]
Ttml,
#[cfg(feature = "sbv")]
Sbv,
#[cfg(feature = "lrc")]
Lrc,
#[cfg(feature = "sami")]
Sami,
#[cfg(feature = "mpl2")]
#[value(name = "mpl2")]
Mpl2,
#[cfg(feature = "scc")]
#[value(name = "scc")]
Scc,
#[cfg(feature = "ebu_stl")]
#[value(name = "stl")]
EbuStl,
}
impl Format {
pub fn from_ext(path: &str) -> Option<Self> {
let lower = path.to_lowercase();
#[cfg(feature = "srt")]
if lower.ends_with(".srt") {
return Some(Format::Srt);
}
#[cfg(feature = "vtt")]
if lower.ends_with(".vtt") {
return Some(Format::Vtt);
}
#[cfg(feature = "ass")]
if lower.ends_with(".ass") {
return Some(Format::Ass);
}
#[cfg(feature = "ssa")]
if lower.ends_with(".ssa") {
return Some(Format::Ssa);
}
#[cfg(feature = "microdvd")]
if lower.ends_with(".sub") {
return Some(Format::MicroDvd);
}
#[cfg(feature = "ttml")]
if lower.ends_with(".ttml") || lower.ends_with(".xml") {
return Some(Format::Ttml);
}
#[cfg(feature = "sbv")]
if lower.ends_with(".sbv") {
return Some(Format::Sbv);
}
#[cfg(feature = "lrc")]
if lower.ends_with(".lrc") {
return Some(Format::Lrc);
}
#[cfg(feature = "sami")]
if lower.ends_with(".smi") || lower.ends_with(".sami") {
return Some(Format::Sami);
}
#[cfg(feature = "mpl2")]
if lower.ends_with(".mpl") || lower.ends_with(".txt") {
return Some(Format::Mpl2);
}
#[cfg(feature = "scc")]
if lower.ends_with(".scc") {
return Some(Format::Scc);
}
#[cfg(feature = "ebu_stl")]
if lower.ends_with(".stl") {
return Some(Format::EbuStl);
}
None
}
}
impl From<&subtitler::model::Format> for Format {
fn from(f: &subtitler::model::Format) -> Self {
use subtitler::model::Format as M;
match f {
#[cfg(feature = "srt")]
M::Srt => Format::Srt,
#[cfg(feature = "vtt")]
M::Vtt => Format::Vtt,
#[cfg(feature = "ass")]
M::Ass => Format::Ass,
#[cfg(feature = "ssa")]
M::Ssa => Format::Ssa,
#[cfg(feature = "microdvd")]
M::MicroDvd => Format::MicroDvd,
#[cfg(feature = "subviewer")]
M::SubViewer => Format::SubViewer,
#[cfg(feature = "ttml")]
M::Ttml => Format::Ttml,
#[cfg(feature = "sbv")]
M::Sbv => Format::Sbv,
#[cfg(feature = "lrc")]
M::Lrc => Format::Lrc,
#[cfg(feature = "sami")]
M::Sami => Format::Sami,
#[cfg(feature = "mpl2")]
M::Mpl2 => Format::Mpl2,
#[cfg(feature = "scc")]
M::Scc => Format::Scc,
#[cfg(feature = "ebu_stl")]
M::EbuStl => Format::EbuStl,
}
}
}
impl From<&Format> for subtitler::model::Format {
fn from(f: &Format) -> Self {
use subtitler::model::Format as M;
match f {
#[cfg(feature = "srt")]
Format::Srt => M::Srt,
#[cfg(feature = "vtt")]
Format::Vtt => M::Vtt,
#[cfg(feature = "ass")]
Format::Ass => M::Ass,
#[cfg(feature = "ssa")]
Format::Ssa => M::Ssa,
#[cfg(feature = "microdvd")]
Format::MicroDvd => M::MicroDvd,
#[cfg(feature = "subviewer")]
Format::SubViewer => M::SubViewer,
#[cfg(feature = "ttml")]
Format::Ttml => M::Ttml,
#[cfg(feature = "sbv")]
Format::Sbv => M::Sbv,
#[cfg(feature = "lrc")]
Format::Lrc => M::Lrc,
#[cfg(feature = "sami")]
Format::Sami => M::Sami,
#[cfg(feature = "mpl2")]
Format::Mpl2 => M::Mpl2,
#[cfg(feature = "scc")]
Format::Scc => M::Scc,
#[cfg(feature = "ebu_stl")]
Format::EbuStl => M::EbuStl,
}
}
}
impl std::fmt::Display for Format {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
#[cfg(feature = "srt")]
Format::Srt => write!(f, "srt"),
#[cfg(feature = "vtt")]
Format::Vtt => write!(f, "vtt"),
#[cfg(feature = "ass")]
Format::Ass => write!(f, "ass"),
#[cfg(feature = "ssa")]
Format::Ssa => write!(f, "ssa"),
#[cfg(feature = "microdvd")]
Format::MicroDvd => write!(f, "microdvd"),
#[cfg(feature = "subviewer")]
Format::SubViewer => write!(f, "subviewer"),
#[cfg(feature = "ttml")]
Format::Ttml => write!(f, "ttml"),
#[cfg(feature = "sbv")]
Format::Sbv => write!(f, "sbv"),
#[cfg(feature = "lrc")]
Format::Lrc => write!(f, "lrc"),
#[cfg(feature = "sami")]
Format::Sami => write!(f, "sami"),
#[cfg(feature = "mpl2")]
Format::Mpl2 => write!(f, "MPL2"),
#[cfg(feature = "scc")]
Format::Scc => write!(f, "SCC"),
#[cfg(feature = "ebu_stl")]
Format::EbuStl => write!(f, "EBU STL"),
}
}
}
#[derive(Parser)]
#[command(name = "subtitler")]
#[command(
about = "Subtitle toolkit: parse, convert, validate, edit, and analyze subtitles across 13 formats."
)]
#[command(version)]
pub struct Cli {
#[command(subcommand)]
pub command: Commands,
}
#[derive(Subcommand)]
pub enum Commands {
Parse(ParseArgs),
Convert(ConvertArgs),
Validate(ValidateArgs),
Edit(EditArgs),
Pipeline(PipelineArgs),
Info(InfoArgs),
Detect(DetectArgs),
Quality(QualityArgs),
Normalize(NormalizeArgs),
Shift(ShiftArgs),
}
#[derive(clap::Args)]
pub struct ParseArgs {
pub input: String,
#[arg(short, long)]
pub format: Option<Format>,
#[arg(short, long)]
pub json: bool,
}
#[derive(clap::Args)]
pub struct ConvertArgs {
pub input: String,
pub output: String,
#[arg(short, long)]
pub from: Option<Format>,
#[arg(short, long)]
pub to: Option<Format>,
#[arg(long)]
pub fps: Option<f64>,
#[arg(long, allow_hyphen_values = true)]
pub shift: Option<i64>,
}
#[derive(clap::Args)]
pub struct ValidateArgs {
pub input: String,
#[arg(long, default_value = "42")]
pub max_chars: usize,
#[arg(long, default_value = "5000")]
pub max_gap: u64,
#[arg(long, default_value = "25.0")]
pub max_cps: f64,
#[arg(long)]
pub basic: bool,
#[arg(short, long)]
pub json: bool,
}
#[derive(clap::Args)]
pub struct EditArgs {
pub input: String,
#[arg(short, long)]
pub output: String,
#[arg(long)]
pub sort: bool,
#[arg(long, allow_hyphen_values = true)]
pub shift: Option<i64>,
#[arg(long)]
pub merge: Option<u64>,
#[arg(long)]
pub split: Option<usize>,
#[arg(long, value_names = &["FROM_FPS", "TO_FPS"], number_of_values = 2)]
pub transform_fps: Option<Vec<f64>>,
#[arg(short, long)]
pub from: Option<Format>,
#[arg(short, long)]
pub to: Option<Format>,
}
#[derive(clap::Args)]
pub struct InfoArgs {
pub input: String,
}
#[derive(clap::Args)]
pub struct DetectArgs {
pub input: String,
}
#[derive(clap::Args)]
pub struct QualityArgs {
pub input: String,
#[arg(long, default_value = "42")]
pub max_chars: usize,
#[arg(long, default_value = "5000")]
pub max_gap: u64,
#[arg(long, default_value = "25.0")]
pub max_cps: f64,
#[arg(short, long)]
pub json: bool,
}
#[derive(clap::Args)]
pub struct NormalizeArgs {
pub input: String,
#[arg(short, long)]
pub output: String,
#[arg(long)]
pub strip_hi: bool,
#[arg(long)]
pub fix_ocr: bool,
#[arg(long)]
pub quotes: bool,
#[arg(long)]
pub whitespace: bool,
#[arg(long)]
pub all: bool,
#[arg(short, long)]
pub format: Option<Format>,
}
#[derive(clap::Args)]
pub struct ShiftArgs {
pub input: String,
#[arg(short, long)]
pub output: String,
#[arg(allow_hyphen_values = true)]
pub offset: i64,
#[arg(short, long)]
pub format: Option<Format>,
}
#[derive(clap::Args)]
pub struct PipelineArgs {
pub input: String,
pub output: String,
#[arg(short, long)]
pub config: String,
#[arg(short, long)]
pub to: Option<Format>,
#[arg(short, long)]
pub from: Option<Format>,
}