use crate::error::{Error, ErrorKind};
use crate::stream::encoding::Encoding;
use crate::tag::Version;
use std::fmt;
use std::str;
pub use self::content::{
Chapter, Comment, Content, EncapsulatedObject, ExtendedLink, ExtendedText, Lyrics,
MpegLocationLookupTable, MpegLocationLookupTableReference, Picture, PictureType, Popularimeter,
Private, SynchronisedLyrics, SynchronisedLyricsType, TableOfContents, TimestampFormat, Unknown,
};
pub use self::timestamp::Timestamp;
mod content;
mod content_cmp;
mod timestamp;
#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
enum ID {
Valid(String),
Invalid(String),
}
#[allow(clippy::derived_hash_with_manual_eq)]
#[derive(Clone, Debug, Eq, Ord, PartialOrd, Hash)]
pub struct Frame {
id: ID,
content: Content,
tag_alter_preservation: bool,
file_alter_preservation: bool,
encoding: Option<Encoding>,
}
impl Frame {
pub(crate) fn compare(&self, other: &Frame) -> bool {
if self.id == other.id {
let content_eq = if let ID::Valid(id) = &self.id {
if id == "WCOM" || id == "WOAR" {
self.content.link() == other.content.link()
} else {
self.content.unique() == other.content.unique()
}
} else {
self.content.unique() == other.content.unique()
};
content_eq
&& (self.encoding.is_none()
|| other.encoding.is_none()
|| self.encoding == other.encoding)
} else {
false
}
}
pub(crate) fn validate(&self) -> crate::Result<()> {
let id = match &self.id {
ID::Valid(v) => v,
ID::Invalid(_) => return Ok(()),
};
match (id.as_str(), &self.content) {
("GRP1", Content::Text(_)) => Ok(()),
(id, Content::Text(_)) if id.starts_with('T') => Ok(()),
("TXXX", Content::ExtendedText(_)) => Ok(()),
(id, Content::Link(_)) if id.starts_with('W') => Ok(()),
("WXXX", Content::ExtendedLink(_)) => Ok(()),
("GEOB", Content::EncapsulatedObject(_)) => Ok(()),
("USLT", Content::Lyrics(_)) => Ok(()),
("SYLT", Content::SynchronisedLyrics(_)) => Ok(()),
("COMM", Content::Comment(_)) => Ok(()),
("POPM", Content::Popularimeter(_)) => Ok(()),
("APIC", Content::Picture(_)) => Ok(()),
("CHAP", Content::Chapter(_)) => Ok(()),
("MLLT", Content::MpegLocationLookupTable(_)) => Ok(()),
("PRIV", Content::Private(_)) => Ok(()),
("CTOC", Content::TableOfContents(_)) => Ok(()),
(_, Content::Unknown(_)) => Ok(()),
(id, content) => {
let content_kind = match content {
Content::Text(_) => "Text",
Content::ExtendedText(_) => "ExtendedText",
Content::Link(_) => "Link",
Content::ExtendedLink(_) => "ExtendedLink",
Content::Comment(_) => "Comment",
Content::Popularimeter(_) => "Popularimeter",
Content::Lyrics(_) => "Lyrics",
Content::SynchronisedLyrics(_) => "SynchronisedLyrics",
Content::Picture(_) => "Picture",
Content::EncapsulatedObject(_) => "EncapsulatedObject",
Content::Chapter(_) => "Chapter",
Content::MpegLocationLookupTable(_) => "MpegLocationLookupTable",
Content::Private(_) => "PrivateFrame",
Content::TableOfContents(_) => "TableOfContents",
Content::Unknown(_) => "Unknown",
};
Err(Error::new(
ErrorKind::InvalidInput,
format!(
"Frame with ID {} and content type {} can not be written as valid ID3",
id, content_kind,
),
))
}
}
}
pub fn with_content(id: impl AsRef<str>, content: Content) -> Self {
assert!({
let l = id.as_ref().len();
l == 3 || l == 4
});
Frame {
id: if id.as_ref().len() == 3 {
match convert_id_2_to_3(id.as_ref()) {
Some(translated) => ID::Valid(translated.to_string()),
None => ID::Invalid(id.as_ref().to_string()),
}
} else {
ID::Valid(id.as_ref().to_string())
},
content,
tag_alter_preservation: false,
file_alter_preservation: false,
encoding: None,
}
}
pub fn set_encoding(mut self, encoding: Option<Encoding>) -> Self {
self.encoding = encoding;
self
}
pub fn text(id: impl AsRef<str>, content: impl Into<String>) -> Self {
Self::with_content(id, Content::Text(content.into()))
}
pub fn link(id: impl AsRef<str>, content: impl Into<String>) -> Self {
Self::with_content(id, Content::Link(content.into()))
}
pub fn id(&self) -> &str {
match self.id {
ID::Valid(ref id) | ID::Invalid(ref id) => id,
}
}
pub fn id_for_version(&self, version: Version) -> Option<&str> {
match (version, &self.id) {
(Version::Id3v22, ID::Valid(id)) => convert_id_3_to_2(id),
(Version::Id3v23, ID::Valid(id))
| (Version::Id3v24, ID::Valid(id))
| (Version::Id3v22, ID::Invalid(id)) => Some(id),
(_, ID::Invalid(_)) => None,
}
}
pub fn content(&self) -> &Content {
&self.content
}
pub fn tag_alter_preservation(&self) -> bool {
self.tag_alter_preservation
}
pub fn set_tag_alter_preservation(&mut self, tag_alter_preservation: bool) {
self.tag_alter_preservation = tag_alter_preservation;
}
pub fn file_alter_preservation(&self) -> bool {
self.file_alter_preservation
}
pub fn set_file_alter_preservation(&mut self, file_alter_preservation: bool) {
self.file_alter_preservation = file_alter_preservation;
}
pub fn encoding(&self) -> Option<Encoding> {
self.encoding
}
pub fn name(&self) -> &str {
match self.id() {
"AENC" => "Audio encryption",
"APIC" => "Attached picture",
"ASPI" => "Audio seek point index",
"COMM" => "Comments",
"COMR" => "Commercial frame",
"ENCR" => "Encryption method registration",
"EQU2" => "Equalisation (2)",
"ETCO" => "Event timing codes",
"GEOB" => "General encapsulated object",
"GRID" => "Group identification registration",
"LINK" => "Linked information",
"MCDI" => "Music CD identifier",
"MLLT" => "MPEG location lookup table",
"OWNE" => "Ownership frame",
"PRIV" => "Private frame",
"PCNT" => "Play counter",
"POPM" => "Popularimeter",
"POSS" => "Position synchronisation frame",
"RBUF" => "Recommended buffer size",
"RVA2" => "Relative volume adjustment (2)",
"RVRB" => "Reverb",
"SEEK" => "Seek frame",
"SIGN" => "Signature frame",
"SYLT" => "Synchronised lyric/text",
"SYTC" => "Synchronised tempo codes",
"TALB" => "Album/Movie/Show title",
"TBPM" => "BPM (beats per minute)",
"TCOM" => "Composer",
"TCON" => "Content type",
"TCOP" => "Copyright message",
"TDEN" => "Encoding time",
"TDLY" => "Playlist delay",
"TDOR" => "Original release time",
"TDRC" => "Recording time",
"TDRL" => "Release time",
"TDTG" => "Tagging time",
"TENC" => "Encoded by",
"TEXT" => "Lyricist/Text writer",
"TFLT" => "File type",
"TIPL" => "Involved people list",
"TIT1" => "Content group description",
"TIT2" => "Title/songname/content description",
"TIT3" => "Subtitle/Description refinement",
"TKEY" => "Initial key",
"TLAN" => "Language(s)",
"TLEN" => "Length",
"TMCL" => "Musician credits list",
"TMED" => "Media type",
"TMOO" => "Mood",
"TOAL" => "Original album/movie/show title",
"TOFN" => "Original filename",
"TOLY" => "Original lyricist(s)/text writer(s)",
"TOPE" => "Original artist(s)/performer(s)",
"TOWN" => "File owner/licensee",
"TPE1" => "Lead performer(s)/Soloist(s)",
"TPE2" => "Band/orchestra/accompaniment",
"TPE3" => "Conductor/performer refinement",
"TPE4" => "Interpreted, remixed, or otherwise modified by",
"TPOS" => "Part of a set",
"TPRO" => "Produced notice",
"TPUB" => "Publisher",
"TRCK" => "Track number/Position in set",
"TRSN" => "Internet radio station name",
"TRSO" => "Internet radio station owner",
"TSOA" => "Album sort order",
"TSOP" => "Performer sort order",
"TSOT" => "Title sort order",
"TSRC" => "ISRC (international standard recording code)",
"TSSE" => "Software/Hardware and settings used for encoding",
"TSST" => "Set subtitle",
"TXXX" => "User defined text information frame",
"UFID" => "Unique file identifier",
"USER" => "Terms of use",
"USLT" => "Unsynchronised lyric/text transcription",
"WCOM" => "Commercial information",
"WCOP" => "Copyright/Legal information",
"WOAF" => "Official audio file webpage",
"WOAR" => "Official artist/performer webpage",
"WOAS" => "Official audio source webpage",
"WORS" => "Official Internet radio station homepage",
"WPAY" => "Payment",
"WPUB" => "Publishers official webpage",
"WXXX" => "User defined URL link frame",
"EQUA" => "Equalization",
"IPLS" => "Involved people list",
"RVAD" => "Relative volume adjustment",
"TDAT" => "Date",
"TIME" => "Time",
"TORY" => "Original release year",
"TRDA" => "Recording dates",
"TSIZ" => "Size",
"TYER" => "Year",
"BUF" => "Recommended buffer size",
"CNT" => "Play counter",
"COM" => "Comments",
"CRA" => "Audio encryption",
"CRM" => "Encrypted meta frame",
"ETC" => "Event timing codes",
"EQU" => "Equalization",
"GEO" => "General encapsulated object",
"IPL" => "Involved people list",
"LNK" => "Linked information",
"MCI" => "Music CD Identifier",
"MLL" => "MPEG location lookup table",
"PIC" => "Attached picture",
"POP" => "Popularimeter",
"REV" => "Reverb",
"RVA" => "Relative volume adjustment",
"SLT" => "Synchronized lyric/text",
"STC" => "Synced tempo codes",
"TAL" => "Album/Movie/Show title",
"TBP" => "BPM (Beats Per Minute)",
"TCM" => "Composer",
"TCO" => "Content type",
"TCR" => "Copyright message",
"TDA" => "Date",
"TDY" => "Playlist delay",
"TEN" => "Encoded by",
"TFT" => "File type",
"TIM" => "Time",
"TKE" => "Initial key",
"TLA" => "Language(s)",
"TLE" => "Length",
"TMT" => "Media type",
"TOA" => "Original artist(s)/performer(s)",
"TOF" => "Original filename",
"TOL" => "Original Lyricist(s)/text writer(s)",
"TOR" => "Original release year",
"TOT" => "Original album/Movie/Show title",
"TP1" => "Lead artist(s)/Lead performer(s)/Soloist(s)/Performing group",
"TP2" => "Band/Orchestra/Accompaniment",
"TP3" => "Conductor/Performer refinement",
"TP4" => "Interpreted, remixed, or otherwise modified by",
"TPA" => "Part of a set",
"TPB" => "Publisher",
"TRC" => "ISRC (International Standard Recording Code)",
"TRD" => "Recording dates",
"TRK" => "Track number/Position in set",
"TSI" => "Size",
"TSS" => "Software/hardware and settings used for encoding",
"TT1" => "Content group description",
"TT2" => "Title/Songname/Content description",
"TT3" => "Subtitle/Description refinement",
"TXT" => "Lyricist/text writer",
"TXX" => "User defined text information frame",
"TYE" => "Year",
"UFI" => "Unique file identifier",
"ULT" => "Unsychronized lyric/text transcription",
"WAF" => "Official audio file webpage",
"WAR" => "Official artist/performer webpage",
"WAS" => "Official audio source webpage",
"WCM" => "Commercial information",
"WCP" => "Copyright/Legal information",
"WPB" => "Publishers official webpage",
"WXX" => "User defined URL link frame",
v => v,
}
}
}
impl PartialEq for Frame {
fn eq(&self, other: &Self) -> bool {
self.id == other.id
&& self.content == other.content
&& self.tag_alter_preservation == other.tag_alter_preservation
&& self.file_alter_preservation == other.file_alter_preservation
&& (self.encoding.is_none()
|| other.encoding.is_none()
|| self.encoding == other.encoding)
}
}
impl fmt::Display for Frame {
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
write!(f, "{} = {}", self.name(), self.content)
}
}
macro_rules! convert_2_to_3_and_back {
( $( $id2:expr, $id3:expr ),* ) => {
fn convert_id_2_to_3(id: impl AsRef<str>) -> Option<&'static str> {
match id.as_ref() {
$($id2 => Some($id3),)*
_ => None,
}
}
fn convert_id_3_to_2(id: impl AsRef<str>) -> Option<&'static str> {
match id.as_ref() {
$($id3 => Some($id2),)*
_ => None,
}
}
}
}
#[rustfmt::skip]
convert_2_to_3_and_back!(
"BUF", "RBUF",
"CNT", "PCNT",
"COM", "COMM",
"CRA", "AENC",
"ETC", "ETCO",
"EQU", "EQUA",
"GEO", "GEOB",
"IPL", "IPLS",
"LNK", "LINK",
"MCI", "MCDI",
"MLL", "MLLT",
"PIC", "APIC",
"POP", "POPM",
"REV", "RVRB",
"RVA", "RVA2",
"SLT", "SYLT",
"STC", "SYTC",
"TAL", "TALB",
"TBP", "TBPM",
"TCM", "TCOM",
"TCO", "TCON",
"TCR", "TCOP",
"TDA", "TDAT",
"TDY", "TDLY",
"TEN", "TENC",
"TFT", "TFLT",
"TIM", "TIME",
"TKE", "TKEY",
"TLA", "TLAN",
"TLE", "TLEN",
"TMT", "TMED",
"TOA", "TOPE",
"TOF", "TOFN",
"TOL", "TOLY",
"TOT", "TOAL",
"TOR", "TORY",
"TP1", "TPE1",
"TP2", "TPE2",
"TP3", "TPE3",
"TP4", "TPE4",
"TPA", "TPOS",
"TPB", "TPUB",
"TRC", "TSRC",
"TRD", "TRDA",
"TRK", "TRCK",
"TSI", "TSIZ",
"TSS", "TSSE",
"TT1", "TIT1",
"TT2", "TIT2",
"TT3", "TIT3",
"TXT", "TEXT",
"TXX", "TXXX",
"TYE", "TYER",
"UFI", "UFID",
"ULT", "USLT",
"WAF", "WOAF",
"WAR", "WOAR",
"WAS", "WOAS",
"WCM", "WCOM",
"WCP", "WCOP",
"WPB", "WPUB",
"WXX", "WXXX"
);
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_display() {
let title_frame = Frame::with_content("TIT2", Content::Text("title".to_owned()));
assert_eq!(
format!("{}", title_frame),
"Title/songname/content description = title"
);
let txxx_frame = Frame::with_content(
"TXXX",
Content::ExtendedText(ExtendedText {
description: "description".to_owned(),
value: "value".to_owned(),
}),
);
assert_eq!(
format!("{}", txxx_frame),
"User defined text information frame = description: value"
);
}
#[test]
fn test_frame_cmp_text() {
let frame_a = Frame::with_content("TIT2", Content::Text("A".to_owned()));
let frame_b = Frame::with_content("TIT2", Content::Text("B".to_owned()));
assert!(
frame_a.compare(&frame_b),
"frames should be counted as equal"
);
}
#[test]
fn test_frame_cmp_wcom() {
let frame_a = Frame::with_content("WCOM", Content::Link("A".to_owned()));
let frame_b = Frame::with_content("WCOM", Content::Link("B".to_owned()));
assert!(
!frame_a.compare(&frame_b),
"frames should not be counted as equal"
);
}
#[test]
fn test_frame_cmp_priv() {
let frame_a = Frame::with_content(
"PRIV",
Content::Unknown(Unknown {
data: vec![1, 2, 3],
version: Version::Id3v24,
}),
);
let frame_b = Frame::with_content(
"PRIV",
Content::Unknown(Unknown {
data: vec![1, 2, 3],
version: Version::Id3v24,
}),
);
assert!(
!frame_a.compare(&frame_b),
"frames should not be counted as equal"
);
}
#[test]
fn test_frame_cmp_popularimeter() {
let frame_a = Frame::with_content(
"POPM",
Content::Popularimeter(Popularimeter {
user: "A".to_owned(),
rating: 1,
counter: 1,
}),
);
let frame_b = Frame::with_content(
"POPM",
Content::Popularimeter(Popularimeter {
user: "A".to_owned(),
rating: 1,
counter: 1,
}),
);
let frame_c = Frame::with_content(
"POPM",
Content::Popularimeter(Popularimeter {
user: "C".to_owned(),
rating: 1,
counter: 1,
}),
);
assert!(
frame_a.compare(&frame_b),
"frames should be counted as equal"
);
assert!(
!frame_a.compare(&frame_c),
"frames should not be counted as equal"
);
}
}