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}