Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add support for IPLS (v2.3) and TIPL/TMCL (v2.4) frames #141

Merged
merged 9 commits into from
Nov 22, 2024
63 changes: 63 additions & 0 deletions src/frame/content.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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(
Holzhaus marked this conversation as resolved.
Show resolved Hide resolved
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,
}
}
Expand Down Expand Up @@ -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]> {
Expand Down Expand Up @@ -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),
}
}
Expand Down Expand Up @@ -850,6 +876,43 @@ impl From<UniqueFileIdentifier> 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<InvolvedPeopleListItem>,
}

/// 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<InvolvedPeopleList> for Frame {
fn from(c: InvolvedPeopleList) -> Self {
Self::with_content("IPLS", Content::InvolvedPeopleList(c))
Holzhaus marked this conversation as resolved.
Show resolved Hide resolved
}
}

#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
#[allow(missing_docs)]
pub struct TableOfContents {
Expand Down
10 changes: 6 additions & 4 deletions src/frame/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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(()),
Expand All @@ -105,6 +106,7 @@ impl Frame {
Content::Private(_) => "PrivateFrame",
Content::TableOfContents(_) => "TableOfContents",
Content::UniqueFileIdentifier(_) => "UFID",
Content::InvolvedPeopleList(_) => "InvolvedPeopleList",
Content::Unknown(_) => "Unknown",
};
Err(Error::new(
Expand Down
177 changes: 173 additions & 4 deletions src/stream/frame/content.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -306,6 +306,17 @@ impl<W: io::Write> Encoder<W> {
Ok(())
}

fn involved_people_list(&mut self, content: &InvolvedPeopleList) -> crate::Result<()> {
self.encoding()?;
for item in &content.items {
self.string(&item.involvement)?;
self.delim()?;
self.string(&item.involvee)?;
self.delim()?;
Holzhaus marked this conversation as resolved.
Show resolved Hide resolved
}
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)?;
Expand Down Expand Up @@ -361,6 +372,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)?,
};

Expand Down Expand Up @@ -411,6 +423,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(),
Expand Down Expand Up @@ -513,6 +526,67 @@ impl<'a> Decoder<'a> {
Ok(Content::Text(text))
}

fn involved_people_list(mut self) -> crate::Result<Content> {
let encoding = self.encoding()?;
let end = match self.version {
Version::Id3v23 | Version::Id3v24 => find_closing_delim(encoding, self.r),
_ => find_delim(encoding, self.r, 0),
}
.unwrap_or(self.r.len());

let data = self.bytes(end)?;

let mut pos = 0;
let items = iter::repeat_with(|| {
find_delim(encoding, data, pos)
.map(|next_pos| {
let substr = encoding.decode(&data[pos..next_pos]);
pos = next_pos + delim_len(encoding);
substr
})
.or_else(|| {
if pos < data.len() {
let substr = encoding.decode(&data[pos..]);
pos = data.len();
Some(substr)
} else {
None
}
})
})
.scan(None, |last_string, string| match (&last_string, string) {
(None, Some(string)) => {
*last_string = Some(string);
Some(Ok(None))
}
(Some(_), Some(second)) => {
let first = last_string.take().expect("option must be some");
let result = first.and_then(|involvement| {
second.map(|involvee| {
Some(InvolvedPeopleListItem {
involvement,
involvee,
})
})
});
Some(result)
}
(Some(_), None) => {
// This can only happen if there is an uneven number of elements.
*last_string = None;
Some(Err(Error::new(
ErrorKind::Parsing,
"uneven number of IPLS strings",
)))
}
(None, None) => None,
})
.filter_map(|item| item.transpose())
.collect::<crate::Result<Vec<InvolvedPeopleListItem>>>()?;

Ok(Content::InvolvedPeopleList(InvolvedPeopleList { items }))
}

fn link_content(self) -> crate::Result<Content> {
Ok(Content::Link(String::from_utf8(self.r.to_vec())?))
}
Expand Down Expand Up @@ -1510,6 +1584,101 @@ mod tests {
}
}

#[test]
fn test_ipls() {
check_involved_people_list("IPLS", Version::Id3v23);
}

#[test]
fn test_tmcl() {
check_involved_people_list("TMCL", Version::Id3v24);
}

#[test]
fn test_tipl() {
check_involved_people_list("TIPL", Version::Id3v24);
}

fn check_involved_people_list(frame_id: &str, version: Version) {
assert!(decode(frame_id, version, &[][..]).is_err());

println!("valid");
for people_list in &[
vec![],
vec![("involvement", "involvee")],
vec![
("double bass", "Israel Crosby"),
("drums (drum set)", "Vernell Fournier"),
("piano", "Ahmad Jamal"),
("producer", "Dave Usher"),
],
] {
for encoding in &[
Encoding::Latin1,
Encoding::UTF8,
Encoding::UTF16,
Encoding::UTF16BE,
] {
println!("`{:?}`, `{:?}`", people_list, encoding);
let mut data = Vec::new();
data.push(*encoding as u8);
for (involvement, involvee) in people_list {
data.extend(bytes_for_encoding(&involvement, *encoding).into_iter());
data.extend(delim_for_encoding(*encoding).into_iter());
data.extend(bytes_for_encoding(&involvee, *encoding).into_iter());
data.extend(delim_for_encoding(*encoding).into_iter());
}

let content = frame::InvolvedPeopleList {
items: people_list
.iter()
.map(|(involvement, involvee)| InvolvedPeopleListItem {
involvement: involvement.to_string(),
involvee: involvee.to_string(),
})
.collect(),
};
assert_eq!(
*decode(frame_id, version, &data[..])
.unwrap()
.0
.involved_people_list()
.unwrap(),
content
);
let mut data_out = Vec::new();
encode(
&mut data_out,
&&Content::InvolvedPeopleList(content),
Version::Id3v23,
*encoding,
)
.unwrap();
assert_eq!(data, data_out);
}
}

println!("invalid");
for encoding in &[
Encoding::Latin1,
Encoding::UTF8,
Encoding::UTF16,
Encoding::UTF16BE,
] {
println!("`{:?}`", encoding);
let mut data = Vec::new();
data.push(*encoding as u8);
data.extend(bytes_for_encoding("involvement", *encoding).into_iter());
data.extend(delim_for_encoding(*encoding).into_iter());
data.extend(bytes_for_encoding("involvee", *encoding).into_iter());
data.extend(delim_for_encoding(*encoding).into_iter());
data.extend(bytes_for_encoding("other involvement", *encoding).into_iter());
data.extend(delim_for_encoding(*encoding).into_iter());
// involveee missing here
assert!(decode(frame_id, version, &data[..]).is_err());
}
}

#[test]
fn test_mllt_4_4() {
let mllt = Content::MpegLocationLookupTable(MpegLocationLookupTable {
Expand Down
Loading