Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/id3/v2/frame/content.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use crate::error::{ID3v2Error, ID3v2ErrorKind, LoftyError, Result};
use crate::id3::v2::frame::FrameValue;
use crate::id3::v2::items::encoded_text_frame::EncodedTextFrame;
use crate::id3::v2::items::identifier::UniqueFileIdentifierFrame;
use crate::id3::v2::items::language_frame::LanguageFrame;
use crate::id3::v2::items::popularimeter::Popularimeter;
use crate::id3::v2::ID3v2Version;
Expand Down Expand Up @@ -28,6 +29,7 @@ pub(super) fn parse_content(
"TXXX" => parse_user_defined(content, false, version)?,
"WXXX" => parse_user_defined(content, true, version)?,
"COMM" | "USLT" => parse_text_language(content, id, version)?,
"UFID" => UniqueFileIdentifierFrame::decode_bytes(content)?.map(FrameValue::UniqueFileIdentifier),
_ if id.starts_with('T') => parse_text(content, version)?,
// Apple proprietary frames
// WFED (Podcast URL), GRP1 (Grouping), MVNM (Movement Name), MVIN (Movement Number)
Expand Down
25 changes: 24 additions & 1 deletion src/id3/v2/frame/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ use crate::picture::Picture;
use crate::tag::item::{ItemValue, TagItem};
use crate::tag::TagType;
use crate::util::text::{encode_text, TextEncoding};
use crate::ItemKey;
use id::FrameID;

use std::borrow::Cow;
Expand All @@ -20,6 +21,10 @@ use crate::id3::v2::items::popularimeter::Popularimeter;
use std::convert::{TryFrom, TryInto};
use std::hash::{Hash, Hasher};

use super::items::identifier::UniqueFileIdentifierFrame;

pub(super) const MUSICBRAINZ_UFID_OWNER: &str = "http://musicbrainz.org";

/// Empty content descriptor in text frame
///
/// Unspecific [`LanguageFrame`]s and [`EncodedTextFrame`] frames
Expand Down Expand Up @@ -204,6 +209,8 @@ pub enum FrameValue {
/// * This is used for **all** frames with an ID of [`FrameID::Outdated`]
/// * This is used for unknown frames
Binary(Vec<u8>),
/// Unique file identifier
UniqueFileIdentifier(UniqueFileIdentifierFrame),
}

impl From<ItemValue> for FrameValue {
Expand Down Expand Up @@ -236,6 +243,7 @@ impl FrameValue {
},
FrameValue::Popularimeter(popularimeter) => popularimeter.as_bytes(),
FrameValue::Binary(binary) => binary.clone(),
FrameValue::UniqueFileIdentifier(frame) => frame.as_bytes(),
})
}
}
Expand Down Expand Up @@ -349,7 +357,22 @@ impl From<TagItem> for Option<Frame<'static>> {
},
ItemValue::Binary(_) => return None,
},
None => return None,
None => match (input.item_key, input.item_value) {
(ItemKey::MusicBrainzRecordingId, ItemValue::Text(recording_id)) => {
if !recording_id.is_ascii() {
return None;
}
let frame = UniqueFileIdentifierFrame {
owner: MUSICBRAINZ_UFID_OWNER.to_owned(),
identifier: recording_id.into_bytes(),
};
frame_id = FrameID::Valid(Cow::Borrowed("UFID"));
value = FrameValue::UniqueFileIdentifier(frame);
},
_ => {
return None;
},
},
},
}

Expand Down
55 changes: 55 additions & 0 deletions src/id3/v2/items/identifier.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
use std::hash::{Hash, Hasher};

use crate::error::{ID3v2Error, ID3v2ErrorKind};
use crate::util::text::{decode_text, encode_text};
use crate::{Result, TextEncoding};

/// An `ID3v2` unique file identifier frame (UFID).
#[derive(Clone, Debug, Eq)]
pub struct UniqueFileIdentifierFrame {
/// The non-empty owner of the identifier.
pub owner: String,
/// The binary payload with up to 64 bytes of data.
pub identifier: Vec<u8>,
}

