Parse PROGRAM-DATE-TIME and DATERANGE start/end as proper datetimes instead of strings

This commit is contained in:
Sebastian Dröge 2022-07-20 11:30:34 +03:00 committed by Sebastian Dröge
parent 7247e02ee5
commit bd7cce75e9
4 changed files with 46 additions and 27 deletions

View file

@ -11,6 +11,7 @@ edition = "2018"
[dependencies]
nom = { version = "7", optional = true }
chrono = { version = "0.4" }
[features]
default = ["parser"]

View file

@ -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<_, _>>()
))
);
}

View file

@ -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(),

View file

@ -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()),