From 2ed1c183379bf676d35b837bed2f68458f6782ee Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Sebastian=20Dr=C3=B6ge?= <sebastian@centricular.com>
Date: Wed, 20 Jul 2022 11:30:34 +0300
Subject: [PATCH] Parse PROGRAM-DATE-TIME and DATERANGE start/end as proper
 datetimes instead of strings

---
 Cargo.toml      |  1 +
 src/parser.rs   | 28 ++++++++++++++++------------
 src/playlist.rs | 33 ++++++++++++++++++++-------------
 tests/lib.rs    | 11 +++++++++--
 4 files changed, 46 insertions(+), 27 deletions(-)

diff --git a/Cargo.toml b/Cargo.toml
index 901ad03..c5508ca 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -11,6 +11,7 @@ edition = "2018"
 
 [dependencies]
 nom = { version = "7", optional = true }
+chrono = { version = "0.4" }
 
 [features]
 default = ["parser"]
diff --git a/src/parser.rs b/src/parser.rs
index ce113d0..134aef3 100644
--- a/src/parser.rs
+++ b/src/parser.rs
@@ -484,7 +484,7 @@ enum SegmentTag {
     Discontinuity,
     Key(Key),
     Map(Map),
-    ProgramDateTime(String),
+    ProgramDateTime(chrono::DateTime<chrono::FixedOffset>),
     DateRange(DateRange),
     Unknown(ExtTag),
     Comment(String),
@@ -509,8 +509,8 @@ fn media_segment_tag(i: &[u8]) -> IResult<&[u8], SegmentTag> {
             SegmentTag::Map(map)
         }),
         map(
-            pair(tag("#EXT-X-PROGRAM-DATE-TIME:"), consume_line),
-            |(_, line)| SegmentTag::ProgramDateTime(line),
+            pair(tag("#EXT-X-PROGRAM-DATE-TIME:"), program_date_time),
+            |(_, pdt)| SegmentTag::ProgramDateTime(pdt),
         ),
         map(pair(tag("#EXT-X-DATERANGE:"), daterange), |(_, range)| {
             SegmentTag::DateRange(range)
@@ -538,6 +538,10 @@ fn key(i: &[u8]) -> IResult<&[u8], Key> {
     map_res(key_value_pairs, Key::from_hashmap)(i)
 }
 
+fn program_date_time(i: &[u8]) -> IResult<&[u8], chrono::DateTime<chrono::FixedOffset>> {
+    map_res(consume_line, |s| chrono::DateTime::parse_from_rfc3339(&s))(i)
+}
+
 fn daterange(i: &[u8]) -> IResult<&[u8], DateRange> {
     map_res(key_value_pairs, DateRange::from_hashmap)(i)
 }
@@ -821,9 +825,9 @@ mod tests {
                     ("BANDWIDTH", "395000"),
                     ("CODECS", "\"avc1.4d001f,mp4a.40.2\"")
                 ]
-                    .into_iter()
-                    .map(|(k, v)| (String::from(k), v.into()))
-                    .collect::<HashMap<_, _>>(),
+                .into_iter()
+                .map(|(k, v)| (String::from(k), v.into()))
+                .collect::<HashMap<_, _>>(),
             )),
         );
     }
@@ -857,9 +861,9 @@ mod tests {
                     ("BANDWIDTH", "300000"),
                     ("CODECS", "\"avc1.42c015,mp4a.40.2\"")
                 ]
-                    .into_iter()
-                    .map(|(k, v)| (String::from(k), v.into()))
-                    .collect::<HashMap<_, _>>()
+                .into_iter()
+                .map(|(k, v)| (String::from(k), v.into()))
+                .collect::<HashMap<_, _>>()
             ))
         );
     }
@@ -875,9 +879,9 @@ mod tests {
                     ("RESOLUTION", "22x22"),
                     ("VIDEO", "1")
                 ]
-                    .into_iter()
-                    .map(|(k, v)| (String::from(k), v.into()))
-                    .collect::<HashMap<_, _>>()
+                .into_iter()
+                .map(|(k, v)| (String::from(k), v.into()))
+                .collect::<HashMap<_, _>>()
             ))
         );
     }
diff --git a/src/playlist.rs b/src/playlist.rs
index 1b8e50d..49202b3 100644
--- a/src/playlist.rs
+++ b/src/playlist.rs
@@ -260,7 +260,7 @@ impl VariantStream {
         let bandwidth = unquoted_string_parse!(attrs, "BANDWIDTH", |s: &str| s
             .parse::<u64>()
             .map_err(|err| format!("Failed to parse BANDWIDTH attribute: {}", err)))
-            .ok_or_else(|| String::from("EXT-X-STREAM-INF without mandatory BANDWIDTH attribute"))?;
+        .ok_or_else(|| String::from("EXT-X-STREAM-INF without mandatory BANDWIDTH attribute"))?;
         let average_bandwidth = unquoted_string_parse!(attrs, "AVERAGE-BANDWIDTH", |s: &str| s
             .parse::<u64>()
             .map_err(|err| format!("Failed to parse AVERAGE-BANDWIDTH: {}", err)));
@@ -843,7 +843,7 @@ pub struct MediaSegment {
     /// `#EXT-X-MAP:<attribute-list>`
     pub map: Option<Map>,
     /// `#EXT-X-PROGRAM-DATE-TIME:<YYYY-MM-DDThh:mm:ssZ>`
-    pub program_date_time: Option<String>,
+    pub program_date_time: Option<chrono::DateTime<chrono::FixedOffset>>,
     /// `#EXT-X-DATERANGE:<attribute-list>`
     pub daterange: Option<DateRange>,
     /// `#EXT-`
@@ -875,7 +875,7 @@ impl MediaSegment {
             writeln!(w)?;
         }
         if let Some(ref v) = self.program_date_time {
-            writeln!(w, "#EXT-X-PROGRAM-DATE-TIME:{}", v)?;
+            writeln!(w, "#EXT-X-PROGRAM-DATE-TIME:{}", v.to_rfc3339())?;
         }
         if let Some(ref v) = self.daterange {
             write!(w, "#EXT-X-DATERANGE:")?;
@@ -1042,12 +1042,12 @@ impl ByteRange {
 /// The EXT-X-DATERANGE tag associates a Date Range (i.e. a range of time
 /// defined by a starting and ending date) with a set of attribute /
 /// value pairs.
-#[derive(Debug, Default, PartialEq, Clone)]
+#[derive(Debug, PartialEq, Clone)]
 pub struct DateRange {
     pub id: String,
     pub class: Option<String>,
-    pub start_date: String,
-    pub end_date: Option<String>,
+    pub start_date: chrono::DateTime<chrono::FixedOffset>,
+    pub end_date: Option<chrono::DateTime<chrono::FixedOffset>>,
     pub duration: Option<f64>,
     pub planned_duration: Option<f64>,
     pub x_prefixed: Option<HashMap<String, QuotedOrUnquoted>>, //  X-<client-attribute>
@@ -1060,10 +1060,13 @@ impl DateRange {
         let id = quoted_string!(attrs, "ID")
             .ok_or_else(|| String::from("EXT-X-DATERANGE without mandatory ID attribute"))?;
         let class = quoted_string!(attrs, "CLASS");
-        let start_date = quoted_string!(attrs, "START-DATE").ok_or_else(|| {
-            String::from("EXT-X-DATERANGE without mandatory START-DATE attribute")
-        })?;
-        let end_date = quoted_string!(attrs, "END-DATE");
+        let start_date =
+            quoted_string_parse!(attrs, "START-DATE", chrono::DateTime::parse_from_rfc3339)
+                .ok_or_else(|| {
+                    String::from("EXT-X-DATERANGE without mandatory START-DATE attribute")
+                })?;
+        let end_date =
+            quoted_string_parse!(attrs, "END-DATE", chrono::DateTime::parse_from_rfc3339);
         let duration = unquoted_string_parse!(attrs, "DURATION", |s: &str| s
             .parse::<f64>()
             .map_err(|err| format!("Failed to parse DURATION attribute: {}", err)));
@@ -1105,8 +1108,12 @@ impl DateRange {
     pub fn write_attributes_to<T: Write>(&self, w: &mut T) -> std::io::Result<()> {
         write_some_attribute_quoted!(w, "ID", &Some(&self.id))?;
         write_some_attribute_quoted!(w, ",CLASS", &self.class)?;
-        write_some_attribute_quoted!(w, ",START-DATE", &Some(&self.start_date))?;
-        write_some_attribute_quoted!(w, ",END-DATE", &self.end_date)?;
+        write_some_attribute_quoted!(w, ",START-DATE", &Some(&self.start_date.to_rfc3339()))?;
+        write_some_attribute_quoted!(
+            w,
+            ",END-DATE",
+            &self.end_date.as_ref().map(|dt| dt.to_rfc3339())
+        )?;
         write_some_attribute!(w, ",DURATION", &self.duration)?;
         write_some_attribute!(w, ",PLANNED-DURATION", &self.planned_duration)?;
         if let Some(x_prefixed) = &self.x_prefixed {
@@ -1149,7 +1156,7 @@ impl Start {
         let time_offset = unquoted_string_parse!(attrs, "TIME-OFFSET", |s: &str| s
             .parse::<f64>()
             .map_err(|err| format!("Failed to parse TIME-OFFSET attribute: {}", err)))
-            .ok_or_else(|| String::from("EXT-X-START without mandatory TIME-OFFSET attribute"))?;
+        .ok_or_else(|| String::from("EXT-X-START without mandatory TIME-OFFSET attribute"))?;
         Ok(Start {
             time_offset,
             precise: is_yes!(attrs, "PRECISE").into(),
diff --git a/tests/lib.rs b/tests/lib.rs
index 8be5a7a..a3d594a 100644
--- a/tests/lib.rs
+++ b/tests/lib.rs
@@ -1,5 +1,6 @@
 #![allow(unused_variables, unused_imports, dead_code)]
 
+use chrono::prelude::*;
 use m3u8_rs::QuotedOrUnquoted::Quoted;
 use m3u8_rs::*;
 use nom::AsBytes;
@@ -356,11 +357,17 @@ fn create_and_parse_media_playlist_full() {
                 }),
                 other_attributes: Default::default(),
             }),
-            program_date_time: Some("broodlordinfestorgg".into()),
+            program_date_time: Some(
+                chrono::FixedOffset::east(8 * 3600)
+                    .ymd(2010, 2, 19)
+                    .and_hms_milli(14, 54, 23, 31),
+            ),
             daterange: Some(DateRange {
                 id: "9999".into(),
                 class: Some("class".into()),
-                start_date: "2018-08-22T21:54:00.079Z".into(),
+                start_date: chrono::FixedOffset::east(8 * 3600)
+                    .ymd(2010, 2, 19)
+                    .and_hms_milli(14, 54, 23, 31),
                 end_date: None,
                 duration: None,
                 planned_duration: Some("40.000".parse().unwrap()),