diff --git a/src/id3/v2/frame/content.rs b/src/id3/v2/frame/content.rs index eedd2654..a8f7ad49 100644 --- a/src/id3/v2/frame/content.rs +++ b/src/id3/v2/frame/content.rs @@ -1,7 +1,7 @@ use crate::error::{ID3v2Error, ID3v2ErrorKind, LoftyError, Result}; use crate::id3::v2::frame::FrameValue; use crate::id3::v2::items::{ - ExtendedTextFrame, LanguageFrame, Popularimeter, UniqueFileIdentifierFrame, + ExtendedTextFrame, LanguageFrame, Popularimeter, UniqueFileIdentifierFrame, ExtendedUrlFrame }; use crate::id3::v2::ID3v2Version; use crate::macros::err; @@ -73,7 +73,7 @@ fn parse_user_defined( Ok(Some(if link { let content = decode_text(content, TextEncoding::Latin1, false)?.unwrap_or_default(); - FrameValue::UserURL(ExtendedTextFrame { + FrameValue::UserURL(ExtendedUrlFrame { encoding, description, content, diff --git a/src/id3/v2/frame/mod.rs b/src/id3/v2/frame/mod.rs index ece6d58b..230753dc 100644 --- a/src/id3/v2/frame/mod.rs +++ b/src/id3/v2/frame/mod.rs @@ -3,19 +3,19 @@ mod header; pub(super) mod id; pub(super) mod read; -use super::items::{ExtendedTextFrame, LanguageFrame, Popularimeter, UniqueFileIdentifierFrame}; +use super::items::{ + ExtendedTextFrame, ExtendedUrlFrame, LanguageFrame, Popularimeter, UniqueFileIdentifierFrame, +}; +use super::util::upgrade::{upgrade_v2, upgrade_v3}; +use super::ID3v2Version; use crate::error::{ID3v2Error, ID3v2ErrorKind, LoftyError, Result}; -use crate::id3::v2::util::upgrade::{upgrade_v2, upgrade_v3}; -use crate::id3::v2::ID3v2Version; use crate::picture::Picture; -use crate::tag::item::{ItemValue, TagItem}; +use crate::tag::item::{ItemKey, ItemValue, TagItem}; use crate::tag::TagType; use crate::util::text::{encode_text, TextEncoding}; -use crate::ItemKey; use id::FrameID; use std::borrow::Cow; - use std::convert::{TryFrom, TryInto}; use std::hash::{Hash, Hasher}; @@ -185,8 +185,8 @@ pub enum FrameValue { URL(String), /// Represents a "WXXX" frame /// - /// Due to the amount of information needed, it is contained in a separate struct, [`ExtendedTextFrame`] - UserURL(ExtendedTextFrame), + /// Due to the amount of information needed, it is contained in a separate struct, [`ExtendedUrlFrame`] + UserURL(ExtendedUrlFrame), /// Represents an "APIC" or "PIC" frame Picture { /// The encoding of the description @@ -232,7 +232,8 @@ impl FrameValue { content.insert(0, *encoding as u8); content }, - FrameValue::UserText(content) | FrameValue::UserURL(content) => content.as_bytes(), + FrameValue::UserText(content) => content.as_bytes(), + FrameValue::UserURL(content) => content.as_bytes(), FrameValue::URL(link) => link.as_bytes().to_vec(), FrameValue::Picture { encoding, picture } => { picture.as_apic_bytes(ID3v2Version::V4, *encoding)? @@ -312,7 +313,7 @@ impl From for Option> { (FrameID::Valid(ref s), ItemValue::Locator(text) | ItemValue::Text(text)) if s == "WXXX" => { - FrameValue::UserURL(ExtendedTextFrame { + FrameValue::UserURL(ExtendedUrlFrame { encoding: TextEncoding::UTF8, description: EMPTY_CONTENT_DESCRIPTOR, content: text, @@ -345,7 +346,7 @@ impl From for Option> { }, ItemValue::Locator(locator) => { frame_id = FrameID::Valid(Cow::Borrowed("WXXX")); - value = FrameValue::UserURL(ExtendedTextFrame { + value = FrameValue::UserURL(ExtendedUrlFrame { encoding: TextEncoding::UTF8, description: String::from(desc), content: locator, @@ -426,14 +427,14 @@ impl<'a> TryFrom<&'a TagItem> for FrameRef<'a> { content: text.clone(), }), ("WXXX", ItemValue::Locator(text) | ItemValue::Text(text)) => { - FrameValue::UserURL(ExtendedTextFrame { + FrameValue::UserURL(ExtendedUrlFrame { encoding: TextEncoding::UTF8, description: EMPTY_CONTENT_DESCRIPTOR, content: text.clone(), }) }, (locator_id, ItemValue::Locator(text)) if locator_id.len() > 4 => { - FrameValue::UserURL(ExtendedTextFrame { + FrameValue::UserURL(ExtendedUrlFrame { encoding: TextEncoding::UTF8, description: EMPTY_CONTENT_DESCRIPTOR, content: text.clone(), @@ -475,7 +476,7 @@ impl<'a> TryFrom<&'a TagItem> for FrameRef<'a> { }, ItemValue::Locator(locator) => { frame_id = FrameID::Valid(Cow::Borrowed("WXXX")); - value = FrameValue::UserURL(ExtendedTextFrame { + value = FrameValue::UserURL(ExtendedUrlFrame { encoding: TextEncoding::UTF8, description: String::from(desc), content: locator.clone(), diff --git a/src/id3/v2/items/encoded_text_frame.rs b/src/id3/v2/items/extended_text_frame.rs similarity index 86% rename from src/id3/v2/items/encoded_text_frame.rs rename to src/id3/v2/items/extended_text_frame.rs index f50ab5d1..a6ca562d 100644 --- a/src/id3/v2/items/encoded_text_frame.rs +++ b/src/id3/v2/items/extended_text_frame.rs @@ -2,11 +2,11 @@ use crate::util::text::{encode_text, TextEncoding}; use std::hash::{Hash, Hasher}; -/// An `ID3v2` text frame +/// An extended `ID3v2` text frame /// -/// This is used in the frames `TXXX` and `WXXX`, where the frames +/// 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 `EncodedTextFrame` in the tag, the description +/// This means for each `ExtendedTextFrame` in the tag, the description /// must be unique. #[derive(Clone, Debug, Eq)] pub struct ExtendedTextFrame { diff --git a/src/id3/v2/items/extended_url_frame.rs b/src/id3/v2/items/extended_url_frame.rs new file mode 100644 index 00000000..ddb003da --- /dev/null +++ b/src/id3/v2/items/extended_url_frame.rs @@ -0,0 +1,43 @@ +use crate::util::text::{encode_text, TextEncoding}; + +use std::hash::{Hash, Hasher}; + +/// An extended `ID3v2` URL frame +/// +/// This is used in the `WXXX` frame, where the frames +/// are told apart by descriptions, rather than their [`FrameID`](crate::id3::v2::FrameID)s. +/// This means for each `ExtendedUrlFrame` in the tag, the description +/// must be unique. +#[derive(Clone, Debug, Eq)] +pub struct ExtendedUrlFrame { + /// The encoding of the description and comment text + pub encoding: TextEncoding, + /// Unique content description + pub description: String, + /// The actual frame content + pub content: String, +} + +impl PartialEq for ExtendedUrlFrame { + fn eq(&self, other: &Self) -> bool { + self.description == other.description + } +} + +impl Hash for ExtendedUrlFrame { + fn hash(&self, state: &mut H) { + self.description.hash(state); + } +} + +impl ExtendedUrlFrame { + /// Convert an [`ExtendedUrlFrame`] to a byte vec + pub fn as_bytes(&self) -> Vec { + let mut bytes = vec![self.encoding as u8]; + + bytes.extend(encode_text(&self.description, self.encoding, true).iter()); + bytes.extend(encode_text(&self.content, self.encoding, false)); + + bytes + } +} diff --git a/src/id3/v2/items/mod.rs b/src/id3/v2/items/mod.rs index 44998d12..a3a0ee2a 100644 --- a/src/id3/v2/items/mod.rs +++ b/src/id3/v2/items/mod.rs @@ -1,13 +1,15 @@ mod encapsulated_object; -mod encoded_text_frame; +mod extended_text_frame; +mod extended_url_frame; mod identifier; mod language_frame; mod popularimeter; mod sync_text; pub use encapsulated_object::{GEOBInformation, GeneralEncapsulatedObject}; -pub use encoded_text_frame::ExtendedTextFrame; +pub use extended_text_frame::ExtendedTextFrame; pub use identifier::UniqueFileIdentifierFrame; +pub use extended_url_frame::ExtendedUrlFrame; pub use language_frame::LanguageFrame; pub use popularimeter::Popularimeter; pub use sync_text::{SyncTextContentType, SyncTextInformation, SynchronizedText, TimestampFormat}; diff --git a/src/id3/v2/items/unsync_text.rs b/src/id3/v2/items/unsync_text.rs new file mode 100644 index 00000000..e69de29b diff --git a/src/id3/v2/tag.rs b/src/id3/v2/tag.rs index 9f8d59d8..2d69d85b 100644 --- a/src/id3/v2/tag.rs +++ b/src/id3/v2/tag.rs @@ -4,7 +4,9 @@ use super::frame::{Frame, FrameFlags, FrameValue, EMPTY_CONTENT_DESCRIPTOR, UNKN use super::ID3v2Version; use crate::error::{LoftyError, Result}; use crate::id3::v2::frame::{FrameRef, MUSICBRAINZ_UFID_OWNER}; -use crate::id3::v2::items::{ExtendedTextFrame, LanguageFrame, UniqueFileIdentifierFrame}; +use crate::id3::v2::items::{ + ExtendedTextFrame, ExtendedUrlFrame, LanguageFrame, UniqueFileIdentifierFrame, +}; use crate::picture::{Picture, PictureType, TOMBSTONE_PICTURE}; use crate::tag::item::{ItemKey, ItemValue, TagItem}; use crate::tag::{try_parse_year, Tag, TagType}; @@ -735,7 +737,7 @@ impl SplitTag for ID3v2Tag { }, ( "WXXX", - FrameValue::UserURL(ExtendedTextFrame { + FrameValue::UserURL(ExtendedUrlFrame { ref description, ref content, .. @@ -819,7 +821,7 @@ impl SplitTag for ID3v2Tag { return false; // Frame consumed }, FrameValue::URL(content) - | FrameValue::UserURL(ExtendedTextFrame { content, .. }) => { + | FrameValue::UserURL(ExtendedUrlFrame { content, .. }) => { ItemValue::Locator(std::mem::take(content)) }, FrameValue::Picture { picture, .. } => { @@ -1072,7 +1074,7 @@ mod tests { use std::borrow::Cow; use crate::id3::v2::frame::MUSICBRAINZ_UFID_OWNER; - use crate::id3::v2::items::{Popularimeter, UniqueFileIdentifierFrame}; + use crate::id3::v2::items::{ExtendedUrlFrame, Popularimeter, UniqueFileIdentifierFrame}; use crate::id3::v2::tag::{ filter_comment_frame_by_description, new_text_frame, DEFAULT_NUMBER_IN_PAIR, }; @@ -1711,7 +1713,7 @@ mod tests { let wxxx_frame = Frame::new( "WXXX", - FrameValue::UserURL(ExtendedTextFrame { + FrameValue::UserURL(ExtendedUrlFrame { encoding: TextEncoding::UTF8, description: String::from("BAR_URL_FRAME"), content: String::from("bar url"),