use std::borrow::Cow; use std::convert::TryFrom; use std::fmt; use std::time::Duration; use derive_more::AsRef; use crate::types::ProtocolVersion; use crate::utils::tag; use crate::{Error, RequiredVersion}; /// Specifies the duration of a [`Media Segment`]. /// /// [`Media Segment`]: crate::media_segment::MediaSegment #[derive(AsRef, Default, Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] pub struct ExtInf<'a> { #[as_ref] duration: Duration, title: Option>, } impl<'a> ExtInf<'a> { pub(crate) const PREFIX: &'static str = "#EXTINF:"; /// Makes a new [`ExtInf`] tag. /// /// # Example /// /// ``` /// # use hls_m3u8::tags::ExtInf; /// use std::time::Duration; /// /// let ext_inf = ExtInf::new(Duration::from_secs(5)); /// ``` #[must_use] pub const fn new(duration: Duration) -> Self { Self { duration, title: None, } } /// Makes a new [`ExtInf`] tag with the given title. /// /// # Example /// /// ``` /// # use hls_m3u8::tags::ExtInf; /// use std::time::Duration; /// /// let ext_inf = ExtInf::with_title(Duration::from_secs(5), "title"); /// ``` #[must_use] pub fn with_title>>(duration: Duration, title: T) -> Self { Self { duration, title: Some(title.into()), } } /// Returns the duration of the associated media segment. /// /// # Example /// /// ``` /// # use hls_m3u8::tags::ExtInf; /// use std::time::Duration; /// /// let ext_inf = ExtInf::new(Duration::from_secs(5)); /// /// assert_eq!(ext_inf.duration(), Duration::from_secs(5)); /// ``` #[must_use] pub const fn duration(&self) -> Duration { self.duration } /// Sets the duration of the associated media segment. /// /// # Example /// /// ``` /// # use hls_m3u8::tags::ExtInf; /// use std::time::Duration; /// /// let mut ext_inf = ExtInf::new(Duration::from_secs(5)); /// /// ext_inf.set_duration(Duration::from_secs(10)); /// /// assert_eq!(ext_inf.duration(), Duration::from_secs(10)); /// ``` pub fn set_duration(&mut self, value: Duration) -> &mut Self { self.duration = value; self } /// Returns the title of the associated media segment. /// /// # Example /// /// ``` /// # use hls_m3u8::tags::ExtInf; /// use std::time::Duration; /// /// let ext_inf = ExtInf::with_title(Duration::from_secs(5), "title"); /// /// assert_eq!(ext_inf.title(), &Some("title".into())); /// ``` #[must_use] pub const fn title(&self) -> &Option> { &self.title } /// Sets the title of the associated media segment. /// /// # Example /// /// ``` /// # use hls_m3u8::tags::ExtInf; /// use std::time::Duration; /// /// let mut ext_inf = ExtInf::with_title(Duration::from_secs(5), "title"); /// /// ext_inf.set_title(Some("better title")); /// /// assert_eq!(ext_inf.title(), &Some("better title".into())); /// ``` pub fn set_title>>(&mut self, value: Option) -> &mut Self { self.title = value.map(|v| v.into()); self } /// Makes the struct independent of its lifetime, by taking ownership of all /// internal [`Cow`]s. /// /// # Note /// /// This is a relatively expensive operation. #[must_use] pub fn into_owned(self) -> ExtInf<'static> { ExtInf { duration: self.duration, title: self.title.map(|v| Cow::Owned(v.into_owned())), } } } /// This tag requires [`ProtocolVersion::V1`], if the duration does not have /// nanoseconds, otherwise it requires [`ProtocolVersion::V3`]. impl<'a> RequiredVersion for ExtInf<'a> { fn required_version(&self) -> ProtocolVersion { if self.duration.subsec_nanos() == 0 { ProtocolVersion::V1 } else { ProtocolVersion::V3 } } } impl<'a> fmt::Display for ExtInf<'a> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", Self::PREFIX)?; write!(f, "{},", self.duration.as_secs_f64())?; if let Some(value) = &self.title { write!(f, "{}", value)?; } Ok(()) } } impl<'a> TryFrom<&'a str> for ExtInf<'a> { type Error = Error; fn try_from(input: &'a str) -> Result { let mut input = tag(input, Self::PREFIX)?.splitn(2, ','); let duration = input.next().unwrap(); let duration = Duration::from_secs_f64( duration .parse() .map_err(|e| Error::parse_float(duration, e))?, ); let title = input .next() .map(str::trim) .filter(|value| !value.is_empty()) .map(|v| Cow::Borrowed(v)); Ok(Self { duration, title }) } } impl<'a> From for ExtInf<'a> { fn from(value: Duration) -> Self { Self::new(value) } } #[cfg(test)] mod test { use super::*; use pretty_assertions::assert_eq; #[test] fn test_display() { assert_eq!( "#EXTINF:5,".to_string(), ExtInf::new(Duration::from_secs(5)).to_string() ); assert_eq!( "#EXTINF:5.5,".to_string(), ExtInf::new(Duration::from_millis(5500)).to_string() ); assert_eq!( "#EXTINF:5.5,title".to_string(), ExtInf::with_title(Duration::from_millis(5500), "title").to_string() ); assert_eq!( "#EXTINF:5,title".to_string(), ExtInf::with_title(Duration::from_secs(5), "title").to_string() ); } #[test] fn test_parser() { // #EXTINF:,[] assert_eq!( ExtInf::try_from("#EXTINF:5").unwrap(), ExtInf::new(Duration::from_secs(5)) ); assert_eq!( ExtInf::try_from("#EXTINF:5,").unwrap(), ExtInf::new(Duration::from_secs(5)) ); assert_eq!( ExtInf::try_from("#EXTINF:5.5").unwrap(), ExtInf::new(Duration::from_millis(5500)) ); assert_eq!( ExtInf::try_from("#EXTINF:5.5,").unwrap(), ExtInf::new(Duration::from_millis(5500)) ); assert_eq!( ExtInf::try_from("#EXTINF:5.5,title").unwrap(), ExtInf::with_title(Duration::from_millis(5500), "title") ); assert_eq!( ExtInf::try_from("#EXTINF:5,title").unwrap(), ExtInf::with_title(Duration::from_secs(5), "title") ); assert!(ExtInf::try_from("#EXTINF:").is_err()); assert!(ExtInf::try_from("#EXTINF:garbage").is_err()); } #[test] fn test_title() { assert_eq!(ExtInf::new(Duration::from_secs(5)).title(), &None); assert_eq!( ExtInf::with_title(Duration::from_secs(5), "title").title(), &Some("title".into()) ); } #[test] fn test_required_version() { assert_eq!( ExtInf::new(Duration::from_secs(4)).required_version(), ProtocolVersion::V1 ); assert_eq!( ExtInf::new(Duration::from_millis(4400)).required_version(), ProtocolVersion::V3 ); } #[test] fn test_from() { assert_eq!( ExtInf::from(Duration::from_secs(1)), ExtInf::new(Duration::from_secs(1)) ); } }