use std::collections::BTreeMap; use std::fmt; use std::str::FromStr; use std::time::Duration; use chrono::{DateTime, FixedOffset}; use crate::attribute::AttributePairs; use crate::types::{ProtocolVersion, RequiredVersion}; use crate::utils::{quote, tag, unquote}; use crate::Error; /// [4.3.2.7. EXT-X-DATERANGE] /// /// [4.3.2.7. EXT-X-DATERANGE]: https://tools.ietf.org/html/rfc8216#section-4.3.2.7 /// /// TODO: Implement properly #[allow(missing_docs)] #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct ExtXDateRange { /// A string that uniquely identifies a [ExtXDateRange] in the Playlist. /// This attribute is required. id: String, /// A client-defined string that specifies some set of attributes and their associated value /// semantics. All Date Ranges with the same CLASS attribute value MUST adhere to these /// semantics. This attribute is OPTIONAL. class: Option, /// The date at which the Date Range begins. This attribute is REQUIRED. start_date: DateTime, /// The date at which the Date Range ends. It MUST be equal to or later than the value of the /// START-DATE attribute. This attribute is OPTIONAL. end_date: Option>, /// The duration of the Date Range. It MUST NOT be negative. A single /// instant in time (e.g., crossing a finish line) SHOULD be /// represented with a duration of 0. This attribute is OPTIONAL. duration: Option, /// The expected duration of the Date Range. It MUST NOT be negative. This /// attribute SHOULD be used to indicate the expected duration of a /// Date Range whose actual duration is not yet known. /// It is OPTIONAL. planned_duration: Option, /// scte35_cmd: Option, /// scte35_out: Option, /// scte35_in: Option, /// This attribute indicates that the end of the range containing it is equal to the /// START-DATE of its Following Range. The Following Range is the /// Date Range of the same CLASS, that has the earliest START-DATE /// after the START-DATE of the range in question. This attribute is /// OPTIONAL. end_on_next: bool, /// The "X-" prefix defines a namespace reserved for client-defined /// attributes. The client-attribute MUST be a legal AttributeName. /// Clients SHOULD use a reverse-DNS syntax when defining their own /// attribute names to avoid collisions. The attribute value MUST be /// a quoted-string, a hexadecimal-sequence, or a decimal-floating- /// point. An example of a client-defined attribute is X-COM-EXAMPLE- /// AD-ID="XYZ123". These attributes are OPTIONAL. client_attributes: BTreeMap, } impl ExtXDateRange { pub(crate) const PREFIX: &'static str = "#EXT-X-DATERANGE:"; /// Makes a new [ExtXDateRange] tag. /// /// # Example /// ``` /// # use hls_m3u8::tags::ExtXDateRange; /// use chrono::{DateTime, FixedOffset}; /// use chrono::offset::TimeZone; /// /// const HOURS_IN_SECS: i32 = 3600; // 1 hour = 3600 seconds /// /// ExtXDateRange::new("id", FixedOffset::east(8 * HOURS_IN_SECS) /// .ymd(2010, 2, 19) /// .and_hms_milli(14, 54, 23, 31)); /// ``` pub fn new(id: T, start_date: DateTime) -> Self { Self { id: id.to_string(), class: None, start_date, end_date: None, duration: None, planned_duration: None, scte35_cmd: None, scte35_out: None, scte35_in: None, end_on_next: false, client_attributes: BTreeMap::new(), } } } impl RequiredVersion for ExtXDateRange { fn required_version(&self) -> ProtocolVersion { ProtocolVersion::V1 } } impl fmt::Display for ExtXDateRange { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}", Self::PREFIX)?; write!(f, "ID={}", quote(&self.id))?; if let Some(value) = &self.class { write!(f, ",CLASS={}", quote(value))?; } write!(f, ",START-DATE={}", quote(&self.start_date))?; if let Some(value) = &self.end_date { write!(f, ",END-DATE={}", quote(value))?; } if let Some(value) = &self.duration { write!(f, ",DURATION={}", value.as_secs_f64())?; } if let Some(value) = &self.planned_duration { write!(f, ",PLANNED-DURATION={}", value.as_secs_f64())?; } if let Some(value) = &self.scte35_cmd { write!(f, ",SCTE35-CMD={}", quote(value))?; } if let Some(value) = &self.scte35_out { write!(f, ",SCTE35-OUT={}", quote(value))?; } if let Some(value) = &self.scte35_in { write!(f, ",SCTE35-IN={}", quote(value))?; } if self.end_on_next { write!(f, ",END-ON-NEXT=YES",)?; } for (k, v) in &self.client_attributes { write!(f, ",{}={}", k, v)?; } Ok(()) } } impl FromStr for ExtXDateRange { type Err = Error; fn from_str(input: &str) -> Result { let input = tag(input, Self::PREFIX)?; let mut id = None; let mut class = None; let mut start_date = None; let mut end_date = None; let mut duration = None; let mut planned_duration = None; let mut scte35_cmd = None; let mut scte35_out = None; let mut scte35_in = None; let mut end_on_next = false; let mut client_attributes = BTreeMap::new(); for (key, value) in input.parse::()? { match key.as_str() { "ID" => id = Some(unquote(value)), "CLASS" => class = Some(unquote(value)), "START-DATE" => start_date = Some(unquote(value)), "END-DATE" => end_date = Some(unquote(value).parse()?), "DURATION" => { duration = Some(Duration::from_secs_f64(value.parse()?)); } "PLANNED-DURATION" => { planned_duration = Some(Duration::from_secs_f64(value.parse()?)); } "SCTE35-CMD" => scte35_cmd = Some(unquote(value)), "SCTE35-OUT" => scte35_out = Some(unquote(value)), "SCTE35-IN" => scte35_in = Some(unquote(value)), "END-ON-NEXT" => { if value != "YES" { return Err(Error::invalid_input()); } end_on_next = true; } _ => { if key.starts_with("X-") { client_attributes.insert(key.split_at(2).1.to_owned(), value.to_owned()); } else { // [6.3.1. General Client Responsibilities] // > ignore any attribute/value pair with an unrecognized AttributeName. } } } } let id = id.ok_or_else(|| Error::missing_value("ID"))?; let start_date = start_date .ok_or_else(|| Error::missing_value("START-DATE"))? .parse()?; if end_on_next && class.is_none() { return Err(Error::invalid_input()); } Ok(Self { id, class, start_date, end_date, duration, planned_duration, scte35_cmd, scte35_out, scte35_in, end_on_next, client_attributes, }) } } #[cfg(test)] mod test { use super::*; use chrono::offset::TimeZone; const HOURS_IN_SECS: i32 = 3600; // 1 hour = 3600 seconds #[test] fn test_required_version() { assert_eq!( ExtXDateRange::new( "id", FixedOffset::east(8 * HOURS_IN_SECS) .ymd(2010, 2, 19) .and_hms_milli(14, 54, 23, 31) ) .required_version(), ProtocolVersion::V1 ); } }