ID3v2: Introduce UrlLinkFrame

This commit is contained in:
Serial 2023-04-11 15:41:13 -04:00 committed by Alex
parent f7f640b7eb
commit 4db2559314
7 changed files with 109 additions and 51 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, TextInformationFrame
TextInformationFrame, UniqueFileIdentifierFrame, UrlLinkFrame,
};
use crate::id3::v2::ID3v2Version;
use crate::macros::err;
@ -32,7 +32,7 @@ pub(super) fn parse_content(
// Apple proprietary frames
// WFED (Podcast URL), GRP1 (Grouping), MVNM (Movement Name), MVIN (Movement Number)
"WFED" | "GRP1" | "MVNM" | "MVIN" => TextInformationFrame::parse(content, version)?.map(FrameValue::Text),
_ if id.starts_with('W') => parse_link(content)?,
_ if id.starts_with('W') => UrlLinkFrame::parse(content)?.map(FrameValue::URL),
"POPM" => Some(FrameValue::Popularimeter(Popularimeter::parse(content)?)),
// SYLT, GEOB, and any unknown frames
_ => Some(FrameValue::Binary(content.to_vec())),
@ -72,16 +72,6 @@ fn parse_text_language(
Ok(Some(value))
}
fn parse_link(content: &mut &[u8]) -> Result<Option<FrameValue>> {
if content.is_empty() {
return Ok(None);
}
let link = decode_text(content, TextEncoding::Latin1, true)?.unwrap_or_default();
Ok(Some(FrameValue::URL(link)))
}
pub(in crate::id3::v2) fn verify_encoding(
encoding: u8,
version: ID3v2Version,

View file

@ -5,11 +5,11 @@ pub(super) mod read;
use super::items::{
AttachedPictureFrame, ExtendedTextFrame, ExtendedUrlFrame, LanguageFrame, Popularimeter,
TextInformationFrame, UniqueFileIdentifierFrame,
TextInformationFrame, UniqueFileIdentifierFrame, UrlLinkFrame,
};
use super::util::upgrade::{upgrade_v2, upgrade_v3};
use super::ID3v2Version;
use crate::error::{ID3v2Error, ID3v2ErrorKind, LoftyError, Result};
use crate::error::{ErrorKind, ID3v2Error, ID3v2ErrorKind, LoftyError, Result};
use crate::tag::item::{ItemKey, ItemValue, TagItem};
use crate::tag::TagType;
use crate::util::text::TextEncoding;
@ -157,28 +157,16 @@ impl<'a> Frame<'a> {
#[derive(PartialEq, Clone, Debug, Eq, Hash)]
pub enum FrameValue {
/// Represents a "COMM" frame
///
/// Due to the amount of information needed, it is contained in a separate struct, [`LanguageFrame`]
Comment(LanguageFrame),
/// Represents a "USLT" frame
///
/// Due to the amount of information needed, it is contained in a separate struct, [`LanguageFrame`]
UnSyncText(LanguageFrame),
/// Represents a "T..." (excluding TXXX) frame
Text(TextInformationFrame),
/// Represents a "TXXX" frame
///
/// Due to the amount of information needed, it is contained in a separate struct, [`ExtendedTextFrame`]
UserText(ExtendedTextFrame),
/// Represents a "W..." (excluding WXXX) frame
///
/// NOTE: URL frame descriptions **must** be unique
///
/// No encoding needs to be provided as all URLs are [`TextEncoding::Latin1`]
URL(String),
URL(UrlLinkFrame),
/// Represents a "WXXX" frame
///
/// Due to the amount of information needed, it is contained in a separate struct, [`ExtendedUrlFrame`]
UserURL(ExtendedUrlFrame),
/// Represents an "APIC" or "PIC" frame
Picture(AttachedPictureFrame),
@ -197,15 +185,25 @@ pub enum FrameValue {
UniqueFileIdentifier(UniqueFileIdentifierFrame),
}
impl From<ItemValue> for FrameValue {
fn from(input: ItemValue) -> Self {
impl TryFrom<ItemValue> for FrameValue {
type Error = LoftyError;
fn try_from(input: ItemValue) -> std::result::Result<FrameValue, Self::Error> {
match input {
ItemValue::Text(text) => FrameValue::Text(TextInformationFrame {
ItemValue::Text(text) => Ok(FrameValue::Text(TextInformationFrame {
encoding: TextEncoding::UTF8,
value: text,
}),
ItemValue::Locator(locator) => FrameValue::URL(locator),
ItemValue::Binary(binary) => FrameValue::Binary(binary),
})),
ItemValue::Locator(locator) => {
if TextEncoding::verify_latin1(&locator) {
Ok(FrameValue::URL(UrlLinkFrame(locator)))
} else {
Err(LoftyError::new(ErrorKind::TextDecode(
"ID3v2 URL frames must be Latin-1",
)))
}
},
ItemValue::Binary(binary) => Ok(FrameValue::Binary(binary)),
}
}
}
@ -222,6 +220,12 @@ impl From<ExtendedTextFrame> for FrameValue {
}
}
impl From<UrlLinkFrame> for FrameValue {
fn from(value: UrlLinkFrame) -> Self {
Self::URL(value)
}
}
impl From<ExtendedUrlFrame> for FrameValue {
fn from(value: ExtendedUrlFrame) -> Self {
Self::UserURL(value)
@ -253,7 +257,7 @@ impl FrameValue {
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(),
FrameValue::URL(link) => link.as_bytes(),
FrameValue::Picture(attached_picture) => attached_picture.as_bytes(ID3v2Version::V4)?,
FrameValue::Popularimeter(popularimeter) => popularimeter.as_bytes(),
FrameValue::Binary(binary) => binary.clone(),
@ -346,7 +350,13 @@ impl From<TagItem> for Option<Frame<'static>> {
(FrameID::Valid(ref s), ItemValue::Binary(text)) if s == "POPM" => {
FrameValue::Popularimeter(Popularimeter::parse(&text).ok()?)
},
(_, value) => value.into(),
(_, item_value) => {
let Ok(value) = item_value.try_into() else {
return None;
};
value
},
};
frame_id = id;
@ -472,7 +482,7 @@ impl<'a> TryFrom<&'a TagItem> for FrameRef<'a> {
("POPM", ItemValue::Binary(contents)) => {
FrameValue::Popularimeter(Popularimeter::parse(contents)?)
},
(_, value) => value.into(),
(_, value) => value.try_into()?,
};
frame_id = id;
@ -512,15 +522,25 @@ impl<'a> TryFrom<&'a TagItem> for FrameRef<'a> {
}
}
impl<'a> Into<FrameValue> for &'a ItemValue {
fn into(self) -> FrameValue {
impl<'a> TryInto<FrameValue> for &'a ItemValue {
type Error = LoftyError;
fn try_into(self) -> std::result::Result<FrameValue, Self::Error> {
match self {
ItemValue::Text(text) => FrameValue::Text(TextInformationFrame {
ItemValue::Text(text) => Ok(FrameValue::Text(TextInformationFrame {
encoding: TextEncoding::UTF8,
value: text.clone(),
}),
ItemValue::Locator(locator) => FrameValue::URL(locator.clone()),
ItemValue::Binary(binary) => FrameValue::Binary(binary.clone()),
})),
ItemValue::Locator(locator) => {
if TextEncoding::verify_latin1(locator) {
Ok(FrameValue::URL(UrlLinkFrame(locator.clone())))
} else {
Err(LoftyError::new(ErrorKind::TextDecode(
"ID3v2 URL frames must be Latin-1",
)))
}
},
ItemValue::Binary(binary) => Ok(FrameValue::Binary(binary.clone())),
}
}
}

View file

@ -7,6 +7,7 @@ mod language_frame;
mod popularimeter;
mod sync_text;
mod text_information_frame;
mod url_link_frame;
pub use attached_picture_frame::AttachedPictureFrame;
pub use encapsulated_object::{GEOBInformation, GeneralEncapsulatedObject};
@ -17,3 +18,4 @@ pub use language_frame::LanguageFrame;
pub use popularimeter::Popularimeter;
pub use sync_text::{SyncTextContentType, SyncTextInformation, SynchronizedText, TimestampFormat};
pub use text_information_frame::TextInformationFrame;
pub use url_link_frame::UrlLinkFrame;

View file

@ -6,11 +6,6 @@ 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

View file

@ -0,0 +1,47 @@
use crate::error::Result;
use crate::util::text::{decode_text, encode_text, TextEncoding};
/// An `ID3v2` URL frame
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
pub struct UrlLinkFrame(pub(crate) String);
impl UrlLinkFrame {
/// Read an [`UrlLinkFrame`] from a slice
///
/// NOTE: This expects the frame header to have already been skipped
///
/// # Errors
///
/// * Unable to decode the text as [`TextEncoding::Latin1`]
pub fn parse(content: &[u8]) -> Result<Option<Self>> {
if content.is_empty() {
return Ok(None);
}
let url = decode_text(&mut &content[..], TextEncoding::Latin1, true)?.unwrap_or_default();
Ok(Some(UrlLinkFrame(url)))
}
/// Convert an [`UrlLinkFrame`] to a byte vec
pub fn as_bytes(&self) -> Vec<u8> {
encode_text(&self.0, TextEncoding::Latin1, false)
}
/// Get the URL of the frame
pub fn url(&self) -> &str {
&self.0
}
/// Change the URL of the frame
///
/// This will return a `bool` indicating whether or not the URL provided is Latin-1
pub fn set_url(&mut self, url: String) -> bool {
if TextEncoding::verify_latin1(&url) {
self.0 = url;
return true;
}
false
}
}

View file

@ -6,7 +6,7 @@ use crate::error::{LoftyError, Result};
use crate::id3::v2::frame::{FrameRef, MUSICBRAINZ_UFID_OWNER};
use crate::id3::v2::items::{
AttachedPictureFrame, ExtendedTextFrame, ExtendedUrlFrame, LanguageFrame, TextInformationFrame,
UniqueFileIdentifierFrame,
UniqueFileIdentifierFrame, UrlLinkFrame,
};
use crate::picture::{Picture, PictureType, TOMBSTONE_PICTURE};
use crate::tag::item::{ItemKey, ItemValue, TagItem};
@ -821,7 +821,7 @@ impl SplitTag for ID3v2Tag {
}
return false; // Frame consumed
},
FrameValue::URL(content)
FrameValue::URL(UrlLinkFrame(content))
| FrameValue::UserURL(ExtendedUrlFrame { content, .. }) => {
ItemValue::Locator(std::mem::take(content))
},
@ -1081,7 +1081,7 @@ mod tests {
};
use crate::id3::v2::{
read_id3v2_header, AttachedPictureFrame, ExtendedTextFrame, Frame, FrameFlags, FrameID,
FrameValue, ID3v2Tag, ID3v2Version, LanguageFrame, TextInformationFrame,
FrameValue, ID3v2Tag, ID3v2Version, LanguageFrame, TextInformationFrame, UrlLinkFrame,
};
use crate::tag::utils::test_utils::read_path;
use crate::util::text::TextEncoding;
@ -1280,7 +1280,7 @@ mod tests {
let mut tag = ID3v2Tag::default();
tag.insert(Frame {
id: FrameID::Valid(Cow::Borrowed("ABCD")),
value: FrameValue::URL(String::from("FOO URL")),
value: FrameValue::URL(UrlLinkFrame(String::from("FOO URL"))),
flags: FrameFlags::default(),
});

View file

@ -31,6 +31,10 @@ impl TextEncoding {
_ => None,
}
}
pub(crate) fn verify_latin1(text: &str) -> bool {
text.chars().all(|c| c as u32 <= 255)
}
}
pub(crate) fn decode_text<R>(