subtp/srt.rs
1//! A parser for the SubRip Subtitle (`.srt`) format provided by [`subtp::srt::SubRip`](SubRip).
2//!
3//! ## Example
4//! ```
5//! use subtp::srt::SubRip;
6//! use subtp::srt::SrtSubtitle;
7//! use subtp::srt::SrtTimestamp;
8//!
9//! let text = r#"1
10//! 00:00:01,000 --> 00:00:02,000
11//! Hello, world!
12//!
13//! 2
14//! 00:00:03,000 --> 00:00:04,000
15//! This is a sample.
16//! Thank you for your reading.
17//! "#;
18//!
19//! let srt = SubRip::parse(text).unwrap();
20//! assert_eq!(srt, SubRip {
21//! subtitles: vec![
22//! SrtSubtitle {
23//! sequence: 1,
24//! start: SrtTimestamp {
25//! hours: 0,
26//! minutes: 0,
27//! seconds: 1,
28//! milliseconds: 0,
29//! },
30//! end: SrtTimestamp {
31//! hours: 0,
32//! minutes: 0,
33//! seconds: 2,
34//! milliseconds: 0,
35//! },
36//! text: vec!["Hello, world!".to_string()],
37//! line_position: None,
38//! },
39//! SrtSubtitle {
40//! sequence: 2,
41//! start: SrtTimestamp {
42//! hours: 0,
43//! minutes: 0,
44//! seconds: 3,
45//! milliseconds: 0,
46//! },
47//! end: SrtTimestamp {
48//! hours: 0,
49//! minutes: 0,
50//! seconds: 4,
51//! milliseconds: 0,
52//! },
53//! text: vec![
54//! "This is a sample.".to_string(),
55//! "Thank you for your reading.".to_string()
56//! ],
57//! line_position: None,
58//! },
59//! ],
60//! });
61//!
62//! let rendered = srt.render();
63//! assert_eq!(rendered, text);
64//! ```
65
66use std::cmp::Ordering;
67use std::fmt::{Display, Formatter};
68use std::time::Duration;
69
70use crate::str_parser;
71use crate::ParseResult;
72
73/// The SubRip Subtitle (`.srt`) format.
74///
75/// Parses from text by [`SubRip::parse`](SubRip::parse)
76/// and renders to text by [`SubRip::render`](SubRip::render).
77///
78/// ## Example
79/// ```
80/// use subtp::srt::SubRip;
81/// use subtp::srt::SrtSubtitle;
82/// use subtp::srt::SrtTimestamp;
83///
84/// let subrip = SubRip {
85/// subtitles: vec![
86/// SrtSubtitle {
87/// sequence: 1,
88/// start: SrtTimestamp {
89/// hours: 0,
90/// minutes: 0,
91/// seconds: 1,
92/// milliseconds: 0,
93/// },
94/// end: SrtTimestamp {
95/// hours: 0,
96/// minutes: 0,
97/// seconds: 2,
98/// milliseconds: 0,
99/// },
100/// text: vec!["Hello, world!".to_string()],
101/// line_position: None,
102/// }
103/// ],
104/// };
105///
106/// assert_eq!(
107/// subrip.render(),
108/// "1\n00:00:01,000 --> 00:00:02,000\nHello, world!\n".to_string()
109/// );
110/// ```
111#[derive(Debug, Clone, PartialEq, Eq, Hash)]
112pub struct SubRip {
113 /// The collection of subtitles.
114 pub subtitles: Vec<SrtSubtitle>,
115}
116
117impl SubRip {
118 /// Parses the SubRip Subtitle format from the given text.
119 ///
120 /// ## Example
121 /// ```
122 /// use subtp::srt::SubRip;
123 ///
124 /// let text = r#"1
125 /// 00:00:01,000 --> 00:00:02,000
126 /// Hello, world!
127 ///
128 /// 2
129 /// 00:00:03,000 --> 00:00:04,000
130 /// This is a sample.
131 /// Thank you for your reading.
132 /// "#;
133 ///
134 /// let srt = SubRip::parse(text).unwrap();
135 /// ```
136 pub fn parse(text: &str) -> ParseResult<Self> {
137 str_parser::srt(text).map_err(|err| err.into())
138 }
139
140 /// Renders the text from the SubRip Subtitle format.
141 ///
142 /// ## Example
143 /// ```
144 /// use subtp::srt::SubRip;
145 /// use subtp::srt::SrtSubtitle;
146 /// use subtp::srt::SrtTimestamp;
147 ///
148 /// let subrip = SubRip {
149 /// subtitles: vec![
150 /// SrtSubtitle {
151 /// sequence: 1,
152 /// start: SrtTimestamp {
153 /// hours: 0,
154 /// minutes: 0,
155 /// seconds: 1,
156 /// milliseconds: 0,
157 /// },
158 /// end: SrtTimestamp {
159 /// hours: 0,
160 /// minutes: 0,
161 /// seconds: 2,
162 /// milliseconds: 0,
163 /// },
164 /// text: vec!["Hello, world!".to_string()],
165 /// line_position: None,
166 /// }
167 /// ],
168 /// };
169 ///
170 /// let rendered = subrip.render();
171 /// ```
172 pub fn render(&self) -> String {
173 self.to_string()
174 }
175}
176
177impl Default for SubRip {
178 fn default() -> Self {
179 Self {
180 subtitles: vec![],
181 }
182 }
183}
184
185impl Display for SubRip {
186 fn fmt(
187 &self,
188 f: &mut Formatter<'_>,
189 ) -> std::fmt::Result {
190 let length = self.subtitles.len();
191 for (i, subtitle) in self
192 .subtitles
193 .iter()
194 .enumerate()
195 {
196 if i + 1 < length {
197 write!(f, "{}\n", subtitle)?;
198 } else {
199 write!(f, "{}", subtitle)?;
200 }
201 }
202
203 Ok(())
204 }
205}
206
207impl Iterator for SubRip {
208 type Item = SrtSubtitle;
209
210 fn next(&mut self) -> Option<Self::Item> {
211 if self.subtitles.is_empty() {
212 None
213 } else {
214 Some(self.subtitles.remove(0))
215 }
216 }
217}
218
219/// The subtitle entry.
220///
221/// ## Example
222/// ```
223/// use subtp::srt::SrtSubtitle;
224/// use subtp::srt::SrtTimestamp;
225///
226/// let subtitle = SrtSubtitle {
227/// sequence: 1,
228/// start: SrtTimestamp {
229/// hours: 0,
230/// minutes: 0,
231/// seconds: 1,
232/// milliseconds: 0,
233/// },
234/// end: SrtTimestamp {
235/// hours: 0,
236/// minutes: 0,
237/// seconds: 2,
238/// milliseconds: 0,
239/// },
240/// text: vec!["Hello, world!".to_string()],
241/// line_position: None,
242/// };
243///
244/// assert_eq!(
245/// subtitle.to_string(),
246/// "1\n00:00:01,000 --> 00:00:02,000\nHello, world!\n".to_string()
247/// );
248/// ```
249///
250/// or using `Default` as follows:
251///
252/// ```
253/// use subtp::srt::SrtSubtitle;
254/// use subtp::srt::SrtTimestamp;
255///
256/// let subtitle = SrtSubtitle {
257/// sequence: 1,
258/// start: SrtTimestamp {
259/// seconds: 1,
260/// ..Default::default()
261/// },
262/// end: SrtTimestamp {
263/// seconds: 2,
264/// ..Default::default()
265/// },
266/// text: vec!["Hello, world!".to_string()],
267/// ..Default::default()
268/// };
269/// ```
270#[derive(Debug, Clone, Eq, Hash)]
271pub struct SrtSubtitle {
272 /// The sequence number.
273 pub sequence: u32,
274 /// The start timestamp.
275 pub start: SrtTimestamp,
276 /// The end timestamp.
277 pub end: SrtTimestamp,
278 /// The subtitle text.
279 pub text: Vec<String>,
280 /// The unofficial line position.
281 pub line_position: Option<LinePosition>,
282}
283
284impl PartialEq<Self> for SrtSubtitle {
285 fn eq(
286 &self,
287 other: &Self,
288 ) -> bool {
289 self.sequence == other.sequence
290 }
291}
292
293impl PartialOrd<Self> for SrtSubtitle {
294 fn partial_cmp(
295 &self,
296 other: &Self,
297 ) -> Option<Ordering> {
298 Some(self.cmp(other))
299 }
300}
301
302impl Ord for SrtSubtitle {
303 fn cmp(
304 &self,
305 other: &Self,
306 ) -> Ordering {
307 self.sequence
308 .cmp(&other.sequence)
309 }
310}
311
312impl Default for SrtSubtitle {
313 fn default() -> Self {
314 Self {
315 sequence: 0,
316 start: SrtTimestamp::default(),
317 end: SrtTimestamp::default(),
318 text: vec![],
319 line_position: None,
320 }
321 }
322}
323
324impl Display for SrtSubtitle {
325 fn fmt(
326 &self,
327 f: &mut Formatter<'_>,
328 ) -> std::fmt::Result {
329 write!(
330 f,
331 "{}\n{} --> {}\n{}\n",
332 self.sequence,
333 self.start,
334 self.end,
335 self.text.join("\n"),
336 )
337 }
338}
339
340/// The timestamp.
341///
342/// ## Example
343/// ```
344/// use subtp::srt::SrtTimestamp;
345///
346/// let timestamp = SrtTimestamp {
347/// hours: 0,
348/// minutes: 0,
349/// seconds: 1,
350/// milliseconds: 0,
351/// };
352///
353/// assert_eq!(
354/// timestamp.to_string(),
355/// "00:00:01,000".to_string()
356/// );
357/// ```
358///
359/// or using `Default` as follows:
360///
361/// ```
362/// use subtp::srt::SrtTimestamp;
363///
364/// let timestamp = SrtTimestamp {
365/// seconds: 1,
366/// ..Default::default()
367/// };
368///
369/// assert_eq!(
370/// timestamp.to_string(),
371/// "00:00:01,000".to_string()
372/// );
373/// ```
374#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash)]
375pub struct SrtTimestamp {
376 /// The hours.
377 pub hours: u8,
378 /// The minutes.
379 pub minutes: u8,
380 /// The seconds.
381 pub seconds: u8,
382 /// The milliseconds.
383 pub milliseconds: u16,
384}
385
386impl Default for SrtTimestamp {
387 fn default() -> Self {
388 Self {
389 hours: 0,
390 minutes: 0,
391 seconds: 0,
392 milliseconds: 0,
393 }
394 }
395}
396
397impl Display for SrtTimestamp {
398 fn fmt(
399 &self,
400 f: &mut Formatter<'_>,
401 ) -> std::fmt::Result {
402 write!(
403 f,
404 "{:02}:{:02}:{:02},{:03}",
405 self.hours, self.minutes, self.seconds, self.milliseconds
406 )
407 }
408}
409
410impl From<Duration> for SrtTimestamp {
411 fn from(duration: Duration) -> Self {
412 let seconds = duration.as_secs();
413 let milliseconds = duration.subsec_millis() as u16;
414
415 let hours = (seconds / 3600) as u8;
416 let minutes = ((seconds % 3600) / 60) as u8;
417 let seconds = (seconds % 60) as u8;
418
419 Self {
420 hours,
421 minutes,
422 seconds,
423 milliseconds,
424 }
425 }
426}
427
428impl Into<Duration> for SrtTimestamp {
429 fn into(self) -> Duration {
430 Duration::new(
431 self.hours as u64 * 3600
432 + self.minutes as u64 * 60
433 + self.seconds as u64,
434 self.milliseconds as u32 * 1_000_000,
435 )
436 }
437}
438
439/// Unofficial line position settings.
440#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash)]
441pub struct LinePosition {
442 /// X1 of the line position.
443 pub x1: u32,
444 /// X2 of the line position.
445 pub x2: u32,
446 /// Y1 of the line position.
447 pub y1: u32,
448 /// Y2 of the line position.
449 pub y2: u32,
450}
451
452impl Default for LinePosition {
453 fn default() -> Self {
454 Self {
455 x1: 0,
456 x2: 0,
457 y1: 0,
458 y2: 0,
459 }
460 }
461}
462
463impl Display for LinePosition {
464 fn fmt(
465 &self,
466 f: &mut Formatter<'_>,
467 ) -> std::fmt::Result {
468 write!(
469 f,
470 "X1:{} X2:{} Y1:{} Y2:{}",
471 self.x1, self.x2, self.y1, self.y2
472 )
473 }
474}
475
476#[cfg(test)]
477mod tests {
478 use super::*;
479
480 #[test]
481 fn parse() {
482 let srt_text = r#"
4831
48400:00:01,000 --> 00:00:02,000
485Hello, world!
486
4872
48800:00:03,000 --> 00:00:04,000
489This is a test.
490
491"#;
492
493 let expected = SubRip {
494 subtitles: vec![
495 SrtSubtitle {
496 sequence: 1,
497 start: SrtTimestamp {
498 hours: 0,
499 minutes: 0,
500 seconds: 1,
501 milliseconds: 0,
502 },
503 end: SrtTimestamp {
504 hours: 0,
505 minutes: 0,
506 seconds: 2,
507 milliseconds: 0,
508 },
509 text: vec!["Hello, world!".to_string()],
510 line_position: None,
511 },
512 SrtSubtitle {
513 sequence: 2,
514 start: SrtTimestamp {
515 hours: 0,
516 minutes: 0,
517 seconds: 3,
518 milliseconds: 0,
519 },
520 end: SrtTimestamp {
521 hours: 0,
522 minutes: 0,
523 seconds: 4,
524 milliseconds: 0,
525 },
526 text: vec!["This is a test.".to_string()],
527 line_position: None,
528 },
529 ],
530 };
531
532 assert_eq!(
533 SubRip::parse(srt_text).unwrap(),
534 expected
535 );
536 }
537
538 #[test]
539 fn render() {
540 let srt = SubRip {
541 subtitles: vec![SrtSubtitle {
542 sequence: 1,
543 start: SrtTimestamp {
544 hours: 0,
545 minutes: 0,
546 seconds: 1,
547 milliseconds: 0,
548 },
549 end: SrtTimestamp {
550 hours: 0,
551 minutes: 0,
552 seconds: 2,
553 milliseconds: 0,
554 },
555 text: vec!["Hello, world!".to_string()],
556 line_position: None,
557 }],
558 };
559 let expected = r#"1
56000:00:01,000 --> 00:00:02,000
561Hello, world!
562"#;
563 assert_eq!(srt.render(), expected);
564
565 let srt = SubRip {
566 subtitles: vec![
567 SrtSubtitle {
568 sequence: 1,
569 start: SrtTimestamp {
570 hours: 0,
571 minutes: 0,
572 seconds: 1,
573 milliseconds: 0,
574 },
575 end: SrtTimestamp {
576 hours: 0,
577 minutes: 0,
578 seconds: 2,
579 milliseconds: 0,
580 },
581 text: vec!["Hello, world!".to_string()],
582 line_position: None,
583 },
584 SrtSubtitle {
585 sequence: 2,
586 start: SrtTimestamp {
587 hours: 0,
588 minutes: 0,
589 seconds: 3,
590 milliseconds: 0,
591 },
592 end: SrtTimestamp {
593 hours: 0,
594 minutes: 0,
595 seconds: 4,
596 milliseconds: 0,
597 },
598 text: vec!["This is a test.".to_string()],
599 line_position: None,
600 },
601 ],
602 };
603 let expected = r#"1
60400:00:01,000 --> 00:00:02,000
605Hello, world!
606
6072
60800:00:03,000 --> 00:00:04,000
609This is a test.
610"#;
611 assert_eq!(srt.render(), expected);
612 }
613
614 #[test]
615 fn iterator() {
616 let srt = SubRip {
617 subtitles: vec![
618 SrtSubtitle {
619 sequence: 1,
620 start: SrtTimestamp {
621 hours: 0,
622 minutes: 0,
623 seconds: 1,
624 milliseconds: 0,
625 },
626 end: SrtTimestamp {
627 hours: 0,
628 minutes: 0,
629 seconds: 2,
630 milliseconds: 0,
631 },
632 text: vec!["Hello, world!".to_string()],
633 line_position: None,
634 },
635 SrtSubtitle {
636 sequence: 2,
637 start: SrtTimestamp {
638 hours: 0,
639 minutes: 0,
640 seconds: 3,
641 milliseconds: 0,
642 },
643 end: SrtTimestamp {
644 hours: 0,
645 minutes: 0,
646 seconds: 4,
647 milliseconds: 0,
648 },
649 text: vec!["This is a test.".to_string()],
650 line_position: None,
651 },
652 ],
653 };
654
655 let mut iter = srt.into_iter();
656
657 assert_eq!(
658 iter.next(),
659 Some(SrtSubtitle {
660 sequence: 1,
661 start: SrtTimestamp {
662 hours: 0,
663 minutes: 0,
664 seconds: 1,
665 milliseconds: 0,
666 },
667 end: SrtTimestamp {
668 hours: 0,
669 minutes: 0,
670 seconds: 2,
671 milliseconds: 0,
672 },
673 text: vec!["Hello, world!".to_string()],
674 line_position: None,
675 })
676 );
677
678 assert_eq!(
679 iter.next(),
680 Some(SrtSubtitle {
681 sequence: 2,
682 start: SrtTimestamp {
683 hours: 0,
684 minutes: 0,
685 seconds: 3,
686 milliseconds: 0,
687 },
688 end: SrtTimestamp {
689 hours: 0,
690 minutes: 0,
691 seconds: 4,
692 milliseconds: 0,
693 },
694 text: vec!["This is a test.".to_string()],
695 line_position: None,
696 })
697 );
698
699 assert_eq!(iter.next(), None);
700 }
701
702 #[test]
703 fn display_subtitle() {
704 let subtitle = SrtSubtitle {
705 sequence: 1,
706 start: SrtTimestamp {
707 hours: 0,
708 minutes: 0,
709 seconds: 1,
710 milliseconds: 0,
711 },
712 end: SrtTimestamp {
713 hours: 0,
714 minutes: 0,
715 seconds: 2,
716 milliseconds: 0,
717 },
718 text: vec!["Hello, world!".to_string()],
719 line_position: None,
720 };
721 let displayed = format!("{}", subtitle);
722 let expected = "1\n00:00:01,000 --> 00:00:02,000\nHello, world!\n";
723 assert_eq!(displayed, expected);
724
725 let subtitle = SrtSubtitle {
726 sequence: 1,
727 start: SrtTimestamp {
728 hours: 0,
729 minutes: 0,
730 seconds: 1,
731 milliseconds: 0,
732 },
733 end: SrtTimestamp {
734 hours: 0,
735 minutes: 0,
736 seconds: 2,
737 milliseconds: 0,
738 },
739 text: vec![
740 "Hello, world!".to_string(),
741 "This is the test.".to_string(),
742 ],
743 line_position: None,
744 };
745 let displayed = format!("{}", subtitle);
746 let expected = "1\n00:00:01,000 --> 00:00:02,000\nHello, world!\nThis is the test.\n";
747 assert_eq!(displayed, expected);
748 }
749
750 #[test]
751 fn order_subtitle() {
752 let subtitle1 = SrtSubtitle {
753 sequence: 1,
754 start: SrtTimestamp {
755 hours: 0,
756 minutes: 0,
757 seconds: 1,
758 milliseconds: 0,
759 },
760 end: SrtTimestamp {
761 hours: 0,
762 minutes: 0,
763 seconds: 2,
764 milliseconds: 0,
765 },
766 text: vec!["First".to_string()],
767 line_position: None,
768 };
769 let subtitle2 = SrtSubtitle {
770 sequence: 2,
771 start: SrtTimestamp {
772 hours: 0,
773 minutes: 0,
774 seconds: 3,
775 milliseconds: 0,
776 },
777 end: SrtTimestamp {
778 hours: 0,
779 minutes: 0,
780 seconds: 4,
781 milliseconds: 0,
782 },
783 text: vec!["Second".to_string()],
784 line_position: None,
785 };
786 assert!(subtitle1 < subtitle2);
787 }
788
789 #[test]
790 fn display_timestamp() {
791 let timestamp = SrtTimestamp {
792 hours: 0,
793 minutes: 0,
794 seconds: 1,
795 milliseconds: 0,
796 };
797 let displayed = format!("{}", timestamp);
798 let expected = "00:00:01,000";
799 assert_eq!(displayed, expected);
800 }
801
802 #[test]
803 fn from_duration_to_timestamp() {
804 let duration = Duration::new(1, 0);
805 let timestamp: SrtTimestamp = duration.into();
806 assert_eq!(
807 timestamp,
808 SrtTimestamp {
809 hours: 0,
810 minutes: 0,
811 seconds: 1,
812 milliseconds: 0,
813 }
814 );
815
816 let duration = Duration::new(3661, 0);
817 let timestamp: SrtTimestamp = duration.into();
818 assert_eq!(
819 timestamp,
820 SrtTimestamp {
821 hours: 1,
822 minutes: 1,
823 seconds: 1,
824 milliseconds: 0,
825 }
826 );
827
828 let duration = Duration::new(3661, 500 * 1_000_000);
829 let timestamp: SrtTimestamp = duration.into();
830 assert_eq!(
831 timestamp,
832 SrtTimestamp {
833 hours: 1,
834 minutes: 1,
835 seconds: 1,
836 milliseconds: 500,
837 }
838 );
839 }
840
841 #[test]
842 fn from_timestamp_to_duration() {
843 let timestamp = SrtTimestamp {
844 hours: 0,
845 minutes: 0,
846 seconds: 1,
847 milliseconds: 0,
848 };
849 let duration: Duration = timestamp.into();
850 assert_eq!(duration, Duration::new(1, 0));
851
852 let timestamp = SrtTimestamp {
853 hours: 1,
854 minutes: 1,
855 seconds: 1,
856 milliseconds: 0,
857 };
858 let duration: Duration = timestamp.into();
859 assert_eq!(duration, Duration::new(3661, 0));
860 }
861
862 #[test]
863 fn operate_timestamp_via_duration() {
864 let start: Duration = SrtTimestamp {
865 hours: 0,
866 minutes: 0,
867 seconds: 1,
868 milliseconds: 0,
869 }
870 .into();
871
872 let end: Duration = SrtTimestamp {
873 hours: 0,
874 minutes: 0,
875 seconds: 5,
876 milliseconds: 0,
877 }
878 .into();
879
880 let duration: Duration = end - start;
881 assert_eq!(duration, Duration::new(4, 0));
882
883 let duration: SrtTimestamp = duration.into();
884 assert_eq!(
885 duration,
886 SrtTimestamp {
887 hours: 0,
888 minutes: 0,
889 seconds: 4,
890 milliseconds: 0,
891 }
892 );
893
894 let end = end + Duration::new(1, 0);
895 let duration: Duration = end - start;
896 assert_eq!(duration, Duration::new(5, 0));
897
898 let duration: SrtTimestamp = duration.into();
899 assert_eq!(
900 duration,
901 SrtTimestamp {
902 hours: 0,
903 minutes: 0,
904 seconds: 5,
905 milliseconds: 0,
906 }
907 );
908 }
909
910 #[test]
911 fn order_timestamp() {
912 let timestamp1 = SrtTimestamp {
913 hours: 0,
914 minutes: 0,
915 seconds: 1,
916 milliseconds: 0,
917 };
918 let timestamp2 = SrtTimestamp {
919 hours: 0,
920 minutes: 0,
921 seconds: 2,
922 milliseconds: 0,
923 };
924 assert!(timestamp1 < timestamp2);
925 }
926}