diff --git a/src/frame/content.rs b/src/frame/content.rs index cb3a53fe4..3c8fad278 100644 --- a/src/frame/content.rs +++ b/src/frame/content.rs @@ -55,6 +55,8 @@ pub enum Content { TableOfContents(TableOfContents), /// A value containing the parsed contents of a unique file identifier frame (UFID). UniqueFileIdentifier(UniqueFileIdentifier), + /// A value containing the parsed contents of an involved people list IPLS + InvolvedPeopleList(InvolvedPeopleList), /// A value containing the bytes of a currently unknown frame type. /// /// Users that wish to write custom decoders must use [`Content::to_unknown`] instead of @@ -114,6 +116,19 @@ impl Content { Self::UniqueFileIdentifier(unique_file_identifier) => Comparable(vec![Cow::Borrowed( unique_file_identifier.owner_identifier.as_bytes(), )]), + Self::InvolvedPeopleList(involved_people_list) => Comparable( + involved_people_list + .items + .iter() + .flat_map(|item| { + [ + Cow::Borrowed(item.involvement.as_bytes()), + Cow::Borrowed(item.involvee.as_bytes()), + ] + .into_iter() + }) + .collect(), + ), Self::Unknown(_) => Incomparable, } } @@ -262,6 +277,14 @@ impl Content { } } + /// Returns the `InvolvedPeopleList` or None if the value is not `IPLS` + pub fn involved_people_list(&self) -> Option<&InvolvedPeopleList> { + match self { + Content::InvolvedPeopleList(involved_people_list) => Some(involved_people_list), + _ => None, + } + } + /// Returns the `Unknown` or None if the value is not `Unknown`. #[deprecated(note = "Use to_unknown")] pub fn unknown(&self) -> Option<&[u8]> { @@ -308,6 +331,9 @@ impl fmt::Display for Content { Content::UniqueFileIdentifier(unique_file_identifier) => { write!(f, "{}", unique_file_identifier) } + Content::InvolvedPeopleList(involved_people_list) => { + write!(f, "{}", involved_people_list) + } Content::Unknown(unknown) => write!(f, "{}", unknown), } } @@ -850,6 +876,43 @@ impl From for Frame { } } +/// The parsed contents of an IPLS frame. +#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub struct InvolvedPeopleList { + /// Items in the People List. + pub items: Vec, +} + +/// The parsed contents of an IPLS frame. +#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub struct InvolvedPeopleListItem { + /// Role of the involved person. + pub involvement: String, + /// Name of the involved person. + pub involvee: String, +} + +impl fmt::Display for InvolvedPeopleList { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let item_count = self.items.len(); + for (i, item) in self.items.iter().enumerate() { + if i == 0 && item_count > 1 { + write!(f, "{}: {} / ", item.involvement, item.involvee)?; + } else { + write!(f, "{}: {}", item.involvement, item.involvee)?; + } + } + + Ok(()) + } +} + +impl From for Frame { + fn from(c: InvolvedPeopleList) -> Self { + Self::with_content("IPLS", Content::InvolvedPeopleList(c)) + } +} + #[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)] #[allow(missing_docs)] pub struct TableOfContents { diff --git a/src/frame/mod.rs b/src/frame/mod.rs index 4a58bf017..dd5fb494a 100644 --- a/src/frame/mod.rs +++ b/src/frame/mod.rs @@ -5,10 +5,10 @@ use std::fmt; use std::str; pub use self::content::{ - Chapter, Comment, Content, EncapsulatedObject, ExtendedLink, ExtendedText, Lyrics, - MpegLocationLookupTable, MpegLocationLookupTableReference, Picture, PictureType, Popularimeter, - Private, SynchronisedLyrics, SynchronisedLyricsType, TableOfContents, TimestampFormat, - UniqueFileIdentifier, Unknown, + Chapter, Comment, Content, EncapsulatedObject, ExtendedLink, ExtendedText, InvolvedPeopleList, + InvolvedPeopleListItem, Lyrics, MpegLocationLookupTable, MpegLocationLookupTableReference, + Picture, PictureType, Popularimeter, Private, SynchronisedLyrics, SynchronisedLyricsType, + TableOfContents, TimestampFormat, UniqueFileIdentifier, Unknown, }; pub use self::timestamp::Timestamp; @@ -84,6 +84,7 @@ impl Frame { ("APIC", Content::Picture(_)) => Ok(()), ("CHAP", Content::Chapter(_)) => Ok(()), ("MLLT", Content::MpegLocationLookupTable(_)) => Ok(()), + ("IPLS", Content::InvolvedPeopleList(_)) => Ok(()), ("PRIV", Content::Private(_)) => Ok(()), ("CTOC", Content::TableOfContents(_)) => Ok(()), ("UFID", Content::UniqueFileIdentifier(_)) => Ok(()), @@ -105,6 +106,7 @@ impl Frame { Content::Private(_) => "PrivateFrame", Content::TableOfContents(_) => "TableOfContents", Content::UniqueFileIdentifier(_) => "UFID", + Content::InvolvedPeopleList(_) => "InvolvedPeopleList", Content::Unknown(_) => "Unknown", }; Err(Error::new( diff --git a/src/stream/frame/content.rs b/src/stream/frame/content.rs index ac700e462..bb0919934 100644 --- a/src/stream/frame/content.rs +++ b/src/stream/frame/content.rs @@ -1,8 +1,8 @@ use crate::frame::{ - Chapter, Comment, Content, EncapsulatedObject, ExtendedLink, ExtendedText, Lyrics, - MpegLocationLookupTable, MpegLocationLookupTableReference, Picture, PictureType, Popularimeter, - Private, SynchronisedLyrics, SynchronisedLyricsType, TableOfContents, TimestampFormat, - UniqueFileIdentifier, Unknown, + Chapter, Comment, Content, EncapsulatedObject, ExtendedLink, ExtendedText, InvolvedPeopleList, + InvolvedPeopleListItem, Lyrics, MpegLocationLookupTable, MpegLocationLookupTableReference, + Picture, PictureType, Popularimeter, Private, SynchronisedLyrics, SynchronisedLyricsType, + TableOfContents, TimestampFormat, UniqueFileIdentifier, Unknown, }; use crate::stream::encoding::Encoding; use crate::stream::frame; @@ -306,6 +306,21 @@ impl Encoder { Ok(()) } + fn involved_people_list( + &mut self, + content: &InvolvedPeopleList, + ) -> crate::Result<()> { + for (i, item) in content.items.iter().enumerate() { + if i != 0 { + self.byte(0)?; + } + self.bytes(item.involvement.as_bytes())?; + self.byte(0)?; + self.bytes(item.involvee.as_bytes())?; + } + Ok(()) + } + fn table_of_contents_content(&mut self, content: &TableOfContents) -> crate::Result<()> { self.string_with_other_encoding(Encoding::Latin1, &content.element_id)?; self.byte(0)?; @@ -361,6 +376,7 @@ pub fn encode( Content::Private(c) => encoder.private_content(c)?, Content::TableOfContents(c) => encoder.table_of_contents_content(c)?, Content::UniqueFileIdentifier(c) => encoder.unique_file_identifier_content(c)?, + Content::InvolvedPeopleList(c) => encoder.involved_people_list(c)?, Content::Unknown(c) => encoder.bytes(&c.data)?, }; @@ -411,6 +427,7 @@ pub fn decode( encoding = Some(enc); Ok(content) } + "IPLS" | "IPL" | "TMCL" | "TIPL" => decoder.involved_people_list(), id if id.starts_with('T') => decoder.text_content(), id if id.starts_with('W') => decoder.link_content(), "GRP1" => decoder.text_content(), @@ -513,6 +530,39 @@ impl<'a> Decoder<'a> { Ok(Content::Text(text)) } + fn involved_people_list(self) -> crate::Result { + self.text_content().and_then(|content| { + if let Content::Text(text) = content { + let string_count = text.split('\0').count(); + if (string_count & 1) == 1 { + return Err(Error { + kind: ErrorKind::Parsing, + description: "Involved People List contains an odd number of strings" + .to_string(), + partial_tag: None, + }); + } + let items = text + .split('\0') + .enumerate() + .filter_map(|(i, s)| (i & 1 == 0).then_some(s)) + .zip( + text.split('\0') + .enumerate() + .filter_map(|(i, s)| (i & 1 == 1).then_some(s)), + ) + .map(|(involvement, involvee)| InvolvedPeopleListItem { + involvement: involvement.to_string(), + involvee: involvee.to_string(), + }) + .collect::>(); + Ok(Content::InvolvedPeopleList(InvolvedPeopleList { items })) + } else { + Ok(content) + } + }) + } + fn link_content(self) -> crate::Result { Ok(Content::Link(String::from_utf8(self.r.to_vec())?)) }