Resolve feature issues

This commit is contained in:
Serial 2021-12-21 17:28:18 -05:00
parent a23e285c8f
commit e5d2c4dc1f
39 changed files with 1554 additions and 1070 deletions

View file

@ -15,6 +15,7 @@ flate2 = { version = "1.0.21", optional = true }
# Ogg
ogg_pager = "0.1.7"
lazy_static = "1.4.0"
paste = "1.0.5"
base64 = "0.13.0"
byteorder = "1.4.3"

View file

@ -36,10 +36,8 @@ pub enum LoftyError {
#[cfg(feature = "id3v2")]
/// Errors that arise while decoding ID3v2 text
TextDecode(&'static str),
#[cfg(feature = "id3v2")]
/// Errors that arise while reading/writing ID3v2 tags
Id3v2(&'static str),
#[cfg(feature = "id3v2")]
/// Arises when an invalid ID3v2 version is found
BadId3v2Version(u8, u8),
#[cfg(feature = "id3v2")]
@ -51,7 +49,6 @@ pub enum LoftyError {
#[cfg(feature = "id3v2")]
/// Arises when invalid data is encountered while reading an ID3v2 synchronized text frame
BadSyncText,
#[cfg(feature = "mp4_ilst")]
/// Arises when an atom contains invalid data
BadAtom(&'static str),
@ -116,9 +113,7 @@ impl Display for LoftyError {
},
#[cfg(feature = "id3v2")]
LoftyError::TextDecode(message) => write!(f, "Text decoding: {}", message),
#[cfg(feature = "id3v2")]
LoftyError::Id3v2(message) => write!(f, "ID3v2: {}", message),
#[cfg(feature = "id3v2")]
LoftyError::BadId3v2Version(major, minor) => write!(
f,
"ID3v2: Found an invalid version (v{}.{}), expected any major revision in: (2, 3, \
@ -134,7 +129,6 @@ impl Display for LoftyError {
),
#[cfg(feature = "id3v2")]
LoftyError::BadSyncText => write!(f, "ID3v2: Encountered invalid data in SYLT frame"),
#[cfg(feature = "mp4_ilst")]
LoftyError::BadAtom(message) => write!(f, "MP4 Atom: {}", message),
// Files

View file

@ -191,6 +191,7 @@ pub mod id3 {
//! * [Frame]
pub use {
crate::logic::id3::v2::flags::Id3v2TagFlags,
crate::logic::id3::v2::frame::{
EncodedTextFrame, Frame, FrameFlags, FrameID, FrameValue, LanguageFrame,
},
@ -200,7 +201,7 @@ pub mod id3 {
crate::logic::id3::v2::items::sync_text::{
SyncTextContentType, SyncTextInformation, SynchronizedText, TimestampFormat,
},
crate::logic::id3::v2::tag::{Id3v2Tag, Id3v2TagFlags},
crate::logic::id3::v2::tag::Id3v2Tag,
crate::logic::id3::v2::util::text_utils::TextEncoding,
crate::logic::id3::v2::util::upgrade::{upgrade_v2, upgrade_v3},
crate::logic::id3::v2::Id3v2Version,
@ -241,12 +242,12 @@ pub mod ape {
//! It is possible for an `APE` file to contain an `ID3v2` tag. For the sake of data preservation,
//! this tag will be read, but **cannot** be written. The only tags allowed by spec are `APEv1/2` and
//! `ID3v1`.
#[cfg(feature = "ape")]
pub use crate::logic::ape::tag::item::ApeItem;
#[cfg(feature = "ape")]
pub use crate::logic::ape::tag::ApeTag;
pub use crate::logic::ape::{ApeFile, ApeProperties};
pub use crate::types::picture::APE_PICTURE_TYPES;
#[cfg(feature = "ape")]
pub use crate::{
logic::ape::tag::{ape_tag::ApeTag, item::ApeItem},
types::picture::APE_PICTURE_TYPES,
};
}
pub mod mp3 {

View file

@ -1,18 +1,20 @@
mod constants;
mod properties;
pub(crate) mod read;
#[cfg(feature = "ape")]
pub(crate) mod tag;
pub(crate) mod write;
use crate::error::Result;
#[cfg(feature = "id3v1")]
use crate::logic::id3::v1::tag::Id3v1Tag;
#[cfg(feature = "id3v2")]
use crate::logic::id3::v2::tag::Id3v2Tag;
use crate::logic::tag_methods;
use crate::types::file::{AudioFile, FileType, TaggedFile};
use crate::{FileProperties, Result, TagType};
use tag::ApeTag;
use crate::types::properties::FileProperties;
use crate::types::tag::{Tag, TagType};
#[cfg(feature = "ape")]
use tag::ape_tag::ApeTag;
use std::io::{Read, Seek};
use std::time::Duration;
@ -107,18 +109,21 @@ pub struct ApeFile {
}
impl From<ApeFile> for TaggedFile {
#[allow(clippy::vec_init_then_push)]
fn from(input: ApeFile) -> Self {
let mut tags = Vec::<Option<Tag>>::with_capacity(3);
#[cfg(feature = "ape")]
tags.push(input.ape_tag.map(Into::into));
#[cfg(feature = "id3v1")]
tags.push(input.id3v1_tag.map(Into::into));
#[cfg(feature = "id3v2")]
tags.push(input.id3v2_tag.map(Into::into));
Self {
ty: FileType::APE,
properties: FileProperties::from(input.properties),
tags: vec![
input.ape_tag.map(Into::into),
input.id3v1_tag.map(Into::into),
input.id3v2_tag.map(Into::into),
]
.into_iter()
.flatten()
.collect(),
tags: tags.into_iter().flatten().collect(),
}
}
}
@ -138,23 +143,16 @@ impl AudioFile for ApeFile {
&self.properties
}
#[allow(clippy::match_same_arms)]
#[allow(unreachable_code)]
fn contains_tag(&self) -> bool {
match self {
#[cfg(feature = "ape")]
ApeFile {
ape_tag: Some(_), ..
} => true,
#[cfg(feature = "id3v1")]
ApeFile {
id3v1_tag: Some(_), ..
} => true,
#[cfg(feature = "id3v2")]
ApeFile {
id3v2_tag: Some(_), ..
} => true,
_ => false,
}
#[cfg(feature = "ape")]
return self.ape_tag.is_some();
#[cfg(feature = "id3v1")]
return self.id3v1_tag.is_some();
#[cfg(feature = "id3v2")]
return self.id3v2_tag.is_some();
false
}
fn contains_tag_type(&self, tag_type: &TagType) -> bool {
@ -170,6 +168,13 @@ impl AudioFile for ApeFile {
}
}
tag_methods! {
ApeFile => ID3v2, id3v2_tag, Id3v2Tag; ID3v1, id3v1_tag, Id3v1Tag; APE, ape_tag, ApeTag
impl ApeFile {
tag_methods! {
#[cfg(feature = "id3v2")];
ID3v2, id3v2_tag, Id3v2Tag;
#[cfg(feature = "id3v1")];
ID3v1, id3v1_tag, Id3v1Tag;
#[cfg(feature = "ape")];
APE, ape_tag, ApeTag
}
}

View file

@ -1,19 +1,18 @@
use super::constants::APE_PREAMBLE;
use super::properties::{properties_gt_3980, properties_lt_3980};
use super::tag::read::read_ape_tag;
#[cfg(feature = "ape")]
use super::tag::{ape_tag::ApeTag, read::read_ape_tag};
use super::{ApeFile, ApeProperties};
use crate::error::{LoftyError, Result};
use crate::logic::ape::tag::read_ape_header;
#[cfg(feature = "id3v1")]
use crate::logic::id3::v1::tag::Id3v1Tag;
#[cfg(any(feature = "id3v2", feature = "id3v1"))]
use crate::logic::id3::{find_id3v1, find_lyrics3v2};
#[cfg(feature = "id3v2")]
use {crate::logic::id3::v2::find_id3v2, crate::logic::id3::v2::read::parse_id3v2};
use crate::logic::id3::v2::{read::parse_id3v2, tag::Id3v2Tag};
use crate::logic::id3::{find_id3v1, find_id3v2, find_lyrics3v2};
use std::io::{Read, Seek, SeekFrom};
use crate::id3::v2::Id3v2Tag;
use crate::logic::ape::tag::ApeTag;
use byteorder::{LittleEndian, ReadBytesExt};
fn read_properties<R>(data: &mut R, stream_len: u64, file_length: u64) -> Result<ApeProperties>
@ -43,22 +42,29 @@ where
let mut stream_len = end - start;
#[cfg(feature = "id3v2")]
let mut id3v2_tag: Option<Id3v2Tag> = None;
#[cfg(feature = "id3v1")]
let mut id3v1_tag: Option<Id3v1Tag> = None;
#[cfg(feature = "ape")]
let mut ape_tag: Option<ApeTag> = None;
// ID3v2 tags are unsupported in APE files, but still possible
if let Some(id3v2_read) = find_id3v2(data, true)? {
stream_len -= id3v2_read.len() as u64;
if let (Some(header), Some(content)) = find_id3v2(data, true)? {
stream_len -= u64::from(header.size);
let id3v2 = parse_id3v2(&mut &*id3v2_read)?;
// Skip over the footer
if id3v2.flags().footer {
data.seek(SeekFrom::Current(10))?;
// Exclude the footer
if header.flags.footer {
stream_len -= 10;
}
id3v2_tag = Some(id3v2)
#[cfg(feature = "id3v2")]
{
let reader = &mut &*content;
let id3v2 = parse_id3v2(reader, header)?;
id3v2_tag = Some(id3v2)
}
}
let mut found_mac = false;
@ -89,10 +95,17 @@ where
return Err(LoftyError::Ape("Found incomplete APE tag"));
}
let (ape, size) = read_ape_tag(data, false)?;
stream_len -= u64::from(size);
let ape_header = read_ape_header(data, false)?;
stream_len -= u64::from(ape_header.size);
ape_tag = Some(ape)
#[cfg(feature = "ape")]
{
let ape = read_ape_tag(data, ape_header)?;
ape_tag = Some(ape)
}
#[cfg(not(feature = "ape"))]
data.seek(SeekFrom::Current(ape_header.size as i64))?;
},
_ => {
return Err(LoftyError::Ape(
@ -111,7 +124,10 @@ where
if found_id3v1 {
stream_len -= 128;
id3v1_tag = id3v1;
#[cfg(feature = "id3v1")]
{
id3v1_tag = id3v1;
}
}
// Next, check for a Lyrics3v2 tag, and skip over it, as it's no use to us
@ -132,10 +148,17 @@ where
data.read_exact(&mut ape_preamble)?;
if &ape_preamble == APE_PREAMBLE {
let (ape, size) = read_ape_tag(data, true)?;
let ape_header = read_ape_header(data, true)?;
stream_len -= u64::from(ape_header.size);
stream_len -= u64::from(size);
ape_tag = Some(ape)
#[cfg(feature = "ape")]
{
let ape = read_ape_tag(data, ape_header)?;
ape_tag = Some(ape)
}
#[cfg(not(feature = "ape"))]
data.seek(SeekFrom::Current(ape_header.size as i64))?;
}
let file_length = data.seek(SeekFrom::Current(0))?;

View file

@ -0,0 +1,351 @@
use crate::error::Result;
use crate::logic::ape::tag::item::{ApeItem, ApeItemRef};
use crate::types::item::{ItemKey, ItemValue, TagItem};
use crate::types::tag::{Accessor, Tag, TagType};
use std::convert::TryInto;
use std::fs::File;
macro_rules! impl_accessor {
($($name:ident, $($key:literal)|+;)+) => {
paste::paste! {
impl Accessor for ApeTag {
$(
fn $name(&self) -> Option<&str> {
$(
if let Some(i) = self.get_key($key) {
if let ItemValue::Text(val) = i.value() {
return Some(val)
}
}
)+
None
}
fn [<set_ $name>](&mut self, value: String) {
self.insert(ApeItem {
read_only: false,
key: String::from(crate::types::item::first_key!($($key)|*)),
value: ItemValue::Text(value)
})
}
fn [<remove_ $name>](&mut self) {
$(
self.remove_key($key);
)+
}
)+
}
}
}
}
#[derive(Default, Debug, PartialEq, Clone)]
/// An `APE` tag
///
/// ## Supported file types
///
/// * [`FileType::APE`](crate::FileType::APE)
/// * [`FileType::MP3`](crate::FileType::MP3)
///
/// ## Item storage
///
/// `APE` isn't a very strict format. An [`ApeItem`] only restricted by its name, meaning it can use
/// a normal [`ItemValue`](crate::ItemValue) unlike other formats.
///
/// Pictures are stored as [`ItemValue::Binary`](crate::ItemValue::Binary), and can be converted with
/// [`Picture::from_ape_bytes`](crate::Picture::from_ape_bytes). For the appropriate item keys, see
/// [APE_PICTURE_TYPES](crate::ape::APE_PICTURE_TYPES).
///
/// ## Conversions
///
/// ### From `Tag`
///
/// When converting pictures, any of type [`PictureType::Undefined`](crate::PictureType::Undefined) will be discarded.
/// For items, see [ApeItem::new].
pub struct ApeTag {
/// Whether or not to mark the tag as read only
pub read_only: bool,
pub(super) items: Vec<ApeItem>,
}
impl_accessor!(
artist, "Artist";
title, "Title";
album, "Album";
album_artist, "Album Artist" | "ALBUMARTST";
genre, "GENRE";
);
impl ApeTag {
/// Get an [`ApeItem`] by key
///
/// NOTE: While `APE` items are supposed to be case-sensitive,
/// this rule is rarely followed, so this will ignore case when searching.
pub fn get_key(&self, key: &str) -> Option<&ApeItem> {
self.items
.iter()
.find(|i| i.key().eq_ignore_ascii_case(key))
}
/// Insert an [`ApeItem`]
///
/// This will remove any item with the same key prior to insertion
pub fn insert(&mut self, value: ApeItem) {
self.remove_key(value.key());
self.items.push(value);
}
/// Remove an [`ApeItem`] by key
///
/// NOTE: Like [`ApeTag::get_key`], this is not case-sensitive
pub fn remove_key(&mut self, key: &str) {
self.items
.iter()
.position(|i| i.key().eq_ignore_ascii_case(key))
.map(|p| self.items.remove(p));
}
/// Returns all of the tag's items
pub fn items(&self) -> &[ApeItem] {
&self.items
}
}
impl ApeTag {
/// Write an `APE` tag to a file
///
/// # Errors
///
/// * Attempting to write the tag to a format that does not support it
/// * An existing tag has an invalid size
pub fn write_to(&self, file: &mut File) -> Result<()> {
Into::<ApeTagRef>::into(self).write_to(file)
}
}
impl From<ApeTag> for Tag {
fn from(input: ApeTag) -> Self {
fn split_pair(
content: &str,
tag: &mut Tag,
current_key: ItemKey,
total_key: ItemKey,
) -> Option<()> {
let mut split = content.splitn(2, '/');
let current = split.next()?.to_string();
tag.insert_item_unchecked(TagItem::new(current_key, ItemValue::Text(current)));
if let Some(total) = split.next() {
tag.insert_item_unchecked(TagItem::new(
total_key,
ItemValue::Text(total.to_string()),
))
}
Some(())
}
let mut tag = Tag::new(TagType::Ape);
for item in input.items {
let item_key = ItemKey::from_key(TagType::Ape, item.key());
// The text pairs need some special treatment
match (item_key, item.value()) {
(ItemKey::TrackNumber | ItemKey::TrackTotal, ItemValue::Text(val))
if split_pair(val, &mut tag, ItemKey::TrackNumber, ItemKey::TrackTotal)
.is_some() =>
{
continue
},
(ItemKey::DiscNumber | ItemKey::DiscTotal, ItemValue::Text(val))
if split_pair(val, &mut tag, ItemKey::DiscNumber, ItemKey::DiscTotal)
.is_some() =>
{
continue
},
(k, _) => tag.insert_item_unchecked(TagItem::new(k, item.value)),
}
}
tag
}
}
impl From<Tag> for ApeTag {
fn from(input: Tag) -> Self {
let mut ape_tag = Self::default();
for item in input.items {
if let Ok(ape_item) = item.try_into() {
ape_tag.insert(ape_item)
}
}
for pic in input.pictures {
if let Some(key) = pic.pic_type.as_ape_key() {
if let Ok(item) =
ApeItem::new(key.to_string(), ItemValue::Binary(pic.as_ape_bytes()))
{
ape_tag.insert(item)
}
}
}
ape_tag
}
}
pub(in crate::logic) struct ApeTagRef<'a> {
pub(crate) read_only: bool,
pub(super) items: Box<dyn Iterator<Item = ApeItemRef<'a>> + 'a>,
}
impl<'a> ApeTagRef<'a> {
pub(crate) fn write_to(&mut self, file: &mut File) -> Result<()> {
super::write::write_to(file, self)
}
}
impl<'a> Into<ApeTagRef<'a>> for &'a Tag {
fn into(self) -> ApeTagRef<'a> {
ApeTagRef {
read_only: false,
items: Box::new(self.items.iter().filter_map(|i| {
i.key().map_key(TagType::Ape, true).map(|key| ApeItemRef {
read_only: false,
key,
value: (&i.item_value).into(),
})
})),
}
}
}
impl<'a> Into<ApeTagRef<'a>> for &'a ApeTag {
fn into(self) -> ApeTagRef<'a> {
ApeTagRef {
read_only: self.read_only,
items: Box::new(self.items.iter().map(Into::into)),
}
}
}
#[cfg(test)]
mod tests {
use crate::ape::{ApeItem, ApeTag};
use crate::{ItemValue, Tag, TagType};
use crate::logic::ape::tag::read_ape_header;
use std::io::{Cursor, Read};
#[test]
#[allow(clippy::similar_names)]
fn parse_ape() {
let mut expected_tag = ApeTag::default();
let title_item = ApeItem::new(
String::from("TITLE"),
ItemValue::Text(String::from("Foo title")),
)
.unwrap();
let artist_item = ApeItem::new(
String::from("ARTIST"),
ItemValue::Text(String::from("Bar artist")),
)
.unwrap();
let album_item = ApeItem::new(
String::from("ALBUM"),
ItemValue::Text(String::from("Baz album")),
)
.unwrap();
let comment_item = ApeItem::new(
String::from("COMMENT"),
ItemValue::Text(String::from("Qux comment")),
)
.unwrap();
let year_item =
ApeItem::new(String::from("YEAR"), ItemValue::Text(String::from("1984"))).unwrap();
let track_number_item =
ApeItem::new(String::from("TRACK"), ItemValue::Text(String::from("1"))).unwrap();
let genre_item = ApeItem::new(
String::from("GENRE"),
ItemValue::Text(String::from("Classical")),
)
.unwrap();
expected_tag.insert(title_item);
expected_tag.insert(artist_item);
expected_tag.insert(album_item);
expected_tag.insert(comment_item);
expected_tag.insert(year_item);
expected_tag.insert(track_number_item);
expected_tag.insert(genre_item);
let mut tag = Vec::new();
std::fs::File::open("tests/tags/assets/test.apev2")
.unwrap()
.read_to_end(&mut tag)
.unwrap();
let mut reader = Cursor::new(tag);
let header = read_ape_header(&mut reader, false).unwrap();
let parsed_tag = crate::logic::ape::tag::read::read_ape_tag(&mut reader, header).unwrap();
assert_eq!(expected_tag.items().len(), parsed_tag.items().len());
for item in expected_tag.items() {
assert!(parsed_tag.items().contains(item))
}
}
#[test]
#[allow(clippy::similar_names)]
fn ape_to_tag() {
let mut tag_bytes = Vec::new();
std::fs::File::open("tests/tags/assets/test.apev2")
.unwrap()
.read_to_end(&mut tag_bytes)
.unwrap();
let mut reader = Cursor::new(tag_bytes);
let header = read_ape_header(&mut reader, false).unwrap();
let ape = crate::logic::ape::tag::read::read_ape_tag(&mut reader, header).unwrap();
let tag: Tag = ape.into();
crate::logic::test_utils::verify_tag(&tag, true, true);
}
#[test]
fn tag_to_ape() {
fn verify_key(tag: &ApeTag, key: &str, expected_val: &str) {
assert_eq!(
tag.get_key(key).map(ApeItem::value),
Some(&ItemValue::Text(String::from(expected_val)))
);
}
let tag = crate::logic::test_utils::create_tag(TagType::Ape);
let ape_tag: ApeTag = tag.into();
verify_key(&ape_tag, "Title", "Foo title");
verify_key(&ape_tag, "Artist", "Bar artist");
verify_key(&ape_tag, "Album", "Baz album");
verify_key(&ape_tag, "Comment", "Qux comment");
verify_key(&ape_tag, "Track", "1");
verify_key(&ape_tag, "Genre", "Classical");
}
}

View file

@ -1,313 +1,55 @@
#[cfg(feature = "ape")]
pub(crate) mod ape_tag;
#[cfg(feature = "ape")]
pub(crate) mod item;
#[cfg(feature = "ape")]
pub(in crate::logic) mod read;
#[cfg(feature = "ape")]
pub(in crate::logic) mod write;
use crate::error::Result;
use crate::logic::ape::tag::item::{ApeItem, ApeItemRef};
use crate::types::item::{ItemKey, ItemValue, TagItem};
use crate::types::tag::{Accessor, Tag, TagType};
use crate::error::{LoftyError, Result};
use std::convert::TryInto;
use std::fs::File;
use std::io::{Read, Seek, SeekFrom};
use std::ops::Neg;
macro_rules! impl_accessor {
($($name:ident, $($key:literal)|+;)+) => {
paste::paste! {
impl Accessor for ApeTag {
$(
fn $name(&self) -> Option<&str> {
$(
if let Some(i) = self.get_key($key) {
if let ItemValue::Text(val) = i.value() {
return Some(val)
}
}
)+
use byteorder::{LittleEndian, ReadBytesExt};
None
}
fn [<set_ $name>](&mut self, value: String) {
self.insert(ApeItem {
read_only: false,
key: String::from(crate::types::item::first_key!($($key)|*)),
value: ItemValue::Text(value)
})
}
fn [<remove_ $name>](&mut self) {
$(
self.remove_key($key);
)+
}
)+
}
}
}
#[derive(Copy, Clone)]
pub(crate) struct ApeHeader {
pub(crate) size: u32,
pub(crate) item_count: u32,
}
#[derive(Default, Debug, PartialEq, Clone)]
/// An `APE` tag
///
/// ## Supported file types
///
/// * [`FileType::APE`](crate::FileType::APE)
/// * [`FileType::MP3`](crate::FileType::MP3)
///
/// ## Item storage
///
/// `APE` isn't a very strict format. An [`ApeItem`] only restricted by its name, meaning it can use
/// a normal [`ItemValue`](crate::ItemValue) unlike other formats.
///
/// Pictures are stored as [`ItemValue::Binary`](crate::ItemValue::Binary), and can be converted with
/// [`Picture::from_ape_bytes`](crate::Picture::from_ape_bytes). For the appropriate item keys, see
/// [APE_PICTURE_TYPES](crate::ape::APE_PICTURE_TYPES).
///
/// ## Conversions
///
/// ### From `Tag`
///
/// When converting pictures, any of type [`PictureType::Undefined`](crate::PictureType::Undefined) will be discarded.
/// For items, see [ApeItem::new].
pub struct ApeTag {
/// Whether or not to mark the tag as read only
pub read_only: bool,
pub(super) items: Vec<ApeItem>,
}
impl_accessor!(
artist, "Artist";
title, "Title";
album, "Album";
album_artist, "Album Artist" | "ALBUMARTST";
genre, "GENRE";
);
impl ApeTag {
/// Get an [`ApeItem`] by key
///
/// NOTE: While `APE` items are supposed to be case-sensitive,
/// this rule is rarely followed, so this will ignore case when searching.
pub fn get_key(&self, key: &str) -> Option<&ApeItem> {
self.items
.iter()
.find(|i| i.key().eq_ignore_ascii_case(key))
}
/// Insert an [`ApeItem`]
///
/// This will remove any item with the same key prior to insertion
pub fn insert(&mut self, value: ApeItem) {
self.remove_key(value.key());
self.items.push(value);
}
/// Remove an [`ApeItem`] by key
///
/// NOTE: Like [`ApeTag::get_key`], this is not case-sensitive
pub fn remove_key(&mut self, key: &str) {
self.items
.iter()
.position(|i| i.key().eq_ignore_ascii_case(key))
.map(|p| self.items.remove(p));
}
/// Returns all of the tag's items
pub fn items(&self) -> &[ApeItem] {
&self.items
}
}
impl ApeTag {
/// Write an `APE` tag to a file
///
/// # Errors
///
/// * Attempting to write the tag to a format that does not support it
/// * An existing tag has an invalid size
pub fn write_to(&self, file: &mut File) -> Result<()> {
Into::<ApeTagRef>::into(self).write_to(file)
}
}
impl From<ApeTag> for Tag {
fn from(input: ApeTag) -> Self {
let mut tag = Tag::new(TagType::Ape);
for item in input.items {
let item = TagItem::new(ItemKey::from_key(TagType::Ape, &*item.key), item.value);
tag.insert_item_unchecked(item)
}
tag
}
}
impl From<Tag> for ApeTag {
fn from(input: Tag) -> Self {
let mut ape_tag = Self::default();
for item in input.items {
if let Ok(ape_item) = item.try_into() {
ape_tag.insert(ape_item)
}
}
for pic in input.pictures {
if let Some(key) = pic.pic_type.as_ape_key() {
if let Ok(item) =
ApeItem::new(key.to_string(), ItemValue::Binary(pic.as_ape_bytes()))
{
ape_tag.insert(item)
}
}
}
ape_tag
}
}
pub(in crate::logic) struct ApeTagRef<'a> {
read_only: bool,
pub(super) items: Box<dyn Iterator<Item = ApeItemRef<'a>> + 'a>,
}
impl<'a> ApeTagRef<'a> {
pub(crate) fn write_to(&mut self, file: &mut File) -> Result<()> {
write::write_to(file, self)
}
}
impl<'a> Into<ApeTagRef<'a>> for &'a Tag {
fn into(self) -> ApeTagRef<'a> {
ApeTagRef {
read_only: false,
items: Box::new(self.items.iter().filter_map(|i| {
i.key().map_key(TagType::Ape, true).map(|key| ApeItemRef {
read_only: false,
key,
value: (&i.item_value).into(),
})
})),
}
}
}
impl<'a> Into<ApeTagRef<'a>> for &'a ApeTag {
fn into(self) -> ApeTagRef<'a> {
ApeTagRef {
read_only: self.read_only,
items: Box::new(self.items.iter().map(Into::into)),
}
}
}
#[cfg(test)]
mod tests {
use crate::ape::{ApeItem, ApeTag};
use crate::{ItemValue, Tag, TagType};
use std::io::{Cursor, Read};
#[test]
fn parse_ape() {
let mut expected_tag = ApeTag::default();
let title_item = ApeItem::new(
String::from("TITLE"),
ItemValue::Text(String::from("Foo title")),
)
.unwrap();
let artist_item = ApeItem::new(
String::from("ARTIST"),
ItemValue::Text(String::from("Bar artist")),
)
.unwrap();
let album_item = ApeItem::new(
String::from("ALBUM"),
ItemValue::Text(String::from("Baz album")),
)
.unwrap();
let comment_item = ApeItem::new(
String::from("COMMENT"),
ItemValue::Text(String::from("Qux comment")),
)
.unwrap();
let year_item =
ApeItem::new(String::from("YEAR"), ItemValue::Text(String::from("1984"))).unwrap();
let track_number_item =
ApeItem::new(String::from("TRACK"), ItemValue::Text(String::from("1"))).unwrap();
let genre_item = ApeItem::new(
String::from("GENRE"),
ItemValue::Text(String::from("Classical")),
)
.unwrap();
expected_tag.insert(title_item);
expected_tag.insert(artist_item);
expected_tag.insert(album_item);
expected_tag.insert(comment_item);
expected_tag.insert(year_item);
expected_tag.insert(track_number_item);
expected_tag.insert(genre_item);
let mut tag = Vec::new();
std::fs::File::open("tests/tags/assets/test.apev2")
.unwrap()
.read_to_end(&mut tag)
.unwrap();
let mut reader = Cursor::new(tag);
let parsed_tag = super::read::read_ape_tag(&mut reader, false).unwrap().0;
assert_eq!(expected_tag.items().len(), parsed_tag.items().len());
for item in expected_tag.items() {
assert!(parsed_tag.items().contains(item))
}
}
#[test]
fn ape_to_tag() {
let mut tag_bytes = Vec::new();
std::fs::File::open("tests/tags/assets/test.apev2")
.unwrap()
.read_to_end(&mut tag_bytes)
.unwrap();
let mut reader = Cursor::new(tag_bytes);
let ape = super::read::read_ape_tag(&mut reader, false).unwrap().0;
let tag: Tag = ape.into();
crate::logic::test_utils::verify_tag(&tag, true, true);
}
#[test]
fn tag_to_ape() {
fn verify_key(tag: &ApeTag, key: &str, expected_val: &str) {
assert_eq!(
tag.get_key(key).map(ApeItem::value),
Some(&ItemValue::Text(String::from(expected_val)))
);
}
let tag = crate::logic::test_utils::create_tag(TagType::Ape);
let ape_tag: ApeTag = tag.into();
verify_key(&ape_tag, "Title", "Foo title");
verify_key(&ape_tag, "Artist", "Bar artist");
verify_key(&ape_tag, "Album", "Baz album");
verify_key(&ape_tag, "Comment", "Qux comment");
verify_key(&ape_tag, "Track", "1");
verify_key(&ape_tag, "Genre", "Classical");
}
pub(crate) fn read_ape_header<R>(data: &mut R, footer: bool) -> Result<ApeHeader>
where
R: Read + Seek,
{
let version = data.read_u32::<LittleEndian>()?;
let mut size = data.read_u32::<LittleEndian>()?;
if size < 32 {
// If the size is < 32, something went wrong during encoding
// The size includes the footer and all items
return Err(LoftyError::Ape("Tag has an invalid size (< 32)"));
}
let item_count = data.read_u32::<LittleEndian>()?;
if footer {
// No point in reading the rest of the footer, just seek back to the end of the header
data.seek(SeekFrom::Current(i64::from(size - 12).neg()))?;
} else {
// There are 12 bytes remaining in the header
// Flags (4)
// Reserved (8)
data.seek(SeekFrom::Current(12))?;
}
// Version 1 doesn't include a header
if version == 2000 {
size += 32
}
Ok(ApeHeader { size, item_count })
}

View file

@ -1,42 +1,19 @@
use super::{ApeItem, ApeTag};
use super::{ape_tag::ApeTag, item::ApeItem, ApeHeader};
use crate::error::{LoftyError, Result};
use crate::logic::ape::constants::INVALID_KEYS;
use crate::types::item::ItemValue;
use std::io::{Read, Seek, SeekFrom};
use std::ops::Neg;
use byteorder::{LittleEndian, ReadBytesExt};
pub(crate) fn read_ape_tag<R>(data: &mut R, footer: bool) -> Result<(ApeTag, u32)>
pub(crate) fn read_ape_tag<R>(data: &mut R, header: ApeHeader) -> Result<ApeTag>
where
R: Read + Seek,
{
let version = data.read_u32::<LittleEndian>()?;
let mut size = data.read_u32::<LittleEndian>()?;
if size < 32 {
// If the size is < 32, something went wrong during encoding
// The size includes the footer and all items
return Err(LoftyError::Ape("Tag has an invalid size (< 32)"));
}
let item_count = data.read_u32::<LittleEndian>()?;
if footer {
// No point in reading the rest of the footer, just seek back to the end of the header
data.seek(SeekFrom::Current(i64::from(size - 12).neg()))?;
} else {
// There are 12 bytes remaining in the header
// Flags (4)
// Reserved (8)
data.seek(SeekFrom::Current(12))?;
}
let mut tag = ApeTag::default();
for _ in 0..item_count {
for _ in 0..header.item_count {
let value_size = data.read_u32::<LittleEndian>()?;
if value_size == 0 {
@ -60,10 +37,6 @@ where
return Err(LoftyError::Ape("Tag item contains an illegal key"));
}
if key.chars().any(|c| !c.is_ascii()) {
return Err(LoftyError::Ape("Tag item contains a non ASCII key"));
}
let read_only = (flags & 1) == 1;
let item_type = (flags & 6) >> 1;
@ -91,13 +64,8 @@ where
tag.insert(item);
}
// Version 1 doesn't include a header
if version == 2000 {
size += 32
}
// Skip over footer
data.seek(SeekFrom::Current(32))?;
Ok((tag, size))
Ok(tag)
}

View file

@ -1,9 +1,8 @@
use super::read::read_ape_tag;
use crate::error::{LoftyError, Result};
use crate::logic::ape::constants::APE_PREAMBLE;
use crate::logic::ape::tag::ApeTagRef;
use crate::logic::id3::v2::find_id3v2;
use crate::logic::id3::{find_id3v1, find_lyrics3v2};
use crate::logic::ape::tag::ape_tag::ApeTagRef;
use crate::logic::id3::{find_id3v1, find_id3v2, find_lyrics3v2};
use crate::probe::Probe;
use crate::types::file::FileType;
use crate::types::item::ItemValueRef;
@ -11,6 +10,7 @@ use crate::types::item::ItemValueRef;
use std::fs::File;
use std::io::{Cursor, Read, Seek, SeekFrom, Write};
use crate::logic::ape::tag::read_ape_header;
use byteorder::{LittleEndian, WriteBytesExt};
#[allow(clippy::shadow_unrelated)]
@ -41,7 +41,11 @@ pub(in crate::logic) fn write_to(data: &mut File, tag: &mut ApeTagRef) -> Result
let start = data.seek(SeekFrom::Current(-8))?;
data.seek(SeekFrom::Current(8))?;
let (mut existing, size) = read_ape_tag(data, false)?;
let header = read_ape_header(data, false)?;
let size = header.size;
let mut existing = read_ape_tag(data, header)?;
// Only keep metadata around that's marked read only
existing.items.retain(|i| i.read_only);
@ -73,7 +77,10 @@ pub(in crate::logic) fn write_to(data: &mut File, tag: &mut ApeTagRef) -> Result
if &ape_preamble == APE_PREAMBLE {
let start = data.seek(SeekFrom::Current(0))? as usize + 24;
let (mut existing, size) = read_ape_tag(data, true)?;
let header = read_ape_header(data, true)?;
let size = header.size;
let mut existing = read_ape_tag(data, header)?;
existing.items.retain(|i| i.read_only);

View file

@ -1,13 +1,18 @@
use crate::error::{LoftyError, Result};
use crate::logic::ape::tag::ApeTagRef;
#[cfg(feature = "ape")]
use crate::logic::ape::tag::ape_tag::ApeTagRef;
#[cfg(feature = "id3v1")]
use crate::logic::id3::v1::tag::Id3v1TagRef;
#[allow(unused_imports)]
use crate::types::tag::{Tag, TagType};
use std::fs::File;
pub(in crate::logic) fn write_to(data: &mut File, tag: &Tag) -> Result<()> {
match tag.tag_type() {
#[cfg(feature = "ape")]
TagType::Ape => Into::<ApeTagRef>::into(tag).write_to(data),
#[cfg(feature = "id3v1")]
TagType::Id3v1 => Into::<Id3v1TagRef>::into(tag).write_to(data),
_ => Err(LoftyError::UnsupportedTag),
}

View file

@ -1,31 +1,13 @@
#[cfg(feature = "id3v1")]
pub(crate) mod v1;
#[cfg(feature = "id3v2")]
pub(crate) mod v2;
use crate::{LoftyError, Result};
use crate::error::{LoftyError, Result};
use v2::{read_id3v2_header, Id3v2Header};
use std::io::{Read, Seek, SeekFrom};
use std::ops::Neg;
// https://github.com/polyfloyd/rust-id3/blob/e142ec656bf70a8153f6e5b34a37f26df144c3c1/src/stream/unsynch.rs#L18-L20
pub(crate) fn unsynch_u32(n: u32) -> u32 {
n & 0xFF | (n & 0xFF00) >> 1 | (n & 0xFF_0000) >> 2 | (n & 0xFF00_0000) >> 3
}
// https://github.com/polyfloyd/rust-id3/blob/e142ec656bf70a8153f6e5b34a37f26df144c3c1/src/stream/unsynch.rs#L9-L15
pub(crate) fn synch_u32(n: u32) -> Result<u32> {
if n > 0x1000_0000 {
return Err(LoftyError::TooMuchData);
}
let mut x: u32 = n & 0x7F | (n & 0xFFFF_FF80) << 1;
x = x & 0x7FFF | (x & 0xFFFF_8000) << 1;
x = x & 0x7F_FFFF | (x & 0xFF80_0000) << 1;
Ok(x)
}
pub(crate) fn find_lyrics3v2<R>(data: &mut R) -> Result<(bool, u32)>
where
R: Read + Seek,
@ -114,3 +96,55 @@ where
Ok((exists, None))
}
#[cfg(feature = "id3v2")]
pub(crate) fn find_id3v2<R>(
data: &mut R,
read: bool,
) -> Result<(Option<Id3v2Header>, Option<Vec<u8>>)>
where
R: Read + Seek,
{
let mut header = None;
let mut id3v2 = None;
if let Ok(id3v2_header) = read_id3v2_header(data) {
if read {
let mut tag = vec![0; id3v2_header.size as usize];
data.read_exact(&mut tag)?;
id3v2 = Some(tag)
} else {
data.seek(SeekFrom::Current(i64::from(id3v2_header.size)))?;
}
if id3v2_header.flags.footer {
data.seek(SeekFrom::Current(10))?;
}
header = Some(id3v2_header);
} else {
data.seek(SeekFrom::Current(-10))?;
}
Ok((header, id3v2))
}
#[cfg(not(feature = "id3v2"))]
pub(crate) fn find_id3v2<R>(data: &mut R, _read: bool) -> Result<(Option<Id3v2Header>, Option<()>)>
where
R: Read + Seek,
{
if let Ok(id3v2_header) = read_id3v2_header(data) {
data.seek(SeekFrom::Current(id3v2_header.size as i64))?;
if id3v2_header.flags.footer {
data.seek(SeekFrom::Current(10))?;
}
Ok((Some(id3v2_header), Some(())))
} else {
data.seek(SeekFrom::Current(-10))?;
Ok((None, None))
}
}

23
src/logic/id3/v2/flags.rs Normal file
View file

@ -0,0 +1,23 @@
#[cfg(feature = "id3v2_restrictions")]
use super::items::restrictions::TagRestrictions;
#[derive(Default, Copy, Clone, Debug, PartialEq)]
#[allow(clippy::struct_excessive_bools)]
/// Flags that apply to the entire tag
pub struct Id3v2TagFlags {
/// Whether or not all frames are unsynchronised. See [`FrameFlags::unsynchronisation`](crate::id3::v2::FrameFlags::unsynchronisation)
pub unsynchronisation: bool,
/// Indicates if the tag is in an experimental stage
pub experimental: bool,
/// Indicates that the tag includes a footer
pub footer: bool,
/// Whether or not to include a CRC-32 in the extended header
///
/// This is calculated if the tag is written
pub crc: bool,
#[cfg(feature = "id3v2_restrictions")]
/// Restrictions on the tag, written in the extended header
///
/// In addition to being setting this flag, all restrictions must be provided. See [`TagRestrictions`]
pub restrictions: (bool, TagRestrictions),
}

View file

@ -52,7 +52,7 @@ where
let id_str = std::str::from_utf8(&frame_header[..4]).map_err(|_| LoftyError::BadFrameID)?;
let (id, size) = if synchsafe {
let size = crate::logic::id3::unsynch_u32(u32::from_be_bytes([
let size = crate::logic::id3::v2::unsynch_u32(u32::from_be_bytes([
frame_header[4],
frame_header[5],
frame_header[6],

View file

@ -1,16 +1,25 @@
pub(crate) mod flags;
#[cfg(feature = "id3v2")]
pub(crate) mod frame;
#[cfg(feature = "id3v2")]
pub(crate) mod items;
#[cfg(feature = "id3v2")]
pub(crate) mod read;
#[cfg(feature = "id3v2")]
pub(crate) mod tag;
#[cfg(feature = "id3v2")]
pub(crate) mod util;
#[cfg(feature = "id3v2")]
pub(in crate::logic) mod write;
use crate::error::Result;
use crate::logic::id3::unsynch_u32;
use crate::error::{LoftyError, Result};
#[cfg(feature = "id3v2_restrictions")]
use crate::logic::id3::v2::items::restrictions::TagRestrictions;
use flags::Id3v2TagFlags;
use std::io::{Read, Seek, SeekFrom};
use std::io::Read;
use byteorder::{BigEndian, ByteOrder};
use byteorder::{BigEndian, ByteOrder, ReadBytesExt};
#[derive(PartialEq, Debug, Clone, Copy)]
/// The ID3v2 version
@ -23,29 +32,112 @@ pub enum Id3v2Version {
V4,
}
pub(crate) fn find_id3v2<R>(data: &mut R, read: bool) -> Result<Option<Vec<u8>>>
// https://github.com/polyfloyd/rust-id3/blob/e142ec656bf70a8153f6e5b34a37f26df144c3c1/src/stream/unsynch.rs#L18-L20
pub(crate) fn unsynch_u32(n: u32) -> u32 {
n & 0xFF | (n & 0xFF00) >> 1 | (n & 0xFF_0000) >> 2 | (n & 0xFF00_0000) >> 3
}
// https://github.com/polyfloyd/rust-id3/blob/e142ec656bf70a8153f6e5b34a37f26df144c3c1/src/stream/unsynch.rs#L9-L15
pub(crate) fn synch_u32(n: u32) -> Result<u32> {
if n > 0x1000_0000 {
return Err(LoftyError::TooMuchData);
}
let mut x: u32 = n & 0x7F | (n & 0xFFFF_FF80) << 1;
x = x & 0x7FFF | (x & 0xFFFF_8000) << 1;
x = x & 0x7F_FFFF | (x & 0xFF80_0000) << 1;
Ok(x)
}
#[derive(Copy, Clone)]
pub(crate) struct Id3v2Header {
pub version: Id3v2Version,
pub flags: Id3v2TagFlags,
pub size: u32,
}
pub(crate) fn read_id3v2_header<R>(bytes: &mut R) -> Result<Id3v2Header>
where
R: Read + Seek,
R: Read,
{
let mut id3v2 = None;
let mut header = [0; 10];
bytes.read_exact(&mut header)?;
let mut id3_header = [0; 10];
data.read_exact(&mut id3_header)?;
if &header[..3] != b"ID3" {
return Err(LoftyError::FakeTag);
}
data.seek(SeekFrom::Current(-10))?;
// Version is stored as [major, minor], but here we don't care about minor revisions unless there's an error.
let version = match header[3] {
2 => Id3v2Version::V2,
3 => Id3v2Version::V3,
4 => Id3v2Version::V4,
major => return Err(LoftyError::BadId3v2Version(major, header[4])),
};
if &id3_header[..3] == b"ID3" {
let size = unsynch_u32(BigEndian::read_u32(&id3_header[6..]));
let flags = header[5];
if read {
let mut tag = vec![0; (size + 10) as usize];
data.read_exact(&mut tag)?;
// Compression was a flag only used in ID3v2.2 (bit 2).
// At the time the ID3v2.2 specification was written, a compression scheme wasn't decided.
// The spec recommends just ignoring the tag in this case.
if version == Id3v2Version::V2 && flags & 0x40 == 0x40 {
return Err(LoftyError::Id3v2("Encountered a compressed ID3v2.2 tag"));
}
id3v2 = Some(tag)
} else {
data.seek(SeekFrom::Current(i64::from(size + 10)))?;
let mut flags_parsed = Id3v2TagFlags {
unsynchronisation: flags & 0x80 == 0x80,
experimental: (version == Id3v2Version::V4 || version == Id3v2Version::V3)
&& flags & 0x20 == 0x20,
footer: (version == Id3v2Version::V4 || version == Id3v2Version::V3)
&& flags & 0x10 == 0x10,
crc: false, // Retrieved later if applicable
#[cfg(feature = "id3v2_restrictions")]
restrictions: (false, TagRestrictions::default()), // Retrieved later if applicable
};
let extended_header =
(version == Id3v2Version::V4 || version == Id3v2Version::V3) && flags & 0x40 == 0x40;
if extended_header {
let extended_size = unsynch_u32(bytes.read_u32::<BigEndian>()?);
if extended_size < 6 {
return Err(LoftyError::Id3v2(
"Found an extended header with an invalid size (< 6)",
));
}
// Useless byte since there's only 1 byte for flags
let _num_flag_bytes = bytes.read_u8()?;
let extended_flags = bytes.read_u8()?;
// The only flags we care about here are the CRC and restrictions
if extended_flags & 0x20 == 0x20 {
flags_parsed.crc = true;
// We don't care about the existing CRC (5) or its length byte (1)
let mut crc = [0; 6];
bytes.read_exact(&mut crc)?;
}
#[cfg(feature = "id3v2_restrictions")]
if extended_flags & 0x10 == 0x10 {
flags_parsed.restrictions.0 = true;
// We don't care about the length byte, it is always 1
let _data_length = bytes.read_u8()?;
flags_parsed.restrictions.1 = TagRestrictions::from_byte(bytes.read_u8()?);
}
}
Ok(id3v2)
let size = unsynch_u32(BigEndian::read_u32(&header[6..]));
Ok(Id3v2Header {
version,
flags: flags_parsed,
size,
})
}

View file

@ -1,99 +1,26 @@
use super::frame::Frame;
use super::tag::Id3v2Tag;
use super::tag::Id3v2TagFlags;
use crate::error::{LoftyError, Result};
use crate::logic::id3::unsynch_u32;
#[cfg(feature = "id3v2_restrictions")]
use crate::logic::id3::v2::items::restrictions::TagRestrictions;
use crate::logic::id3::v2::Id3v2Version;
use super::Id3v2Header;
use crate::error::Result;
use std::io::Read;
use byteorder::{BigEndian, ReadBytesExt};
pub(crate) fn parse_id3v2<R>(bytes: &mut R) -> Result<Id3v2Tag>
#[allow(clippy::similar_names)]
pub(crate) fn parse_id3v2<R>(bytes: &mut R, header: Id3v2Header) -> Result<Id3v2Tag>
where
R: Read,
{
let mut header = [0; 10];
bytes.read_exact(&mut header)?;
if &header[..3] != b"ID3" {
return Err(LoftyError::FakeTag);
}
// Version is stored as [major, minor], but here we don't care about minor revisions unless there's an error.
let version = match header[3] {
2 => Id3v2Version::V2,
3 => Id3v2Version::V3,
4 => Id3v2Version::V4,
major => return Err(LoftyError::BadId3v2Version(major, header[4])),
};
let flags = header[5];
// Compression was a flag only used in ID3v2.2 (bit 2).
// At the time the ID3v2.2 specification was written, a compression scheme wasn't decided.
// The spec recommends just ignoring the tag in this case.
if version == Id3v2Version::V2 && flags & 0x40 == 0x40 {
return Err(LoftyError::Id3v2("Encountered a compressed ID3v2.2 tag"));
}
let mut flags_parsed = Id3v2TagFlags {
unsynchronisation: flags & 0x80 == 0x80,
experimental: (version == Id3v2Version::V4 || version == Id3v2Version::V3)
&& flags & 0x20 == 0x20,
footer: (version == Id3v2Version::V4 || version == Id3v2Version::V3)
&& flags & 0x10 == 0x10,
crc: false, // Retrieved later if applicable
#[cfg(feature = "id3v2_restrictions")]
restrictions: (false, TagRestrictions::default()), // Retrieved later if applicable
};
let extended_header =
(version == Id3v2Version::V4 || version == Id3v2Version::V3) && flags & 0x40 == 0x40;
if extended_header {
let extended_size = unsynch_u32(bytes.read_u32::<BigEndian>()?);
if extended_size < 6 {
return Err(LoftyError::Id3v2(
"Found an extended header with an invalid size (< 6)",
));
}
// Useless byte since there's only 1 byte for flags
let _num_flag_bytes = bytes.read_u8()?;
let extended_flags = bytes.read_u8()?;
// The only flags we care about here are the CRC and restrictions
if extended_flags & 0x20 == 0x20 {
flags_parsed.crc = true;
// We don't care about the existing CRC (5) or its length byte (1)
let mut crc = [0; 6];
bytes.read_exact(&mut crc)?;
}
#[cfg(feature = "id3v2_restrictions")]
if extended_flags & 0x10 == 0x10 {
flags_parsed.restrictions.0 = true;
// We don't care about the length byte, it is always 1
let _data_length = bytes.read_u8()?;
flags_parsed.restrictions.1 = TagRestrictions::from_byte(bytes.read_u8()?);
}
}
let mut tag_bytes = vec![0; header.size as usize];
bytes.read_exact(&mut tag_bytes)?;
let mut tag = Id3v2Tag::default();
tag.original_version = version;
tag.set_flags(flags_parsed);
tag.original_version = header.version;
tag.set_flags(header.flags);
let reader = &mut &*tag_bytes;
loop {
match Frame::read(bytes, version)? {
match Frame::read(reader, header.version)? {
None => break,
Some(f) => drop(tag.insert(f)),
}

View file

@ -1,21 +1,16 @@
use super::flags::Id3v2TagFlags;
use super::frame::{EncodedTextFrame, FrameFlags, LanguageFrame};
use super::frame::{Frame, FrameID, FrameValue};
#[cfg(feature = "id3v2_restrictions")]
use super::items::restrictions::TagRestrictions;
use super::util::text_utils::TextEncoding;
use super::Id3v2Version;
use crate::error::{LoftyError, Result};
use crate::error::Result;
use crate::logic::id3::v2::frame::FrameRef;
use crate::probe::Probe;
use crate::types::file::FileType;
use crate::types::item::{ItemKey, ItemValue, TagItem};
use crate::types::tag::{Accessor, Tag, TagType};
use std::convert::TryInto;
use std::fs::File;
use byteorder::ByteOrder;
macro_rules! impl_accessor {
($($name:ident, $id:literal;)+) => {
paste::paste! {
@ -192,18 +187,6 @@ impl Id3v2Tag {
pub fn write_to(&self, file: &mut File) -> Result<()> {
Into::<Id3v2TagRef>::into(self).write_to(file)
}
/// Write the tag to a chunk file
///
/// NOTE: This is only for chunk files (eg. `WAV` and `AIFF`)
///
/// # Errors
///
/// * Attempting to write the tag to a format that does not support it
/// * Attempting to write an encrypted frame without a valid method symbol or data length indicator
pub fn write_to_chunk_file<B: ByteOrder>(&self, file: &mut File) -> Result<()> {
Into::<Id3v2TagRef>::into(self).write_to_chunk_file::<B>(file)
}
}
impl IntoIterator for Id3v2Tag {
@ -217,10 +200,50 @@ impl IntoIterator for Id3v2Tag {
impl From<Id3v2Tag> for Tag {
fn from(input: Id3v2Tag) -> Self {
fn split_pair(
content: &str,
tag: &mut Tag,
current_key: ItemKey,
total_key: ItemKey,
) -> Option<()> {
let mut split = content.splitn(2, &['\0', '/'][..]);
let current = split.next()?.to_string();
tag.insert_item_unchecked(TagItem::new(current_key, ItemValue::Text(current)));
if let Some(total) = split.next() {
tag.insert_item_unchecked(TagItem::new(
total_key,
ItemValue::Text(total.to_string()),
))
}
Some(())
}
let mut tag = Self::new(TagType::Id3v2);
for frame in input.frames {
let item_key = ItemKey::from_key(TagType::Id3v2, frame.id_str());
let id = frame.id_str();
// The text pairs need some special treatment
match (id, frame.content()) {
("TRCK", FrameValue::Text { value: content, .. })
if split_pair(content, &mut tag, ItemKey::TrackNumber, ItemKey::TrackTotal)
.is_some() =>
{
continue
},
("TPOS", FrameValue::Text { value: content, .. })
if split_pair(content, &mut tag, ItemKey::DiscNumber, ItemKey::DiscTotal)
.is_some() =>
{
continue
},
_ => {},
}
let item_key = ItemKey::from_key(TagType::Id3v2, id);
let item_value = match frame.value {
FrameValue::Comment(LanguageFrame { content, .. })
| FrameValue::UnSyncText(LanguageFrame { content, .. })
@ -273,27 +296,6 @@ impl From<Tag> for Id3v2Tag {
}
}
#[derive(Default, Copy, Clone, Debug, PartialEq)]
#[allow(clippy::struct_excessive_bools)]
/// Flags that apply to the entire tag
pub struct Id3v2TagFlags {
/// Whether or not all frames are unsynchronised. See [`FrameFlags::unsynchronisation`](crate::id3::v2::FrameFlags::unsynchronisation)
pub unsynchronisation: bool,
/// Indicates if the tag is in an experimental stage
pub experimental: bool,
/// Indicates that the tag includes a footer
pub footer: bool,
/// Whether or not to include a CRC-32 in the extended header
///
/// This is calculated if the tag is written
pub crc: bool,
#[cfg(feature = "id3v2_restrictions")]
/// Restrictions on the tag, written in the extended header
///
/// In addition to being setting this flag, all restrictions must be provided. See [`TagRestrictions`]
pub restrictions: (bool, TagRestrictions),
}
pub(crate) struct Id3v2TagRef<'a> {
pub(crate) flags: Id3v2TagFlags,
pub(crate) frames: Box<dyn Iterator<Item = FrameRef<'a>> + 'a>,
@ -303,20 +305,6 @@ impl<'a> Id3v2TagRef<'a> {
pub(in crate::logic) fn write_to(&mut self, file: &mut File) -> Result<()> {
super::write::write_id3v2(file, self)
}
pub(in crate::logic) fn write_to_chunk_file<B: ByteOrder>(
&mut self,
file: &mut File,
) -> Result<()> {
let probe = Probe::new(file).guess_file_type()?;
match probe.file_type() {
Some(ft) if ft == FileType::WAV || ft == FileType::AIFF => {},
_ => return Err(LoftyError::UnsupportedTag),
}
super::write::write_id3v2_to_chunk_file::<B>(probe.into_inner(), self)
}
}
impl<'a> Into<Id3v2TagRef<'a>> for &'a Tag {
@ -347,9 +335,11 @@ mod tests {
use crate::id3::v2::{Frame, FrameFlags, FrameValue, Id3v2Tag, LanguageFrame, TextEncoding};
use crate::{Tag, TagType};
use crate::logic::id3::v2::read_id3v2_header;
use std::io::Read;
#[test]
#[allow(clippy::similar_names)]
fn parse_id3v2() {
let mut expected_tag = Id3v2Tag::default();
@ -450,12 +440,14 @@ mod tests {
let mut reader = std::io::Cursor::new(&tag[..]);
let parsed_tag = crate::logic::id3::v2::read::parse_id3v2(&mut reader).unwrap();
let header = read_id3v2_header(&mut reader).unwrap();
let parsed_tag = crate::logic::id3::v2::read::parse_id3v2(&mut reader, header).unwrap();
assert_eq!(expected_tag, parsed_tag);
}
#[test]
#[allow(clippy::similar_names)]
fn id3v2_to_tag() {
let mut tag_bytes = Vec::new();
std::fs::File::open("tests/tags/assets/test.id3v2")
@ -465,7 +457,8 @@ mod tests {
let mut reader = std::io::Cursor::new(&tag_bytes[..]);
let id3v2 = crate::logic::id3::v2::read::parse_id3v2(&mut reader).unwrap();
let header = read_id3v2_header(&mut reader).unwrap();
let id3v2 = crate::logic::id3::v2::read::parse_id3v2(&mut reader, header).unwrap();
let tag: Tag = id3v2.into();

View file

@ -183,6 +183,7 @@ mod tests {
use crate::id3::v2::TextEncoding;
use std::io::Cursor;
#[allow(clippy::non_ascii_literal)]
const TEST_STRING: &str = "løft¥";
#[test]

View file

@ -1,7 +1,7 @@
use crate::error::{LoftyError, Result};
use crate::id3::v2::Id3v2Version;
use crate::logic::id3::synch_u32;
use crate::logic::id3::v2::frame::{FrameFlags, FrameRef, FrameValueRef};
use crate::logic::id3::v2::synch_u32;
use std::io::Write;

View file

@ -1,24 +1,30 @@
mod chunk_file;
mod frame;
use super::find_id3v2;
use super::Id3v2TagFlags;
use crate::error::{LoftyError, Result};
use crate::logic::id3::synch_u32;
use crate::logic::id3::v2::tag::{Id3v2TagFlags, Id3v2TagRef};
use crate::logic::id3::v2::tag::Id3v2TagRef;
use crate::logic::id3::{find_id3v2, v2::synch_u32};
use crate::probe::Probe;
use crate::types::file::FileType;
use std::fs::File;
use std::io::{Cursor, Read, Seek, SeekFrom, Write};
use byteorder::{BigEndian, ByteOrder, WriteBytesExt};
use byteorder::{BigEndian, ByteOrder, LittleEndian, WriteBytesExt};
#[allow(clippy::shadow_unrelated)]
pub(in crate::logic) fn write_id3v2(data: &mut File, tag: &mut Id3v2TagRef) -> Result<()> {
let probe = Probe::new(data).guess_file_type()?;
match probe.file_type() {
Some(ft) if ft == FileType::APE || ft == FileType::MP3 => {},
Some(FileType::APE | FileType::MP3) => {},
Some(FileType::WAV) => {
return write_id3v2_to_chunk_file::<LittleEndian>(probe.into_inner(), tag)
},
Some(FileType::AIFF) => {
return write_id3v2_to_chunk_file::<BigEndian>(probe.into_inner(), tag)
},
_ => return Err(LoftyError::UnsupportedTag),
}

View file

@ -5,11 +5,13 @@ pub(crate) mod tag;
pub(in crate::logic) mod write;
use crate::error::Result;
#[cfg(feature = "id3v2")]
use crate::logic::id3::v2::tag::Id3v2Tag;
use crate::logic::tag_methods;
use crate::types::file::{AudioFile, FileType, TaggedFile};
use crate::types::properties::FileProperties;
use crate::types::tag::TagType;
use crate::types::tag::{Tag, TagType};
#[cfg(feature = "aiff_text_chunks")]
use tag::AiffTextChunks;
use std::io::{Read, Seek};
@ -28,16 +30,17 @@ pub struct AiffFile {
impl From<AiffFile> for TaggedFile {
fn from(input: AiffFile) -> Self {
let mut tags = Vec::<Option<Tag>>::with_capacity(3);
#[cfg(feature = "aiff_text_chunks")]
tags.push(input.text_chunks.map(Into::into));
#[cfg(feature = "id3v2")]
tags.push(input.id3v2_tag.map(Into::into));
Self {
ty: FileType::AIFF,
properties: input.properties,
tags: vec![
input.text_chunks.map(Into::into),
input.id3v2_tag.map(Into::into),
]
.into_iter()
.flatten()
.collect(),
tags: tags.into_iter().flatten().collect(),
}
}
}
@ -57,19 +60,32 @@ impl AudioFile for AiffFile {
&self.properties
}
#[allow(unreachable_code)]
fn contains_tag(&self) -> bool {
self.id3v2_tag.is_some() || self.text_chunks.is_some()
#[cfg(feature = "id3v2")]
return self.id3v2_tag.is_some();
#[cfg(feature = "aiff_text_chunks")]
return self.text_chunks.is_some();
false
}
fn contains_tag_type(&self, tag_type: &TagType) -> bool {
match tag_type {
#[cfg(feature = "id3v2")]
TagType::Id3v2 => self.id3v2_tag.is_some(),
#[cfg(feature = "aiff_text_chunks")]
TagType::AiffText => self.text_chunks.is_some(),
_ => false,
}
}
}
tag_methods! {
AiffFile => ID3v2, id3v2_tag, Id3v2Tag; Text_Chunks, text_chunks, AiffTextChunks
impl AiffFile {
tag_methods! {
#[cfg(feature = "id3v2")];
ID3v2, id3v2_tag, Id3v2Tag;
#[cfg(feature = "aiff_text_chunks")];
Text_Chunks, text_chunks, AiffTextChunks
}
}

View file

@ -1,18 +1,19 @@
use crate::error::{LoftyError, Result};
#[cfg(feature = "id3v2")]
use crate::logic::id3::v2::tag::Id3v2TagRef;
#[cfg(feature = "aiff_text_chunks")]
use crate::logic::iff::aiff::tag::AiffTextChunksRef;
#[allow(unused_imports)]
use crate::types::tag::{Tag, TagType};
use std::fs::File;
use byteorder::BigEndian;
pub(crate) fn write_to(data: &mut File, tag: &Tag) -> Result<()> {
match tag.tag_type() {
#[cfg(feature = "aiff_text_chunks")]
TagType::AiffText => Into::<AiffTextChunksRef>::into(tag).write_to(data),
#[cfg(feature = "id3v2")]
TagType::Id3v2 => Into::<Id3v2TagRef>::into(tag).write_to_chunk_file::<BigEndian>(data),
TagType::Id3v2 => Into::<Id3v2TagRef>::into(tag).write_to(data),
_ => Err(LoftyError::UnsupportedTag),
}
}

View file

@ -1,10 +1,13 @@
use crate::error::Result;
#[cfg(feature = "id3v2")]
use crate::logic::id3::v2::read::parse_id3v2;
#[cfg(feature = "id3v2")]
use crate::logic::id3::v2::tag::Id3v2Tag;
use std::io::{Read, Seek, SeekFrom};
use std::marker::PhantomData;
use crate::logic::id3::v2::read_id3v2_header;
use byteorder::{ByteOrder, ReadBytesExt};
pub(in crate::logic) struct Chunks<B>
@ -45,6 +48,8 @@ impl<B: ByteOrder> Chunks<B> {
Ok(content)
}
#[cfg(feature = "id3v2")]
#[allow(clippy::similar_names)]
pub fn id3_chunk<R>(&mut self, data: &mut R) -> Result<Id3v2Tag>
where
R: Read + Seek,
@ -52,7 +57,10 @@ impl<B: ByteOrder> Chunks<B> {
let mut value = vec![0; self.size as usize];
data.read_exact(&mut value)?;
let id3v2 = parse_id3v2(&mut &*value)?;
let reader = &mut &*value;
let header = read_id3v2_header(reader)?;
let id3v2 = parse_id3v2(reader, header)?;
// Skip over the footer
if id3v2.flags().footer {
@ -62,6 +70,27 @@ impl<B: ByteOrder> Chunks<B> {
Ok(id3v2)
}
#[cfg(not(feature = "id3v2"))]
#[allow(clippy::similar_names)]
pub fn id3_chunk<R>(&mut self, data: &mut R) -> Result<()>
where
R: Read + Seek,
{
let mut value = vec![0; self.size as usize];
data.read_exact(&mut value)?;
let mut reader = &mut &*value;
let header = read_id3v2_header(reader)?;
// Skip over the footer
if header.flags.footer {
data.seek(SeekFrom::Current(10))?;
}
Ok(())
}
pub fn correct_position<R>(&mut self, data: &mut R) -> Result<()>
where
R: Read + Seek,

View file

@ -5,12 +5,14 @@ pub(crate) mod tag;
pub(in crate::logic) mod write;
use crate::error::Result;
#[cfg(feature = "id3v2")]
use crate::logic::id3::v2::tag::Id3v2Tag;
use crate::logic::tag_methods;
use crate::types::file::{AudioFile, FileType, TaggedFile};
use crate::types::properties::FileProperties;
use crate::types::tag::TagType;
use crate::types::tag::{Tag, TagType};
use properties::WavProperties;
#[cfg(feature = "riff_info_list")]
use tag::RiffInfoList;
use std::io::{Read, Seek};
@ -29,16 +31,17 @@ pub struct WavFile {
impl From<WavFile> for TaggedFile {
fn from(input: WavFile) -> Self {
let mut tags = Vec::<Option<Tag>>::with_capacity(3);
#[cfg(feature = "riff_info_list")]
tags.push(input.riff_info.map(Into::into));
#[cfg(feature = "id3v2")]
tags.push(input.id3v2_tag.map(Into::into));
Self {
ty: FileType::WAV,
properties: FileProperties::from(input.properties),
tags: vec![
input.riff_info.map(Into::into),
input.id3v2_tag.map(Into::into),
]
.into_iter()
.flatten()
.collect(),
tags: tags.into_iter().flatten().collect(),
}
}
}
@ -58,19 +61,33 @@ impl AudioFile for WavFile {
&self.properties
}
#[allow(unreachable_code)]
fn contains_tag(&self) -> bool {
self.id3v2_tag.is_some() || self.riff_info.is_some()
#[cfg(feature = "id3v2")]
return self.id3v2_tag.is_some();
#[cfg(feature = "riff_info_list")]
return self.riff_info.is_some();
false
}
fn contains_tag_type(&self, tag_type: &TagType) -> bool {
match tag_type {
#[cfg(feature = "id3v2")]
TagType::Id3v2 => self.id3v2_tag.is_some(),
#[cfg(feature = "riff_info_list")]
TagType::RiffInfo => self.riff_info.is_some(),
_ => false,
}
}
}
tag_methods! {
WavFile => ID3v2, id3v2_tag, Id3v2Tag; RIFF_INFO, riff_info, RiffInfoList
impl WavFile {
tag_methods! {
#[cfg(feature = "id3v2")];
ID3v2, id3v2_tag, Id3v2Tag;
#[cfg(feature = "riff_info_list")];
RIFF_INFO, riff_info, RiffInfoList
}
}

View file

@ -2,6 +2,7 @@
use super::tag::RiffInfoList;
use super::WavFile;
use crate::error::{LoftyError, Result};
#[cfg(feature = "id3v2")]
use crate::logic::id3::v2::tag::Id3v2Tag;
use crate::logic::iff::chunk::Chunks;
@ -39,6 +40,7 @@ where
#[cfg(feature = "riff_info_list")]
let mut riff_info = RiffInfoList::default();
#[cfg(feature = "id3v2")]
let mut id3v2_tag: Option<Id3v2Tag> = None;
let mut chunks = Chunks::<LittleEndian>::new();
@ -78,11 +80,13 @@ where
#[cfg(not(feature = "riff_info_list"))]
{
data.seek(SeekFrom::Current(i64::from(size)))?;
data.seek(SeekFrom::Current(i64::from(chunks.size)))?;
}
},
#[cfg(feature = "id3v2")]
b"ID3 " | b"id3 " => id3v2_tag = Some(chunks.id3_chunk(data)?),
#[cfg(not(feature = "id3v2"))]
b"ID3 " | b"id3 " => chunks.id3_chunk(data)?,
_ => {
data.seek(SeekFrom::Current(i64::from(chunks.size)))?;
},

View file

@ -1,16 +1,19 @@
use crate::error::{LoftyError, Result};
#[cfg(feature = "id3v2")]
use crate::logic::id3::v2::tag::Id3v2TagRef;
#[cfg(feature = "riff_info_list")]
use crate::logic::iff::wav::tag::RiffInfoListRef;
#[allow(unused_imports)]
use crate::types::tag::{Tag, TagType};
use std::fs::File;
use byteorder::LittleEndian;
pub(crate) fn write_to(data: &mut File, tag: &Tag) -> Result<()> {
match tag.tag_type() {
#[cfg(feature = "riff_info_list")]
TagType::RiffInfo => Into::<RiffInfoListRef>::into(tag).write_to(data),
TagType::Id3v2 => Into::<Id3v2TagRef>::into(tag).write_to_chunk_file::<LittleEndian>(data),
#[cfg(feature = "id3v2")]
TagType::Id3v2 => Into::<Id3v2TagRef>::into(tag).write_to(data),
_ => Err(LoftyError::UnsupportedTag),
}
}

View file

@ -5,8 +5,10 @@ pub(crate) mod mp3;
pub(crate) mod mp4;
pub(crate) mod ogg;
use crate::error::Result;
use crate::error::{LoftyError, Result};
#[cfg(feature = "mp4_ilst")]
use crate::logic::mp4::ilst::IlstRef;
#[cfg(feature = "vorbis_comments")]
use crate::logic::ogg::tag::VorbisCommentsRef;
use crate::types::file::FileType;
use crate::types::tag::Tag;
@ -14,37 +16,50 @@ use ogg::constants::{OPUSTAGS, VORBIS_COMMENT_HEAD};
use std::fs::File;
#[allow(unreachable_patterns)]
pub(crate) fn write_tag(tag: &Tag, file: &mut File, file_type: FileType) -> Result<()> {
match file_type {
FileType::AIFF => iff::aiff::write::write_to(file, tag),
FileType::APE => ape::write::write_to(file, tag),
FileType::FLAC => {
ogg::flac::write::write_to(file, &mut Into::<VorbisCommentsRef>::into(tag))
},
#[cfg(feature = "vorbis_comments")]
FileType::FLAC => ogg::flac::write::write_to(file, &mut Into::<VorbisCommentsRef>::into(tag)),
FileType::MP3 => mp3::write::write_to(file, tag),
#[cfg(feature = "mp4_ilst")]
FileType::MP4 => mp4::ilst::write::write_to(file, &mut Into::<IlstRef>::into(tag)),
FileType::Opus => ogg::write::write_to(file, tag, OPUSTAGS),
FileType::Vorbis => ogg::write::write_to(file, tag, VORBIS_COMMENT_HEAD),
FileType::WAV => iff::wav::write::write_to(file, tag),
_ => Err(LoftyError::UnsupportedTag),
}
}
macro_rules! tag_methods {
($impl_for:ident => $($display_name:tt, $name:ident, $ty:ty);*) => {
impl $impl_for {
paste::paste! {
$(
#[doc = "Gets the " $display_name "tag if it exists"]
pub fn $name(&self) -> Option<&$ty> {
self.$name.as_ref()
}
($(
$(#[$attr:meta])?;
$display_name:tt,
$name:ident,
$ty:ty);*
) => {
paste::paste! {
$(
$(#[$attr])?
#[doc = "Gets the " $display_name "tag if it exists"]
pub fn $name(&self) -> Option<&$ty> {
self.$name.as_ref()
}
#[doc = "Sets the " $display_name]
pub fn [<set_ $name>](&mut self, tag: $ty) {
self.$name = Some(tag)
}
)*
}
$(#[$attr])?
#[doc = "Sets the " $display_name]
pub fn [<set_ $name>](&mut self, tag: $ty) {
self.$name = Some(tag)
}
$(#[$attr])?
#[doc = "Removes the " $display_name]
pub fn [<remove_ $name>](&mut self) {
self.$name = None
}
)*
}
}
}

View file

@ -3,12 +3,17 @@ pub(crate) mod header;
pub(crate) mod read;
pub(in crate::logic) mod write;
use crate::logic::ape::tag::ApeTag;
use crate::error::Result;
#[cfg(feature = "ape")]
use crate::logic::ape::tag::ape_tag::ApeTag;
#[cfg(feature = "id3v1")]
use crate::logic::id3::v1::tag::Id3v1Tag;
#[cfg(feature = "id3v2")]
use crate::logic::id3::v2::tag::Id3v2Tag;
use crate::logic::tag_methods;
use crate::types::file::{AudioFile, FileType, TaggedFile};
use crate::{FileProperties, Result, TagType};
use crate::types::properties::FileProperties;
use crate::types::tag::{Tag, TagType};
use header::{ChannelMode, Layer, MpegVersion};
use std::io::{Read, Seek};
@ -120,18 +125,21 @@ pub struct Mp3File {
}
impl From<Mp3File> for TaggedFile {
#[allow(clippy::vec_init_then_push)]
fn from(input: Mp3File) -> Self {
let mut tags = Vec::<Option<Tag>>::with_capacity(3);
#[cfg(feature = "id3v2")]
tags.push(input.id3v2_tag.map(Into::into));
#[cfg(feature = "id3v1")]
tags.push(input.id3v1_tag.map(Into::into));
#[cfg(feature = "ape")]
tags.push(input.ape_tag.map(Into::into));
Self {
ty: FileType::MP3,
properties: FileProperties::from(input.properties),
tags: vec![
input.id3v2_tag.map(Into::into),
input.id3v1_tag.map(Into::into),
input.ape_tag.map(Into::into),
]
.into_iter()
.flatten()
.collect(),
tags: tags.into_iter().flatten().collect(),
}
}
}
@ -150,20 +158,38 @@ impl AudioFile for Mp3File {
&self.properties
}
#[allow(unreachable_code)]
fn contains_tag(&self) -> bool {
self.id3v2_tag.is_some() || self.id3v1_tag.is_some() || self.ape_tag.is_some()
#[cfg(feature = "id3v2")]
return self.id3v2_tag.is_some();
#[cfg(feature = "id3v1")]
return self.id3v1_tag.is_some();
#[cfg(feature = "ape")]
return self.ape_tag.is_some();
false
}
fn contains_tag_type(&self, tag_type: &TagType) -> bool {
match tag_type {
#[cfg(feature = "ape")]
TagType::Ape => self.ape_tag.is_some(),
#[cfg(feature = "id3v2")]
TagType::Id3v2 => self.id3v2_tag.is_some(),
#[cfg(feature = "id3v1")]
TagType::Id3v1 => self.id3v1_tag.is_some(),
_ => false,
}
}
}
tag_methods! {
Mp3File => ID3v2, id3v2_tag, Id3v2Tag; ID3v1, id3v1_tag, Id3v1Tag; APE, ape_tag, ApeTag
impl Mp3File {
tag_methods! {
#[cfg(feature = "id3v2")];
ID3v2, id3v2_tag, Id3v2Tag;
#[cfg(feature = "id3v1")];
ID3v1, id3v1_tag, Id3v1Tag;
#[cfg(feature = "ape")];
APE, ape_tag, ApeTag
}
}

View file

@ -1,16 +1,21 @@
use super::header::{verify_frame_sync, Header, XingHeader};
use super::{Mp3File, Mp3Properties};
use crate::error::{LoftyError, Result};
#[cfg(feature = "id3v2")]
use crate::id3::v2::Id3v2Tag;
use crate::logic::ape::tag::ApeTag;
use crate::logic::id3::unsynch_u32;
#[cfg(feature = "ape")]
use crate::logic::ape::tag::ape_tag::ApeTag;
use crate::logic::ape::tag::read_ape_header;
#[cfg(feature = "id3v1")]
use crate::logic::id3::v1::tag::Id3v1Tag;
#[cfg(feature = "id3v2")]
use crate::logic::id3::v2::read::parse_id3v2;
use crate::logic::id3::v2::read_id3v2_header;
use std::io::{Read, Seek, SeekFrom};
use std::time::Duration;
use byteorder::{BigEndian, ByteOrder, ReadBytesExt};
use byteorder::ReadBytesExt;
fn read_properties(
first_frame: (Header, u64),
@ -67,8 +72,11 @@ pub(crate) fn read_from<R>(data: &mut R) -> Result<Mp3File>
where
R: Read + Seek,
{
#[cfg(feature = "id3v2")]
let mut id3v2_tag: Option<Id3v2Tag> = None;
#[cfg(feature = "id3v1")]
let mut id3v1_tag: Option<Id3v1Tag> = None;
#[cfg(feature = "ape")]
let mut ape_tag: Option<ApeTag> = None;
let mut first_mpeg_frame = (None, 0);
@ -99,20 +107,21 @@ where
let mut remaining_header = [0; 6];
data.read_exact(&mut remaining_header)?;
let size = (unsynch_u32(BigEndian::read_u32(&remaining_header[2..])) + 10) as usize;
data.seek(SeekFrom::Current(-10))?;
let header = read_id3v2_header(
&mut &*[header.as_slice(), remaining_header.as_slice()].concat(),
)?;
let skip_footer = header.flags.footer;
let mut id3v2_read = vec![0; size];
data.read_exact(&mut id3v2_read)?;
let id3v2 = parse_id3v2(&mut &*id3v2_read)?;
// Skip over the footer
if id3v2.flags().footer {
data.seek(SeekFrom::Current(10))?;
#[cfg(feature = "id3v2")]
{
let id3v2 = parse_id3v2(data, header)?;
id3v2_tag = Some(id3v2);
}
id3v2_tag = Some(id3v2);
// Skip over the footer
if skip_footer {
data.seek(SeekFrom::Current(10))?;
}
continue;
},
@ -122,7 +131,11 @@ where
let mut id3v1_read = [0; 128];
data.read_exact(&mut id3v1_read)?;
id3v1_tag = Some(crate::logic::id3::v1::read::parse_id3v1(id3v1_read));
#[cfg(feature = "id3v1")]
{
id3v1_tag = Some(crate::logic::id3::v1::read::parse_id3v1(id3v1_read));
}
continue;
},
[b'A', b'P', b'E', b'T'] => {
@ -130,7 +143,21 @@ where
data.read_exact(&mut header_remaining)?;
if &header_remaining == b"AGEX" {
ape_tag = Some(crate::logic::ape::tag::read::read_ape_tag(data, false)?.0);
let ape_header = read_ape_header(data, false)?;
#[cfg(not(feature = "ape"))]
{
let size = ape_header.size;
data.seek(SeekFrom::Current(size as i64))?;
}
#[cfg(feature = "ape")]
{
ape_tag = Some(crate::logic::ape::tag::read::read_ape_tag(
data, ape_header,
)?);
}
continue;
}
},

View file

@ -1,15 +1,22 @@
use crate::error::{LoftyError, Result};
use crate::logic::ape::tag::ApeTagRef;
#[cfg(feature = "ape")]
use crate::logic::ape::tag::ape_tag::ApeTagRef;
#[cfg(feature = "id3v1")]
use crate::logic::id3::v1::tag::Id3v1TagRef;
#[cfg(feature = "id3v2")]
use crate::logic::id3::v2::tag::Id3v2TagRef;
#[allow(unused_imports)]
use crate::types::tag::{Tag, TagType};
use std::fs::File;
pub(crate) fn write_to(data: &mut File, tag: &Tag) -> Result<()> {
match tag.tag_type() {
#[cfg(feature = "ape")]
TagType::Ape => Into::<ApeTagRef>::into(tag).write_to(data),
#[cfg(feature = "id3v1")]
TagType::Id3v1 => Into::<Id3v1TagRef>::into(tag).write_to(data),
#[cfg(feature = "id3v2")]
TagType::Id3v2 => Into::<Id3v2TagRef>::into(tag).write_to(data),
_ => Err(LoftyError::UnsupportedTag),
}

View file

@ -1,4 +1,5 @@
mod atom_info;
#[cfg(feature = "mp4_ilst")]
pub(crate) mod ilst;
mod moov;
mod properties;
@ -115,9 +116,15 @@ impl From<Mp4File> for TaggedFile {
Self {
ty: FileType::MP4,
properties: FileProperties::from(input.properties),
tags: if let Some(ilst) = input.ilst {
vec![ilst.into()]
} else {
tags: {
#[cfg(feature = "mp4_ilst")]
if let Some(ilst) = input.ilst {
vec![ilst.into()]
} else {
Vec::new()
}
#[cfg(not(feature = "mp4_ilst"))]
Vec::new()
},
}
@ -138,12 +145,20 @@ impl AudioFile for Mp4File {
&self.properties
}
#[allow(unreachable_code)]
fn contains_tag(&self) -> bool {
self.ilst.is_some()
#[cfg(feature = "mp4_ilst")]
return self.ilst.is_some();
false
}
#[allow(unreachable_code)]
fn contains_tag_type(&self, tag_type: &TagType) -> bool {
tag_type == &TagType::Mp4Ilst && self.ilst.is_some()
#[cfg(feature = "mp4_ilst")]
return tag_type == &TagType::Mp4Ilst && self.ilst.is_some();
false
}
}
@ -154,6 +169,9 @@ impl Mp4File {
}
}
tag_methods! {
Mp4File => ilst, ilst, Ilst
impl Mp4File {
tag_methods! {
#[cfg(feature = "mp4_ilst")];
ilst, ilst, Ilst
}
}

View file

@ -1,17 +1,18 @@
use super::atom_info::AtomInfo;
use super::atom_info::{AtomIdent, AtomInfo};
#[cfg(feature = "mp4_ilst")]
use super::ilst::{read::parse_ilst, Ilst};
use super::read::skip_unneeded;
use super::trak::Trak;
use super::AtomIdent;
use crate::error::{LoftyError, Result};
use std::io::{Read, Seek};
#[cfg(feature = "mp4_ilst")]
use byteorder::{BigEndian, ReadBytesExt};
pub(crate) struct Moov {
pub(crate) traks: Vec<Trak>,
#[cfg(feature = "mp4_ilst")]
// Represents a parsed moov.udta.meta.ilst since we don't need anything else
pub(crate) meta: Option<Ilst>,
}
@ -44,12 +45,14 @@ impl Moov {
R: Read + Seek,
{
let mut traks = Vec::new();
#[cfg(feature = "mp4_ilst")]
let mut meta = None;
while let Ok(atom) = AtomInfo::read(data) {
if let AtomIdent::Fourcc(fourcc) = atom.ident {
match &fourcc {
b"trak" => traks.push(Trak::parse(data, &atom)?),
#[cfg(feature = "mp4_ilst")]
b"udta" => {
meta = meta_from_udta(data, atom.len - 8)?;
},
@ -62,10 +65,15 @@ impl Moov {
skip_unneeded(data, atom.extended, atom.len)?
}
Ok(Self { traks, meta })
Ok(Self {
traks,
#[cfg(feature = "mp4_ilst")]
meta,
})
}
}
#[cfg(feature = "mp4_ilst")]
fn meta_from_udta<R>(data: &mut R, len: u64) -> Result<Option<Ilst>>
where
R: Read + Seek,
@ -109,7 +117,6 @@ where
skip_unneeded(data, atom.extended, atom.len)?;
}
#[cfg(feature = "mp4_ilst")]
if islt.0 {
return parse_ilst(data, islt.1 - 8).map(Some);
}

View file

@ -1,8 +1,7 @@
use super::atom_info::AtomInfo;
use super::atom_info::{AtomIdent, AtomInfo};
use super::read::nested_atom;
use super::read::skip_unneeded;
use super::trak::Trak;
use super::AtomIdent;
use super::{Mp4Codec, Mp4Properties};
use crate::error::{LoftyError, Result};

View file

@ -1,9 +1,8 @@
use super::atom_info::AtomInfo;
use super::atom_info::{AtomIdent, AtomInfo};
use super::moov::Moov;
use super::properties::read_properties;
use super::Mp4File;
use crate::error::{LoftyError, Result};
use crate::mp4::AtomIdent;
use std::io::{Read, Seek, SeekFrom};
@ -40,6 +39,7 @@ where
Ok(Mp4File {
ftyp,
#[cfg(feature = "mp4_ilst")]
ilst: moov.meta,
properties: read_properties(data, &moov.traks, file_length)?,
})

View file

@ -1,6 +1,5 @@
use super::atom_info::AtomInfo;
use super::atom_info::{AtomIdent, AtomInfo};
use super::read::skip_unneeded;
use super::AtomIdent;
use crate::error::Result;
use std::io::{Read, Seek, SeekFrom};

View file

@ -57,6 +57,9 @@ impl AudioFile for FlacFile {
}
}
tag_methods! {
FlacFile => Vorbis_Comments, vorbis_comments, VorbisComments
impl FlacFile {
tag_methods! {
#[cfg(feature = "vorbis_comments")];
Vorbis_Comments, vorbis_comments, VorbisComments
}
}

View file

@ -44,6 +44,15 @@ pub struct TaggedFile {
pub(crate) tags: Vec<Tag>,
}
#[cfg(any(
feature = "id3v1",
feature = "riff_info_list",
feature = "aiff_text_chunks",
feature = "vorbis_comments",
feature = "id3v2",
feature = "mp4_ilst",
feature = "ape"
))]
impl TaggedFile {
/// Gets the file's "Primary tag", or the one most likely to be used in the target format
///
@ -69,13 +78,20 @@ impl TaggedFile {
/// See [`primary_tag`](Self::primary_tag) for an explanation
pub fn primary_tag_type(&self) -> TagType {
match self.ty {
#[cfg(feature = "id3v2")]
#[cfg(all(not(feature = "id3v2"), feature = "aiff_text_chunks"))]
FileType::AIFF => TagType::AiffText,
#[cfg(all(not(feature = "id3v2"), feature = "riff_info_list"))]
FileType::WAV => TagType::RiffInfo,
#[cfg(all(not(feature = "id3v2"), feature = "id3v1"))]
FileType::MP3 => TagType::Id3v1,
#[cfg(all(not(feature = "id3v2"), not(feature = "id3v1"), feature = "ape"))]
FileType::MP3 => TagType::Ape,
FileType::AIFF | FileType::MP3 | FileType::WAV => TagType::Id3v2,
#[cfg(feature = "ape")]
#[cfg(all(not(feature = "ape"), feature = "id3v1"))]
FileType::MP3 => TagType::Id3v1,
FileType::APE => TagType::Ape,
#[cfg(feature = "vorbis_comments")]
FileType::FLAC | FileType::Opus | FileType::Vorbis => TagType::VorbisComments,
#[cfg(feature = "mp4_ilst")]
FileType::MP4 => TagType::Mp4Ilst,
}
}
@ -136,16 +152,6 @@ impl TaggedFile {
.map(|pos| self.tags.remove(pos))
}
/// Returns the file's [`FileType`]
pub fn file_type(&self) -> &FileType {
&self.ty
}
/// Returns a reference to the file's [`FileProperties`]
pub fn properties(&self) -> &FileProperties {
&self.properties
}
/// Attempts to write all tags to a path
///
/// # Errors
@ -169,6 +175,18 @@ impl TaggedFile {
}
}
impl TaggedFile {
/// Returns the file's [`FileType`]
pub fn file_type(&self) -> &FileType {
&self.ty
}
/// Returns a reference to the file's [`FileProperties`]
pub fn properties(&self) -> &FileProperties {
&self.properties
}
}
#[derive(PartialEq, Copy, Clone, Debug)]
#[allow(missing_docs)]
/// The type of file read
@ -187,22 +205,25 @@ impl FileType {
/// Returns if the target FileType supports a [`TagType`]
pub fn supports_tag_type(&self, tag_type: &TagType) -> bool {
match self {
FileType::AIFF => tag_type == &TagType::Id3v2 || tag_type == &TagType::AiffText,
FileType::APE => {
tag_type == &TagType::Ape
|| tag_type == &TagType::Id3v1
|| tag_type == &TagType::Id3v2
},
FileType::MP3 => {
tag_type == &TagType::Id3v2
|| tag_type == &TagType::Ape
|| tag_type == &TagType::Id3v1
},
FileType::Opus | FileType::FLAC | FileType::Vorbis => {
tag_type == &TagType::VorbisComments
#[cfg(feature = "id3v2")]
FileType::AIFF | FileType::APE | FileType::MP3 | FileType::WAV
if tag_type == &TagType::Id3v2 =>
{
true
},
#[cfg(feature = "aiff_text_chunks")]
FileType::AIFF if tag_type == &TagType::AiffText => true,
#[cfg(feature = "id3v1")]
FileType::APE | FileType::MP3 if tag_type == &TagType::Id3v1 => true,
#[cfg(feature = "ape")]
FileType::APE | FileType::MP3 if tag_type == &TagType::Ape => true,
#[cfg(feature = "vorbis_comments")]
FileType::Opus | FileType::FLAC | FileType::Vorbis => tag_type == &TagType::VorbisComments,
#[cfg(feature = "mp4_ilst")]
FileType::MP4 => tag_type == &TagType::Mp4Ilst,
FileType::WAV => tag_type == &TagType::Id3v2 || tag_type == &TagType::RiffInfo,
#[cfg(feature = "riff_info_list")]
FileType::WAV => tag_type == &TagType::RiffInfo,
_ => false,
}
}
@ -262,7 +283,7 @@ impl FileType {
}
pub(crate) fn from_buffer_inner(buf: &[u8]) -> Result<(Option<Self>, u32)> {
use crate::logic::id3::unsynch_u32;
use crate::logic::id3::v2::unsynch_u32;
if buf.is_empty() {
return Err(LoftyError::EmptyFile);

View file

@ -1,5 +1,6 @@
use crate::logic::id3::v1::constants::VALID_ITEMKEYS;
use crate::TagType;
use crate::types::tag::TagType;
use std::collections::HashMap;
macro_rules! first_key {
($key:tt $(| $remaining:expr)*) => {
@ -9,16 +10,332 @@ macro_rules! first_key {
pub(crate) use first_key;
// This is used to create the ItemKey enum and its to and from key conversions
// This is used to create the key/ItemKey maps
//
// First comes the ItemKey variant as an ident (ex. Artist), then a collection of the appropriate mappings.
// Ex. Artist => [TagType::Ape => "Artist"]
// First comes the feature attribute, followed by the name of the map.
// Ex:
//
// #[cfg(feature = "ape")]
// APE_MAP;
//
// This is followed by the key value pairs separated by `=>`, with the key being the
// format-specific key and the value being the appropriate ItemKey variant.
// Ex. "Artist" => Artist
//
// Some formats have multiple keys that map to the same ItemKey variant, which can be added with '|'.
// The standard key(s) **must** come before any popular non-standard keys.
// Keys should appear in order of popularity.
macro_rules! item_keys {
($($variant:ident => [$($($tag_type:pat)|* => $($key:tt)|+),+]),+) => {
macro_rules! gen_map {
($(#[$meta:meta])? $NAME:ident; $($($key:literal)|+ => $variant:ident),+) => {
$(#[$meta])?
lazy_static::lazy_static! {
static ref $NAME: HashMap<&'static str, ItemKey> = {
let mut map = HashMap::new();
$(
$(
map.insert($key, ItemKey::$variant);
)+
)+
map
};
}
$(#[$meta])?
impl $NAME {
pub(crate) fn get_item_key(&self, key: &str) -> Option<ItemKey> {
self.iter().find(|(k, _)| k.eq_ignore_ascii_case(key)).map(|(_, v)| v.clone())
}
pub(crate) fn get_key(&self, item_key: &ItemKey) -> Option<&str> {
match item_key {
$(
ItemKey::$variant => Some(first_key!($($key)|*)),
)+
_ => None
}
}
}
}
}
gen_map!(
#[cfg(feature = "aiff_text_chunks")]
AIFF_TEXT_MAP;
"NAME" => TrackTitle,
"AUTH" => TrackArtist,
"(c) " => CopyrightMessage
);
gen_map!(
#[cfg(feature = "ape")]
APE_MAP;
"Album" => AlbumTitle,
"DiscSubtitle" => SetSubtitle,
"Grouping" => ContentGroup,
"Title" => TrackTitle,
"Subtitle" => TrackSubtitle,
"ALBUMSORT" => AlbumTitleSortOrder,
"ALBUMARTISTSORT" => AlbumArtistSortOrder,
"TITLESORT" => TrackTitleSortOrder,
"ARTISTSORT" => TrackArtistSortOrder,
"Album Artist" | "ALBUMARTIST" => AlbumArtist,
"Artist" => TrackArtist,
"Arranger" => Arranger,
"Writer" => Writer,
"Composer" => Composer,
"Conductor" => Conductor,
"Engineer" => Engineer,
"Lyricist" => Lyricist,
"DjMixer" => MixDj,
"Mixer" => MixEngineer,
"Performer" => Performer,
"Producer" => Producer,
"Label" => Label,
"MixArtist" => Remixer,
"Disc" => DiscNumber,
"Disc" => DiscTotal,
"Track" => TrackNumber,
"Track" => TrackTotal,
"Year" => Year,
"ISRC" => ISRC,
"Barcode" => Barcode,
"CatalogNumber" => CatalogNumber,
"Compilation" => FlagCompilation,
"Media" => OriginalMediaType,
"EncodedBy" => EncodedBy,
"Genre" => Genre,
"Mood" => Mood,
"Copyright" => CopyrightMessage,
"Comment" => Comment,
"language" => Language,
"Script" => Script,
"Lyrics" => Lyrics
);
gen_map! (
#[cfg(feature = "id3v2")]
ID3V2_MAP;
"TALB" => AlbumTitle,
"TSST" => SetSubtitle,
"TIT1" | "GRP1" => ContentGroup,
"TIT2" => TrackTitle,
"TIT3" => TrackSubtitle,
"TOAL" => OriginalAlbumTitle,
"TOPE" => OriginalArtist,
"TOLY" => OriginalLyricist,
"TSOA" => AlbumTitleSortOrder,
"TSO2" => AlbumArtistSortOrder,
"TSOT" => TrackTitleSortOrder,
"TSOP" => TrackArtistSortOrder,
"TSOC" => ComposerSortOrder,
"TPE2" => AlbumArtist,
"TPE1" => TrackArtist,
"TEXT" => Writer,
"TCOM" => Composer,
"TPE3" => Conductor,
"TIPL" => InvolvedPeople,
"TEXT" => Lyricist,
"TMCL" => MusicianCredits,
"IPRO" => Producer,
"TPUB" => Publisher,
"TPUB" => Label,
"TRSN" => InternetRadioStationName,
"TRSO" => InternetRadioStationOwner,
"TPE4" => Remixer,
"TPOS" => DiscNumber,
"TPOS" => DiscTotal,
"TRCK" => TrackNumber,
"TRCK" => TrackTotal,
"POPM" => Popularimeter,
"TDRC" => RecordingDate,
"TDOR" => OriginalReleaseDate,
"TSRC" => ISRC,
"MVNM" => Movement,
"MVIN" => MovementIndex,
"TCMP" => FlagCompilation,
"PCST" => FlagPodcast,
"TFLT" => FileType,
"TOWN" => FileOwner,
"TDTG" => TaggingTime,
"TLEN" => Length,
"TOFN" => OriginalFileName,
"TMED" => OriginalMediaType,
"TENC" => EncodedBy,
"TSSE" => EncoderSoftware,
"TSSE" => EncoderSettings,
"TDEN" => EncodingTime,
"WOAF" => AudioFileURL,
"WOAS" => AudioSourceURL,
"WCOM" => CommercialInformationURL,
"WCOP" => CopyrightURL,
"WOAR" => TrackArtistURL,
"WORS" => RadioStationURL,
"WPAY" => PaymentURL,
"WPUB" => PublisherURL,
"TCON" => Genre,
"TLEY" => InitialKey,
"TMOO" => Mood,
"TBPM" => BPM,
"TCOP" => CopyrightMessage,
"TDES" => PodcastDescription,
"TCAT" => PodcastSeriesCategory,
"WFED" => PodcastURL,
"TDRL" => PodcastReleaseDate,
"TGID" => PodcastGlobalUniqueID,
"TKWD" => PodcastKeywords,
"COMM" => Comment,
"TLAN" => Language,
"USLT" => Lyrics
);
gen_map! (
#[cfg(feature = "mp4_ilst")]
ILST_MAP;
"\u{a9}alb" => AlbumTitle,
"----:com.apple.iTunes:DISCSUBTITLE" => SetSubtitle,
"tvsh" => ShowName,
"\u{a9}grp" => ContentGroup,
"\u{a9}nam" => TrackTitle,
"----:com.apple.iTunes:SUBTITLE" => TrackSubtitle,
"soal" => AlbumTitleSortOrder,
"soaa" => AlbumArtistSortOrder,
"sonm" => TrackTitleSortOrder,
"soar" => TrackArtistSortOrder,
"sosn" => ShowNameSortOrder,
"soco" => ComposerSortOrder,
"aART" => AlbumArtist,
"\u{a9}ART" => TrackArtist,
"\u{a9}wrt" => Composer,
"----:com.apple.iTunes:CONDUCTOR" => Conductor,
"----:com.apple.iTunes:ENGINEER" => Engineer,
"----:com.apple.iTunes:LYRICIST" => Lyricist,
"----:com.apple.iTunes:DJMIXER" => MixDj,
"----:com.apple.iTunes:MIXER" => MixEngineer,
"----:com.apple.iTunes:PRODUCER" => Producer,
"----:com.apple.iTunes:LABEL" => Label,
"----:com.apple.iTunes:REMIXER" => Remixer,
"disk" => DiscNumber,
"disk" => DiscTotal,
"trkn" => TrackNumber,
"trkn" => TrackTotal,
"rate" => LawRating,
"\u{a9}day" => RecordingDate,
"----:com.apple.iTunes:ISRC" => ISRC,
"----:com.apple.iTunes:BARCODE" => Barcode,
"----:com.apple.iTunes:CATALOGNUMBER" => CatalogNumber,
"cpil" => FlagCompilation,
"pcst" => FlagPodcast,
"----:com.apple.iTunes:MEDIA" => OriginalMediaType,
"\u{a9}too" => EncoderSoftware,
"\u{a9}gen" => Genre,
"----:com.apple.iTunes:MOOD" => Mood,
"tmpo" => BPM,
"cprt" => CopyrightMessage,
"----:com.apple.iTunes:LICENSE" => License,
"ldes" => PodcastDescription,
"catg" => PodcastSeriesCategory,
"purl" => PodcastURL,
"egid" => PodcastGlobalUniqueID,
"keyw" => PodcastKeywords,
"\u{a9}cmt" => Comment,
"desc" => Description,
"----:com.apple.iTunes:LANGUAGE" => Language,
"----:com.apple.iTunes:SCRIPT" => Script,
"\u{a9}lyr" => Lyrics
);
gen_map! (
#[cfg(feature = "riff_info_list")]
RIFF_INFO_MAP;
"IPRD" => AlbumTitle,
"INAM" => TrackTitle,
"IART" => TrackArtist,
"IWRI" => Writer,
"IMUS" => Composer,
"IPRO" => Producer,
"IPRT" | "ITRK" => TrackNumber,
"IFRM" => TrackTotal,
"IRTD" => LawRating,
"ICRD" => RecordingDate,
"ISRF" => OriginalMediaType,
"ITCH" => EncodedBy,
"ISFT" => EncoderSoftware,
"IGNR" => Genre,
"ICOP" => CopyrightMessage,
"ICMT" => Comment,
"ILNG" => Language
);
gen_map!(
#[cfg(feature = "vorbis_comments")]
VORBIS_MAP;
"ALBUM" => AlbumTitle,
"DISCSUBTITLE" => SetSubtitle,
"GROUPING" => ContentGroup,
"TITLE" => TrackTitle,
"SUBTITLE" => TrackSubtitle,
"ALBUMSORT" => AlbumTitleSortOrder,
"ALBUMARTISTSORT" => AlbumArtistSortOrder,
"TITLESORT" => TrackTitleSortOrder,
"ARTISTSORT" => TrackArtistSortOrder,
"ALBUMARTIST" => AlbumArtist,
"ARTIST" => TrackArtist,
"ARRANGER" => Arranger,
"AUTHOR" | "WRITER" => Writer,
"COMPOSER" => Composer,
"CONDUCTOR" => Conductor,
"ENGINEER" => Engineer,
"LYRICIST" => Lyricist,
"DJMIXER" => MixDj,
"MIXER" => MixEngineer,
"PERFORMER" => Performer,
"PRODUCER" => Producer,
"PUBLISHER" => Publisher,
"LABEL" => Label,
"REMIXER" => Remixer,
"DISCNUMBER" => DiscNumber,
"DISCTOTAL" | "TOTALDISCS" => DiscTotal,
"TRACKNUMBER" => TrackNumber,
"TRACKTOTAL" | "TOTALTRACKS" => TrackTotal,
"DATE" => RecordingDate,
"YEAR" => Year,
"ORIGINALDATE" => OriginalReleaseDate,
"ISRC" => ISRC,
"CATALOGNUMBER" => CatalogNumber,
"COMPILATION" => FlagCompilation,
"MEDIA" => OriginalMediaType,
"ENCODED-BY" => EncodedBy,
"ENCODER" => EncoderSoftware,
"ENCODING" | "ENCODERSETTINGS" => EncoderSettings,
"GENRE" => Genre,
"MOOD" => Mood,
"BPM" => BPM,
"COPYRIGHT" => CopyrightMessage,
"LICENSE" => License,
"COMMENT" => Comment,
"LANGUAGE" => Language,
"SCRIPT" => Script,
"LYRICS" => Lyrics
);
macro_rules! gen_item_keys {
(
MAPS => [
$(
$(#[$feat:meta])?
[$tag_type:pat, $MAP:ident]
),+
];
KEYS => [
$($variant:ident),+ $(,)?
]
) => {
#[derive(PartialEq, Clone, Debug, Eq, Hash)]
#[allow(missing_docs)]
#[non_exhaustive]
@ -41,372 +358,175 @@ macro_rules! item_keys {
pub fn from_key(tag_type: TagType, key: &str) -> Self {
match tag_type {
$(
$(
$($tag_type)|* if $(key.eq_ignore_ascii_case($key))||* => ItemKey::$variant,
)+
$(#[$feat])?
$tag_type => $MAP.get_item_key(key).unwrap_or_else(|| Self::Unknown(key.to_string())),
)+
_ => Self::Unknown(key.to_string()),
_ => Self::Unknown(key.to_string())
}
}
/// Maps the variant to a format-specific key
///
/// Use `allow_unknown` to include [`ItemKey::Unknown`]. It is up to the caller
/// to determine if the unknown key actually fits the format's specifications.
pub fn map_key(&self, tag_type: TagType, allow_unknown: bool) -> Option<&str> {
match (tag_type, self) {
match tag_type {
$(
$(
($($tag_type)|*, ItemKey::$variant) => Some(first_key!($($key)|*)),
)+
$(#[$feat])?
$tag_type => if let Some(key) = $MAP.get_key(self) {
return Some(key)
},
)+
(_, ItemKey::Unknown(unknown)) if allow_unknown => Some(&*unknown),
_ => None,
_ => {}
}
if let ItemKey::Unknown(ref unknown) = self {
if allow_unknown {
return Some(unknown)
}
}
None
}
}
};
}
}
item_keys!(
// Titles
AlbumTitle => [
TagType::Id3v2 => "TALB", TagType::Mp4Ilst => "\u{a9}alb",
TagType::VorbisComments => "ALBUM", TagType::Ape => "Album",
TagType::RiffInfo => "IPRD"
],
SetSubtitle => [
TagType::Id3v2 => "TSST", TagType::Mp4Ilst => "----:com.apple.iTunes:DISCSUBTITLE",
TagType::VorbisComments => "DISCSUBTITLE", TagType::Ape => "DiscSubtitle"
],
ShowName => [
TagType::Mp4Ilst => "tvsh"
],
ContentGroup => [
TagType::Id3v2 => "TIT1" | "GRP1", TagType::Mp4Ilst => "\u{a9}grp",
TagType::VorbisComments => "GROUPING", TagType::Ape => "Grouping"
],
TrackTitle => [
TagType::Id3v2 => "TIT2", TagType::Mp4Ilst => "\u{a9}nam",
TagType::VorbisComments => "TITLE", TagType::Ape => "Title",
TagType::RiffInfo => "INAM", TagType::AiffText => "NAME"
],
TrackSubtitle => [
TagType::Id3v2 => "TIT3", TagType::Mp4Ilst => "----:com.apple.iTunes:SUBTITLE",
TagType::VorbisComments => "SUBTITLE", TagType::Ape => "Subtitle"
],
gen_item_keys!(
MAPS => [
#[cfg(feature = "aiff_text_chunks")]
[TagType::AiffText, AIFF_TEXT_MAP],
// Original names
OriginalAlbumTitle => [
TagType::Id3v2 => "TOAL"
],
OriginalArtist => [
TagType::Id3v2 => "TOPE"
],
OriginalLyricist => [
TagType::Id3v2 => "TOLY"
],
#[cfg(feature = "ape")]
[TagType::Ape, APE_MAP],
// Sorting
AlbumTitleSortOrder => [
TagType::Id3v2 => "TSOA", TagType::Mp4Ilst => "soal",
TagType::VorbisComments | TagType::Ape => "ALBUMSORT"
],
AlbumArtistSortOrder => [
TagType::Id3v2 => "TSO2", TagType::Mp4Ilst => "soaa",
TagType::VorbisComments | TagType::Ape => "ALBUMARTISTSORT"
],
TrackTitleSortOrder => [
TagType::Id3v2 => "TSOT", TagType::Mp4Ilst => "sonm",
TagType::VorbisComments | TagType::Ape => "TITLESORT"
],
TrackArtistSortOrder => [
TagType::Id3v2 => "TSOP", TagType::Mp4Ilst => "soar",
TagType::VorbisComments | TagType::Ape => "ARTISTSORT"
],
ShowNameSortOrder => [
TagType::Mp4Ilst => "sosn"
],
ComposerSortOrder => [
TagType::Id3v2 => "TSOC", TagType::Mp4Ilst => "soco"
],
#[cfg(feature = "id3v2")]
[TagType::Id3v2, ID3V2_MAP],
#[cfg(feature = "mp4_ilst")]
[TagType::Mp4Ilst, ILST_MAP],
// People & Organizations
AlbumArtist => [
TagType::Id3v2 => "TPE2", TagType::Mp4Ilst => "aART",
TagType::VorbisComments => "ALBUMARTIST", TagType::Ape => "Album Artist" | "ALBUMARTIST"
],
TrackArtist => [
TagType::Id3v2 => "TPE1", TagType::Mp4Ilst => "\u{a9}ART",
TagType::VorbisComments => "ARTIST", TagType::Ape => "Artist",
TagType::RiffInfo => "IART", TagType::AiffText => "AUTH"
],
Arranger => [
TagType::VorbisComments => "ARRANGER", TagType::Ape => "Arranger"
],
Writer => [
TagType::Id3v2 => "TEXT",
TagType::VorbisComments => "AUTHOR" | "WRITER", TagType::Ape => "Writer",
TagType::RiffInfo => "IWRI"
],
Composer => [
TagType::Id3v2 => "TCOM", TagType::Mp4Ilst => "\u{a9}wrt",
TagType::VorbisComments => "COMPOSER", TagType::Ape => "Composer",
TagType::RiffInfo => "IMUS"
],
Conductor => [
TagType::Id3v2 => "TPE3", TagType::Mp4Ilst => "----:com.apple.iTunes:CONDUCTOR",
TagType::VorbisComments => "CONDUCTOR", TagType::Ape => "Conductor"
],
Engineer => [
TagType::Mp4Ilst => "----:com.apple.iTunes:ENGINEER", TagType::VorbisComments => "ENGINEER",
TagType::Ape => "Engineer"
],
InvolvedPeople => [
TagType::Id3v2 => "TIPL"
],
Lyricist => [
TagType::Id3v2 => "TEXT", TagType::Mp4Ilst => "----:com.apple.iTunes:LYRICIST",
TagType::VorbisComments => "LYRICIST", TagType::Ape => "Lyricist"
],
MixDj => [
TagType::Mp4Ilst => "----:com.apple.iTunes:DJMIXER", TagType::VorbisComments => "DJMIXER",
TagType::Ape => "DjMixer"
],
MixEngineer => [
TagType::Mp4Ilst => "----:com.apple.iTunes:MIXER", TagType::VorbisComments => "MIXER",
TagType::Ape => "Mixer"
],
MusicianCredits => [
TagType::Id3v2 => "TMCL"
],
Performer => [
TagType::VorbisComments => "PERFORMER", TagType::Ape => "Performer"
],
Producer => [
TagType::Mp4Ilst => "----:com.apple.iTunes:PRODUCER", TagType::VorbisComments => "PRODUCER",
TagType::Ape => "Producer", TagType::RiffInfo => "IPRO"
],
Publisher => [
TagType::Id3v2 => "TPUB", TagType::VorbisComments => "PUBLISHER"
],
Label => [
TagType::Id3v2 => "TPUB", TagType::Mp4Ilst => "----:com.apple.iTunes:LABEL",
TagType::VorbisComments => "LABEL", TagType::Ape => "Label"
],
InternetRadioStationName => [
TagType::Id3v2 => "TRSN"
],
InternetRadioStationOwner => [
TagType::Id3v2 => "TRSO"
],
Remixer => [
TagType::Id3v2 => "TPE4", TagType::Mp4Ilst => "----:com.apple.iTunes:REMIXER",
TagType::VorbisComments => "REMIXER", TagType::Ape => "MixArtist"
],
#[cfg(feature = "riff_info_list")]
[TagType::RiffInfo, RIFF_INFO_MAP],
// Counts & Indexes
DiscNumber => [
TagType::Id3v2 => "TPOS", TagType::Mp4Ilst => "disk",
TagType::VorbisComments => "DISCNUMBER", TagType::Ape => "Disc"
],
DiscTotal => [
TagType::Id3v2 => "TPOS", TagType::Mp4Ilst => "disk",
TagType::VorbisComments => "DISCTOTAL" | "TOTALDISCS", TagType::Ape => "Disc"
],
TrackNumber => [
TagType::Id3v2 => "TRCK", TagType::Mp4Ilst => "trkn",
TagType::VorbisComments => "TRACKNUMBER", TagType::Ape => "Track",
TagType::RiffInfo => "IPRT" | "ITRK"
],
TrackTotal => [
TagType::Id3v2 => "TRCK", TagType::Mp4Ilst => "trkn",
TagType::VorbisComments => "TRACKTOTAL" | "TOTALTRACKS", TagType::Ape => "Track",
TagType::RiffInfo => "IFRM"
],
Popularimeter => [
TagType::Id3v2 => "POPM"
],
LawRating => [
TagType::Mp4Ilst => "rate", TagType::RiffInfo => "IRTD"
],
#[cfg(feature = "vorbis_comments")]
[TagType::VorbisComments, VORBIS_MAP]
];
// Dates
RecordingDate => [
TagType::Id3v2 => "TDRC", TagType::Mp4Ilst => "\u{a9}day",
TagType::VorbisComments => "DATE", TagType::RiffInfo => "ICRD"
],
Year => [
TagType::Id3v2 => "TDRC", TagType::VorbisComments => "DATE" | "YEAR",
TagType::Ape => "Year"
],
OriginalReleaseDate => [
TagType::Id3v2 => "TDOR", TagType::VorbisComments => "ORIGINALDATE"
],
KEYS => [
// Titles
AlbumTitle,
SetSubtitle,
ShowName,
ContentGroup,
TrackTitle,
TrackSubtitle,
// Identifiers
ISRC => [
TagType::Id3v2 => "TSRC", TagType::Mp4Ilst => "----:com.apple.iTunes:ISRC",
TagType::VorbisComments => "ISRC", TagType::Ape => "ISRC"
],
Barcode => [
TagType::Mp4Ilst => "----:com.apple.iTunes:BARCODE", TagType::Ape => "Barcode"
],
CatalogNumber => [
TagType::Mp4Ilst => "----:com.apple.iTunes:CATALOGNUMBER", TagType::VorbisComments => "CATALOGNUMBER",
TagType::Ape => "CatalogNumber"
],
Movement => [
TagType::Id3v2 => "MVNM"
],
MovementIndex => [
TagType::Id3v2 => "MVIN"
],
// Original names
OriginalAlbumTitle,
OriginalArtist,
OriginalLyricist,
// Flags
FlagCompilation => [
TagType::Id3v2 => "TCMP", TagType::Mp4Ilst => "cpil",
TagType::VorbisComments => "COMPILATION", TagType::Ape => "Compilation"
],
FlagPodcast => [
TagType::Id3v2 => "PCST", TagType::Mp4Ilst => "pcst"
],
// Sorting
AlbumTitleSortOrder,
AlbumArtistSortOrder,
TrackTitleSortOrder,
TrackArtistSortOrder,
ShowNameSortOrder,
ComposerSortOrder,
// File information
FileType => [
TagType::Id3v2 => "TFLT"
],
FileOwner => [
TagType::Id3v2 => "TOWN"
],
TaggingTime => [
TagType::Id3v2 => "TDTG"
],
Length => [
TagType::Id3v2 => "TLEN"
],
OriginalFileName => [
TagType::Id3v2 => "TOFN"
],
OriginalMediaType => [
TagType::Id3v2 => "TMED", TagType::Mp4Ilst => "----:com.apple.iTunes:MEDIA",
TagType::VorbisComments => "MEDIA", TagType::Ape => "Media",
TagType::RiffInfo => "ISRF"
],
// People & Organizations
AlbumArtist,
TrackArtist,
Arranger,
Writer,
Composer,
Conductor,
Engineer,
InvolvedPeople,
Lyricist,
MixDj,
MixEngineer,
MusicianCredits,
Performer,
Producer,
Publisher,
Label,
InternetRadioStationName,
InternetRadioStationOwner,
Remixer,
// Encoder information
EncodedBy => [
TagType::Id3v2 => "TENC", TagType::VorbisComments => "ENCODED-BY",
TagType::Ape => "EncodedBy", TagType::RiffInfo => "ITCH"
],
EncoderSoftware => [
TagType::Id3v2 => "TSSE", TagType::Mp4Ilst => "\u{a9}too",
TagType::VorbisComments => "ENCODER", TagType::RiffInfo => "ISFT"
],
EncoderSettings => [
TagType::Id3v2 => "TSSE", TagType::VorbisComments => "ENCODING" | "ENCODERSETTINGS"
],
EncodingTime => [
TagType::Id3v2 => "TDEN"
],
// Counts & Indexes
DiscNumber,
DiscTotal,
TrackNumber,
TrackTotal,
Popularimeter,
LawRating,
// URLs
AudioFileURL => [
TagType::Id3v2 => "WOAF"
],
AudioSourceURL => [
TagType::Id3v2 => "WOAS"
],
CommercialInformationURL => [
TagType::Id3v2 => "WCOM"
],
CopyrightURL => [
TagType::Id3v2 => "WCOP"
],
TrackArtistURL => [
TagType::Id3v2 => "WOAR"
],
RadioStationURL => [
TagType::Id3v2 => "WORS"
],
PaymentURL => [
TagType::Id3v2 => "WPAY"
],
PublisherURL => [
TagType::Id3v2 => "WPUB"
],
// Dates
RecordingDate,
Year,
OriginalReleaseDate,
// Identifiers
ISRC,
Barcode,
CatalogNumber,
Movement,
MovementIndex,
// Style
Genre => [
TagType::Id3v2 => "TCON", TagType::Mp4Ilst => "\u{a9}gen",
TagType::VorbisComments => "GENRE", TagType::RiffInfo => "IGNR",
TagType::Ape => "Genre"
],
InitialKey => [
TagType::Id3v2 => "TKEY"
],
Mood => [
TagType::Id3v2 => "TMOO", TagType::Mp4Ilst => "----:com.apple.iTunes:MOOD",
TagType::VorbisComments => "MOOD", TagType::Ape => "Mood"
],
BPM => [
TagType::Id3v2 => "TBPM", TagType::Mp4Ilst => "tmpo",
TagType::VorbisComments => "BPM"
],
// Flags
FlagCompilation,
FlagPodcast,
// Legal
CopyrightMessage => [
TagType::Id3v2 => "TCOP", TagType::Mp4Ilst => "cprt",
TagType::VorbisComments => "COPYRIGHT", TagType::Ape => "Copyright",
TagType::RiffInfo => "ICOP", TagType::AiffText => "(c) "
],
License => [
TagType::Mp4Ilst => "----:com.apple.iTunes:LICENSE", TagType::VorbisComments => "LICENSE"
],
// File Information
FileType,
FileOwner,
TaggingTime,
Length,
OriginalFileName,
OriginalMediaType,
// Podcast
PodcastDescription => [
TagType::Id3v2 => "TDES", TagType::Mp4Ilst => "ldes"
],
PodcastSeriesCategory => [
TagType::Id3v2 => "TCAT", TagType::Mp4Ilst => "catg"
],
PodcastURL => [
TagType::Id3v2 => "WFED", TagType::Mp4Ilst => "purl"
],
PodcastReleaseDate => [
TagType::Id3v2 => "TDRL"
],
PodcastGlobalUniqueID => [
TagType::Id3v2 => "TGID", TagType::Mp4Ilst => "egid"
],
PodcastKeywords => [
TagType::Id3v2 => "TKWD", TagType::Mp4Ilst => "keyw"
],
// Encoder information
EncodedBy,
EncoderSoftware,
EncoderSettings,
EncodingTime,
// Miscellaneous
Comment => [
TagType::Id3v2 => "COMM", TagType::Mp4Ilst => "\u{a9}cmt",
TagType::VorbisComments => "COMMENT", TagType::Ape => "Comment",
TagType::RiffInfo => "ICMT"
],
Description => [
TagType::Mp4Ilst => "desc"
],
Language => [
TagType::Id3v2 => "TLAN", TagType::Mp4Ilst => "----:com.apple.iTunes:LANGUAGE",
TagType::VorbisComments => "LANGUAGE", TagType::Ape => "language",
TagType::RiffInfo => "ILNG"
],
Script => [
TagType::Mp4Ilst => "----:com.apple.iTunes:SCRIPT", TagType::VorbisComments => "SCRIPT",
TagType::Ape => "Script"
],
Lyrics => [
TagType::Id3v2 => "USLT", TagType::Mp4Ilst => "\u{a9}lyr",
TagType::VorbisComments => "LYRICS", TagType::Ape => "Lyrics"
// URLs
AudioFileURL,
AudioSourceURL,
CommercialInformationURL,
CopyrightURL,
TrackArtistURL,
RadioStationURL,
PaymentURL,
PublisherURL,
// Style
Genre,
InitialKey,
Mood,
BPM,
// Legal
CopyrightMessage,
License,
// Podcast
PodcastDescription,
PodcastSeriesCategory,
PodcastURL,
PodcastReleaseDate,
PodcastGlobalUniqueID,
PodcastKeywords,
// Miscellaneous
Comment,
Description,
Language,
Script,
Lyrics,
]
);
@ -485,7 +605,10 @@ impl TagItem {
}
pub(crate) fn re_map(&self, tag_type: TagType) -> Option<()> {
#[cfg(feature = "id3v1")]
if tag_type == TagType::Id3v1 {
use crate::logic::id3::v1::constants::VALID_ITEMKEYS;
return VALID_ITEMKEYS.contains(&self.item_key).then(|| ());
}

View file

@ -3,9 +3,12 @@ use crate::{LoftyError, Result};
use {crate::logic::id3::v2::util::text_utils::TextEncoding, crate::logic::id3::v2::Id3v2Version};
use std::borrow::Cow;
#[cfg(feature = "id3v2")]
use std::io::Write;
use std::io::{Cursor, Read};
use std::io::{Seek, SeekFrom, Write};
use std::io::{Seek, SeekFrom};
#[cfg(feature = "id3v2")]
use byteorder::WriteBytesExt;
#[cfg(any(feature = "vorbis_comments", feature = "id3v2",))]
use byteorder::{BigEndian, ReadBytesExt};

View file

@ -312,25 +312,18 @@ impl Tag {
/// The tag's format
#[derive(Copy, Clone, Debug, PartialEq)]
pub enum TagType {
#[cfg(feature = "ape")]
/// This covers both APEv1 and APEv2 as it doesn't matter much
Ape,
#[cfg(feature = "id3v1")]
/// Represents an ID3v1 tag
Id3v1,
#[cfg(feature = "id3v2")]
/// This covers all ID3v2 versions since they all get upgraded to ID3v2.4
Id3v2,
#[cfg(feature = "mp4_ilst")]
/// Represents an MP4 ILST atom
Mp4Ilst,
#[cfg(feature = "vorbis_comments")]
/// Represents vorbis comments
VorbisComments,
#[cfg(feature = "riff_info_list")]
/// Represents a RIFF INFO LIST
RiffInfo,
#[cfg(feature = "aiff_text_chunks")]
/// Represents AIFF text chunks
AiffText,
}