oxide_browser/
subtitle.rs1#[derive(Clone, Debug)]
4pub struct SubtitleCue {
5 pub start_ms: u64,
6 pub end_ms: u64,
7 pub text: String,
8}
9
10fn parse_timestamp(s: &str) -> Option<u64> {
11 let s = s.trim();
12 let (hms, ms_part) = if let Some(i) = s.rfind([',', '.']) {
14 (&s[..i], &s[i + 1..])
15 } else {
16 return None;
17 };
18 let parts: Vec<&str> = hms.split(':').collect();
19 if parts.len() != 3 {
20 return None;
21 }
22 let h: u64 = parts[0].parse().ok()?;
23 let m: u64 = parts[1].parse().ok()?;
24 let sec: u64 = parts[2].parse().ok()?;
25 let ms: u64 = ms_part.parse().ok()?;
26 Some(((h * 60 + m) * 60 + sec) * 1000 + ms)
27}
28
29pub fn parse_srt(data: &str) -> Vec<SubtitleCue> {
31 let mut out = Vec::new();
32 let blocks: Vec<&str> = data.split("\n\n").collect();
33 for block in blocks {
34 let lines: Vec<&str> = block.lines().filter(|l| !l.trim().is_empty()).collect();
35 if lines.len() < 2 {
36 continue;
37 }
38 let mut i = 0;
39 if lines[0].trim().chars().all(|c| c.is_ascii_digit()) {
40 i = 1;
41 }
42 if i >= lines.len() {
43 continue;
44 }
45 let time_line = lines[i];
46 let Some(arrow) = time_line.find("-->") else {
47 continue;
48 };
49 let left = time_line[..arrow].trim();
50 let right = time_line[arrow + 3..].trim();
51 let right = right.split_whitespace().next().unwrap_or(right);
53 let Some(start) = parse_timestamp(left) else {
54 continue;
55 };
56 let Some(end) = parse_timestamp(right) else {
57 continue;
58 };
59 let text = lines[i + 1..].join("\n");
60 if text.is_empty() {
61 continue;
62 }
63 out.push(SubtitleCue {
64 start_ms: start,
65 end_ms: end,
66 text,
67 });
68 }
69 out
70}
71
72pub fn parse_vtt(data: &str) -> Vec<SubtitleCue> {
74 let mut out = Vec::new();
75 let mut lines = data.lines().peekable();
76 if let Some(first) = lines.peek() {
77 if first.trim().eq_ignore_ascii_case("WEBVTT") {
78 lines.next();
79 }
80 }
81 let mut buf: Vec<String> = Vec::new();
82 for line in lines {
83 let line = line.trim_end();
84 if line.is_empty() {
85 if !buf.is_empty() {
86 if let Some(cue) = parse_vtt_block(&buf) {
87 out.push(cue);
88 }
89 buf.clear();
90 }
91 continue;
92 }
93 buf.push(line.to_string());
94 }
95 if !buf.is_empty() {
96 if let Some(cue) = parse_vtt_block(&buf) {
97 out.push(cue);
98 }
99 }
100 out
101}
102
103fn parse_vtt_block(lines: &[String]) -> Option<SubtitleCue> {
104 if lines.is_empty() {
105 return None;
106 }
107 let mut i = 0;
108 let time_line = if lines[0].contains("-->") {
109 &lines[0]
110 } else if lines.len() >= 2 && lines[1].contains("-->") {
111 i = 1;
112 &lines[1]
113 } else {
114 return None;
115 };
116 let arrow = time_line.find("-->")?;
117 let left = time_line[..arrow].trim();
118 let right = time_line[arrow + 3..].trim();
119 let right = right.split_whitespace().next()?;
120 let start = parse_timestamp(left)?;
121 let end = parse_timestamp(right)?;
122 let text = lines[i + 1..].join("\n");
123 if text.is_empty() {
124 return None;
125 }
126 Some(SubtitleCue {
127 start_ms: start,
128 end_ms: end,
129 text,
130 })
131}
132
133pub fn cue_text_at(cues: &[SubtitleCue], t_ms: u64) -> Option<&str> {
135 cues.iter()
136 .find(|c| t_ms >= c.start_ms && t_ms <= c.end_ms)
137 .map(|c| c.text.as_str())
138}