subtitle_retimer 0.1.0

Basic subtitle commandline editor


use std::{fmt::Write, fs::{self, File, OpenOptions}, io::{BufRead, BufReader, BufWriter, Read, Write as OtherWrite}, ops::AddAssign, path::PathBuf, str::FromStr, time::Instant};
use chrono::{NaiveTime, TimeDelta};
use itertools::Itertools;
use std::io::Seek;


use clap::Parser;
#[derive(Parser, Debug)]
#[command(version, about)]
struct Args {
    /// Input file
    file: std::path::PathBuf,

    /// Subtitle retiming in ms
    #[arg(short, long, default_value_t = 0, allow_hyphen_values(true))]
    delta: i32,

    #[arg(short, long)]
    output: Option<String>

}

fn main() {
    let args = Args::parse();

    let mut input = File::open(args.file.clone()).unwrap();
    let mut output = String::new();
    process(&mut input, &mut output, args.delta);

    if let Some(output_file_name) = args.output {
        let full_file = output_file_name + ".srt";
        fs::write(full_file, output).unwrap();
    } else {
        fs::write(&args.file, output).unwrap();
    }

}



fn update_timestamp(s: &str, delta: i32) -> String {
    let t = NaiveTime::parse_from_str(s.trim(), "%H:%M:%S,%3f")
        .expect("Failed to parse timestamp");

    let new_t = t.overflowing_add_signed(TimeDelta::milliseconds(delta as i64)).0;

    new_t.format("%H:%M:%S,%3f").to_string()
}

fn process(input: &mut File, output: &mut String, delta: i32) {
    let mut reader = BufReader::new(input);

    let mut temp = String::with_capacity(30);
    'outer: loop {
        // Parse the subtitle index
        reader.read_line(output).unwrap();

        // Parse the timing
        temp.clear();
        reader.read_line(&mut temp).unwrap();
        let (start, end) = temp.trim().split_once("-->").unwrap();
        let (start, end) = (update_timestamp(start, delta), update_timestamp(end, delta));
        output.write_fmt(format_args!("{start} --> {end}\n")).unwrap();

        // Parse the subtitle
        while &temp != "\n" {
            temp.clear();
            let status = reader.read_line(&mut temp).unwrap();
            if status == 0 {break 'outer;}
            output.push_str(&temp);
        }
    }
}

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

    #[test]
    fn test_timestamp_update()  {
        assert_eq!(update_timestamp(&"00:00:24,000", 1), String::from("00:00:24,001"));
        assert_eq!(update_timestamp(&"00:00:24,000", 1001), String::from("00:00:25,001"));
        assert_eq!(update_timestamp(&"00:00:24,000", 60000), String::from("00:01:24,000"));
    }

    #[test]
    fn test_consitency() {
        let mut input = File::open("./test.srt").unwrap();
        let mut og = String::new();
        input.read_to_string(&mut og).unwrap();
        input.rewind().unwrap();

        let mut output = String::new();
        process(&mut input, &mut output, 0);
        assert_eq!(og, output);
        input.rewind().unwrap();

        output.clear();
        process(&mut input, &mut output, 123456);
        fs::write("temp.srt", &output).unwrap();
        let mut input2 = File::open("./temp.srt").unwrap();
        output.clear();
        process(&mut input2, &mut output, -123456);
        drop(input2);
        fs::remove_file(PathBuf::from("./temp.srt")).unwrap();
        assert_eq!(og, output);
    }
}