mirror of
https://github.com/Serial-ATA/lofty-rs
synced 2025-03-04 23:07:20 +00:00
ID3v2: Introduce UrlLinkFrame
This commit is contained in:
parent
f7f640b7eb
commit
4db2559314
7 changed files with 109 additions and 51 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, 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,
|
||||
|
|
|
@ -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())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
47
src/id3/v2/items/url_link_frame.rs
Normal file
47
src/id3/v2/items/url_link_frame.rs
Normal 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
|
||||
}
|
||||
}
|
|
@ -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(),
|
||||
});
|
||||
|
||||
|
|
|
@ -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>(
|
||||
|
|
Loading…
Add table
Reference in a new issue