pub(super) mod content;
mod header;
pub(super) mod id;
pub(super) mod read;
use super::header::Id3v2Version;
use super::items::{
AttachedPictureFrame, CommentFrame, EventTimingCodesFrame, ExtendedTextFrame, ExtendedUrlFrame,
KeyValueFrame, OwnershipFrame, Popularimeter, PrivateFrame, RelativeVolumeAdjustmentFrame,
TextInformationFrame, UniqueFileIdentifierFrame, UnsynchronizedTextFrame, UrlLinkFrame,
};
use super::util::upgrade::{upgrade_v2, upgrade_v3};
use crate::error::{ErrorKind, Id3v2Error, Id3v2ErrorKind, LoftyError, Result};
use crate::tag::item::{ItemKey, ItemValue, TagItem};
use crate::tag::TagType;
use crate::util::text::TextEncoding;
use id::FrameId;
use std::borrow::Cow;
use std::convert::{TryFrom, TryInto};
use std::hash::{Hash, Hasher};
pub(super) const MUSICBRAINZ_UFID_OWNER: &str = "http://musicbrainz.org";
pub(super) const EMPTY_CONTENT_DESCRIPTOR: String = String::new();
pub(super) const UNKNOWN_LANGUAGE: [u8; 3] = *b"XXX";
#[derive(Clone, Debug, Eq)]
pub struct Frame<'a> {
pub(super) id: FrameId<'a>,
pub(super) value: FrameValue,
pub(super) flags: FrameFlags,
}
impl<'a> PartialEq for Frame<'a> {
fn eq(&self, other: &Self) -> bool {
match self.value {
FrameValue::Text { .. } => self.id == other.id,
_ => self.id == other.id && self.value == other.value,
}
}
}
impl<'a> Hash for Frame<'a> {
fn hash<H: Hasher>(&self, state: &mut H) {
match self.value {
FrameValue::Text { .. } => self.id.hash(state),
_ => {
self.id.hash(state);
self.content().hash(state);
},
}
}
}
impl<'a> Frame<'a> {
pub fn new<I, V>(id: I, value: V, flags: FrameFlags) -> Result<Self>
where
I: Into<Cow<'a, str>>,
V: Into<FrameValue>,
{
Self::new_cow(id.into(), value.into(), flags)
}
fn new_cow(id: Cow<'a, str>, value: FrameValue, flags: FrameFlags) -> Result<Self> {
let id_upgraded = match id.len() {
4 => match upgrade_v3(&id) {
None => id,
Some(upgraded) => Cow::Borrowed(upgraded),
},
3 => match upgrade_v2(&id) {
None => id,
Some(upgraded) => Cow::Borrowed(upgraded),
},
_ => {
return Err(Id3v2Error::new(Id3v2ErrorKind::BadFrameId(
id.into_owned().into_bytes(),
))
.into())
},
};
let id = FrameId::new_cow(id_upgraded)?;
Ok(Self { id, value, flags })
}
pub fn id_str(&self) -> &str {
self.id.as_str()
}
pub fn content(&self) -> &FrameValue {
&self.value
}
pub fn flags(&self) -> &FrameFlags {
&self.flags
}
pub fn set_flags(&mut self, flags: FrameFlags) {
self.flags = flags
}
pub(crate) fn text(id: Cow<'a, str>, content: String) -> Self {
Self {
id: FrameId::Valid(id),
value: FrameValue::Text(TextInformationFrame {
encoding: TextEncoding::UTF8,
value: content,
}),
flags: FrameFlags::default(),
}
}
}
#[non_exhaustive]
#[derive(PartialEq, Clone, Debug, Eq, Hash)]
pub enum FrameValue {
Comment(CommentFrame),
UnsynchronizedText(UnsynchronizedTextFrame),
Text(TextInformationFrame),
UserText(ExtendedTextFrame),
Url(UrlLinkFrame),
UserUrl(ExtendedUrlFrame),
Picture(AttachedPictureFrame),
Popularimeter(Popularimeter),
KeyValue(KeyValueFrame),
RelativeVolumeAdjustment(RelativeVolumeAdjustmentFrame),
UniqueFileIdentifier(UniqueFileIdentifierFrame),
Ownership(OwnershipFrame),
EventTimingCodes(EventTimingCodesFrame),
Private(PrivateFrame),
Binary(Vec<u8>),
}
impl TryFrom<ItemValue> for FrameValue {
type Error = LoftyError;
fn try_from(input: ItemValue) -> std::result::Result<FrameValue, Self::Error> {
match input {
ItemValue::Text(text) => Ok(FrameValue::Text(TextInformationFrame {
encoding: TextEncoding::UTF8,
value: text,
})),
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)),
}
}
}
impl From<CommentFrame> for FrameValue {
fn from(value: CommentFrame) -> Self {
Self::Comment(value)
}
}
impl From<UnsynchronizedTextFrame> for FrameValue {
fn from(value: UnsynchronizedTextFrame) -> Self {
Self::UnsynchronizedText(value)
}
}
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)
}
}
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)
}
}
impl From<AttachedPictureFrame> for FrameValue {
fn from(value: AttachedPictureFrame) -> Self {
Self::Picture(value)
}
}
impl From<Popularimeter> for FrameValue {
fn from(value: Popularimeter) -> Self {
Self::Popularimeter(value)
}
}
impl From<KeyValueFrame> for FrameValue {
fn from(value: KeyValueFrame) -> Self {
Self::KeyValue(value)
}
}
impl From<RelativeVolumeAdjustmentFrame> for FrameValue {
fn from(value: RelativeVolumeAdjustmentFrame) -> Self {
Self::RelativeVolumeAdjustment(value)
}
}
impl From<UniqueFileIdentifierFrame> for FrameValue {
fn from(value: UniqueFileIdentifierFrame) -> Self {
Self::UniqueFileIdentifier(value)
}
}
impl From<OwnershipFrame> for FrameValue {
fn from(value: OwnershipFrame) -> Self {
Self::Ownership(value)
}
}
impl From<EventTimingCodesFrame> for FrameValue {
fn from(value: EventTimingCodesFrame) -> Self {
Self::EventTimingCodes(value)
}
}
impl From<PrivateFrame> for FrameValue {
fn from(value: PrivateFrame) -> Self {
Self::Private(value)
}
}
impl FrameValue {
pub(super) fn as_bytes(&self) -> Result<Vec<u8>> {
Ok(match self {
FrameValue::Comment(comment) => comment.as_bytes()?,
FrameValue::UnsynchronizedText(lf) => lf.as_bytes()?,
FrameValue::Text(tif) => tif.as_bytes(),
FrameValue::UserText(content) => content.as_bytes(),
FrameValue::UserUrl(content) => content.as_bytes(),
FrameValue::Url(link) => link.as_bytes(),
FrameValue::Picture(attached_picture) => attached_picture.as_bytes(Id3v2Version::V4)?,
FrameValue::Popularimeter(popularimeter) => popularimeter.as_bytes(),
FrameValue::KeyValue(content) => content.as_bytes(),
FrameValue::RelativeVolumeAdjustment(frame) => frame.as_bytes(),
FrameValue::UniqueFileIdentifier(frame) => frame.as_bytes(),
FrameValue::Ownership(frame) => frame.as_bytes()?,
FrameValue::EventTimingCodes(frame) => frame.as_bytes(),
FrameValue::Private(frame) => frame.as_bytes(),
FrameValue::Binary(binary) => binary.clone(),
})
}
pub(super) fn name(&self) -> &'static str {
match self {
FrameValue::Comment(_) => "Comment",
FrameValue::UnsynchronizedText(_) => "UnsynchronizedText",
FrameValue::Text { .. } => "Text",
FrameValue::UserText(_) => "UserText",
FrameValue::Url(_) => "Url",
FrameValue::UserUrl(_) => "UserUrl",
FrameValue::Picture { .. } => "Picture",
FrameValue::Popularimeter(_) => "Popularimeter",
FrameValue::KeyValue(_) => "KeyValue",
FrameValue::UniqueFileIdentifier(_) => "UniqueFileIdentifier",
FrameValue::RelativeVolumeAdjustment(_) => "RelativeVolumeAdjustment",
FrameValue::Ownership(_) => "Ownership",
FrameValue::EventTimingCodes(_) => "EventTimingCodes",
FrameValue::Private(_) => "Private",
FrameValue::Binary(_) => "Binary",
}
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Default)]
#[allow(clippy::struct_excessive_bools)]
pub struct FrameFlags {
pub tag_alter_preservation: bool,
pub file_alter_preservation: bool,
pub read_only: bool,
pub grouping_identity: Option<u8>,
pub compression: bool,
pub encryption: Option<u8>,
pub unsynchronisation: bool, pub data_length_indicator: Option<u32>,
}
impl From<TagItem> for Option<Frame<'static>> {
fn from(input: TagItem) -> Self {
let frame_id;
let value;
match input.key().try_into().map(FrameId::into_owned) {
Ok(id) => {
value = match (&id, input.item_value) {
(FrameId::Valid(ref s), ItemValue::Text(text)) if s == "COMM" => {
FrameValue::Comment(CommentFrame {
encoding: TextEncoding::UTF8,
language: UNKNOWN_LANGUAGE,
description: EMPTY_CONTENT_DESCRIPTOR,
content: text,
})
},
(FrameId::Valid(ref s), ItemValue::Text(text)) if s == "USLT" => {
FrameValue::UnsynchronizedText(UnsynchronizedTextFrame {
encoding: TextEncoding::UTF8,
language: UNKNOWN_LANGUAGE,
description: EMPTY_CONTENT_DESCRIPTOR,
content: text,
})
},
(FrameId::Valid(ref s), ItemValue::Locator(text) | ItemValue::Text(text))
if s == "WXXX" =>
{
FrameValue::UserUrl(ExtendedUrlFrame {
encoding: TextEncoding::UTF8,
description: EMPTY_CONTENT_DESCRIPTOR,
content: text,
})
},
(FrameId::Valid(ref s), ItemValue::Text(text)) if s == "TXXX" => {
FrameValue::UserText(ExtendedTextFrame {
encoding: TextEncoding::UTF8,
description: EMPTY_CONTENT_DESCRIPTOR,
content: text,
})
},
(FrameId::Valid(ref s), ItemValue::Binary(text)) if s == "POPM" => {
FrameValue::Popularimeter(Popularimeter::parse(&mut &text[..]).ok()?)
},
(_, item_value) => {
let Ok(value) = item_value.try_into() else {
return None;
};
value
},
};
frame_id = id;
},
Err(_) => match input.item_key.map_key(TagType::Id3v2, true) {
Some(desc) => match input.item_value {
ItemValue::Text(text) => {
frame_id = FrameId::Valid(Cow::Borrowed("TXXX"));
value = FrameValue::UserText(ExtendedTextFrame {
encoding: TextEncoding::UTF8,
description: String::from(desc),
content: text,
})
},
ItemValue::Locator(locator) => {
frame_id = FrameId::Valid(Cow::Borrowed("WXXX"));
value = FrameValue::UserUrl(ExtendedUrlFrame {
encoding: TextEncoding::UTF8,
description: String::from(desc),
content: locator,
})
},
ItemValue::Binary(_) => 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;
},
},
},
}
Some(Frame {
id: frame_id,
value,
flags: FrameFlags::default(),
})
}
}
#[derive(Clone)]
pub(crate) struct FrameRef<'a> {
pub id: FrameId<'a>,
pub value: Cow<'a, FrameValue>,
pub flags: FrameFlags,
}
impl<'a> Frame<'a> {
pub(crate) fn as_opt_ref(&'a self) -> Option<FrameRef<'a>> {
if let FrameId::Valid(id) = &self.id {
Some(FrameRef {
id: FrameId::Valid(Cow::Borrowed(id)),
value: Cow::Borrowed(self.content()),
flags: self.flags,
})
} else {
None
}
}
}
impl<'a> TryFrom<&'a TagItem> for FrameRef<'a> {
type Error = LoftyError;
fn try_from(tag_item: &'a TagItem) -> std::result::Result<Self, Self::Error> {
let id: Result<FrameId<'a>> = tag_item.key().try_into();
let frame_id: FrameId<'a>;
let value: FrameValue;
match id {
Ok(id) => {
let id_str = id.as_str();
value = match (id_str, tag_item.value()) {
("COMM", ItemValue::Text(text)) => FrameValue::Comment(CommentFrame {
encoding: TextEncoding::UTF8,
language: UNKNOWN_LANGUAGE,
description: EMPTY_CONTENT_DESCRIPTOR,
content: text.clone(),
}),
("USLT", ItemValue::Text(text)) => {
FrameValue::UnsynchronizedText(UnsynchronizedTextFrame {
encoding: TextEncoding::UTF8,
language: UNKNOWN_LANGUAGE,
description: EMPTY_CONTENT_DESCRIPTOR,
content: text.clone(),
})
},
("WXXX", ItemValue::Locator(text) | ItemValue::Text(text)) => {
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(ExtendedUrlFrame {
encoding: TextEncoding::UTF8,
description: EMPTY_CONTENT_DESCRIPTOR,
content: text.clone(),
})
},
("TXXX", ItemValue::Text(text)) => FrameValue::UserText(ExtendedTextFrame {
encoding: TextEncoding::UTF8,
description: EMPTY_CONTENT_DESCRIPTOR,
content: text.clone(),
}),
(text_id, ItemValue::Text(text)) if text_id.len() > 4 => {
FrameValue::UserText(ExtendedTextFrame {
encoding: TextEncoding::UTF8,
description: EMPTY_CONTENT_DESCRIPTOR,
content: text.clone(),
})
},
("POPM", ItemValue::Binary(contents)) => {
FrameValue::Popularimeter(Popularimeter::parse(&mut &contents[..])?)
},
(_, value) => value.try_into()?,
};
frame_id = id;
},
Err(_) => {
let item_key = tag_item.key();
let Some(desc) = item_key.map_key(TagType::Id3v2, true) else {
return Err(Id3v2Error::new(Id3v2ErrorKind::UnsupportedFrameId(
item_key.clone(),
))
.into());
};
match tag_item.value() {
ItemValue::Text(text) => {
frame_id = FrameId::Valid(Cow::Borrowed("TXXX"));
value = FrameValue::UserText(ExtendedTextFrame {
encoding: TextEncoding::UTF8,
description: String::from(desc),
content: text.clone(),
})
},
ItemValue::Locator(locator) => {
frame_id = FrameId::Valid(Cow::Borrowed("WXXX"));
value = FrameValue::UserUrl(ExtendedUrlFrame {
encoding: TextEncoding::UTF8,
description: String::from(desc),
content: locator.clone(),
})
},
_ => {
return Err(Id3v2Error::new(Id3v2ErrorKind::UnsupportedFrameId(
item_key.clone(),
))
.into())
},
}
},
}
Ok(FrameRef {
id: frame_id,
value: Cow::Owned(value),
flags: FrameFlags::default(),
})
}
}
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) => Ok(FrameValue::Text(TextInformationFrame {
encoding: TextEncoding::UTF8,
value: text.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())),
}
}
}