1use std::collections::HashMap;
27use std::time::Duration;
28
29use crate::time::Rational;
30
31#[derive(Debug, Clone)]
53pub struct ChapterInfo {
54 id: i64,
56 title: Option<String>,
58 start: Duration,
60 end: Duration,
62 time_base: Option<Rational>,
67 metadata: Option<HashMap<String, String>>,
71}
72
73impl ChapterInfo {
74 #[must_use]
76 pub fn builder() -> ChapterInfoBuilder {
77 ChapterInfoBuilder::default()
78 }
79
80 #[must_use]
82 #[inline]
83 pub fn id(&self) -> i64 {
84 self.id
85 }
86
87 #[must_use]
89 #[inline]
90 pub fn title(&self) -> Option<&str> {
91 self.title.as_deref()
92 }
93
94 #[must_use]
96 #[inline]
97 pub fn start(&self) -> Duration {
98 self.start
99 }
100
101 #[must_use]
103 #[inline]
104 pub fn end(&self) -> Duration {
105 self.end
106 }
107
108 #[must_use]
110 #[inline]
111 pub fn time_base(&self) -> Option<Rational> {
112 self.time_base
113 }
114
115 #[must_use]
117 #[inline]
118 pub fn metadata(&self) -> Option<&HashMap<String, String>> {
119 self.metadata.as_ref()
120 }
121
122 #[must_use]
124 #[inline]
125 pub fn has_title(&self) -> bool {
126 self.title.is_some()
127 }
128
129 #[must_use]
134 #[inline]
135 pub fn duration(&self) -> Duration {
136 self.end.saturating_sub(self.start)
137 }
138}
139
140impl Default for ChapterInfo {
141 fn default() -> Self {
142 Self {
143 id: 0,
144 title: None,
145 start: Duration::ZERO,
146 end: Duration::ZERO,
147 time_base: None,
148 metadata: None,
149 }
150 }
151}
152
153#[derive(Debug, Clone, Default)]
174pub struct ChapterInfoBuilder {
175 id: i64,
176 title: Option<String>,
177 start: Duration,
178 end: Duration,
179 time_base: Option<Rational>,
180 metadata: Option<HashMap<String, String>>,
181}
182
183impl ChapterInfoBuilder {
184 #[must_use]
186 pub fn id(mut self, id: i64) -> Self {
187 self.id = id;
188 self
189 }
190
191 #[must_use]
193 pub fn title(mut self, title: impl Into<String>) -> Self {
194 self.title = Some(title.into());
195 self
196 }
197
198 #[must_use]
200 pub fn start(mut self, start: Duration) -> Self {
201 self.start = start;
202 self
203 }
204
205 #[must_use]
207 pub fn end(mut self, end: Duration) -> Self {
208 self.end = end;
209 self
210 }
211
212 #[must_use]
214 pub fn time_base(mut self, time_base: Rational) -> Self {
215 self.time_base = Some(time_base);
216 self
217 }
218
219 #[must_use]
221 pub fn metadata(mut self, metadata: HashMap<String, String>) -> Self {
222 self.metadata = Some(metadata);
223 self
224 }
225
226 #[must_use]
228 pub fn build(self) -> ChapterInfo {
229 ChapterInfo {
230 id: self.id,
231 title: self.title,
232 start: self.start,
233 end: self.end,
234 time_base: self.time_base,
235 metadata: self.metadata,
236 }
237 }
238}
239
240#[cfg(test)]
241mod tests {
242 use super::*;
243
244 #[test]
245 fn chapter_info_builder_should_set_all_fields() {
246 let mut meta = HashMap::new();
247 meta.insert("language".to_string(), "eng".to_string());
248
249 let info = ChapterInfo::builder()
250 .id(3)
251 .title("Intro")
252 .start(Duration::from_secs(0))
253 .end(Duration::from_secs(60))
254 .time_base(Rational::new(1, 1000))
255 .metadata(meta)
256 .build();
257
258 assert_eq!(info.id(), 3);
259 assert_eq!(info.title(), Some("Intro"));
260 assert_eq!(info.start(), Duration::ZERO);
261 assert_eq!(info.end(), Duration::from_secs(60));
262 assert_eq!(info.time_base(), Some(Rational::new(1, 1000)));
263 assert_eq!(info.metadata().unwrap()["language"], "eng");
264 }
265
266 #[test]
267 fn chapter_info_duration_should_return_end_minus_start() {
268 let info = ChapterInfo::builder()
269 .start(Duration::from_secs(10))
270 .end(Duration::from_secs(70))
271 .build();
272
273 assert_eq!(info.duration(), Duration::from_secs(60));
274 }
275
276 #[test]
277 fn chapter_info_duration_should_return_zero_when_end_before_start() {
278 let info = ChapterInfo::builder()
279 .start(Duration::from_secs(70))
280 .end(Duration::from_secs(10))
281 .build();
282
283 assert_eq!(info.duration(), Duration::ZERO);
284 }
285
286 #[test]
287 fn chapter_info_with_no_title_should_return_none() {
288 let info = ChapterInfo::builder().id(1).build();
289
290 assert_eq!(info.title(), None);
291 assert!(!info.has_title());
292 }
293
294 #[test]
295 fn chapter_info_with_title_should_have_title() {
296 let info = ChapterInfo::builder().title("Chapter One").build();
297
298 assert_eq!(info.title(), Some("Chapter One"));
299 assert!(info.has_title());
300 }
301
302 #[test]
303 fn chapter_info_default_should_have_zero_times() {
304 let info = ChapterInfo::default();
305
306 assert_eq!(info.id(), 0);
307 assert_eq!(info.start(), Duration::ZERO);
308 assert_eq!(info.end(), Duration::ZERO);
309 assert!(info.title().is_none());
310 assert!(info.time_base().is_none());
311 assert!(info.metadata().is_none());
312 }
313
314 #[test]
315 fn chapter_info_builder_without_metadata_should_return_none() {
316 let info = ChapterInfo::builder().id(1).title("Test").build();
317
318 assert!(info.metadata().is_none());
319 }
320
321 #[test]
322 fn chapter_info_builder_clone_should_produce_equal_instance() {
323 let builder = ChapterInfo::builder()
324 .id(5)
325 .title("Cloned")
326 .start(Duration::from_secs(100))
327 .end(Duration::from_secs(200));
328
329 let info1 = builder.clone().build();
330 let info2 = builder.build();
331
332 assert_eq!(info1.id(), info2.id());
333 assert_eq!(info1.title(), info2.title());
334 assert_eq!(info1.start(), info2.start());
335 assert_eq!(info1.end(), info2.end());
336 }
337}