subtitler 2.3.0

Parse, convert, validate, edit, and generate subtitles in 13 formats (SRT, VTT, ASS/SSA, MicroDVD, SubViewer, TTML, SBV, LRC, SAMI, MPL2, SCC, EBU STL). Full CLI included.
Documentation
use crate::model::Format;
use std::path::PathBuf;
use thiserror::Error;

#[derive(Debug, Error)]
pub enum ParseError {
  #[error("could not detect subtitle format")]
  UnknownFormat,
  #[error("format {0:?} is not enabled (enable its cargo feature)")]
  Unsupported(Format),
  #[error("internal error: {0}")]
  Anyhow(#[from] anyhow::Error),
  #[error("parse error: {0}")]
  Decode(#[from] SubtitleError),
  #[error("I/O error: {0}")]
  Io(#[from] std::io::Error),
  #[cfg(feature = "http")]
  #[error("HTTP error: {0}")]
  Http(#[from] reqwest::Error),
}

#[derive(Debug, Error)]
pub enum SubtitleError {
  #[error("invalid timestamp in {format:?}: {value}")]
  InvalidTimestamp { format: Format, value: String },

  #[error("expected {expected} at row {row} in {format:?}, got: {got}")]
  UnexpectedLine {
    format: Format,
    row: usize,
    expected: &'static str,
    got: String,
  },

  #[error("invalid {format:?} line: {line}")]
  InvalidLine { format: Format, line: String },

  #[error("XML error in {format:?}: {error}")]
  Xml { format: Format, error: String },

  #[error("invalid {role} frame in {format:?}: {value}")]
  InvalidFrame {
    format: Format,
    role: &'static str,
    value: String,
  },

  #[error("invalid {encoding} encoding: {error}")]
  InvalidEncoding { encoding: String, error: String },

  #[error("unsupported encoding: {encoding}. Try converting to UTF-8 first.")]
  UnsupportedEncoding { encoding: String },

  #[error("invalid {format:?} data: {reason}")]
  InvalidFormat { format: Format, reason: String },

  #[error("refusing to overwrite existing file: {path}")]
  FileExists { path: PathBuf },

  #[error("invalid UTF-8: {0}")]
  InvalidUtf8(#[from] std::string::FromUtf8Error),

  #[error("I/O error: {0}")]
  Io(#[from] std::io::Error),
}

#[cfg(test)]
mod tests {
  use super::*;

  #[test]
  fn invalid_timestamp_display() {
    let e = SubtitleError::InvalidTimestamp {
      format: Format::Srt,
      value: "nope".into(),
    };
    assert_eq!(e.to_string(), "invalid timestamp in Srt: nope");
  }

  #[test]
  fn unexpected_line_display() {
    let e = SubtitleError::UnexpectedLine {
      format: Format::Srt,
      row: 7,
      expected: "timestamp",
      got: "hello".into(),
    };
    assert_eq!(
      e.to_string(),
      "expected timestamp at row 7 in Srt, got: hello"
    );
  }

  #[test]
  fn from_utf8_error_wraps() {
    let bad = vec![0xFF, 0xFE, 0xFD];
    let utf8_err = String::from_utf8(bad).unwrap_err();
    let e = SubtitleError::from(utf8_err);
    assert!(matches!(e, SubtitleError::InvalidUtf8(_)));
    assert!(e.to_string().starts_with("invalid UTF-8:"));
  }
}