use std::{fmt, fs, error::Error, path::Path, ffi::OsStr, collections::HashMap, cmp::Ordering};
const MILLIS_PER_SECOND: usize = 1000;
const MILLIS_PER_MINUTE: usize = 60 * MILLIS_PER_SECOND;
const MILLIS_PER_HOUR: usize = 60 * MILLIS_PER_MINUTE;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct SimpleTime {
hours: usize,
minutes: usize,
seconds: usize,
milliseconds: usize,
}
impl SimpleTime {
pub fn from_parts(
hours: usize, minutes: usize, seconds: usize, milliseconds: usize) -> SimpleTime {
if minutes >= 60 {
panic!("SimpleTime requires minutes be in [0, 60] (got {})", minutes);
}
if seconds >= 60 {
panic!("SimpleTime requires seconds be in [0, 60] (got {})", seconds);
}
if milliseconds > 999 {
panic!("SimpleTime requires milliseconds be in [0, 999] (got {})", milliseconds);
}
SimpleTime {
hours,
minutes,
seconds,
milliseconds,
}
}
pub fn from_milliseconds(m: usize) -> SimpleTime {
let mut t = m;
let hours = t / MILLIS_PER_HOUR;
t -= hours * MILLIS_PER_HOUR;
let minutes = t / MILLIS_PER_MINUTE;
t -= minutes * MILLIS_PER_MINUTE;
let seconds = t / MILLIS_PER_SECOND;
t -= seconds * MILLIS_PER_SECOND;
let milliseconds = t;
SimpleTime {
hours,
minutes,
seconds,
milliseconds,
}
}
pub fn to_milliseconds(&self) -> usize {
self.hours * MILLIS_PER_HOUR
+ self.minutes * MILLIS_PER_MINUTE
+ self.seconds * MILLIS_PER_SECOND
+ self.milliseconds
}
pub fn hour(&self) -> usize { self.hours }
pub fn minute(&self) -> usize { self.minutes }
pub fn second(&self) -> usize { self.seconds }
pub fn millisecond(&self) -> usize { self.milliseconds }
pub fn offset(&mut self, offset: isize)
-> Result<(), NegativeSimpleTime> {
let new_millis: i128 = self.to_milliseconds() as i128 + offset as i128;
if new_millis < 0 {
return Err(NegativeSimpleTime)
}
else {
*self = SimpleTime::from_milliseconds(new_millis as usize);
return Ok(())
}
}
}
impl PartialOrd for SimpleTime {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
self.to_milliseconds().partial_cmp(&other.to_milliseconds())
}
}
#[derive(Debug, Clone)]
pub struct NegativeSimpleTime;
impl Error for NegativeSimpleTime {}
impl fmt::Display for NegativeSimpleTime {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "attempted to create negative SimpleTime")
}
}
pub fn parse_file(fname: &str) -> Result<Caption, Box<dyn Error>> {
match Path::new(&fname).extension().and_then(OsStr::to_str) {
Some(ext) => {
match ext {
"vtt" | "txt" => Ok(VttParser::from_file(fname)?),
"srt" => Ok(SrtParser::from_file(fname)?),
_ => Err(CaptionParserError::UnsupportedFileType(ext.to_string()))?,
}
}
None => Err(CaptionParserError::UnknownExtension(fname.to_string()))?,
}
}
pub fn write_caption(fname: &str, caption: &Caption) -> Result<(), Box<dyn Error>> {
match Path::new(&fname).extension().and_then(OsStr::to_str) {
Some(ext) => {
match ext {
"vtt" | "txt" => VttWriter::to_file(&fname, &caption)?,
"srt" => SrtWriter::to_file(&fname, &caption)?,
_ => Err(CaptionParserError::UnsupportedFileType(fname.to_string()))?,
}
},
_ => {
Err(CaptionParserError::UnknownExtension(fname.to_string()))?
},
}
Ok(())
}
#[derive(Debug, Clone)]
pub enum CaptionParserError {
UnsupportedFileType(String),
UnknownExtension(String)
}
impl Error for CaptionParserError {}
impl fmt::Display for CaptionParserError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
CaptionParserError::UnsupportedFileType(s) => write!(f, "type {} is unsupported", s),
CaptionParserError::UnknownExtension(s) => write!(f, "unknown extension for file {}", s),
}
}
}
pub struct VttParser;
impl VttParser {
pub fn from_file(fname: &str) -> Result<Caption, Box<dyn Error>> {
let s = fs::read_to_string(fname)?;
let cap = VttParser::parse(&s)?;
Ok(cap)
}
pub fn parse(contents: &str) -> Result<Caption, VttParserError> {
let (header, vtt_line) = VttParser::header(&contents)?;
let start_line = vtt_line + 1;
let total_lines = contents.lines().count();
let blocks_remaining = (total_lines - start_line) / 4;
if (blocks_remaining as f32) != ((total_lines as f32 - start_line as f32) / 4.0) {
return Err(VttParserError::UnexpectedEndOfFile)?;
}
let mut blocks: Vec<CaptionBlock> = Vec::with_capacity(blocks_remaining);
let mut line_iter = contents.lines();
for _ in 0..(start_line) {
line_iter.next();
}
let lines: Vec<&str> = line_iter.collect();
for i in 0..blocks_remaining {
let block_line_start = i * 4;
let block_line_end = (i * 4) + 3;
let current_block = lines[block_line_start..(block_line_end + 1)]
.iter()
.map(|a| a.to_string())
.collect::<Vec<String>>()
.join("\n");
blocks.push(VttParser::block(¤t_block)?);
}
Ok(
Caption {
header,
blocks
}
)
}
fn header(s: &str) -> Result<(Option<String>, usize), VttParserError> {
let is_webvtt = |x| x == "WEBVTT";
if let Some(n) = s.lines().position(is_webvtt) {
let header_opt = match n {
0 | 1 | 2 => None,
nn => {
let header = s.lines().take(nn - 2)
.map(|a| a.to_string())
.collect::<Vec<String>>()
.join("\n");
Some(header)
},
};
Ok((header_opt, n))
}
else {
Err(VttParserError::UnexpectedEndOfFile)
}
}
fn block(s: &str) -> Result<CaptionBlock, VttParserError> {
if s.lines().count() != 4 {
return Err(VttParserError::UnexpectedEndOfFile);
}
let mut s_iter = s.lines();
match s_iter.next() {
Some("") => {},
Some(s) => {
return Err(VttParserError::ExpectedBlankLine(s.to_string()));
},
_ => { return Err(VttParserError::UnexpectedEndOfFile) },
}
let block_line = s_iter.next().ok_or(VttParserError::UnexpectedEndOfFile)?;
let _ = VttParser::block_number(block_line)?;
let header_line = s_iter.next().ok_or(VttParserError::UnexpectedEndOfFile)?;
let (speaker, start, end) = VttParser::block_header(header_line)?;
let text_line = s_iter.next().ok_or(VttParserError::UnexpectedEndOfFile)?;
let text = VttParser::block_text(text_line);
Ok(CaptionBlock {
speaker,
start,
end,
text,
})
}
fn block_number(s: &str) -> Result<usize, VttParserError> {
let r = s.parse::<usize>();
match r {
Ok(n) => Ok(n),
Err(_) => Err(VttParserError::ExpectedBlockNumber(String::from(s))),
}
}
pub fn block_timestamp(s: &str) -> Result<SimpleTime, VttParserError> {
let vtt_timestamp_len: usize = 12;
if s.len() != vtt_timestamp_len {
return Err(VttParserError::InvalidTimestamp(String::from(s)));
}
let hours = match s[0..2].parse::<usize>() {
Ok(n) => n,
Err(_) => {
return Err(VttParserError::InvalidTimestamp(String::from(s)));
},
};
if s.chars().nth(2).unwrap() != ':' {
return Err(VttParserError::InvalidTimestamp(
String::from(s)));
}
let minutes = match s[3..5].parse::<usize>() {
Ok(n) => n,
Err(_) => {
return Err(VttParserError::InvalidTimestamp(String::from(s)));
},
};
if s.chars().nth(2).unwrap() != ':' {
return Err(VttParserError::InvalidTimestamp(
String::from(s)));
}
let seconds = match s[6..8].parse::<usize>() {
Ok(n) => {
n
},
Err(_) => {
return Err(VttParserError::InvalidTimestamp(String::from(s)));
},
};
if s.chars().nth(8).unwrap() != '.' {
return Err(VttParserError::InvalidTimestamp(
String::from(s)));
}
let milliseconds = match s[9..12].parse::<usize>() {
Ok(n) => n,
Err(_) => {
return Err(VttParserError::InvalidTimestamp(String::from(s)));
},
};
Ok(
SimpleTime::from_parts(
hours,
minutes,
seconds,
milliseconds
)
)
}
fn block_header(s: &str) -> Result<(Option<String>, SimpleTime, SimpleTime), VttParserError> {
if s.len() == 0 {
return Err(VttParserError::UnexpectedEndOfFile);
}
if s.chars().nth(0).unwrap().is_numeric() {
let (start, end) = VttParser::block_header_timestamps(s)?;
return Ok((None, start, end));
} else {
let first_loc = match s.find(char::is_numeric) {
Some(n) => n,
None => Err(VttParserError::BlockHeaderInvalid(
String::from(s)))?,
};
match s.get(first_loc - 1..first_loc) {
Some(" ") => {},
_ => {
return Err(VttParserError::BlockHeaderInvalid(String::from(s)));
},
};
let name = match s.get(..first_loc - 1) {
Some(x) => x,
_ => {
return Err(VttParserError::BlockHeaderInvalid(
String::from(s)));
},
};
let (start, end) = VttParser::block_header_timestamps(
match s.get(first_loc..) {
Some(s) => s,
None => {
return Err(VttParserError::BlockHeaderInvalid(
String::from(s)));
},
}
)?;
return Ok((Some(name.to_string()), start, end));
}
}
fn block_header_timestamps(s: &str) -> Result<(SimpleTime, SimpleTime), VttParserError> {
let total_words = s.split(' ').count();
if total_words == 3 {
let first = s.split(' ').nth(0);
let second = s.split(' ').nth(1);
let third = s.split(' ').nth(2);
if let Some(ts1) = first {
if let Some("-->") = second {
if let Some(ts2) = third {
let start = VttParser::block_timestamp(ts1)?;
let end = VttParser::block_timestamp(ts2)?;
return Ok((start, end));
} else {
return Err(
VttParserError::InvalidTimestamp(
String::from(s)));
}
} else {
return Err(
VttParserError::InvalidTimestamp(
String::from(s)));
}
} else {
return Err(VttParserError::InvalidTimestamp(
String::from(s)));
}
} else {
return Err(
VttParserError::InvalidTimestamp(String::from(s)));
}
}
fn block_text(s: &str) -> String {
s.to_string()
}
}
#[derive(Debug, Clone)]
pub enum VttParserError {
UnexpectedEndOfFile,
FileNotReadable(String),
ExpectedBlankLine(String),
ExpectedBlockNumber(String),
BlockHeaderInvalid(String),
InvalidTimestamp(String),
}
impl fmt::Display for VttParserError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
VttParserError::UnexpectedEndOfFile => write!(f, "unexpected end of file"),
VttParserError::FileNotReadable(s) => {
write!(f, "could not read file {}", s)
},
VttParserError::ExpectedBlankLine(s) => {
write!(f, "expected blank line, got {}", s)
},
VttParserError::ExpectedBlockNumber(s) => {
write!(f, "expected VTT block number, got {}", s)
},
VttParserError::BlockHeaderInvalid(s) => {
write!(f, "invalid VTT block from line {}", s)
},
VttParserError::InvalidTimestamp(s) => {
write!(f, "invalid VTT block from word {}", s)
},
}
}
}
impl Error for VttParserError {}
pub struct VttWriter;
impl VttWriter {
pub fn to_file(fname: &str, cap: &Caption) -> Result<(), Box<dyn Error>> {
fs::write(fname, VttWriter::write(&cap))?;
Ok(())
}
pub fn write(cap: &Caption) -> String {
let mut components: Vec<String> = Vec::with_capacity(cap.blocks.len() + 1);
components.push(VttWriter::header(&cap));
let mut block_num = 1;
for block in cap.blocks.iter() {
components.push(VttWriter::block(block, block_num));
block_num += 1;
}
components.join("\n")
}
fn block(cb: &CaptionBlock, n: usize) -> String {
let ts_start = VttWriter::timestamp(&cb.start);
let ts_end = VttWriter::timestamp(&cb.end);
format!(
"{}\n{}\n{}\n",
n,
match &cb.speaker {
Some(person) => format!("{} {} --> {}", person, ts_start, ts_end),
None => format!("{} --> {}", ts_start, ts_end),
},
cb.text
)
}
fn timestamp(t: &SimpleTime) -> String {
format!(
"{:02}:{:02}:{:02}.{:03}",
t.hour(),
t.minute(),
t.second(),
t.millisecond()
)
}
fn header(cap: &Caption) -> String {
let webvtt = "WEBVTT\n";
match &cap.header {
Some(s) => format!("{}\n\n{}", s, webvtt),
None => webvtt.to_string(),
}
}
}
pub struct SrtParser;
impl SrtParser {
pub fn from_file(fname: &str) -> Result<Caption, Box<dyn Error>> {
let s = fs::read_to_string(fname)?;
let cap = SrtParser::parse(&s)?;
Ok(cap)
}
pub fn parse(contents: &str) -> Result<Caption, SrtParserError> {
let contents = &("\n".to_owned() + contents);
let total_lines = contents.lines().count();
let blocks_remaining = total_lines / 4;
if (blocks_remaining as f32) != (total_lines as f32 ) / 4.0 {
return Err(SrtParserError::UnexpectedEndOfFile)?;
}
let mut blocks: Vec<CaptionBlock> = Vec::with_capacity(blocks_remaining);
let lines: Vec<&str> = contents.lines().collect();
for i in 0..blocks_remaining {
let block_line_start = i * 4;
let block_line_end = (i * 4) + 3;
let current_block = lines[block_line_start..(block_line_end + 1)]
.iter()
.map(|a| a.to_string())
.collect::<Vec<String>>()
.join("\n");
blocks.push(SrtParser::block(¤t_block)?);
}
Ok(
Caption {
header: None,
blocks
}
)
}
fn block(s: &str) -> Result<CaptionBlock, SrtParserError> {
if s.lines().count() != 4 {
return Err(SrtParserError::UnexpectedEndOfFile);
}
let mut s_iter = s.lines();
match s_iter.next() {
Some("") => {},
Some(s) => {
return Err(SrtParserError::ExpectedBlankLine(s.to_string()));
},
_ => { return Err(SrtParserError::UnexpectedEndOfFile) },
}
let block_line = s_iter.next().ok_or(SrtParserError::UnexpectedEndOfFile)?;
let _ = SrtParser::block_number(block_line)?;
let header_line = s_iter.next().ok_or(SrtParserError::UnexpectedEndOfFile)?;
let (start, end) = SrtParser::block_timestamps(header_line)?;
let text_line = s_iter.next().ok_or(SrtParserError::UnexpectedEndOfFile)?;
let (speaker, text) = SrtParser::block_text(text_line)?;
Ok(CaptionBlock {
speaker,
start,
end,
text,
})
}
fn block_number(s: &str) -> Result<usize, SrtParserError> {
let r = s.parse::<usize>();
match r {
Ok(n) => Ok(n),
Err(_) => Err(SrtParserError::ExpectedBlockNumber(String::from(s))),
}
}
pub fn block_timestamp(s: &str) -> Result<SimpleTime, SrtParserError> {
let vtt_timestamp_len: usize = 12;
if s.len() != vtt_timestamp_len {
return Err(SrtParserError::InvalidTimestamp(String::from(s)));
}
let hours = match s[0..2].parse::<usize>() {
Ok(n) => n,
Err(_) => {
return Err(SrtParserError::InvalidTimestamp(String::from(s)));
},
};
if s.chars().nth(2).unwrap() != ':' {
return Err(SrtParserError::InvalidTimestamp(
String::from(s)));
}
let minutes = match s[3..5].parse::<usize>() {
Ok(n) => n,
Err(_) => {
return Err(SrtParserError::InvalidTimestamp(String::from(s)));
},
};
if s.chars().nth(2).unwrap() != ':' {
return Err(SrtParserError::InvalidTimestamp(
String::from(s)));
}
let seconds = match s[6..8].parse::<usize>() {
Ok(n) => {
n
},
Err(_) => {
return Err(SrtParserError::InvalidTimestamp(String::from(s)));
},
};
if s.chars().nth(8).unwrap() != ',' {
return Err(SrtParserError::InvalidTimestamp(
String::from(s)));
}
let milliseconds = match s[9..12].parse::<usize>() {
Ok(n) => n,
Err(_) => {
return Err(SrtParserError::InvalidTimestamp(String::from(s)));
},
};
Ok(
SimpleTime::from_parts(
hours,
minutes,
seconds,
milliseconds
)
)
}
fn block_timestamps(s: &str) -> Result<(SimpleTime, SimpleTime), SrtParserError> {
let total_words = s.split(' ').count();
if total_words == 3 {
let first = s.split(' ').nth(0);
let second = s.split(' ').nth(1);
let third = s.split(' ').nth(2);
if let Some(ts1) = first {
if let Some("-->") = second {
if let Some(ts2) = third {
let start = SrtParser::block_timestamp(ts1)?;
let end = SrtParser::block_timestamp(ts2)?;
return Ok((start, end));
} else {
return Err(
SrtParserError::InvalidTimestamp(
String::from(s)));
}
} else {
return Err(
SrtParserError::InvalidTimestamp(
String::from(s)));
}
} else {
return Err(SrtParserError::InvalidTimestamp(
String::from(s)));
}
} else {
return Err(
SrtParserError::InvalidTimestamp(String::from(s)));
}
}
fn block_text(s: &str) -> Result<(Option<String>, String), SrtParserError> {
if let Some(n0) = s.chars().position(|x| x == '[') {
if n0 != 0 {
return Err(SrtParserError::InvalidSpeakerPlacement(s.to_string()));
}
if let Some(n1) = s.chars().position(|x| x == ']') {
let speaker = s.get((n0 + 1)..n1).unwrap().to_string();
let text = s.get((n1 + 2)..).unwrap().to_string();
return Ok((Some(speaker.to_string()), text.to_string()));
}
else {
return Err(SrtParserError::InvalidSpeakerPlacement(s.to_string()));
}
}
Ok((None, s.to_string()))
}
}
#[derive(Debug, Clone)]
pub enum SrtParserError {
UnexpectedEndOfFile,
FileNotReadable(String),
ExpectedBlankLine(String),
ExpectedBlockNumber(String),
BlockHeaderInvalid(String),
InvalidTimestamp(String),
InvalidSpeakerPlacement(String),
}
impl fmt::Display for SrtParserError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
SrtParserError::UnexpectedEndOfFile => write!(f, "unexpected end of file"),
SrtParserError::FileNotReadable(s) => {
write!(f, "could not read file {}", s)
},
SrtParserError::ExpectedBlankLine(s) => {
write!(f, "expected blank line, got {}", s)
},
SrtParserError::ExpectedBlockNumber(s) => {
write!(f, "expected SRT block number, got {}", s)
},
SrtParserError::BlockHeaderInvalid(s) => {
write!(f, "invalid SRT block from line {}", s)
},
SrtParserError::InvalidTimestamp(s) => {
write!(f, "invalid SRT block from word {}", s)
},
SrtParserError::InvalidSpeakerPlacement(s) => {
write!(f, "invalid SRT speaker placement in line {}", s)
},
}
}
}
impl Error for SrtParserError {}
pub struct SrtWriter;
impl SrtWriter {
pub fn to_file(fname: &str, cap: &Caption) -> Result<(), Box<dyn Error>> {
fs::write(fname, SrtWriter::write(&cap))?;
Ok(())
}
pub fn write(cap: &Caption) -> String {
let mut components: Vec<String> = Vec::with_capacity(cap.blocks.len());
let mut block_num = 1;
for block in cap.blocks.iter() {
components.push(SrtWriter::block(block, block_num));
block_num += 1;
}
components.join("\n")
}
fn block(cb: &CaptionBlock, n: usize) -> String {
let ts_start = SrtWriter::timestamp(&cb.start);
let ts_end = SrtWriter::timestamp(&cb.end);
format!(
"{}\n{}\n{}\n",
n,
format!("{} --> {}", ts_start, ts_end),
match &cb.speaker {
Some(person) => format!("[{}] {}", person, cb.text),
None => format!("{}", cb.text),
},
)
}
fn timestamp(t: &SimpleTime) -> String {
format!(
"{:02}:{:02}:{:02},{:03}",
t.hour(),
t.minute(),
t.second(),
t.millisecond()
)
}
}
#[derive(Debug, Clone)]
pub struct CaptionBlock {
speaker: Option<String>,
start: SimpleTime,
end: SimpleTime,
text: String,
}
impl CaptionBlock {
pub fn from(speaker: Option<String>, start: SimpleTime, end: SimpleTime, text: String) -> Result<CaptionBlock, CaptionBlockError> {
let diff = (end.to_milliseconds() as i128) - (start.to_milliseconds() as i128);
if diff < 0 {
Err(CaptionBlockError::EndsBeforeStart(start, end))
}
else {
Ok(
CaptionBlock {
speaker,
start,
end,
text,
}
)
}
}
pub fn text(&self) -> String {
self.text.clone()
}
pub fn speaker(&self) -> Option<String> {
self.speaker.clone()
}
pub fn start(&self) -> SimpleTime {
self.start.clone()
}
pub fn end(&self) -> SimpleTime {
self.end.clone()
}
pub fn length_millis(&self) -> usize {
self.end.to_milliseconds() - self.start.to_milliseconds()
}
pub fn offset_milliseconds(&mut self, n: isize) -> Result<(), NegativeSimpleTime> {
self.start.offset(n)?;
self.end.offset(n)?;
Ok(())
}
pub fn set_start(&mut self, start: SimpleTime) -> Result<(), NegativeSimpleTime> {
if start.to_milliseconds() <= self.end.to_milliseconds() {
self.start = start;
Ok(())
}
else {
Err(NegativeSimpleTime)
}
}
pub fn set_end(&mut self, end: SimpleTime) -> Result<(), NegativeSimpleTime> {
if end.to_milliseconds() >= self.start.to_milliseconds() {
self.end = end;
Ok(())
}
else {
Err(NegativeSimpleTime)
}
}
}
#[derive(Debug)]
pub enum CaptionBlockError {
EndsBeforeStart(SimpleTime, SimpleTime)
}
#[derive(Debug, Eq, Ord, PartialEq, PartialOrd)]
struct SimpleSpeaker {
name: String,
talk_time: usize,
}
impl SimpleSpeaker {
pub fn new(name: String, talk_time: usize) -> SimpleSpeaker {
SimpleSpeaker {
name,
talk_time,
}
}
}
#[derive(Debug)]
pub struct Caption {
pub header: Option<String>,
pub blocks: Vec<CaptionBlock>,
}
impl Caption {
pub fn from(header_str: Option<&str>, blocks: Vec<CaptionBlock>) -> Caption {
Caption {
header: match header_str {
Some(s) => Some(s.to_string()),
None => None,
},
blocks: blocks,
}
}
pub fn offset_milliseconds(&mut self, n: isize) -> Result<(), NegativeSimpleTime> {
for b in self.blocks.iter_mut() {
b.start.offset(n)?;
b.end.offset(n)?;
}
Ok(())
}
pub fn time_head(&self) -> usize {
self.blocks[0].start.to_milliseconds()
}
pub fn time_tail(&self) -> usize {
self.blocks.iter().last().unwrap().end.to_milliseconds()
}
pub fn crop(&mut self, from: Option<SimpleTime>, to: Option<SimpleTime>) {
let from_time = match from {
Some(t) => t,
None => SimpleTime::from_milliseconds(0),
};
let to_time = match to {
Some(t) => t,
None => {
SimpleTime::from_milliseconds(self.time_tail())
}
};
let swapped = from_time.to_milliseconds() > to_time.to_milliseconds();
let start_time = if !swapped { from_time } else { to_time };
let end_time = if !swapped { to_time } else { from_time };
let pos_start = self.blocks.iter()
.position(|x| x.end() >= start_time)
.unwrap();
let pos_end = self.blocks.iter()
.rposition(|x| x.start() <= end_time)
.unwrap();
if self.blocks[pos_end].end() > end_time {
self.blocks[pos_end].set_end(end_time).unwrap();
}
if self.blocks[pos_start].start() < start_time {
self.blocks[pos_start].set_start(start_time).unwrap();
}
self.blocks = self.blocks[pos_start..pos_end + 1].to_vec();
self.offset_milliseconds(0 - start_time.to_milliseconds() as isize).unwrap();
}
pub fn print_report(&self) {
let mut map = HashMap::new();
map.insert(String::from("UNKNOWN"), 0);
let mut total_milliseconds_talk = 0;
for block in self.blocks.iter() {
let talk_time = block.length_millis();
total_milliseconds_talk += talk_time;
match block.speaker() {
Some(spk) => {
if let Some(count) = map.get_mut(&spk) {
*count += talk_time;
} else {
map.insert(spk, talk_time);
}
},
None => {
let x = map.get_mut(&"UNKNOWN".to_string()).unwrap();
*x += talk_time;
},
}
}
let total_speakers = map.keys().collect::<Vec<&String>>().len();
println!("Start: {}", VttWriter::timestamp(&self.blocks[0].start));
println!("End: {}", VttWriter::timestamp(&self.blocks.iter().last().unwrap().end));
println!("Total talk time: {}", VttWriter::timestamp(
&SimpleTime::from_milliseconds(total_milliseconds_talk)));
if total_speakers == 1 {
println!("Speakers are unknown")
} else {
let mut speakers: Vec<SimpleSpeaker> = Vec::with_capacity(total_speakers);
for (speaker, time) in &map {
let t = time.to_owned();
speakers.push(SimpleSpeaker::new(speaker.to_string(), t));
}
speakers.sort_by(|a, b| b.talk_time.cmp(&a.talk_time));
println!("{}", (0..35).map(|_| "-").collect::<String>());
println!("{:30} | % ", "Speaker");
println!("{}", (0..35).map(|_| "-").collect::<String>());
for speaker in &speakers {
if speaker.name == "UNKNOWN" {
continue;
}
let perc_talked = (speaker.talk_time * 100)/ total_milliseconds_talk;
println!("{0:30} | {1:02}", speaker.name, perc_talked);
}
}
}
pub fn concatenate(captions: Vec<Caption>) -> Caption {
let total_blocks = captions.iter()
.map(|c| c.blocks.iter().count())
.sum();
let mut cb: Vec<CaptionBlock> = Vec::with_capacity(total_blocks);
let mut last_ts: usize = 0;
for cap in captions.iter() {
for b in cap.blocks.iter() {
let mut copy_b = b.clone();
copy_b.offset_milliseconds(last_ts as isize)
.expect("Something logically impossible has occured");
cb.push(copy_b);
}
last_ts += cap.time_tail();
}
Caption {
header: None,
blocks: cb,
}
}
}
#[cfg(test)]
mod test {
use super::*;
mod simple_time {
#[test]
fn test_to_from_millis_works() {
let st = super::SimpleTime::from_parts(23, 54, 17, 837);
assert_eq!(st.to_milliseconds(), 86057837);
let st2 = super::SimpleTime::from_milliseconds(86897);
assert_eq!(st2.to_milliseconds(), 86897);
}
#[test]
fn test_offset() {
const MILLS: isize = 123456;
let mut st = super::SimpleTime::from_parts(0, 0, 0, 0);
st.offset(MILLS).expect("Failed offset");
assert_eq!(st.to_milliseconds(), 123456);
}
#[test]
fn test_offset_negative_time() {
const MILLS: isize = -123;
let mut st = super::SimpleTime::from_milliseconds(0);
let r = st.offset(MILLS);
match r {
Ok(()) => panic!("Test failure; was okay going negative"),
Err(_) => assert_eq!(0, 0),
};
}
#[test]
fn partial_eq() {
let a = super::SimpleTime::from_milliseconds(250);
let b = super::SimpleTime::from_milliseconds(500);
let c = super::SimpleTime::from_milliseconds(500);
assert_eq!(b, c);
assert_eq!(a < b, true);
assert_eq!(a == b, false);
}
}
mod caption {
use super::*;
#[test]
fn offset_caption() {
let mut c = Caption {
header: None,
blocks: vec!(CaptionBlock {
speaker: None,
start: SimpleTime::from_milliseconds(0),
end: SimpleTime::from_milliseconds(1000),
text: "John Dies at the End".to_string(),
})
};
c.offset_milliseconds(500).expect("Should be fine");
assert_eq!(c.blocks[0].start.to_milliseconds(), 500);
assert_eq!(c.blocks[0].end.to_milliseconds(), 1500);
}
fn toy_caption() -> Caption {
Caption {
header: None,
blocks: vec!(
CaptionBlock {
speaker: None,
start: SimpleTime::from_milliseconds(0),
end: SimpleTime::from_milliseconds(1000),
text: "John Dies at the End".to_string(),
},
CaptionBlock {
speaker: None,
start: SimpleTime::from_milliseconds(1500),
end: SimpleTime::from_milliseconds(2000),
text: "a".to_string(),
},
CaptionBlock {
speaker: None,
start: SimpleTime::from_milliseconds(2500),
end: SimpleTime::from_milliseconds(3000),
text: "b".to_string(),
},
CaptionBlock {
speaker: None,
start: SimpleTime::from_milliseconds(3500),
end: SimpleTime::from_milliseconds(4000),
text: "a".to_string(),
},
CaptionBlock {
speaker: None,
start: SimpleTime::from_milliseconds(4500),
end: SimpleTime::from_milliseconds(5000),
text: "b".to_string(),
},
)
}
}
#[test]
fn crop_caption_from_only() {
let mut c = toy_caption();
c.crop(
Some(SimpleTime::from_milliseconds(500)),
None
);
assert_eq!(c.blocks.len(), 5);
assert_eq!(c.blocks[0].start.to_milliseconds(), 0);
assert_eq!(c.blocks[0].end.to_milliseconds(), 500);
assert_eq!(c.blocks[4].start.to_milliseconds(), 4000);
assert_eq!(c.blocks[4].end.to_milliseconds(), 4500);
}
#[test]
fn crop_caption_drop_first() {
let mut c = toy_caption();
c.crop(
Some(SimpleTime::from_milliseconds(1250)),
None
);
assert_eq!(c.blocks.len(), 4);
assert_eq!(c.blocks[0].start.to_milliseconds(), 250);
assert_eq!(c.blocks[0].end.to_milliseconds(), 750);
assert_eq!(c.blocks[3].start.to_milliseconds(), 3250);
assert_eq!(c.blocks[3].end.to_milliseconds(), 3750);
}
#[test]
fn crop_caption_drop_last() {
let mut c = toy_caption();
c.crop(
None,
Some(SimpleTime::from_milliseconds(4250)),
);
assert_eq!(c.blocks.len(), 4);
assert_eq!(c.blocks[0].start.to_milliseconds(), 0);
assert_eq!(c.blocks[0].end.to_milliseconds(), 1000);
assert_eq!(c.blocks[3].start.to_milliseconds(), 3500);
assert_eq!(c.blocks[3].end.to_milliseconds(), 4000);
}
#[test]
fn crop_caption_truncate_last() {
let mut c = toy_caption();
c.crop(
None,
Some(SimpleTime::from_milliseconds(4750))
);
assert_eq!(c.blocks.len(), 5);
assert_eq!(c.blocks[4].start.to_milliseconds(), 4500);
assert_eq!(c.blocks[4].end.to_milliseconds(), 4750);
}
#[test]
fn crop_caption_drop_many() {
let mut c = toy_caption();
c.crop(
Some(SimpleTime::from_milliseconds(2750)),
Some(SimpleTime::from_milliseconds(3750))
);
assert_eq!(c.blocks.len(), 2);
assert_eq!(c.blocks[0].start.to_milliseconds(), 0);
assert_eq!(c.blocks[0].end.to_milliseconds(), 250);
assert_eq!(c.blocks[1].start.to_milliseconds(), 750);
assert_eq!(c.blocks[1].end.to_milliseconds(), 1000);
}
#[test]
fn concatenate_captions() {
let c1 = Caption {
header: None,
blocks: vec!(CaptionBlock {
speaker: None,
start: SimpleTime::from_milliseconds(0),
end: SimpleTime::from_milliseconds(1000),
text: "John Dies at the End".to_string(),
})
};
let c2 = Caption {
header: None,
blocks: vec!(CaptionBlock {
speaker: None,
start: SimpleTime::from_milliseconds(0),
end: SimpleTime::from_milliseconds(1000),
text: "John is dead now".to_string(),
})
};
let c3 = Caption {
header: None,
blocks: vec!(
CaptionBlock {
speaker: None,
start: SimpleTime::from_milliseconds(0),
end: SimpleTime::from_milliseconds(1000),
text: "Go read the book!".to_string(),
},
CaptionBlock {
speaker: None,
start: SimpleTime::from_milliseconds(1200),
end: SimpleTime::from_milliseconds(2400),
text: "Seriously.".to_string(),
},
),
};
let c = Caption::concatenate(vec!(c1, c2, c3));
assert_eq!(c.blocks[0].start.to_milliseconds(), 0);
assert_eq!(c.blocks[0].end.to_milliseconds(), 1000);
assert_eq!(c.blocks[1].start.to_milliseconds(), 1000);
assert_eq!(c.blocks[1].end.to_milliseconds(), 2000);
assert_eq!(c.blocks[2].start.to_milliseconds(), 2000);
assert_eq!(c.blocks[2].end.to_milliseconds(), 3000);
assert_eq!(c.blocks[3].start.to_milliseconds(), 3200);
assert_eq!(c.blocks[3].end.to_milliseconds(), 4400);
}
fn toy_cb() -> CaptionBlock {
CaptionBlock {
speaker: None,
start: SimpleTime::from_milliseconds(1500),
end: SimpleTime::from_milliseconds(2250),
text: "Blanky McBlankface".to_string(),
}
}
#[test]
fn get_cb_length() {
let cb = toy_cb();
assert_eq!(cb.length_millis(), 750);
}
#[test]
fn set_start() {
let mut cb = toy_cb();
assert!(matches!(
cb.set_start(SimpleTime::from_milliseconds(1250)),
Ok(())
));
assert_eq!(cb.start().to_milliseconds(), 1250);
assert!(matches!(
cb.set_start(SimpleTime::from_milliseconds(2251)),
Err(NegativeSimpleTime)
));
}
#[test]
fn set_end() {
let mut cb = toy_cb();
assert!(matches!(
cb.set_end(SimpleTime::from_milliseconds(2000)),
Ok(())
));
assert_eq!(cb.end().to_milliseconds(), 2000);
assert!(matches!(
cb.set_end(SimpleTime::from_milliseconds(100)),
Err(NegativeSimpleTime),
));
}
}
mod vtt_writer {
use super::*;
#[test]
fn write() {
let cap = Caption {
header: None,
blocks: vec!(
CaptionBlock {
speaker: Some("Pete Molfese".to_string()),
start: SimpleTime::from_milliseconds(0),
end: SimpleTime::from_milliseconds(1000),
text: "Hello, world!".to_string(),
}
)
};
let should_get = format!(
"WEBVTT\n\n{}\n{} {} --> {}\n{}\n",
1,
"Pete Molfese",
"00:00:00.000",
"00:00:01.000",
"Hello, world!"
);
assert_eq!(VttWriter::write(&cap), should_get);
}
#[test]
fn write_with_header() {
let cap = Caption {
header: Some("This is a VERY cool test".to_string()),
blocks: vec!(
CaptionBlock {
speaker: None,
start: SimpleTime::from_milliseconds(0),
end: SimpleTime::from_milliseconds(1500),
text: "We're doing very cool things".to_string(),
},
CaptionBlock {
speaker: None,
start: SimpleTime::from_milliseconds(1500),
end: SimpleTime::from_milliseconds(2500),
text: "The COOLEST things!".to_string(),
}
),
};
let should_get = format!(
"{}\n\nWEBVTT\n\n{}\n{} --> {}\n{}\n\n{}\n{} --> {}\n{}\n",
"This is a VERY cool test",
1,
"00:00:00.000",
"00:00:01.500",
"We're doing very cool things",
2,
"00:00:01.500",
"00:00:02.500",
"The COOLEST things!"
);
assert_eq!(VttWriter::write(&cap), should_get);
}
}
mod vtt_parser {
use super::*;
#[test]
fn parse() {
let block = format!(
"\n{}\n{}\n{}\n",
1,
"Pete Molfese 00:00:00.000 --> 00:00:01.000",
"Hello, welcome to the caption tool!"
);
let s = format!("WEBVTT\n{}", block);
let cap = VttParser::parse(&s)
.expect("Should have passed!");
assert_eq!(cap.header, None);
let expected_block = CaptionBlock::from(
Some("Pete Molfese".to_string()),
SimpleTime::from_milliseconds(0),
SimpleTime::from_milliseconds(1000),
"Hello, welcome to the caption tool!".to_string()
).unwrap();
assert_eq!(cap.blocks.len(), 1);
let received_block = &cap.blocks[0];
assert_eq!(expected_block.speaker, received_block.speaker);
assert_eq!(expected_block.start, received_block.start);
assert_eq!(expected_block.end, received_block.end);
assert_eq!(expected_block.text, received_block.text);
}
#[test]
fn parse_header() {
let h1 = "This is an event!";
let h2 = "Loads of cool presenters, all fabulous!";
let fake_block = format!("\n{}\n{}\n{}\n", 1, " ", " ");
let s = format!("{}\n{}\n\n\nWEBVTT\n{}", h1, h2, fake_block);
let (header, line_number) = VttParser::header(&s)
.expect("Should not have failed to parse!");
assert_eq!(header, Some(format!("{}\n{}", h1, h2)));
assert_eq!(line_number, 4);
}
#[test]
fn parse_no_header() {
let fake_block = format!("\n{}\n{}\n{}\n", 1, " ", " ");
let s = format!("WEBVTT\n{}", fake_block);
let (header, line_number) = VttParser::header(&s)
.expect("Should not have failed to parse!");
assert_eq!(header, None);
assert_eq!(line_number, 0);
}
#[test]
fn test_parse_block_no() {
let n = VttParser::block_number("1").expect("");
assert_eq!(n, 1);
let n = VttParser::block_number("a");
match n {
Ok(_) => panic!("Test failure! VttParser parses 'a'"),
Err(e) => {
match e {
VttParserError::UnexpectedEndOfFile => {
panic!("Test failure! VttParser wrong err");
},
VttParserError::ExpectedBlockNumber(s) => {
assert_eq!(s, "a");
},
_ => panic!("Unknown test failure")
};
},
};
}
#[test]
fn test_parse_block_header_no_name() {
let test_str_1 = "00:00:00.000 --> 00:00:01.001";
let r = VttParser::block_header(test_str_1);
match r {
Ok((None, start, end)) => {
assert_eq!(start.to_milliseconds(), 0);
assert_eq!(end.to_milliseconds(), 1001);
}
_ => panic!("Test failed"),
}
}
#[test]
fn test_parse_block_header_with_name() {
let test_str_2 = "Pete Molfese 00:00:00.000 --> 00:00:01.001";
let r = VttParser::block_header(test_str_2);
match r {
Ok((Some(s), start, end)) => {
assert_eq!(s, "Pete Molfese");
assert_eq!(start.to_milliseconds(), 0);
assert_eq!(end.to_milliseconds(), 1001);
},
Ok((None, _start, _end)) => {
panic!("Did not parse out any names");
},
Err(e) => {
panic!("Test failed with error {:?}", e );
},
}
}
#[test]
fn test_parse_block_header_missing_start() {
let test_str_3 = "--> 00:00:01.001";
let r = VttParser::block_header(test_str_3);
match r {
Ok((name, start, end)) => {
panic!("Parsed {:?}, {:?}, {:?} when should have failed", name, start, end);
},
Err(e) => {
match e {
VttParserError::InvalidTimestamp(_s) => {},
_ => panic!("Test failed in unexpected way"),
};
},
};
}
#[test]
fn test_parse_block_text() {
let test_str = "The quick brown fox jumps over the lazy dog.";
let text = VttParser::block_text(test_str);
assert_eq!(text, test_str.to_string());
}
#[test]
fn test_parse_block() {
let start = "00:00:00.000";
let end = "00:00:01.000";
let text = "The quick brown fox jumps over the lazy dog";
let test_input = format!("\n{}\n{} --> {}\n{}\n", 1, start, end, text);
let cb = VttParser::block(&test_input)
.expect("Failed test");
assert_eq!(cb.start().to_milliseconds(), 0);
assert_eq!(cb.end().to_milliseconds(), 1000);
assert_eq!(cb.speaker(), None);
assert_eq!(cb.text(), text);
}
#[test]
fn test_parse_block_fails_insufficient_lines() {
let x = VttParser::block("thing\n");
match x {
Err(VttParserError::UnexpectedEndOfFile) => {},
_ => panic!("Didn't get unexpected EOF {:?}", x),
};
}
}
mod srt_parser {
use super::*;
#[test]
fn parse() {
let s = format!(
"{}\n{}\n{}\n",
1,
"00:00:00,000 --> 00:00:01,000",
"[Peter Molfese] Hello, welcome to the caption tool!"
);
let cap = SrtParser::parse(&s)
.expect("Should have passed!");
assert_eq!(cap.header, None);
let expected_block = CaptionBlock::from(
Some("Peter Molfese".to_string()),
SimpleTime::from_milliseconds(0),
SimpleTime::from_milliseconds(1000),
"Hello, welcome to the caption tool!".to_string()
).unwrap();
assert_eq!(cap.blocks.len(), 1);
let received_block = &cap.blocks[0];
assert_eq!(expected_block.speaker, received_block.speaker);
assert_eq!(expected_block.start, received_block.start);
assert_eq!(expected_block.end, received_block.end);
assert_eq!(expected_block.text, received_block.text);
}
#[test]
fn test_parse_block_no() {
let n = SrtParser::block_number("1").expect("");
assert_eq!(n, 1);
let n = SrtParser::block_number("a");
match n {
Ok(_) => panic!("Test failure! SrtParser parses 'a'"),
Err(e) => {
match e {
SrtParserError::UnexpectedEndOfFile => {
panic!("Test failure! SrtParser wrong err");
},
SrtParserError::ExpectedBlockNumber(s) => {
assert_eq!(s, "a");
},
_ => panic!("Unknown test failure")
};
},
};
}
#[test]
fn test_parse_block_timestamps() {
let test_str_1 = "00:00:00,000 --> 00:00:01,001";
let r = SrtParser::block_timestamps(test_str_1);
match r {
Ok((start, end)) => {
assert_eq!(start.to_milliseconds(), 0);
assert_eq!(end.to_milliseconds(), 1001);
}
_ => panic!("Test failed"),
}
}
#[test]
fn test_parse_block_timestamps_missing_start() {
let test_str_3 = "--> 00:00:01,001";
let r = SrtParser::block_timestamps(test_str_3);
match r {
Ok((start, end)) => {
panic!("Parsed {:?}, {:?} when should have failed", start, end);
},
Err(e) => {
match e {
SrtParserError::InvalidTimestamp(_s) => {},
_ => panic!("Test failed in unexpected way"),
};
},
};
}
#[test]
fn test_parse_block_text() {
let spk = "Peter Molfese";
let txt = "The quick brown fox jumps over the lazy dog.";
let test_str = format!("[{}] {}", spk, txt);
let (speaker, text) = SrtParser::block_text(&test_str)
.expect("Should be fine");
assert_eq!(speaker, Some(spk.to_string()));
assert_eq!(text, txt.to_string());
}
#[test]
fn test_parse_block() {
let start = "00:00:00,000";
let end = "00:00:01,000";
let text = "The quick brown fox jumps over the lazy dog";
let test_input = format!("\n{}\n{} --> {}\n{}\n", 1, start, end, text);
let cb = SrtParser::block(&test_input)
.expect("Failed test");
assert_eq!(cb.start().to_milliseconds(), 0);
assert_eq!(cb.end().to_milliseconds(), 1000);
assert_eq!(cb.speaker(), None);
assert_eq!(cb.text(), text);
}
#[test]
fn test_parse_block_fails_insufficient_lines() {
let x = SrtParser::block("thing\n");
match x {
Err(SrtParserError::UnexpectedEndOfFile) => {},
_ => panic!("Didn't get unexpected EOF {:?}", x),
};
}
}
mod srt_writer {
use super::*;
#[test]
fn write() {
let cap = Caption {
header: None,
blocks: vec!(
CaptionBlock {
speaker: Some("Pete Molfese".to_string()),
start: SimpleTime::from_milliseconds(0),
end: SimpleTime::from_milliseconds(1000),
text: "Hello, world!".to_string(),
}
)
};
let should_get = format!(
"{}\n{} --> {}\n{}\n",
1,
"00:00:00,000",
"00:00:01,000",
"[Pete Molfese] Hello, world!"
);
assert_eq!(SrtWriter::write(&cap), should_get);
}
}
}