impl UniqueFileIdentifierFrame {
/// Encode the frame contents as bytes.
pub fn as_bytes(&self) -> Vec<u8> {
let Self { owner, identifier } = self;

let mut content = Vec::with_capacity(owner.len() + 1 + identifier.len());
content.extend(encode_text(owner.as_str(), TextEncoding::Latin1, true));
content.extend_from_slice(identifier);

content
}

/// Decode the frame contents from bytes.
pub fn decode_bytes(input: &mut &[u8]) -> Result<Option<Self>> {
if input.is_empty() {
return Ok(None);
}

let Some(owner) = decode_text(input, TextEncoding::Latin1, true)? else {
return Err(ID3v2Error::new(ID3v2ErrorKind::Other(
"Missing owner in UFID frame",
)).into());
};
let identifier = input.to_vec();

Ok(Some(Self { owner, identifier }))
}
}

impl PartialEq for UniqueFileIdentifierFrame {
fn eq(&self, other: &Self) -> bool {
self.owner == other.owner
}
}

impl Hash for UniqueFileIdentifierFrame {
fn hash<H: Hasher>(&self, state: &mut H) {
self.owner.hash(state);
}
}
1 change: 1 addition & 0 deletions src/id3/v2/items/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
pub(super) mod encapsulated_object;
pub(super) mod encoded_text_frame;
pub(super) mod identifier;
pub(super) mod language_frame;
pub(super) mod popularimeter;
pub(super) mod sync_text;
113 changes: 108 additions & 5 deletions src/id3/v2/tag.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,21 @@ use super::frame::id::FrameID;
use super::frame::{Frame, FrameFlags, FrameValue, EMPTY_CONTENT_DESCRIPTOR, UNKNOWN_LANGUAGE};
use super::ID3v2Version;
use crate::error::{LoftyError, Result};
use crate::id3::v2::frame::FrameRef;
use crate::id3::v2::frame::{FrameRef, MUSICBRAINZ_UFID_OWNER};
use crate::id3::v2::items::encoded_text_frame::EncodedTextFrame;
use crate::id3::v2::items::identifier::UniqueFileIdentifierFrame;
use crate::id3::v2::items::language_frame::LanguageFrame;
use crate::picture::{Picture, PictureType, TOMBSTONE_PICTURE};
use crate::tag::item::{ItemKey, ItemValue, TagItem};
use crate::tag::{try_parse_year, Tag, TagType};
use crate::traits::{Accessor, MergeTag, SplitTag, TagExt};
use crate::util::text::TextEncoding;
use crate::util::text::{decode_text, TextEncoding};

use std::borrow::Cow;
use std::convert::TryInto;
use std::fmt::Display;
use std::fs::{File, OpenOptions};
use std::io::Write;
use std::io::{Cursor, Write};
use std::ops::Deref;
use std::path::Path;

Expand Down Expand Up @@ -741,6 +742,29 @@ impl SplitTag for ID3v2Tag {
}
false // Frame consumed
},
(
"UFID",
FrameValue::UniqueFileIdentifier(UniqueFileIdentifierFrame {
ref owner,
ref identifier,
..
}),
) => {
if owner == MUSICBRAINZ_UFID_OWNER {
let mut identifier = Cursor::new(identifier);
let Ok(recording_id) = decode_text(&mut identifier, TextEncoding::Latin1, false) else {
return true; // Keep frame
};
tag.items.push(TagItem::new(
ItemKey::MusicBrainzRecordingId,
ItemValue::Text(recording_id.unwrap_or_default()),
));
false // Frame consumed
} else {
// Unsupported owner
true // Keep frame
}
},
(id, value) => {
let item_key = ItemKey::from_key(TagType::ID3v2, id);

Expand Down Expand Up @@ -798,6 +822,9 @@ impl SplitTag for ID3v2Tag {
ItemValue::Binary(popularimeter.as_bytes())
},
FrameValue::Binary(binary) => ItemValue::Binary(std::mem::take(binary)),
FrameValue::UniqueFileIdentifier(_) => {
return true; // Keep unsupported frame
},
};

