ID3v2: Introduce TextInformationFrame

This commit is contained in:
Serial 2023-04-10 17:01:38 -04:00 committed by Alex
parent 2dc7569017
commit f7f640b7eb
5 changed files with 116 additions and 78 deletions

View file

@ -2,7 +2,7 @@ use crate::error::{ID3v2Error, ID3v2ErrorKind, Result};
use crate::id3::v2::frame::FrameValue;
use crate::id3::v2::items::{
AttachedPictureFrame, ExtendedTextFrame, ExtendedUrlFrame, LanguageFrame, Popularimeter,
UniqueFileIdentifierFrame,
UniqueFileIdentifierFrame, TextInformationFrame
};
use crate::id3::v2::ID3v2Version;
use crate::macros::err;
@ -28,10 +28,10 @@ pub(super) fn parse_content(
"WXXX" => ExtendedUrlFrame::parse(content, version)?.map(FrameValue::UserURL),
"COMM" | "USLT" => parse_text_language(content, id, version)?,
"UFID" => UniqueFileIdentifierFrame::parse(content)?.map(FrameValue::UniqueFileIdentifier),
_ if id.starts_with('T') => parse_text(content, version)?,
_ if id.starts_with('T') => TextInformationFrame::parse(content, version)?.map(FrameValue::Text),
// Apple proprietary frames
// WFED (Podcast URL), GRP1 (Grouping), MVNM (Movement Name), MVIN (Movement Number)
"WFED" | "GRP1" | "MVNM" | "MVIN" => parse_text(content, version)?,
"WFED" | "GRP1" | "MVNM" | "MVIN" => TextInformationFrame::parse(content, version)?.map(FrameValue::Text),
_ if id.starts_with('W') => parse_link(content)?,
"POPM" => Some(FrameValue::Popularimeter(Popularimeter::parse(content)?)),
// SYLT, GEOB, and any unknown frames
@ -72,20 +72,6 @@ fn parse_text_language(
Ok(Some(value))
}
fn parse_text(content: &mut &[u8], version: ID3v2Version) -> Result<Option<FrameValue>> {
if content.len() < 2 {
return Ok(None);
}
let encoding = verify_encoding(content.read_u8()?, version)?;
let text = decode_text(content, encoding, true)?.unwrap_or_default();
Ok(Some(FrameValue::Text {
encoding,
value: text,
}))
}
fn parse_link(content: &mut &[u8]) -> Result<Option<FrameValue>> {
if content.is_empty() {
return Ok(None);

View file

@ -5,14 +5,14 @@ pub(super) mod read;
use super::items::{
AttachedPictureFrame, ExtendedTextFrame, ExtendedUrlFrame, LanguageFrame, Popularimeter,
UniqueFileIdentifierFrame,
TextInformationFrame, UniqueFileIdentifierFrame,
};
use super::util::upgrade::{upgrade_v2, upgrade_v3};
use super::ID3v2Version;
use crate::error::{ID3v2Error, ID3v2ErrorKind, LoftyError, Result};
use crate::tag::item::{ItemKey, ItemValue, TagItem};
use crate::tag::TagType;
use crate::util::text::{encode_text, TextEncoding};
use crate::util::text::TextEncoding;
use id::FrameID;
use std::borrow::Cow;
@ -143,10 +143,10 @@ impl<'a> Frame<'a> {
pub(crate) fn text(id: Cow<'a, str>, content: String) -> Self {
Self {
id: FrameID::Valid(id),
value: FrameValue::Text {
value: FrameValue::Text(TextInformationFrame {
encoding: TextEncoding::UTF8,
value: content,
},
}),
flags: FrameFlags::default(),
}
}
@ -165,14 +165,7 @@ pub enum FrameValue {
/// Due to the amount of information needed, it is contained in a separate struct, [`LanguageFrame`]
UnSyncText(LanguageFrame),
/// Represents a "T..." (excluding TXXX) frame
///
/// NOTE: Text frame descriptions **must** be unique
Text {
/// The encoding of the text
encoding: TextEncoding,
/// The text itself
value: String,
},
Text(TextInformationFrame),
/// Represents a "TXXX" frame
///
/// Due to the amount of information needed, it is contained in a separate struct, [`ExtendedTextFrame`]
@ -207,16 +200,22 @@ pub enum FrameValue {
impl From<ItemValue> for FrameValue {
fn from(input: ItemValue) -> Self {
match input {
ItemValue::Text(text) => FrameValue::Text {
ItemValue::Text(text) => FrameValue::Text(TextInformationFrame {
encoding: TextEncoding::UTF8,
value: text,
},
}),
ItemValue::Locator(locator) => FrameValue::URL(locator),
ItemValue::Binary(binary) => FrameValue::Binary(binary),
}
}
}
impl From<TextInformationFrame> for FrameValue {
fn from(value: TextInformationFrame) -> Self {
Self::Text(value)
}
}
impl From<ExtendedTextFrame> for FrameValue {
fn from(value: ExtendedTextFrame) -> Self {
Self::UserText(value)
@ -251,12 +250,7 @@ impl FrameValue {
pub(super) fn as_bytes(&self) -> Result<Vec<u8>> {
Ok(match self {
FrameValue::Comment(lf) | FrameValue::UnSyncText(lf) => lf.as_bytes()?,
FrameValue::Text { encoding, value } => {
let mut content = encode_text(value, *encoding, false);
content.insert(0, *encoding as u8);
content
},
FrameValue::Text(tif) => tif.as_bytes(),
FrameValue::UserText(content) => content.as_bytes(),
FrameValue::UserURL(content) => content.as_bytes(),
FrameValue::URL(link) => link.as_bytes().to_vec(),
@ -521,10 +515,10 @@ impl<'a> TryFrom<&'a TagItem> for FrameRef<'a> {
impl<'a> Into<FrameValue> for &'a ItemValue {
fn into(self) -> FrameValue {
match self {
ItemValue::Text(text) => FrameValue::Text {
ItemValue::Text(text) => FrameValue::Text(TextInformationFrame {
encoding: TextEncoding::UTF8,
value: text.clone(),
},
}),
ItemValue::Locator(locator) => FrameValue::URL(locator.clone()),
ItemValue::Binary(binary) => FrameValue::Binary(binary.clone()),
}

View file

@ -6,6 +6,7 @@ mod identifier;
mod language_frame;
mod popularimeter;
mod sync_text;
mod text_information_frame;
pub use attached_picture_frame::AttachedPictureFrame;
pub use encapsulated_object::{GEOBInformation, GeneralEncapsulatedObject};
@ -15,3 +16,4 @@ pub use identifier::UniqueFileIdentifierFrame;
pub use language_frame::LanguageFrame;
pub use popularimeter::Popularimeter;
pub use sync_text::{SyncTextContentType, SyncTextInformation, SynchronizedText, TimestampFormat};
pub use text_information_frame::TextInformationFrame;

View file

@ -0,0 +1,56 @@
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;
/// An `ID3v2` text frame
///
/// This is used in the `TXXX` frame, where the frames
/// are told apart by descriptions, rather than their [`FrameID`](crate::id3::v2::FrameID)s.
/// This means for each `ExtendedTextFrame` in the tag, the description
/// must be unique.
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
pub struct TextInformationFrame {
/// The encoding of the text
pub encoding: TextEncoding,
/// The text itself
pub value: String,
}
impl TextInformationFrame {
/// 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(content: &[u8], version: ID3v2Version) -> Result<Option<Self>> {
if content.len() < 2 {
return Ok(None);
}
let content = &mut &content[..];
let encoding = verify_encoding(content.read_u8()?, version)?;
let text = decode_text(content, encoding, true)?.unwrap_or_default();
Ok(Some(TextInformationFrame {
encoding,
value: text,
}))
}
/// Convert an [`TextInformationFrame`] to a byte vec
pub fn as_bytes(&self) -> Vec<u8> {
let mut content = encode_text(&self.value, self.encoding, false);
content.insert(0, self.encoding as u8);
content
}
}

View file

@ -5,7 +5,7 @@ use super::ID3v2Version;
use crate::error::{LoftyError, Result};
use crate::id3::v2::frame::{FrameRef, MUSICBRAINZ_UFID_OWNER};
use crate::id3::v2::items::{
AttachedPictureFrame, ExtendedTextFrame, ExtendedUrlFrame, LanguageFrame,
AttachedPictureFrame, ExtendedTextFrame, ExtendedUrlFrame, LanguageFrame, TextInformationFrame,
UniqueFileIdentifierFrame,
};
use crate::picture::{Picture, PictureType, TOMBSTONE_PICTURE};
@ -184,7 +184,7 @@ impl ID3v2Tag {
pub fn get_text(&self, id: &str) -> Option<Cow<'_, str>> {
let frame = self.get(id);
if let Some(Frame {
value: FrameValue::Text { value, .. },
value: FrameValue::Text(TextInformationFrame { value, .. }),
..
}) = frame
{
@ -303,7 +303,7 @@ impl ID3v2Tag {
fn split_num_pair(&self, id: &str) -> (Option<u32>, Option<u32>) {
if let Some(Frame {
value: FrameValue::Text { ref value, .. },
value: FrameValue::Text(TextInformationFrame { ref value, .. }),
..
}) = self.get(id)
{
@ -390,10 +390,10 @@ fn filter_comment_frame_by_description_mut<'a>(
fn new_text_frame(id: FrameID<'_>, value: String, flags: FrameFlags) -> Frame<'_> {
Frame {
id,
value: FrameValue::Text {
value: FrameValue::Text(TextInformationFrame {
encoding: TextEncoding::UTF8,
value,
},
}),
flags,
}
}
@ -505,7 +505,7 @@ impl Accessor for ID3v2Tag {
fn year(&self) -> Option<u32> {
if let Some(Frame {
value: FrameValue::Text { value, .. },
value: FrameValue::Text(TextInformationFrame { value, .. }),
..
}) = self.get("TDRC")
{
@ -695,19 +695,19 @@ impl SplitTag for ID3v2Tag {
// The text pairs need some special treatment
match (id.as_str(), &mut frame.value) {
("TRCK", FrameValue::Text { value: content, .. })
("TRCK", FrameValue::Text(TextInformationFrame { value: content, .. }))
if split_pair(content, &mut tag, ItemKey::TrackNumber, ItemKey::TrackTotal)
.is_some() =>
{
false // Frame consumed
},
("TPOS", FrameValue::Text { value: content, .. })
("TPOS", FrameValue::Text(TextInformationFrame { value: content, .. }))
if split_pair(content, &mut tag, ItemKey::DiscNumber, ItemKey::DiscTotal)
.is_some() =>
{
false // Frame consumed
},
("MVIN", FrameValue::Text { value: content, .. })
("MVIN", FrameValue::Text(TextInformationFrame { value: content, .. }))
if split_pair(
content,
&mut tag,
@ -812,7 +812,7 @@ impl SplitTag for ID3v2Tag {
// round trips?
return true; // Keep frame
},
FrameValue::Text { value: content, .. } => {
FrameValue::Text(TextInformationFrame { value: content, .. }) => {
for c in content.split(V4_MULTI_VALUE_SEPARATOR) {
tag.items.push(TagItem::new(
item_key.clone(),
@ -1081,7 +1081,7 @@ mod tests {
};
use crate::id3::v2::{
read_id3v2_header, AttachedPictureFrame, ExtendedTextFrame, Frame, FrameFlags, FrameID,
FrameValue, ID3v2Tag, ID3v2Version, LanguageFrame,
FrameValue, ID3v2Tag, ID3v2Version, LanguageFrame, TextInformationFrame,
};
use crate::tag::utils::test_utils::read_path;
use crate::util::text::TextEncoding;
@ -1111,10 +1111,10 @@ mod tests {
expected_tag.insert(
Frame::new(
"TPE1",
FrameValue::Text {
FrameValue::Text(TextInformationFrame {
encoding,
value: String::from("Bar artist"),
},
}),
flags,
)
.unwrap(),
@ -1123,10 +1123,10 @@ mod tests {
expected_tag.insert(
Frame::new(
"TIT2",
FrameValue::Text {
FrameValue::Text(TextInformationFrame {
encoding,
value: String::from("Foo title"),
},
}),
flags,
)
.unwrap(),
@ -1135,10 +1135,10 @@ mod tests {
expected_tag.insert(
Frame::new(
"TALB",
FrameValue::Text {
FrameValue::Text(TextInformationFrame {
encoding,
value: String::from("Baz album"),
},
}),
flags,
)
.unwrap(),
@ -1161,10 +1161,10 @@ mod tests {
expected_tag.insert(
Frame::new(
"TDRC",
FrameValue::Text {
FrameValue::Text(TextInformationFrame {
encoding,
value: String::from("1984"),
},
}),
flags,
)
.unwrap(),
@ -1173,10 +1173,10 @@ mod tests {
expected_tag.insert(
Frame::new(
"TRCK",
FrameValue::Text {
FrameValue::Text(TextInformationFrame {
encoding,
value: String::from("1"),
},
}),
flags,
)
.unwrap(),
@ -1185,10 +1185,10 @@ mod tests {
expected_tag.insert(
Frame::new(
"TCON",
FrameValue::Text {
FrameValue::Text(TextInformationFrame {
encoding,
value: String::from("Classical"),
},
}),
flags,
)
.unwrap(),
@ -1306,10 +1306,10 @@ mod tests {
assert_eq!(
frame.content(),
&FrameValue::Text {
&FrameValue::Text(TextInformationFrame {
encoding: TextEncoding::UTF8,
value: String::from(value)
}
})
);
}
@ -1346,64 +1346,64 @@ mod tests {
tag.insert(Frame {
id: FrameID::Valid(Cow::Borrowed("TIT2")),
value: FrameValue::Text {
value: FrameValue::Text(TextInformationFrame {
encoding,
value: String::from("TempleOS Hymn Risen (Remix)"),
},
}),
flags,
});
tag.insert(Frame {
id: FrameID::Valid(Cow::Borrowed("TPE1")),
value: FrameValue::Text {
value: FrameValue::Text(TextInformationFrame {
encoding,
value: String::from("Dave Eddy"),
},
}),
flags,
});
tag.insert(Frame {
id: FrameID::Valid(Cow::Borrowed("TRCK")),
value: FrameValue::Text {
value: FrameValue::Text(TextInformationFrame {
encoding: TextEncoding::Latin1,
value: String::from("1"),
},
}),
flags,
});
tag.insert(Frame {
id: FrameID::Valid(Cow::Borrowed("TALB")),
value: FrameValue::Text {
value: FrameValue::Text(TextInformationFrame {
encoding,
value: String::from("Summer"),
},
}),
flags,
});
tag.insert(Frame {
id: FrameID::Valid(Cow::Borrowed("TDRC")),
value: FrameValue::Text {
value: FrameValue::Text(TextInformationFrame {
encoding,
value: String::from("2017"),
},
}),
flags,
});
tag.insert(Frame {
id: FrameID::Valid(Cow::Borrowed("TCON")),
value: FrameValue::Text {
value: FrameValue::Text(TextInformationFrame {
encoding,
value: String::from("Electronic"),
},
}),
flags,
});
tag.insert(Frame {
id: FrameID::Valid(Cow::Borrowed("TLEN")),
value: FrameValue::Text {
value: FrameValue::Text(TextInformationFrame {
encoding: TextEncoding::UTF16,
value: String::from("213017"),
},
}),
flags,
});