mirror of
https://github.com/Serial-ATA/lofty-rs
synced 2025-03-04 23:07:20 +00:00
ID3v2: Introduce TextInformationFrame
This commit is contained in:
parent
2dc7569017
commit
f7f640b7eb
5 changed files with 116 additions and 78 deletions
|
@ -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);
|
||||
|
|
|
@ -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()),
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
56
src/id3/v2/items/text_information_frame.rs
Normal file
56
src/id3/v2/items/text_information_frame.rs
Normal 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
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
});
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue