use encoding_rs::*;
use std::fmt;
use std::fs;
use std::io::prelude::*;
use std::ops::Index;
use std::path::Path;
const ONE_SECOND_MILLIS: u32 = 1000;
const ONE_MINUTE_MILLIS: u32 = 60 * ONE_SECOND_MILLIS;
const ONE_HOUR_MILLIS: u32 = 60 * ONE_MINUTE_MILLIS;
#[derive(Debug)]
pub enum ParsingError {
ParseIntError(std::num::ParseIntError),
IOError(std::io::Error),
MalformedTimestamp,
BadSubtitleStructure(usize),
BadEncodingName,
}
impl fmt::Display for ParsingError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ParsingError::ParseIntError(error) => write!(f, "{}", error),
ParsingError::IOError(error) => write!(f, "{}", error),
ParsingError::MalformedTimestamp => write!(f, "tried parsing a malformed timestamp"),
ParsingError::BadEncodingName => write!(f, "incorrect encoding name provided; refer to https://encoding.spec.whatwg.org/#names-and-labels for available encodings"),
ParsingError::BadSubtitleStructure(num) => {
let number = if num > &0 { num.to_string() } else { String::from("unknown") };
write!(f, "tried parsing an incorrectly formatted subtitle (subtitle number {})", number)
}
}
}
}
impl std::error::Error for ParsingError {}
impl From<std::num::ParseIntError> for ParsingError {
fn from(error: std::num::ParseIntError) -> Self {
ParsingError::ParseIntError(error)
}
}
impl From<std::io::Error> for ParsingError {
fn from(error: std::io::Error) -> Self {
ParsingError::IOError(error)
}
}
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Hash)]
pub struct Timestamp {
milliseconds: u32,
}
impl Timestamp {
pub const MAX_TIMESTAMP_MILLIS: u32 =
255 * ONE_HOUR_MILLIS + 59 * ONE_MINUTE_MILLIS + 59 * ONE_SECOND_MILLIS + 999;
pub fn convert_to_milliseconds(hours: u8, minutes: u8, seconds: u8, milliseconds: u16) -> u32 {
(((hours as u32 * 60) + minutes as u32) * 60 + seconds as u32) * 1000 + milliseconds as u32
}
pub fn new(hours: u8, minutes: u8, seconds: u8, milliseconds: u16) -> Timestamp {
Timestamp {
milliseconds: Self::convert_to_milliseconds(hours, minutes, seconds, milliseconds),
}
}
pub fn from_milliseconds(millis: u32) -> Timestamp {
Timestamp {
milliseconds: millis,
}
}
pub fn parse(s: &str) -> Result<Timestamp, ParsingError> {
let mut iter = s.splitn(3, ':');
let hours = iter
.next()
.ok_or(ParsingError::MalformedTimestamp)?
.parse()?;
let minutes = iter
.next()
.ok_or(ParsingError::MalformedTimestamp)?
.parse()?;
let mut second_iter = iter
.next()
.ok_or(ParsingError::MalformedTimestamp)?
.splitn(2, &[',', '.']);
let seconds = second_iter
.next()
.ok_or(ParsingError::MalformedTimestamp)?
.parse()?;
let milliseconds = second_iter
.next()
.ok_or(ParsingError::MalformedTimestamp)?
.parse()?;
Ok(Timestamp::new(hours, minutes, seconds, milliseconds))
}
pub fn add_hours(&mut self, n: i64) {
self.add_milliseconds(n * ONE_HOUR_MILLIS as i64);
}
pub fn add_minutes(&mut self, n: i64) {
self.add_milliseconds(n * ONE_MINUTE_MILLIS as i64);
}
pub fn add_seconds(&mut self, n: i64) {
self.add_milliseconds(n * ONE_SECOND_MILLIS as i64);
}
pub fn add_milliseconds(&mut self, n: i64) {
let millis: i64 = self.milliseconds as i64 + n;
if millis < 0 || millis > Self::MAX_TIMESTAMP_MILLIS as i64 {
panic!("Surpassed limits of Timestamp!");
}
self.milliseconds = millis as u32;
}
pub fn add(&mut self, timestamp: &Timestamp) {
self.add_milliseconds(timestamp.milliseconds as i64);
}
pub fn sub(&mut self, timestamp: &Timestamp) {
self.add_milliseconds(-(timestamp.milliseconds as i64));
}
pub fn get(&self) -> (u8, u8, u8, u16) {
let mut millis = self.milliseconds;
let hours = (self.milliseconds / ONE_HOUR_MILLIS) as u8;
millis -= (hours as u32) * ONE_HOUR_MILLIS;
let minuts = (millis / ONE_MINUTE_MILLIS) as u8;
millis -= (minuts as u32) * ONE_MINUTE_MILLIS;
let seconds = (millis / ONE_SECOND_MILLIS) as u8;
millis -= (seconds as u32) * ONE_SECOND_MILLIS;
(hours, minuts, seconds, millis as u16)
}
pub fn set(&mut self, hours: u8, minutes: u8, seconds: u8, milliseconds: u16) {
self.milliseconds =
Timestamp::convert_to_milliseconds(hours, minutes, seconds, milliseconds);
}
}
impl fmt::Display for Timestamp {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let (hours, minutes, seconds, milliseconds) = self.get();
write!(
f,
"{:02}:{:02}:{:02},{:03}",
hours, minutes, seconds, milliseconds
)
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Subtitle {
pub num: usize,
pub start_time: Timestamp,
pub end_time: Timestamp,
pub text: String,
}
impl Subtitle {
pub fn new(num: usize, start_time: Timestamp, end_time: Timestamp, text: String) -> Subtitle {
Subtitle {
num,
start_time,
end_time,
text,
}
}
pub fn parse(input: String) -> Result<Subtitle, ParsingError> {
let mut iter = input.trim_start_matches('\n').splitn(3, '\n');
let num = iter
.next()
.ok_or(ParsingError::BadSubtitleStructure(0))?
.parse::<usize>()?;
let time = iter.next().ok_or(ParsingError::BadSubtitleStructure(num))?;
let mut time_iter = time.split(" --> ");
let start = Timestamp::parse(
time_iter
.next()
.ok_or(ParsingError::BadSubtitleStructure(num))?,
)?;
let end_with_possible_position_info = time_iter
.next()
.ok_or(ParsingError::BadSubtitleStructure(num))?;
let end = Timestamp::parse(
end_with_possible_position_info
.split(' ')
.next()
.ok_or(ParsingError::BadSubtitleStructure(num))?,
)?;
let text = iter.next().unwrap_or_default();
Ok(Subtitle::new(num, start, end, text.to_string()))
}
pub fn add_hours(&mut self, n: i64) {
self.start_time.add_hours(n);
self.end_time.add_hours(n);
}
pub fn add_minutes(&mut self, n: i64) {
self.start_time.add_minutes(n);
self.end_time.add_minutes(n);
}
pub fn add_seconds(&mut self, n: i64) {
self.start_time.add_seconds(n);
self.end_time.add_seconds(n);
}
pub fn add_milliseconds(&mut self, n: i64) {
self.start_time.add_milliseconds(n);
self.end_time.add_milliseconds(n);
}
pub fn add(&mut self, timestamp: &Timestamp) {
self.start_time.add(timestamp);
self.end_time.add(timestamp);
}
pub fn sub(&mut self, timestamp: &Timestamp) {
self.start_time.sub(timestamp);
self.end_time.sub(timestamp);
}
}
impl fmt::Display for Subtitle {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{}\n{} --> {}\n{}",
self.num, self.start_time, self.end_time, self.text
)
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Subtitles(Vec<Subtitle>);
impl Subtitles {
pub fn new() -> Subtitles {
Default::default()
}
pub fn new_from_vec(v: Vec<Subtitle>) -> Subtitles {
Subtitles(v)
}
pub fn parse_from_str(mut input: String) -> Result<Subtitles, ParsingError> {
let mut res = Subtitles::new();
input = input.trim_start_matches('\u{feff}').to_string();
if input.contains('\r') {
input = input.replace('\r', "");
}
for s in input
.split_terminator("\n\n")
.filter(|&x| x.contains(char::is_alphanumeric))
{
res.push(Subtitle::parse(s.to_string())?);
}
Ok(res)
}
pub fn parse_from_file(
path: impl AsRef<Path>,
encoding: Option<&str>,
) -> Result<Subtitles, ParsingError> {
let mut f = fs::File::open(path)?;
if let Some(enc) = encoding {
let mut buffer = Vec::new();
f.read_to_end(&mut buffer)?;
let (cow, ..) = Encoding::for_label(enc.as_bytes())
.ok_or(ParsingError::BadEncodingName)?
.decode(buffer.as_slice());
Subtitles::parse_from_str(cow[..].to_string())
} else {
let mut buffer = String::new();
f.read_to_string(&mut buffer)?;
Subtitles::parse_from_str(buffer)
}
}
pub fn write_to_file(
&self,
path: impl AsRef<Path>,
encoding: Option<&str>,
) -> Result<(), ParsingError> {
let mut f = fs::File::create(path)?;
if let Some(enc) = encoding {
let string = &self.to_string();
let (cow, ..) = Encoding::for_label(enc.as_bytes())
.ok_or(ParsingError::BadEncodingName)?
.encode(string);
f.write_all(&cow)?;
} else {
f.write_all(self.to_string().as_bytes())?;
}
Ok(())
}
pub fn to_vec(self) -> Vec<Subtitle> {
self.0
}
pub fn len(&self) -> usize {
self.0.len()
}
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
pub fn push(&mut self, sub: Subtitle) {
self.0.push(sub);
}
pub fn sort(&mut self) {
self.0.sort();
}
}
impl IntoIterator for Subtitles {
type Item = Subtitle;
type IntoIter = std::vec::IntoIter<Self::Item>;
fn into_iter(self) -> Self::IntoIter {
self.0.into_iter()
}
}
impl<'l> IntoIterator for &'l Subtitles {
type Item = &'l Subtitle;
type IntoIter = std::slice::Iter<'l, Subtitle>;
fn into_iter(self) -> Self::IntoIter {
self.0.iter()
}
}
impl<'l> IntoIterator for &'l mut Subtitles {
type Item = &'l mut Subtitle;
type IntoIter = std::slice::IterMut<'l, Subtitle>;
fn into_iter(self) -> Self::IntoIter {
self.0.iter_mut()
}
}
impl<I: std::slice::SliceIndex<[Subtitle]>> Index<I> for Subtitles {
type Output = I::Output;
fn index(&self, i: I) -> &Self::Output {
&self.0[i]
}
}
impl fmt::Display for Subtitles {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if !self.is_empty() {
let mut s = String::new();
for sub in &self[..self.len() - 1] {
s.push_str(&format!("{}\n\n", &sub.to_string()));
}
s.push_str(&self[self.len() - 1].to_string());
write!(f, "{}", s)
} else {
Ok(())
}
}
}
mod tests {
#![allow(unused_imports)]
use super::*;
#[test]
fn add_time_timestamp() {
let mut timestamp = Timestamp::new(0, 0, 0, 0);
timestamp.add_milliseconds(1200);
assert_eq!(timestamp, Timestamp::new(0, 0, 1, 200));
timestamp.add_seconds(65);
assert_eq!(timestamp, Timestamp::new(0, 1, 6, 200));
timestamp.add_minutes(122);
assert_eq!(timestamp, Timestamp::new(2, 3, 6, 200));
timestamp.add_hours(-1);
assert_eq!(timestamp, Timestamp::new(1, 3, 6, 200));
timestamp.add_seconds(-7);
assert_eq!(timestamp, Timestamp::new(1, 2, 59, 200));
}
#[test]
#[should_panic(expected = "Surpassed limits of Timestamp!")]
fn timestamp_overflow_panic() {
let mut timestamp = Timestamp::new(0, 0, 0, 0);
timestamp.add_hours(255);
timestamp.add_minutes(60);
println!("Expected a panic, got: {}", timestamp);
}
#[test]
#[should_panic(expected = "Surpassed limits of Timestamp!")]
fn timestamp_negative_panic() {
let mut timestamp = Timestamp::new(0, 0, 0, 0);
timestamp.add_minutes(-10);
println!("Expected a panic, got: {}", timestamp);
}
#[test]
fn timestamp_parsing() {
assert_eq!(
Timestamp::parse("12:35:42,756").unwrap(),
Timestamp::new(12, 35, 42, 756)
);
assert_eq!(
Timestamp::parse("32:00:46,000").unwrap(),
Timestamp::new(32, 0, 46, 000)
);
assert_eq!(
Timestamp::parse("12:35:42.756").unwrap(),
Timestamp::new(12, 35, 42, 756)
);
assert_eq!(
Timestamp::parse("32:00:46.000").unwrap(),
Timestamp::new(32, 0, 46, 000)
);
}
#[test]
fn timestamp_to_str() {
assert_eq!(Timestamp::new(0, 0, 0, 0).to_string(), "00:00:00,000");
assert_eq!(Timestamp::new(0, 1, 20, 500).to_string(), "00:01:20,500");
}
#[test]
fn subtitle_parsing() {
let input = "1\n00:00:00,000 --> 00:00:01,000\nHello world!\nNew line!";
let result = Subtitle::new(
1,
Timestamp::new(0, 0, 0, 0),
Timestamp::new(0, 0, 1, 0),
"Hello world!\nNew line!".to_string(),
);
assert_eq!(Subtitle::parse(input.to_string()).unwrap(), result);
}
#[test]
fn subtitle_ordering() {
let sub1 =
Subtitle::parse("1\n00:00:00,000 --> 00:00:02,000\nHello world!".to_string()).unwrap();
let sub2 = Subtitle::parse("2\n00:00:02,500 --> 00:00:05,000\nTest subtitle.".to_string())
.unwrap();
let sub3 =
Subtitle::parse("2\n00:00:03,500 --> 00:00:06,000\nTest subtitle two.".to_string())
.unwrap();
assert!(sub1 < sub2);
assert!(sub2 < sub3);
}
#[test]
fn add_time_subtitle() {
let mut sub =
Subtitle::parse("1\n00:00:00,000 --> 00:00:02,000\nHello world!".to_string()).unwrap();
sub.add_seconds(10);
assert_eq!(
sub.to_string(),
"1\n00:00:10,000 --> 00:00:12,000\nHello world!"
);
sub.add_seconds(110);
assert_eq!(
sub.to_string(),
"1\n00:02:00,000 --> 00:02:02,000\nHello world!"
);
let t1 = Timestamp::new(0, 0, 0, 0);
let t2 = Timestamp::new(1, 20, 0, 0);
sub.add(&t1);
assert_eq!(
sub.to_string(),
"1\n00:02:00,000 --> 00:02:02,000\nHello world!"
);
sub.add(&t2);
assert_eq!(
sub.to_string(),
"1\n01:22:00,000 --> 01:22:02,000\nHello world!"
);
}
#[test]
fn sub_time_subtitle() {
let mut sub =
Subtitle::parse("1\n01:22:10,000 --> 01:22:12,000\nHello world!".to_string()).unwrap();
sub.add_seconds(-120);
assert_eq!(
sub.to_string(),
"1\n01:20:10,000 --> 01:20:12,000\nHello world!"
);
sub.add_seconds(-10);
assert_eq!(
sub.to_string(),
"1\n01:20:00,000 --> 01:20:02,000\nHello world!"
);
let t1 = Timestamp::new(0, 0, 0, 0);
let t2 = Timestamp::new(1, 20, 0, 0);
sub.sub(&t1);
assert_eq!(
sub.to_string(),
"1\n01:20:00,000 --> 01:20:02,000\nHello world!"
);
sub.sub(&t2);
assert_eq!(
sub.to_string(),
"1\n00:00:00,000 --> 00:00:02,000\nHello world!"
);
}
#[test]
fn sub_to_string() {
let input = Subtitle::new(
1,
Timestamp::new(0, 0, 0, 0),
Timestamp::new(0, 0, 1, 0),
"Hello world!\nNew line!".to_string(),
);
let result = "1\n00:00:00,000 --> 00:00:01,000\nHello world!\nNew line!";
assert_eq!(input.to_string(), result);
}
#[test]
fn subtitles_from_str_parsing() {
let subs = "1\n00:00:00,000 --> 00:00:01,000\nHello world!\nExtra!\n\n\
2\n00:00:01,500 --> 00:00:02,500\nThis is a subtitle!";
let parsed_subs = Subtitles::parse_from_str(subs.to_string()).unwrap();
assert_eq!(
parsed_subs[0],
Subtitle::new(
1,
Timestamp::new(0, 0, 0, 0),
Timestamp::new(0, 0, 1, 0),
"Hello world!\nExtra!".to_string()
)
);
assert_eq!(
parsed_subs[1],
Subtitle::new(
2,
Timestamp::new(0, 0, 1, 500),
Timestamp::new(0, 0, 2, 500),
"This is a subtitle!".to_string()
)
);
}
#[test]
fn sort_subtitles() {
let subs = "2\n00:00:01,500 --> 00:00:02,500\nThis is a subtitle!\n\n\
1\n00:00:00,000 --> 00:00:01,000\nHello world!\nExtra!\n\n\
3\n00:00:02,500 --> 00:00:03,000\nFinal subtitle.\n";
let mut parsed_subs = Subtitles::parse_from_str(subs.to_string()).unwrap();
parsed_subs.sort();
let true_sort = "1\n00:00:00,000 --> 00:00:01,000\nHello world!\nExtra!\n\n\
2\n00:00:01,500 --> 00:00:02,500\nThis is a subtitle!\n\n\
3\n00:00:02,500 --> 00:00:03,000\nFinal subtitle.\n";
let sorted_subs = Subtitles::parse_from_str(true_sort.to_string()).unwrap();
assert_eq!(parsed_subs, sorted_subs);
}
#[test]
fn empty_subtitles_display() {
let out = Subtitles::new().to_string();
assert_eq!(out, String::new());
}
#[test]
fn empty_subtitles_parse() {
let subs = Subtitles::parse_from_str(String::new()).expect("Failed to parse empty subs");
assert_eq!(subs.len(), 0);
}
#[test]
fn subtitle_with_position_information() {
let input = "1\n00:00:07,001 --> 00:00:09,015 position:50,00%,middle align:middle size:80,00% line:84,67%\nThis is a subtitle text";
let result = Subtitle::new(
1,
Timestamp::new(0, 0, 7, 1),
Timestamp::new(0, 0, 9, 15),
"This is a subtitle text".to_string(),
);
assert_eq!(Subtitle::parse(input.to_string()).unwrap(), result);
}
#[test]
fn empty_text_for_timestamp() {
let subs = "1\n00:00:00,000 --> 00:00:01,000\n\n\n\
2\n00:00:01,500 --> 00:00:02,500\nThis is a subtitle!";
let parsed_subs = Subtitles::parse_from_str(subs.to_string()).unwrap();
assert_eq!(
parsed_subs[0],
Subtitle::new(
1,
Timestamp::new(0, 0, 0, 0),
Timestamp::new(0, 0, 1, 0),
String::new(),
)
);
assert_eq!(
parsed_subs[1],
Subtitle::new(
2,
Timestamp::new(0, 0, 1, 500),
Timestamp::new(0, 0, 2, 500),
"This is a subtitle!".to_string()
)
);
}
}