tag.items.push(TagItem::new(item_key, item_value));
Expand Down Expand Up @@ -1036,6 +1063,8 @@ impl<'a, I: Iterator<Item = FrameRef<'a>> + Clone + 'a> Id3v2TagRef<'a, I> {
mod tests {
use std::borrow::Cow;

use crate::id3::v2::frame::MUSICBRAINZ_UFID_OWNER;
use crate::id3::v2::items::identifier::UniqueFileIdentifierFrame;
use crate::id3::v2::items::popularimeter::Popularimeter;
use crate::id3::v2::tag::{
filter_comment_frame_by_description, new_text_frame, DEFAULT_NUMBER_IN_PAIR,
Expand Down Expand Up @@ -1741,9 +1770,9 @@ mod tests {
Some(&Frame {
id: FrameID::Valid(Cow::Borrowed("TXXX")),
value: FrameValue::UserText(EncodedTextFrame {
encoding: TextEncoding::UTF8,
description: String::from("FOO_BAR"),
content: String::from("foo content"),
encoding: TextEncoding::UTF8, // Not considered by PartialEq!
content: Default::default(), // Not considered by PartialEq!
}),
flags: FrameFlags::default(),
})
Expand Down Expand Up @@ -2041,4 +2070,78 @@ mod tests {
assert_invalid("1//2");
assert_invalid("0x1/0x2");
}

#[test]
fn ufid_frame_with_musicbrainz_record_id() {
let mut id3v2 = ID3v2Tag::default();
let unknown_ufid_frame = UniqueFileIdentifierFrame {
owner: "other".to_owned(),
identifier: b"0123456789".to_vec(),
};
id3v2.insert(
Frame::new(
"UFID",
FrameValue::UniqueFileIdentifier(unknown_ufid_frame.clone()),
FrameFlags::default(),
)
.unwrap(),
);
let musicbrainz_recording_id = b"189002e7-3285-4e2e-92a3-7f6c30d407a2";
let musicbrainz_recording_id_frame = UniqueFileIdentifierFrame {
owner: MUSICBRAINZ_UFID_OWNER.to_owned(),
identifier: musicbrainz_recording_id.to_vec(),
};
id3v2.insert(
Frame::new(
"UFID",
FrameValue::UniqueFileIdentifier(musicbrainz_recording_id_frame.clone()),
FrameFlags::default(),
)
.unwrap(),
);
assert_eq!(2, id3v2.len());

let (split_remainder, split_tag) = id3v2.split_tag();
assert_eq!(split_remainder.0.len(), 1);
assert_eq!(split_tag.len(), 1);
assert_eq!(
ItemValue::Text(String::from_utf8(musicbrainz_recording_id.to_vec()).unwrap()),
*split_tag
.get_items(&ItemKey::MusicBrainzRecordingId)
.next()
.unwrap()
.value()
);

let id3v2 = split_remainder.merge_tag(split_tag);
assert_eq!(2, id3v2.len());
match &id3v2.frames[..] {
[Frame {
id: _,
value:
FrameValue::UniqueFileIdentifier(UniqueFileIdentifierFrame {
owner: first_owner,
identifier: first_identifier,
}),
flags: _,
}, Frame {
id: _,
value:
FrameValue::UniqueFileIdentifier(UniqueFileIdentifierFrame {
owner: second_owner,
identifier: second_identifier,
}),
flags: _,
}] => {
assert_eq!(&unknown_ufid_frame.owner, first_owner);
assert_eq!(&unknown_ufid_frame.identifier, first_identifier);
assert_eq!(&musicbrainz_recording_id_frame.owner, second_owner);
assert_eq!(
&musicbrainz_recording_id_frame.identifier,
second_identifier
);
},
_ => unreachable!(),
}
}
}
2 changes: 2 additions & 0 deletions src/id3/v2/write/frame.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ fn verify_frame(frame: &FrameRef<'_>) -> Result<()> {
| ("TXXX", FrameValue::UserText(_))
| ("WXXX", FrameValue::UserURL(_))
| (_, FrameValue::Binary(_))
| ("UFID", FrameValue::UniqueFileIdentifier(_))
| ("WFED" | "GRP1" | "MVNM" | "MVIN", FrameValue::Text { .. }) => Ok(()),
(id, FrameValue::Text { .. }) if id.starts_with('T') => Ok(()),
(id, FrameValue::URL(_)) if id.starts_with('W') => Ok(()),
Expand All @@ -46,6 +47,7 @@ fn verify_frame(frame: &FrameRef<'_>) -> Result<()> {
FrameValue::Picture { .. } => "Picture",
FrameValue::Popularimeter(_) => "Popularimeter",
FrameValue::Binary(_) => "Binary",
FrameValue::UniqueFileIdentifier(_) => "UniqueFileIdentifier",
},
))
.into()),
Expand Down