pub(super) mod content;
mod header;
pub(super) mod id;
pub(super) mod read;
use crate::error::{ID3v2Error, ID3v2ErrorKind, LoftyError, Result};
use crate::id3::v2::items::encoded_text_frame::EncodedTextFrame;
use crate::id3::v2::items::language_frame::LanguageFrame;
use crate::id3::v2::util::upgrade::{upgrade_v2, upgrade_v3};
use crate::id3::v2::ID3v2Version;
use crate::picture::Picture;
use crate::tag::item::{ItemKey, ItemValue, TagItem};
use crate::tag::TagType;
use crate::util::text::{encode_text, TextEncoding};
use id::FrameID;
use std::borrow::Cow;
use crate::id3::v2::items::popularimeter::Popularimeter;
use std::convert::{TryFrom, TryInto};
use std::hash::{Hash, Hasher};
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>(id: I, value: FrameValue, flags: FrameFlags) -> Result<Self>
where
I: Into<Cow<'a, str>>,
{
Self::new_cow(id.into(), value, 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).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 {
encoding: TextEncoding::UTF8,
value: content,
},
flags: FrameFlags::default(),
}
}
}
#[non_exhaustive]
#[derive(PartialEq, Clone, Debug, Eq, Hash)]
pub enum FrameValue {
Comment(LanguageFrame),
UnSyncText(LanguageFrame),
Text {
encoding: TextEncoding,
value: String,
},
UserText(EncodedTextFrame),
URL(String),
UserURL(EncodedTextFrame),
Picture {
encoding: TextEncoding,
picture: Picture,
},
Popularimeter(Popularimeter),
Binary(Vec<u8>),
}
impl From<ItemValue> for FrameValue {
fn from(input: ItemValue) -> Self {
match input {
ItemValue::Text(text) => FrameValue::Text {
encoding: TextEncoding::UTF8,
value: text,
},
ItemValue::Locator(locator) => FrameValue::URL(locator),
ItemValue::Binary(binary) => FrameValue::Binary(binary),
}
}
}
impl FrameValue {
pub(super) fn as_bytes(&self) -> Result<Vec<u8>> {
Ok(match self {
FrameValue::Comment(lf) | FrameValue::UnSyncText(lf) => lf.as_bytes()?,
FrameValue::Text { encoding, value } => {
let mut content = encode_text(value, *encoding, false);
content.insert(0, *encoding as u8);
content
},
FrameValue::UserText(content) | 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)?
},
FrameValue::Popularimeter(popularimeter) => popularimeter.as_bytes(),
FrameValue::Binary(binary) => binary.clone(),
})
}
}
#[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(LanguageFrame {
encoding: TextEncoding::UTF8,
language: UNKNOWN_LANGUAGE,
description: EMPTY_CONTENT_DESCRIPTOR,
content: text,
})
},
(FrameID::Valid(ref s), ItemValue::Text(text)) if s == "USLT" => {
FrameValue::UnSyncText(LanguageFrame {
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(EncodedTextFrame {
encoding: TextEncoding::UTF8,
description: EMPTY_CONTENT_DESCRIPTOR,
content: text,
})
},
(FrameID::Valid(ref s), ItemValue::Text(text)) if s == "TXXX" => {
FrameValue::UserText(EncodedTextFrame {
encoding: TextEncoding::UTF8,
description: EMPTY_CONTENT_DESCRIPTOR,
content: text,
})
},
(FrameID::Valid(ref s), ItemValue::Binary(text)) if s == "POPM" => {
FrameValue::Popularimeter(Popularimeter::from_bytes(&text).ok()?)
},
(_, value) => value.into(),
};
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(EncodedTextFrame {
encoding: TextEncoding::UTF8,
description: String::from(desc),
content: text,
})
},
ItemValue::Locator(locator) => {
frame_id = FrameID::Valid(Cow::Borrowed("WXXX"));
value = FrameValue::UserURL(EncodedTextFrame {
encoding: TextEncoding::UTF8,
description: String::from(desc),
content: locator,
})
},
ItemValue::Binary(_) => return None,
},
None => return None,
},
}
Some(Frame {
id: frame_id,
value,
flags: FrameFlags::default(),
})
}
}
pub(crate) struct FrameRef<'a> {
pub id: &'a str,
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,
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 = match tag_item.key() {
ItemKey::Unknown(unknown) if unknown.len() == 4 => {
id::FrameID::verify_id(unknown)?;
Ok(unknown.as_str())
},
k => k
.map_key(TagType::ID3v2, false)
.ok_or_else(|| ID3v2Error::new(ID3v2ErrorKind::BadFrameID)),
}?;
Ok(FrameRef {
id,
value: Cow::Owned(match (id, tag_item.value()) {
("COMM", ItemValue::Text(text)) => FrameValue::Comment(LanguageFrame {
encoding: TextEncoding::UTF8,
language: UNKNOWN_LANGUAGE,
description: EMPTY_CONTENT_DESCRIPTOR,
content: text.clone(),
}),
("USLT", ItemValue::Text(text)) => FrameValue::UnSyncText(LanguageFrame {
encoding: TextEncoding::UTF8,
language: UNKNOWN_LANGUAGE,
description: EMPTY_CONTENT_DESCRIPTOR,
content: text.clone(),
}),
("WXXX", ItemValue::Locator(text) | ItemValue::Text(text)) => {
FrameValue::UserURL(EncodedTextFrame {
encoding: TextEncoding::UTF8,
description: EMPTY_CONTENT_DESCRIPTOR,
content: text.clone(),
})
},
("TXXX", ItemValue::Text(text)) => FrameValue::UserText(EncodedTextFrame {
encoding: TextEncoding::UTF8,
description: EMPTY_CONTENT_DESCRIPTOR,
content: text.clone(),
}),
("POPM", ItemValue::Binary(contents)) => {
FrameValue::Popularimeter(Popularimeter::from_bytes(contents)?)
},
(_, value) => value.into(),
}),
flags: FrameFlags::default(),
})
}
}
impl<'a> Into<FrameValue> for &'a ItemValue {
fn into(self) -> FrameValue {
match self {
ItemValue::Text(text) => FrameValue::Text {
encoding: TextEncoding::UTF8,
value: text.clone(),
},
ItemValue::Locator(locator) => FrameValue::URL(locator.clone()),
ItemValue::Binary(binary) => FrameValue::Binary(binary.clone()),
}
}
}