1use std::fmt;
4use std::hash::{Hash, Hasher};
5
6use serde::{Deserialize, Serialize};
7
8#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct Chapter {
11 pub start_time: f64,
13 pub end_time: f64,
15 pub title: Option<String>,
17}
18
19impl Chapter {
20 pub fn duration(&self) -> f64 {
26 self.end_time - self.start_time
27 }
28
29 pub fn duration_minutes(&self) -> f64 {
35 self.duration() / 60.0
36 }
37
38 pub fn contains_timestamp(&self, timestamp: f64) -> bool {
48 timestamp >= self.start_time && timestamp < self.end_time
49 }
50
51 pub fn has_title(&self) -> bool {
57 self.title.is_some()
58 }
59
60 pub fn title_or<'a>(&'a self, default: &'a str) -> &'a str {
70 self.title.as_deref().unwrap_or(default)
71 }
72
73 pub fn title_contains(&self, query: &str) -> bool {
83 self.title
84 .as_ref()
85 .is_some_and(|title| title.to_lowercase().contains(&query.to_lowercase()))
86 }
87
88 pub fn title_matches(&self, query: &str) -> bool {
98 self.title
99 .as_ref()
100 .is_some_and(|title| title.to_lowercase() == query.to_lowercase())
101 }
102
103 pub fn title_starts_with(&self, prefix: &str) -> bool {
113 self.title
114 .as_ref()
115 .is_some_and(|title| title.to_lowercase().starts_with(&prefix.to_lowercase()))
116 }
117
118 pub fn duration_in_range(&self, min_duration: f64, max_duration: f64) -> bool {
129 let duration = self.duration();
130 duration >= min_duration && duration <= max_duration
131 }
132}
133
134pub struct ChapterList<'a> {
136 chapters: &'a [Chapter],
137}
138
139impl<'a> ChapterList<'a> {
140 pub fn new(chapters: &'a [Chapter]) -> Self {
142 Self { chapters }
143 }
144
145 pub fn search_by_title(&self, query: &str) -> Vec<&'a Chapter> {
155 self.chapters
156 .iter()
157 .filter(|chapter| chapter.title_contains(query))
158 .collect()
159 }
160
161 pub fn find_by_exact_title(&self, title: &str) -> Option<&'a Chapter> {
171 self.chapters.iter().find(|chapter| chapter.title_matches(title))
172 }
173
174 pub fn find_by_title_prefix(&self, prefix: &str) -> Vec<&'a Chapter> {
184 self.chapters
185 .iter()
186 .filter(|chapter| chapter.title_starts_with(prefix))
187 .collect()
188 }
189
190 pub fn find_by_timestamp(&self, timestamp: f64) -> Option<&'a Chapter> {
200 self.chapters
201 .iter()
202 .find(|chapter| chapter.contains_timestamp(timestamp))
203 }
204
205 pub fn filter_by_duration(&self, min_duration: f64, max_duration: f64) -> Vec<&'a Chapter> {
216 self.chapters
217 .iter()
218 .filter(|chapter| chapter.duration_in_range(min_duration, max_duration))
219 .collect()
220 }
221
222 pub fn with_titles(&self) -> Vec<&'a Chapter> {
228 self.chapters.iter().filter(|chapter| chapter.has_title()).collect()
229 }
230
231 pub fn count(&self) -> usize {
233 self.chapters.len()
234 }
235
236 pub fn total_duration(&self) -> f64 {
238 self.chapters.iter().map(|c| c.duration()).sum()
239 }
240
241 pub fn validate(&self) -> ChapterValidation {
247 let mut errors = Vec::new();
248 let mut warnings = Vec::new();
249
250 if self.chapters.is_empty() {
251 return ChapterValidation {
252 is_valid: true,
253 errors,
254 warnings,
255 };
256 }
257
258 Self::validate_individual_chapters(self.chapters, &mut errors, &mut warnings);
259 Self::validate_chapter_ordering(self.chapters, &mut errors, &mut warnings);
260
261 ChapterValidation {
262 is_valid: errors.is_empty(),
263 errors,
264 warnings,
265 }
266 }
267
268 fn validate_individual_chapters(chapters: &[Chapter], errors: &mut Vec<String>, warnings: &mut Vec<String>) {
269 for (i, chapter) in chapters.iter().enumerate() {
270 if chapter.start_time < 0.0 {
271 errors.push(format!(
272 "Chapter {} has negative start time: {:.2}s",
273 i + 1,
274 chapter.start_time
275 ));
276 }
277
278 if chapter.end_time < 0.0 {
279 errors.push(format!(
280 "Chapter {} has negative end time: {:.2}s",
281 i + 1,
282 chapter.end_time
283 ));
284 }
285
286 if chapter.start_time >= chapter.end_time {
287 errors.push(format!(
288 "Chapter {} has invalid time range: start ({:.2}s) >= end ({:.2}s)",
289 i + 1,
290 chapter.start_time,
291 chapter.end_time
292 ));
293 }
294
295 if !chapter.has_title() {
296 warnings.push(format!("Chapter {} has no title", i + 1));
297 }
298
299 if chapter.duration() < 1.0 {
300 warnings.push(format!("Chapter {} is very short ({:.2}s)", i + 1, chapter.duration()));
301 }
302 }
303 }
304
305 fn validate_chapter_ordering(chapters: &[Chapter], errors: &mut Vec<String>, warnings: &mut Vec<String>) {
306 for i in 0..chapters.len().saturating_sub(1) {
307 let current = &chapters[i];
308 let next = &chapters[i + 1];
309
310 if current.start_time > next.start_time {
311 errors.push(format!(
312 "Chapters {} and {} are out of order (current starts at {:.2}s, next starts at {:.2}s)",
313 i + 1,
314 i + 2,
315 current.start_time,
316 next.start_time
317 ));
318 }
319
320 if current.end_time > next.start_time {
321 errors.push(format!(
322 "Chapters {} and {} overlap (current ends at {:.2}s, next starts at {:.2}s)",
323 i + 1,
324 i + 2,
325 current.end_time,
326 next.start_time
327 ));
328 }
329
330 if current.end_time < next.start_time {
331 let gap = next.start_time - current.end_time;
332 if gap > 0.1 {
333 warnings.push(format!(
334 "Gap of {:.2}s between chapters {} and {} ({:.2}s to {:.2}s)",
335 gap,
336 i + 1,
337 i + 2,
338 current.end_time,
339 next.start_time
340 ));
341 }
342 }
343 }
344 }
345
346 pub fn is_sorted(&self) -> bool {
348 self.chapters
349 .windows(2)
350 .all(|pair| pair[0].start_time <= pair[1].start_time)
351 }
352
353 pub fn has_overlaps(&self) -> bool {
355 self.chapters
356 .windows(2)
357 .any(|pair| pair[0].end_time > pair[1].start_time)
358 }
359}
360
361#[derive(Debug, Clone, PartialEq, Eq)]
363pub struct ChapterValidation {
364 pub is_valid: bool,
366 pub errors: Vec<String>,
368 pub warnings: Vec<String>,
370}
371
372impl ChapterValidation {
373 pub fn valid() -> Self {
375 Self {
376 is_valid: true,
377 errors: Vec::new(),
378 warnings: Vec::new(),
379 }
380 }
381
382 pub fn invalid(errors: Vec<String>) -> Self {
384 Self {
385 is_valid: false,
386 errors,
387 warnings: Vec::new(),
388 }
389 }
390
391 pub fn with_warning(mut self, warning: String) -> Self {
393 self.warnings.push(warning);
394 self
395 }
396
397 pub fn with_warnings(mut self, warnings: Vec<String>) -> Self {
399 self.warnings.extend(warnings);
400 self
401 }
402
403 pub fn has_issues(&self) -> bool {
405 !self.errors.is_empty() || !self.warnings.is_empty()
406 }
407}
408
409impl fmt::Display for Chapter {
411 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
412 write!(
413 f,
414 "Chapter(start={:.2}s, end={:.2}s, title={:?})",
415 self.start_time,
416 self.end_time,
417 self.title.as_deref().unwrap_or("untitled")
418 )
419 }
420}
421
422impl fmt::Display for ChapterValidation {
423 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
424 write!(
425 f,
426 "ChapterValidation(valid={}, errors={}, warnings={})",
427 self.is_valid,
428 self.errors.len(),
429 self.warnings.len()
430 )
431 }
432}
433
434impl PartialEq for Chapter {
435 fn eq(&self, other: &Self) -> bool {
436 self.start_time.to_bits() == other.start_time.to_bits()
437 && self.end_time.to_bits() == other.end_time.to_bits()
438 && self.title == other.title
439 }
440}
441
442impl Hash for Chapter {
444 fn hash<H: Hasher>(&self, state: &mut H) {
445 self.start_time.to_bits().hash(state);
447 self.end_time.to_bits().hash(state);
448 self.title.hash(state);
449 }
450}