mirror of
https://github.com/Serial-ATA/lofty-rs
synced 2024-12-13 14:12:31 +00:00
Resolve feature issues
This commit is contained in:
parent
a23e285c8f
commit
e5d2c4dc1f
39 changed files with 1554 additions and 1070 deletions
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
13
src/lib.rs
13
src/lib.rs
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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))?;
|
||||
|
|
351
src/logic/ape/tag/ape_tag.rs
Normal file
351
src/logic/ape/tag/ape_tag.rs
Normal 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");
|
||||
}
|
||||
}
|
|
@ -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 })
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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),
|
||||
}
|
||||
|
|
|
@ -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
23
src/logic/id3/v2/flags.rs
Normal 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),
|
||||
}
|
|
@ -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],
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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)),
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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),
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)))?;
|
||||
},
|
||||
|
|
|
@ -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),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
)*
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
},
|
||||
|
|
|
@ -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),
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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};
|
||||
|
||||
|
|
|
@ -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)?,
|
||||
})
|
||||
|
|
|
@ -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};
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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(|| ());
|
||||
}
|
||||
|
||||
|
|
|
@ -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};
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue