From bd7cce75e98a0219a71e5251d3717fbb700dd740 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Dr=C3=B6ge?= 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), 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> { + 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::>(), + .into_iter() + .map(|(k, v)| (String::from(k), v.into())) + .collect::>(), )), ); } @@ -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::>() + .into_iter() + .map(|(k, v)| (String::from(k), v.into())) + .collect::>() )) ); } @@ -875,9 +879,9 @@ mod tests { ("RESOLUTION", "22x22"), ("VIDEO", "1") ] - .into_iter() - .map(|(k, v)| (String::from(k), v.into())) - .collect::>() + .into_iter() + .map(|(k, v)| (String::from(k), v.into())) + .collect::>() )) ); } 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::() .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::() .map_err(|err| format!("Failed to parse AVERAGE-BANDWIDTH: {}", err))); @@ -843,7 +843,7 @@ pub struct MediaSegment { /// `#EXT-X-MAP:` pub map: Option, /// `#EXT-X-PROGRAM-DATE-TIME:` - pub program_date_time: Option, + pub program_date_time: Option>, /// `#EXT-X-DATERANGE:` pub daterange: Option, /// `#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, - pub start_date: String, - pub end_date: Option, + pub start_date: chrono::DateTime, + pub end_date: Option>, pub duration: Option, pub planned_duration: Option, pub x_prefixed: Option>, // X- @@ -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::() .map_err(|err| format!("Failed to parse DURATION attribute: {}", err))); @@ -1105,8 +1108,12 @@ impl DateRange { pub fn write_attributes_to(&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::() .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()),