diff --git a/src/id3/v2/frame/content.rs b/src/id3/v2/frame/content.rs index 9f270a02..ef00c297 100644 --- a/src/id3/v2/frame/content.rs +++ b/src/id3/v2/frame/content.rs @@ -3,6 +3,7 @@ use crate::id3::v2::frame::FrameValue; use crate::id3::v2::items::{ AttachedPictureFrame, CommentFrame, ExtendedTextFrame, ExtendedUrlFrame, Popularimeter, TextInformationFrame, UniqueFileIdentifierFrame, UnsynchronizedTextFrame, UrlLinkFrame, + KeyValueFrame }; use crate::id3::v2::Id3v2Version; use crate::macros::err; @@ -28,6 +29,7 @@ pub(super) fn parse_content( "WXXX" => ExtendedUrlFrame::parse(reader, version)?.map(FrameValue::UserUrl), "COMM" => CommentFrame::parse(reader, version)?.map(FrameValue::Comment), "USLT" => UnsynchronizedTextFrame::parse(reader, version)?.map(FrameValue::UnsynchronizedText), + "TIPL" => KeyValueFrame::parse(reader, version)?.map(FrameValue::KeyValueFrame), "UFID" => UniqueFileIdentifierFrame::parse(reader, parse_mode)?.map(FrameValue::UniqueFileIdentifier), _ if id.starts_with('T') => TextInformationFrame::parse(reader, version)?.map(FrameValue::Text), // Apple proprietary frames diff --git a/src/id3/v2/frame/mod.rs b/src/id3/v2/frame/mod.rs index 64bf0e38..e1ac50f3 100644 --- a/src/id3/v2/frame/mod.rs +++ b/src/id3/v2/frame/mod.rs @@ -6,6 +6,7 @@ pub(super) mod read; use super::items::{ AttachedPictureFrame, CommentFrame, ExtendedTextFrame, ExtendedUrlFrame, Popularimeter, TextInformationFrame, UniqueFileIdentifierFrame, UnsynchronizedTextFrame, UrlLinkFrame, + KeyValueFrame }; use super::util::upgrade::{upgrade_v2, upgrade_v3}; use super::Id3v2Version; @@ -178,6 +179,8 @@ pub enum FrameValue { Picture(AttachedPictureFrame), /// Represents a "POPM" frame Popularimeter(Popularimeter), + /// Represents an "IPLS" or "TPIL" frame + KeyValueFrame(KeyValueFrame), /// Binary data /// /// NOTES: @@ -262,6 +265,12 @@ impl From for FrameValue { } } +impl From for FrameValue { + fn from(value: KeyValueFrame) -> Self { + Self::KeyValueFrame(value) + } +} + impl From for FrameValue { fn from(value: UniqueFileIdentifierFrame) -> Self { Self::UniqueFileIdentifier(value) @@ -279,6 +288,7 @@ impl FrameValue { FrameValue::Url(link) => link.as_bytes(), FrameValue::Picture(attached_picture) => attached_picture.as_bytes(Id3v2Version::V4)?, FrameValue::Popularimeter(popularimeter) => popularimeter.as_bytes(), + FrameValue::KeyValueFrame(content) => content.as_bytes(), FrameValue::Binary(binary) => binary.clone(), FrameValue::UniqueFileIdentifier(frame) => frame.as_bytes(), }) diff --git a/src/id3/v2/items/key_value_frame.rs b/src/id3/v2/items/key_value_frame.rs new file mode 100644 index 00000000..99b742be --- /dev/null +++ b/src/id3/v2/items/key_value_frame.rs @@ -0,0 +1,69 @@ +use crate::error::Result; +use crate::id3::v2::frame::content::verify_encoding; +use crate::id3::v2::Id3v2Version; +use crate::util::text::{decode_text, encode_text, TextEncoding}; + +use byteorder::ReadBytesExt; + +use std::io::Read; + +/// An `ID3v2` text frame +#[derive(Clone, Debug, Eq, PartialEq, Hash)] +pub struct KeyValueFrame { + /// The encoding of the text + pub encoding: TextEncoding, + /// The text itself + pub values: Vec<(String, String)>, +} + +impl KeyValueFrame { + /// Read an [`TextInformationFrame`] from a slice + /// + /// NOTE: This expects the frame header to have already been skipped + /// + /// # Errors + /// + /// * Unable to decode the text + /// + /// ID3v2.2: + /// + /// * The encoding is not [`TextEncoding::Latin1`] or [`TextEncoding::UTF16`] + pub fn parse(reader: &mut R, version: Id3v2Version) -> Result> + where + R: Read, + { + let Ok(encoding_byte) = reader.read_u8() else { + return Ok(None); + }; + + let encoding = verify_encoding(encoding_byte, version)?; + + let mut values = vec![]; + + loop { + let key = decode_text(reader, encoding, true)?; + let value = decode_text(reader, encoding, true)?; + if key.bytes_read == 0 || value.bytes_read == 0 { + break; + } + + values.push((key.content, value.content)); + } + + + Ok(Some(Self{ encoding, values })) + } + + /// Convert an [`TextInformationFrame`] to a byte vec + pub fn as_bytes(&self) -> Vec { + let mut content = vec![]; + + for (key, value) in &self.values { + content.append(&mut encode_text(key, self.encoding, true)); + content.append(&mut encode_text(value, self.encoding, true)); + } + + content.insert(0, self.encoding as u8); + content + } +} diff --git a/src/id3/v2/items/mod.rs b/src/id3/v2/items/mod.rs index 37ebc132..101cda37 100644 --- a/src/id3/v2/items/mod.rs +++ b/src/id3/v2/items/mod.rs @@ -9,6 +9,7 @@ mod popularimeter; mod sync_text; mod text_information_frame; mod url_link_frame; +mod key_value_frame; pub use attached_picture_frame::AttachedPictureFrame; pub use audio_text_frame::{scramble, AudioTextFrame, AudioTextFrameFlags}; @@ -21,3 +22,4 @@ pub use popularimeter::Popularimeter; pub use sync_text::{SyncTextContentType, SynchronizedText, TimestampFormat}; pub use text_information_frame::TextInformationFrame; pub use url_link_frame::UrlLinkFrame; +pub use key_value_frame::KeyValueFrame; diff --git a/src/id3/v2/tag.rs b/src/id3/v2/tag.rs index f491d0b8..3d0bff3b 100644 --- a/src/id3/v2/tag.rs +++ b/src/id3/v2/tag.rs @@ -788,6 +788,9 @@ impl SplitTag for Id3v2Tag { FrameValue::Popularimeter(popularimeter) => { ItemValue::Binary(popularimeter.as_bytes()) }, + FrameValue::KeyValueFrame(_) => { + return true; // Keep frame + }, FrameValue::Binary(binary) => ItemValue::Binary(std::mem::take(binary)), FrameValue::UniqueFileIdentifier(_) => { return true; // Keep unsupported frame diff --git a/src/id3/v2/write/frame.rs b/src/id3/v2/write/frame.rs index 6fc9d3e6..0c167e76 100644 --- a/src/id3/v2/write/frame.rs +++ b/src/id3/v2/write/frame.rs @@ -47,6 +47,7 @@ fn verify_frame(frame: &FrameRef<'_>) -> Result<()> { FrameValue::UserUrl(_) => "UserUrl", FrameValue::Picture { .. } => "Picture", FrameValue::Popularimeter(_) => "Popularimeter", + FrameValue::KeyValueFrame(_) => "KeyValueData", FrameValue::Binary(_) => "Binary", FrameValue::UniqueFileIdentifier(_) => "UniqueFileIdentifier", },