mirror of
https://github.com/Serial-ATA/lofty-rs
synced 2024-11-10 06:34:18 +00:00
Add format specific tag structs
This allows for the use of format-specific elements, such as ID3v2 frame headers
This commit is contained in:
parent
e84731c375
commit
bc0c246dbf
83 changed files with 3218 additions and 2247 deletions
|
@ -17,14 +17,12 @@ ogg_pager = "0.1.7"
|
|||
# Mp4
|
||||
simdutf8 = { version = "0.1.3", optional = true }
|
||||
|
||||
# Quick string accessor methods for Tag
|
||||
paste = { version = "1.0.5", optional = true }
|
||||
|
||||
paste = "1.0.5"
|
||||
base64 = "0.13.0"
|
||||
byteorder = "1.4.3"
|
||||
|
||||
[features]
|
||||
default = ["mp4_atoms", "vorbis_comments", "ape", "id3v1", "id3v2", "aiff_text_chunks", "riff_info_list", "quick_tag_accessors"]
|
||||
default = ["mp4_atoms", "vorbis_comments", "ape", "id3v1", "id3v2", "aiff_text_chunks", "riff_info_list"]
|
||||
mp4_atoms = ["simdutf8"]
|
||||
vorbis_comments = []
|
||||
ape = []
|
||||
|
@ -33,7 +31,6 @@ id3v2 = ["flate2"]
|
|||
id3v2_restrictions = []
|
||||
aiff_text_chunks = []
|
||||
riff_info_list = []
|
||||
quick_tag_accessors = ["paste"]
|
||||
|
||||
[dev-dependencies]
|
||||
criterion = { version = "0.3.5", features = ["html_reports"] }
|
||||
|
|
|
@ -10,7 +10,7 @@ Parse, convert, and write metadata to various audio formats.
|
|||
|
||||
| File Format | Extensions | Read | Write | Metadata Format(s) |
|
||||
|-------------|-------------------------------------------------|------|-------|----------------------------------------------------|
|
||||
| Ape | `ape` |**X** |**X** |`APEv2`, `APEv1`, `ID3v2` (Not officially), `ID3v1` |
|
||||
| Ape | `ape` |**X** |**X** |`APEv2`, `APEv1`, `ID3v2` (Read only), `ID3v1` |
|
||||
| AIFF | `aiff`, `aif` |**X** |**X** |`ID3v2`, `Text Chunks` |
|
||||
| FLAC | `flac` |**X** |**X** |`Vorbis Comments` |
|
||||
| MP3 | `mp3` |**X** |**X** |`ID3v2`, `ID3v1`, `APEv2`, `APEv1` |
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
use ogg_pager::PageError;
|
||||
use std::fmt::{Display, Formatter};
|
||||
|
||||
/// Result of tag operations.
|
||||
/// Result of tag operations
|
||||
pub type Result<T> = std::result::Result<T, LoftyError>;
|
||||
|
||||
/// Errors that could occur within Lofty.
|
||||
/// Errors that could occur within Lofty
|
||||
#[derive(Debug)]
|
||||
pub enum LoftyError {
|
||||
// File extension/format related errors
|
||||
|
@ -75,7 +75,6 @@ pub enum LoftyError {
|
|||
|
||||
// Conversions for external errors
|
||||
/// Errors that arise while parsing OGG pages
|
||||
#[cfg(feature = "vorbis_comments")]
|
||||
OggPage(ogg_pager::PageError),
|
||||
/// Unable to convert bytes to a String
|
||||
FromUtf8(std::string::FromUtf8Error),
|
||||
|
@ -87,7 +86,6 @@ impl Display for LoftyError {
|
|||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
// Conversions
|
||||
#[cfg(feature = "vorbis_comments")]
|
||||
LoftyError::OggPage(ref err) => write!(f, "{}", err),
|
||||
LoftyError::FromUtf8(ref err) => write!(f, "{}", err),
|
||||
LoftyError::Io(ref err) => write!(f, "{}", err),
|
||||
|
|
73
src/lib.rs
73
src/lib.rs
|
@ -112,7 +112,8 @@
|
|||
//!
|
||||
//! See [`id3`](crate::id3) for important warnings and notes on reading tags.
|
||||
|
||||
#![deny(clippy::pedantic, clippy::all, missing_docs)]
|
||||
#![deny(clippy::pedantic, clippy::all)]
|
||||
// TODO missing_docs
|
||||
#![allow(
|
||||
clippy::too_many_lines,
|
||||
clippy::cast_precision_loss,
|
||||
|
@ -127,7 +128,9 @@
|
|||
clippy::semicolon_if_nothing_returned,
|
||||
clippy::used_underscore_binding,
|
||||
clippy::new_without_default,
|
||||
clippy::unused_self
|
||||
clippy::unused_self,
|
||||
clippy::from_over_into,
|
||||
clippy::upper_case_acronyms
|
||||
)]
|
||||
|
||||
pub use crate::error::{LoftyError, Result};
|
||||
|
@ -141,12 +144,6 @@ pub use crate::types::{
|
|||
tag::{Tag, TagType},
|
||||
};
|
||||
|
||||
#[cfg(any(feature = "id3v2", feature = "ape"))]
|
||||
pub use crate::types::item::TagItemFlags;
|
||||
|
||||
#[cfg(feature = "id3v2")]
|
||||
pub use crate::types::tag::TagFlags;
|
||||
|
||||
mod types;
|
||||
|
||||
/// Various concrete file types, used when inference is unnecessary
|
||||
|
@ -154,7 +151,10 @@ pub mod files {
|
|||
pub use crate::logic::ape::{ApeFile, ApeProperties};
|
||||
pub use crate::logic::iff::{
|
||||
aiff::AiffFile,
|
||||
wav::{WavFile, WavFormat, WavProperties},
|
||||
wav::{
|
||||
properties::{WavFormat, WavProperties},
|
||||
WavFile,
|
||||
},
|
||||
};
|
||||
pub use crate::logic::mp3::{
|
||||
header::{ChannelMode, Layer, MpegVersion},
|
||||
|
@ -163,12 +163,19 @@ pub mod files {
|
|||
pub use crate::logic::mp4::{Mp4Codec, Mp4File, Mp4Properties};
|
||||
pub use crate::logic::ogg::{
|
||||
flac::FlacFile,
|
||||
opus::{OpusFile, OpusProperties},
|
||||
vorbis::{VorbisFile, VorbisProperties},
|
||||
opus::{properties::OpusProperties, OpusFile},
|
||||
vorbis::{properties::VorbisProperties, VorbisFile},
|
||||
};
|
||||
pub use crate::types::file::AudioFile;
|
||||
}
|
||||
|
||||
/// Various concrete tag types, used when format-specific features are necessary
|
||||
pub mod tags {
|
||||
pub use crate::logic::id3::v1::tag::Id3v1Tag;
|
||||
pub use crate::logic::iff::{aiff::tag::AiffTextChunks, wav::tag::RiffInfoList};
|
||||
pub use crate::logic::ogg::tag::VorbisComments;
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "id3v1", feature = "id3v2"))]
|
||||
/// ID3v1/v2 specific items
|
||||
pub mod id3 {
|
||||
|
@ -180,48 +187,19 @@ pub mod id3 {
|
|||
//! ID3v2 items and utilities
|
||||
//!
|
||||
//! # ID3v2 notes and warnings
|
||||
//!
|
||||
//! ID3v2 does things differently than other formats.
|
||||
//!
|
||||
//! ## Unknown Keys
|
||||
//!
|
||||
//! ID3v2 **does not** support [`ItemKey::Unknown`](crate::ItemKey::Unknown) and they will be ignored.
|
||||
//! Instead, [`ItemKey::Id3v2Specific`](crate::ItemKey::Id3v2Specific) with an [`Id3v2Frame`](crate::id3::v2::Id3v2Frame) variant must be used.
|
||||
//!
|
||||
//! ## Frame ID mappings
|
||||
//!
|
||||
//! Certain [`ItemKey`](crate::ItemKey)s are unable to map to an ID3v2 frame, as they are a part of a larger
|
||||
//! collection (such as `TIPL` and `TMCL`).
|
||||
//!
|
||||
//! For example, if the key is `Arranger` (part of `TIPL`), there is no mapping available.
|
||||
//!
|
||||
//! In this case, the caller is expected to build these lists. If these [`ItemKey`](crate::ItemKey)s are inserted
|
||||
//! using [`Tag::insert_item_unchecked`](crate::Tag::insert_item_unchecked), they will simply be ignored.
|
||||
//!
|
||||
//! ## Special frames
|
||||
//!
|
||||
//! ID3v2 has multiple frames that have no equivalent in other formats:
|
||||
//!
|
||||
//! * COMM - Comments (Unlike comments in other formats)
|
||||
//! * USLT - Unsynchronized text (Unlike lyrics/text in other formats)
|
||||
//! * TXXX - User defined text
|
||||
//! * WXXX - User defined URL
|
||||
//! * SYLT - Synchronized text
|
||||
//! * GEOB - Encapsulated object (file)
|
||||
//!
|
||||
//! These frames all require different amounts of information, so they cannot be mapped to a traditional [`ItemKey`](crate::ItemKey) variant.
|
||||
//! The solution is to use [`ItemKey::Id3v2Specific`](crate::ItemKey::Id3v2Specific) alongside [`Id3v2Frame`](crate::id3::v2::Id3v2Frame).
|
||||
//!
|
||||
//! NOTE: Unlike the above issue, this one does not require unchecked insertion.
|
||||
// TODO
|
||||
|
||||
pub use {
|
||||
crate::logic::id3::v2::frame::{Id3v2Frame, LanguageSpecificFrame},
|
||||
crate::logic::id3::v2::frame::{
|
||||
EncodedTextFrame, Frame, FrameFlags, FrameID, FrameValue, LanguageFrame,
|
||||
},
|
||||
crate::logic::id3::v2::items::encapsulated_object::{
|
||||
GEOBInformation, GeneralEncapsulatedObject,
|
||||
},
|
||||
crate::logic::id3::v2::items::sync_text::{
|
||||
SyncTextContentType, SyncTextInformation, SynchronizedText, TimestampFormat,
|
||||
},
|
||||
crate::logic::id3::v2::tag::{Id3v2Tag, Id3v2TagFlags},
|
||||
crate::logic::id3::v2::util::text_utils::TextEncoding,
|
||||
crate::logic::id3::v2::util::upgrade::{upgrade_v2, upgrade_v3},
|
||||
crate::logic::id3::v2::Id3v2Version,
|
||||
|
@ -253,6 +231,11 @@ pub mod id3 {
|
|||
}
|
||||
}
|
||||
|
||||
/// MP4 specific items
|
||||
pub mod mp4 {
|
||||
pub use crate::logic::mp4::ilst::{Atom, AtomData, AtomIdent};
|
||||
}
|
||||
|
||||
/// Various items related to [`Picture`](crate::picture::Picture)s
|
||||
pub mod picture {
|
||||
pub use crate::types::picture::{MimeType, Picture, PictureInformation, PictureType};
|
||||
|
|
|
@ -1,11 +1,18 @@
|
|||
mod constants;
|
||||
mod properties;
|
||||
pub(crate) mod read;
|
||||
#[cfg(feature = "ape")]
|
||||
pub(crate) mod tag;
|
||||
pub(crate) mod write;
|
||||
|
||||
#[cfg(feature = "id3v1")]
|
||||
use crate::logic::id3::v1::tag::Id3v1Tag;
|
||||
use crate::logic::id3::v2::tag::Id3v2Tag;
|
||||
use crate::logic::tag_methods;
|
||||
use crate::types::file::{AudioFile, FileType, TaggedFile};
|
||||
use crate::{FileProperties, Result, Tag, TagType};
|
||||
use crate::{FileProperties, Result, TagType};
|
||||
|
||||
use tag::ApeTag;
|
||||
|
||||
use std::io::{Read, Seek};
|
||||
use std::time::Duration;
|
||||
|
@ -61,13 +68,13 @@ impl ApeProperties {
|
|||
pub struct ApeFile {
|
||||
#[cfg(feature = "id3v1")]
|
||||
/// An ID3v1 tag
|
||||
pub(crate) id3v1: Option<Tag>,
|
||||
pub(crate) id3v1_tag: Option<Id3v1Tag>,
|
||||
#[cfg(feature = "id3v2")]
|
||||
/// An ID3v2 tag (Not officially supported)
|
||||
pub(crate) id3v2: Option<Tag>,
|
||||
pub(crate) id3v2_tag: Option<Id3v2Tag>,
|
||||
#[cfg(feature = "ape")]
|
||||
/// An APEv1/v2 tag
|
||||
pub(crate) ape: Option<Tag>,
|
||||
pub(crate) ape_tag: Option<ApeTag>,
|
||||
/// The file's audio properties
|
||||
pub(crate) properties: ApeProperties,
|
||||
}
|
||||
|
@ -77,10 +84,14 @@ impl From<ApeFile> for TaggedFile {
|
|||
Self {
|
||||
ty: FileType::APE,
|
||||
properties: FileProperties::from(input.properties),
|
||||
tags: vec![input.id3v1, input.id3v2, input.ape]
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.collect(),
|
||||
tags: vec![
|
||||
input.ape_tag.map(|at| at.into()),
|
||||
input.id3v1_tag.map(|id3v1| id3v1.into()),
|
||||
input.id3v2_tag.map(|id3v2| id3v2.into()),
|
||||
]
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -100,54 +111,38 @@ impl AudioFile for ApeFile {
|
|||
&self.properties
|
||||
}
|
||||
|
||||
#[allow(clippy::match_same_arms)]
|
||||
fn contains_tag(&self) -> bool {
|
||||
self.ape.is_some() || self.id3v1.is_some() || self.id3v2.is_some()
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
fn contains_tag_type(&self, tag_type: &TagType) -> bool {
|
||||
match tag_type {
|
||||
TagType::Ape => self.ape.is_some(),
|
||||
TagType::Id3v1 => self.id3v1.is_some(),
|
||||
TagType::Id3v2 => self.id3v2.is_some(),
|
||||
#[cfg(feature = "ape")]
|
||||
TagType::Ape => self.ape_tag.is_some(),
|
||||
#[cfg(feature = "id3v1")]
|
||||
TagType::Id3v1 => self.id3v1_tag.is_some(),
|
||||
#[cfg(feature = "id3v2")]
|
||||
TagType::Id3v2 => self.id3v2_tag.is_some(),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ApeFile {
|
||||
#[cfg(feature = "id3v2")]
|
||||
/// Returns a reference to the ID3v2 tag if it exists
|
||||
pub fn id3v2_tag(&self) -> Option<&Tag> {
|
||||
self.id3v2.as_ref()
|
||||
}
|
||||
|
||||
#[cfg(feature = "id3v2")]
|
||||
/// Returns a mutable reference to the ID3v2 tag if it exists
|
||||
pub fn id3v2_tag_mut(&mut self) -> Option<&mut Tag> {
|
||||
self.id3v2.as_mut()
|
||||
}
|
||||
|
||||
#[cfg(feature = "id3v1")]
|
||||
/// Returns a reference to the ID3v1 tag if it exists
|
||||
pub fn id3v1_tag(&self) -> Option<&Tag> {
|
||||
self.id3v1.as_ref()
|
||||
}
|
||||
|
||||
#[cfg(feature = "id3v1")]
|
||||
/// Returns a mutable reference to the ID3v1 tag if it exists
|
||||
pub fn id3v1_tag_mut(&mut self) -> Option<&mut Tag> {
|
||||
self.id3v1.as_mut()
|
||||
}
|
||||
|
||||
#[cfg(feature = "ape")]
|
||||
/// Returns a reference to the APEv1/2 tag if it exists
|
||||
pub fn ape_tag(&self) -> Option<&Tag> {
|
||||
self.ape.as_ref()
|
||||
}
|
||||
|
||||
#[cfg(feature = "ape")]
|
||||
/// Returns a mutable reference to the APEv1/2 tag if it exists
|
||||
pub fn ape_tag_mut(&mut self) -> Option<&mut Tag> {
|
||||
self.ape.as_mut()
|
||||
}
|
||||
tag_methods! {
|
||||
ApeFile => ID3v2, id3v2_tag, Id3v2Tag; ID3v1, id3v1_tag, Id3v1Tag; APE, ape_tag, ApeTag
|
||||
}
|
||||
|
|
|
@ -3,14 +3,17 @@ use super::properties::{properties_gt_3980, properties_lt_3980};
|
|||
use super::tag::read::read_ape_tag;
|
||||
use super::{ApeFile, ApeProperties};
|
||||
use crate::error::{LoftyError, Result};
|
||||
use crate::logic::id3::find_lyrics3v2;
|
||||
use crate::logic::id3::v1::find_id3v1;
|
||||
use crate::logic::id3::v2::find_id3v2;
|
||||
use crate::logic::id3::v2::read::parse_id3v2;
|
||||
use crate::types::tag::Tag;
|
||||
#[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 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) -> Result<ApeProperties>
|
||||
|
@ -40,22 +43,22 @@ where
|
|||
|
||||
let mut stream_len = end - start;
|
||||
|
||||
let mut id3v2: Option<Tag> = None;
|
||||
let mut id3v1: Option<Tag> = None;
|
||||
let mut ape: Option<Tag> = None;
|
||||
let mut id3v2_tag: Option<Id3v2Tag> = None;
|
||||
let mut id3v1_tag: Option<Id3v1Tag> = None;
|
||||
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;
|
||||
|
||||
let id3v2_tag = parse_id3v2(&mut &*id3v2_read)?;
|
||||
let id3v2 = parse_id3v2(&mut &*id3v2_read)?;
|
||||
|
||||
// Skip over the footer
|
||||
if id3v2_tag.flags().footer {
|
||||
if id3v2.flags().footer {
|
||||
data.seek(SeekFrom::Current(10))?;
|
||||
}
|
||||
|
||||
id3v2 = Some(id3v2_tag)
|
||||
id3v2_tag = Some(id3v2)
|
||||
}
|
||||
|
||||
let mut found_mac = false;
|
||||
|
@ -86,10 +89,10 @@ where
|
|||
return Err(LoftyError::Ape("Found incomplete APE tag"));
|
||||
}
|
||||
|
||||
let (ape_tag, size) = read_ape_tag(data, false)?;
|
||||
|
||||
let (ape, size) = read_ape_tag(data, false)?;
|
||||
stream_len -= u64::from(size);
|
||||
ape = Some(ape_tag)
|
||||
|
||||
ape_tag = Some(ape)
|
||||
}
|
||||
_ => {
|
||||
return Err(LoftyError::Ape(
|
||||
|
@ -104,11 +107,11 @@ where
|
|||
//
|
||||
// Starts with ['T', 'A', 'G']
|
||||
// Exactly 128 bytes long (including the identifier)
|
||||
let (found_id3v1, id3v1_tag) = find_id3v1(data, true)?;
|
||||
let (found_id3v1, id3v1) = find_id3v1(data, true)?;
|
||||
|
||||
if found_id3v1 {
|
||||
stream_len -= 128;
|
||||
id3v1 = id3v1_tag;
|
||||
id3v1_tag = id3v1;
|
||||
}
|
||||
|
||||
// Next, check for a Lyrics3v2 tag, and skip over it, as it's no use to us
|
||||
|
@ -129,19 +132,22 @@ where
|
|||
data.read_exact(&mut ape_preamble)?;
|
||||
|
||||
if &ape_preamble == APE_PREAMBLE {
|
||||
let (ape_tag, size) = read_ape_tag(data, true)?;
|
||||
let (ape, size) = read_ape_tag(data, true)?;
|
||||
|
||||
stream_len -= u64::from(size);
|
||||
ape = Some(ape_tag)
|
||||
ape_tag = Some(ape)
|
||||
}
|
||||
|
||||
// Go back to the MAC header to read properties
|
||||
data.seek(SeekFrom::Start(mac_start))?;
|
||||
|
||||
Ok(ApeFile {
|
||||
id3v1,
|
||||
id3v2,
|
||||
ape,
|
||||
#[cfg(feature = "id3v1")]
|
||||
id3v1_tag,
|
||||
#[cfg(feature = "id3v2")]
|
||||
id3v2_tag,
|
||||
#[cfg(feature = "ape")]
|
||||
ape_tag,
|
||||
properties: read_properties(data, stream_len)?,
|
||||
})
|
||||
}
|
||||
|
|
65
src/logic/ape/tag/item.rs
Normal file
65
src/logic/ape/tag/item.rs
Normal file
|
@ -0,0 +1,65 @@
|
|||
use crate::error::{LoftyError, Result};
|
||||
use crate::logic::ape::constants::INVALID_KEYS;
|
||||
use crate::types::item::{ItemValue, ItemValueRef, TagItem};
|
||||
use crate::types::tag::TagType;
|
||||
|
||||
use std::convert::TryFrom;
|
||||
|
||||
pub struct ApeItem {
|
||||
pub read_only: bool,
|
||||
pub(crate) key: String,
|
||||
pub(crate) value: ItemValue,
|
||||
}
|
||||
|
||||
impl ApeItem {
|
||||
pub fn new(key: String, value: ItemValue) -> Result<Self> {
|
||||
if INVALID_KEYS.contains(&&*key.to_uppercase()) {
|
||||
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"));
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
read_only: false,
|
||||
key,
|
||||
value,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn set_read_only(&mut self) {
|
||||
self.read_only = true
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<TagItem> for ApeItem {
|
||||
type Error = LoftyError;
|
||||
|
||||
fn try_from(value: TagItem) -> std::prelude::rust_2015::Result<Self, Self::Error> {
|
||||
Self::new(
|
||||
value
|
||||
.item_key
|
||||
.map_key(&TagType::Ape, false)
|
||||
.ok_or(LoftyError::Ape(
|
||||
"Attempted to convert an unsupported item key",
|
||||
))?
|
||||
.to_string(),
|
||||
value.item_value,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub(in crate::logic) struct ApeItemRef<'a> {
|
||||
pub read_only: bool,
|
||||
pub value: ItemValueRef<'a>,
|
||||
}
|
||||
|
||||
impl<'a> Into<ApeItemRef<'a>> for &'a ApeItem {
|
||||
fn into(self) -> ApeItemRef<'a> {
|
||||
ApeItemRef {
|
||||
read_only: self.read_only,
|
||||
value: (&self.value).into(),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,2 +1,128 @@
|
|||
mod item;
|
||||
pub(in crate::logic) mod read;
|
||||
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::{Tag, TagType};
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::convert::TryInto;
|
||||
use std::fs::File;
|
||||
|
||||
#[derive(Default)]
|
||||
/// An APE tag
|
||||
pub struct ApeTag {
|
||||
pub read_only: bool,
|
||||
pub(super) items: HashMap<String, ApeItem>,
|
||||
}
|
||||
|
||||
impl ApeTag {
|
||||
pub fn get_key(&self, key: &str) -> Option<&ApeItem> {
|
||||
self.items.get(key)
|
||||
}
|
||||
|
||||
pub fn push_item(&mut self, value: ApeItem) {
|
||||
let _ = self.items.insert(value.key.clone(), value);
|
||||
}
|
||||
|
||||
pub fn remove_key(&mut self, key: &str) {
|
||||
let _ = self.items.remove(key);
|
||||
}
|
||||
}
|
||||
|
||||
impl ApeTag {
|
||||
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.push_item(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.push_item(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ape_tag
|
||||
}
|
||||
}
|
||||
|
||||
pub(in crate::logic) struct ApeTagRef<'a> {
|
||||
read_only: bool,
|
||||
pub(super) items: HashMap<&'a str, ApeItemRef<'a>>,
|
||||
}
|
||||
|
||||
impl<'a> ApeTagRef<'a> {
|
||||
pub(crate) fn write_to(&self, file: &mut File) -> Result<()> {
|
||||
write::write_to(file, self)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Into<ApeTagRef<'a>> for &'a Tag {
|
||||
fn into(self) -> ApeTagRef<'a> {
|
||||
let mut items = HashMap::<&'a str, ApeItemRef<'a>>::new();
|
||||
|
||||
for item in &self.items {
|
||||
let key = item.key().map_key(&TagType::Ape, true).unwrap();
|
||||
|
||||
items.insert(
|
||||
key,
|
||||
ApeItemRef {
|
||||
read_only: false,
|
||||
value: (&item.item_value).into(),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
ApeTagRef {
|
||||
read_only: false,
|
||||
items,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Into<ApeTagRef<'a>> for &'a ApeTag {
|
||||
fn into(self) -> ApeTagRef<'a> {
|
||||
ApeTagRef {
|
||||
read_only: self.read_only,
|
||||
items: {
|
||||
let mut items = HashMap::<&str, ApeItemRef<'a>>::new();
|
||||
|
||||
for (k, v) in &self.items {
|
||||
items.insert(k.as_str(), v.into());
|
||||
}
|
||||
|
||||
items
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
use super::{ApeItem, ApeTag};
|
||||
use crate::error::{LoftyError, Result};
|
||||
use crate::logic::ape::constants::INVALID_KEYS;
|
||||
use crate::types::item::{ItemKey, ItemValue, TagItem, TagItemFlags};
|
||||
use crate::types::tag::{Tag, TagType};
|
||||
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<(Tag, u32)>
|
||||
pub(crate) fn read_ape_tag<R>(data: &mut R, footer: bool) -> Result<(ApeTag, u32)>
|
||||
where
|
||||
R: Read + Seek,
|
||||
{
|
||||
|
@ -34,7 +34,7 @@ where
|
|||
data.seek(SeekFrom::Current(12))?;
|
||||
}
|
||||
|
||||
let mut tag = Tag::new(TagType::Ape);
|
||||
let mut tag = ApeTag::default();
|
||||
|
||||
for _ in 0..item_count {
|
||||
let value_size = data.read_u32::<LittleEndian>()?;
|
||||
|
@ -64,10 +64,7 @@ where
|
|||
return Err(LoftyError::Ape("Tag item contains a non ASCII key"));
|
||||
}
|
||||
|
||||
let item_flags = TagItemFlags {
|
||||
read_only: (flags & 1) == 1,
|
||||
..TagItemFlags::default()
|
||||
};
|
||||
let read_only = (flags & 1) == 1;
|
||||
|
||||
let item_type = (flags & 6) >> 1;
|
||||
|
||||
|
@ -85,13 +82,13 @@ where
|
|||
_ => return Err(LoftyError::Ape("Tag item contains an invalid item type")),
|
||||
};
|
||||
|
||||
let mut item = TagItem::new(
|
||||
ItemKey::from_key(&TagType::Ape, &*key).unwrap(),
|
||||
parsed_value,
|
||||
);
|
||||
let mut item = ApeItem::new(key, parsed_value)?;
|
||||
|
||||
item.set_flags(item_flags);
|
||||
tag.insert_item(item);
|
||||
if read_only {
|
||||
item.set_read_only()
|
||||
}
|
||||
|
||||
tag.push_item(item);
|
||||
}
|
||||
|
||||
// Version 1 doesn't include a header
|
||||
|
|
|
@ -1,19 +1,26 @@
|
|||
use super::read::read_ape_tag;
|
||||
use crate::error::{LoftyError, Result};
|
||||
use crate::logic::ape::constants::APE_PREAMBLE;
|
||||
use crate::logic::id3::find_lyrics3v2;
|
||||
use crate::logic::id3::v1::find_id3v1;
|
||||
use crate::logic::ape::tag::item::ApeItemRef;
|
||||
use crate::logic::ape::tag::ApeTagRef;
|
||||
use crate::logic::id3::v2::find_id3v2;
|
||||
use crate::types::item::{ItemValue, TagItem};
|
||||
use crate::types::picture::Picture;
|
||||
use crate::types::tag::{Tag, TagType};
|
||||
use crate::logic::id3::{find_id3v1, find_lyrics3v2};
|
||||
use crate::probe::Probe;
|
||||
use crate::types::file::FileType;
|
||||
use crate::types::item::ItemValueRef;
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::fs::File;
|
||||
use std::io::{Cursor, Read, Seek, SeekFrom, Write};
|
||||
|
||||
use byteorder::{LittleEndian, WriteBytesExt};
|
||||
|
||||
pub(crate) fn write_to(data: &mut File, tag: &Tag) -> Result<()> {
|
||||
pub(in crate::logic) fn write_to(data: &mut File, tag: &ApeTagRef) -> Result<()> {
|
||||
match Probe::new().file_type(data) {
|
||||
Some(ft) if ft == FileType::APE || ft == FileType::MP3 => {}
|
||||
_ => return Err(LoftyError::UnsupportedTag),
|
||||
}
|
||||
|
||||
// We don't actually need the ID3v2 tag, but reading it will seek to the end of it if it exists
|
||||
find_id3v2(data, false)?;
|
||||
|
||||
|
@ -34,9 +41,9 @@ pub(crate) fn write_to(data: &mut File, tag: &Tag) -> Result<()> {
|
|||
let (mut existing, size) = read_ape_tag(data, false)?;
|
||||
|
||||
// Only keep metadata around that's marked read only
|
||||
existing.retain(|i| i.flags().read_only);
|
||||
existing.items.retain(|_i, v| v.read_only);
|
||||
|
||||
if existing.item_count() > 0 {
|
||||
if !existing.items.is_empty() {
|
||||
read_only = Some(existing)
|
||||
}
|
||||
|
||||
|
@ -65,9 +72,9 @@ pub(crate) fn write_to(data: &mut File, tag: &Tag) -> Result<()> {
|
|||
|
||||
let (mut existing, size) = read_ape_tag(data, true)?;
|
||||
|
||||
existing.retain(|i| i.flags().read_only);
|
||||
existing.items.retain(|_, v| v.read_only);
|
||||
|
||||
if existing.item_count() > 0 {
|
||||
if !existing.items.is_empty() {
|
||||
read_only = Some(existing)
|
||||
}
|
||||
|
||||
|
@ -80,18 +87,10 @@ pub(crate) fn write_to(data: &mut File, tag: &Tag) -> Result<()> {
|
|||
}
|
||||
|
||||
// Preserve any metadata marked as read only
|
||||
// If there is any read only metadata, we will have to clone the TagItems
|
||||
let tag = if let Some(read_only) = read_only {
|
||||
use std::collections::HashSet;
|
||||
|
||||
let mut items = [read_only.items(), tag.items()].concat();
|
||||
|
||||
let mut unique_items = HashSet::new();
|
||||
items.retain(|i| unique_items.insert(i.clone()));
|
||||
|
||||
create_ape_tag(&items, tag.pictures())?
|
||||
create_ape_tag(&Into::<ApeTagRef>::into(&read_only).items)?
|
||||
} else {
|
||||
create_ape_tag(tag.items(), tag.pictures())?
|
||||
create_ape_tag(&tag.items)?
|
||||
};
|
||||
|
||||
data.seek(SeekFrom::Start(0))?;
|
||||
|
@ -118,69 +117,44 @@ pub(crate) fn write_to(data: &mut File, tag: &Tag) -> Result<()> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn create_ape_tag(items: &[TagItem], pictures: &[Picture]) -> Result<Vec<u8>> {
|
||||
fn create_ape_tag(items: &HashMap<&str, ApeItemRef>) -> Result<Vec<u8>> {
|
||||
// Unnecessary to write anything if there's no metadata
|
||||
if items.is_empty() && pictures.is_empty() {
|
||||
if items.is_empty() {
|
||||
Ok(Vec::<u8>::new())
|
||||
} else {
|
||||
let mut tag = Cursor::new(Vec::<u8>::new());
|
||||
|
||||
let item_count = (items.len() + pictures.len()) as u32;
|
||||
let item_count = items.len() as u32;
|
||||
|
||||
for item in items {
|
||||
let (size, flags, value) = match item.value() {
|
||||
ItemValue::Binary(value) => {
|
||||
let mut flags = 1_u32 << 1;
|
||||
for (k, v) in items {
|
||||
let (mut flags, value) = match v.value {
|
||||
ItemValueRef::Binary(value) => {
|
||||
tag.write_u32::<LittleEndian>(value.len() as u32)?;
|
||||
|
||||
if item.flags().read_only {
|
||||
flags |= 1_u32
|
||||
}
|
||||
|
||||
(value.len() as u32, flags, value.as_slice())
|
||||
(1_u32 << 1, value)
|
||||
}
|
||||
ItemValue::Text(value) => {
|
||||
let value = value.as_bytes();
|
||||
ItemValueRef::Text(value) => {
|
||||
tag.write_u32::<LittleEndian>(value.len() as u32)?;
|
||||
|
||||
let mut flags = 0_u32;
|
||||
|
||||
if item.flags().read_only {
|
||||
flags |= 1_u32
|
||||
}
|
||||
|
||||
(value.len() as u32, flags, value)
|
||||
(0_u32, value.as_bytes())
|
||||
}
|
||||
ItemValue::Locator(value) => {
|
||||
let mut flags = 2_u32 << 1;
|
||||
ItemValueRef::Locator(value) => {
|
||||
tag.write_u32::<LittleEndian>(value.len() as u32)?;
|
||||
|
||||
if item.flags().read_only {
|
||||
flags |= 1_u32
|
||||
}
|
||||
|
||||
(value.len() as u32, flags, value.as_bytes())
|
||||
(2_u32 << 1, value.as_bytes())
|
||||
}
|
||||
_ => continue,
|
||||
};
|
||||
|
||||
tag.write_u32::<LittleEndian>(size)?;
|
||||
if v.read_only {
|
||||
flags |= 1_u32
|
||||
}
|
||||
|
||||
tag.write_u32::<LittleEndian>(flags)?;
|
||||
tag.write_all(item.key().map_key(&TagType::Ape).unwrap().as_bytes())?;
|
||||
tag.write_all(k.as_bytes())?;
|
||||
tag.write_u8(0)?;
|
||||
tag.write_all(value)?;
|
||||
}
|
||||
|
||||
for pic in pictures {
|
||||
let key = pic.pic_type.as_ape_key();
|
||||
let bytes = pic.as_ape_bytes();
|
||||
// Binary item
|
||||
let flags = 1_u32 << 1;
|
||||
|
||||
tag.write_u32::<LittleEndian>(bytes.len() as u32)?;
|
||||
tag.write_u32::<LittleEndian>(flags)?;
|
||||
tag.write_all(key.as_bytes())?;
|
||||
tag.write_u8(0)?;
|
||||
tag.write_all(&bytes)?;
|
||||
}
|
||||
|
||||
let size = tag.get_ref().len();
|
||||
|
||||
if size as u64 + 32 > u64::from(u32::MAX) {
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
use crate::error::{LoftyError, Result};
|
||||
use crate::logic::ape::tag::ApeTagRef;
|
||||
use crate::logic::id3::v1::tag::Id3v1TagRef;
|
||||
use crate::types::tag::{Tag, TagType};
|
||||
|
||||
use std::fs::File;
|
||||
|
||||
pub(crate) fn write_to(data: &mut File, tag: &Tag) -> Result<()> {
|
||||
pub(in crate::logic) fn write_to(data: &mut File, tag: &Tag) -> Result<()> {
|
||||
match tag.tag_type() {
|
||||
TagType::Ape => super::tag::write::write_to(data, tag),
|
||||
TagType::Id3v1 => crate::logic::id3::v1::write::write_id3v1(data, tag),
|
||||
TagType::Ape => Into::<ApeTagRef>::into(tag).write_to(data),
|
||||
TagType::Id3v1 => Into::<Id3v1TagRef>::into(tag).write_to(data),
|
||||
_ => Err(LoftyError::UnsupportedTag),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -53,3 +53,64 @@ where
|
|||
|
||||
Ok((exists, size))
|
||||
}
|
||||
|
||||
#[cfg(feature = "id3v1")]
|
||||
pub(in crate::logic) fn find_id3v1<R>(
|
||||
data: &mut R,
|
||||
read: bool,
|
||||
) -> Result<(bool, Option<v1::tag::Id3v1Tag>)>
|
||||
where
|
||||
R: Read + Seek,
|
||||
{
|
||||
let mut id3v1 = None;
|
||||
let mut exists = false;
|
||||
|
||||
data.seek(SeekFrom::End(-128))?;
|
||||
|
||||
let mut id3v1_header = [0; 3];
|
||||
data.read_exact(&mut id3v1_header)?;
|
||||
|
||||
data.seek(SeekFrom::Current(-3))?;
|
||||
|
||||
if &id3v1_header == b"TAG" {
|
||||
exists = true;
|
||||
|
||||
if read {
|
||||
let mut id3v1_tag = [0; 128];
|
||||
data.read_exact(&mut id3v1_tag)?;
|
||||
|
||||
data.seek(SeekFrom::End(-128))?;
|
||||
|
||||
id3v1 = Some(v1::read::parse_id3v1(id3v1_tag))
|
||||
}
|
||||
} else {
|
||||
// No ID3v1 tag found
|
||||
data.seek(SeekFrom::End(0))?;
|
||||
}
|
||||
|
||||
Ok((exists, id3v1))
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "id3v1"))]
|
||||
pub(in crate::logic) fn find_id3v1<R>(data: &mut R, read: bool) -> Result<(bool, Option<()>)>
|
||||
where
|
||||
R: Read + Seek,
|
||||
{
|
||||
let mut exists = false;
|
||||
|
||||
data.seek(SeekFrom::End(-128))?;
|
||||
|
||||
let mut id3v1_header = [0; 3];
|
||||
data.read_exact(&mut id3v1_header)?;
|
||||
|
||||
data.seek(SeekFrom::Current(-3))?;
|
||||
|
||||
if &id3v1_header == b"TAG" {
|
||||
exists = true;
|
||||
} else {
|
||||
// No ID3v1 tag found
|
||||
data.seek(SeekFrom::End(0))?;
|
||||
}
|
||||
|
||||
Ok((exists, None))
|
||||
}
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
use crate::types::item::ItemKey;
|
||||
|
||||
/// All possible genres for ID3v1
|
||||
pub const GENRES: [&str; 192] = [
|
||||
"Blues",
|
||||
|
@ -193,3 +195,11 @@ pub const GENRES: [&str; 192] = [
|
|||
"Garage Rock",
|
||||
"Psybient",
|
||||
];
|
||||
|
||||
pub const VALID_ITEMKEYS: [ItemKey; 5] = [
|
||||
ItemKey::TrackTitle,
|
||||
ItemKey::TrackArtist,
|
||||
ItemKey::AlbumTitle,
|
||||
ItemKey::Year,
|
||||
ItemKey::Comment,
|
||||
];
|
||||
|
|
|
@ -1,41 +1,4 @@
|
|||
pub(crate) mod constants;
|
||||
pub(in crate::logic) mod read;
|
||||
pub(crate) mod tag;
|
||||
pub(in crate::logic) mod write;
|
||||
|
||||
use crate::error::Result;
|
||||
use crate::types::tag::Tag;
|
||||
|
||||
use std::io::{Read, Seek, SeekFrom};
|
||||
|
||||
pub(in crate::logic) fn find_id3v1<R>(data: &mut R, read: bool) -> Result<(bool, Option<Tag>)>
|
||||
where
|
||||
R: Read + Seek,
|
||||
{
|
||||
let mut id3v1 = None;
|
||||
let mut exists = false;
|
||||
|
||||
data.seek(SeekFrom::End(-128))?;
|
||||
|
||||
let mut id3v1_header = [0; 3];
|
||||
data.read_exact(&mut id3v1_header)?;
|
||||
|
||||
data.seek(SeekFrom::Current(-3))?;
|
||||
|
||||
if &id3v1_header == b"TAG" {
|
||||
exists = true;
|
||||
|
||||
if read {
|
||||
let mut id3v1_tag = [0; 128];
|
||||
data.read_exact(&mut id3v1_tag)?;
|
||||
|
||||
data.seek(SeekFrom::End(-128))?;
|
||||
|
||||
id3v1 = Some(read::parse_id3v1(id3v1_tag))
|
||||
}
|
||||
} else {
|
||||
// No ID3v1 tag found
|
||||
data.seek(SeekFrom::End(0))?;
|
||||
}
|
||||
|
||||
Ok((exists, id3v1))
|
||||
}
|
||||
|
|
|
@ -1,54 +1,42 @@
|
|||
use super::constants::GENRES;
|
||||
use crate::types::item::{ItemKey, ItemValue, TagItem};
|
||||
use crate::types::tag::{Tag, TagType};
|
||||
use super::tag::Id3v1Tag;
|
||||
|
||||
pub fn parse_id3v1(reader: [u8; 128]) -> Tag {
|
||||
let mut tag = Tag::new(TagType::Id3v1);
|
||||
pub fn parse_id3v1(reader: [u8; 128]) -> Id3v1Tag {
|
||||
let mut tag = Id3v1Tag {
|
||||
title: None,
|
||||
artist: None,
|
||||
album: None,
|
||||
year: None,
|
||||
comment: None,
|
||||
track_number: None,
|
||||
genre: None,
|
||||
};
|
||||
|
||||
let reader = &reader[3..];
|
||||
|
||||
if let Some(title) = decode_text(ItemKey::TrackTitle, &reader[..30]) {
|
||||
tag.insert_item_unchecked(title);
|
||||
}
|
||||
|
||||
if let Some(artist) = decode_text(ItemKey::TrackArtist, &reader[30..60]) {
|
||||
tag.insert_item_unchecked(artist);
|
||||
}
|
||||
|
||||
if let Some(album) = decode_text(ItemKey::AlbumTitle, &reader[60..90]) {
|
||||
tag.insert_item_unchecked(album);
|
||||
}
|
||||
|
||||
if let Some(year) = decode_text(ItemKey::Year, &reader[90..94]) {
|
||||
tag.insert_item_unchecked(year);
|
||||
}
|
||||
tag.title = decode_text(&reader[..30]);
|
||||
tag.artist = decode_text(&reader[30..60]);
|
||||
tag.album = decode_text(&reader[60..90]);
|
||||
tag.year = decode_text(&reader[90..94]);
|
||||
|
||||
let range = if reader[119] == 0 && reader[122] != 0 {
|
||||
tag.insert_item_unchecked(TagItem::new(
|
||||
ItemKey::TrackNumber,
|
||||
ItemValue::UInt(u32::from(reader[122])),
|
||||
));
|
||||
tag.track_number = Some(reader[122]);
|
||||
|
||||
94_usize..123
|
||||
} else {
|
||||
94..124
|
||||
};
|
||||
|
||||
if let Some(comment) = decode_text(ItemKey::Comment, &reader[range]) {
|
||||
tag.insert_item_unchecked(comment);
|
||||
}
|
||||
tag.comment = decode_text(&reader[range]);
|
||||
|
||||
if reader[124] < GENRES.len() as u8 {
|
||||
tag.insert_item_unchecked(TagItem::new(
|
||||
ItemKey::Genre,
|
||||
ItemValue::Text(GENRES[reader[125] as usize].to_string()),
|
||||
));
|
||||
tag.genre = Some(reader[124]);
|
||||
}
|
||||
|
||||
tag
|
||||
}
|
||||
|
||||
fn decode_text(key: ItemKey, data: &[u8]) -> Option<TagItem> {
|
||||
fn decode_text(data: &[u8]) -> Option<String> {
|
||||
let read = data
|
||||
.iter()
|
||||
.filter(|c| **c != 0)
|
||||
|
@ -58,6 +46,6 @@ fn decode_text(key: ItemKey, data: &[u8]) -> Option<TagItem> {
|
|||
if read.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(TagItem::new(key, ItemValue::Text(read)))
|
||||
Some(read)
|
||||
}
|
||||
}
|
||||
|
|
179
src/logic/id3/v1/tag.rs
Normal file
179
src/logic/id3/v1/tag.rs
Normal file
|
@ -0,0 +1,179 @@
|
|||
use crate::error::Result;
|
||||
use crate::logic::id3::v1::constants::GENRES;
|
||||
use crate::types::item::{ItemKey, ItemValue, TagItem};
|
||||
use crate::types::tag::{Tag, TagType};
|
||||
|
||||
use std::fs::File;
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
/// An ID3v1 tag
|
||||
///
|
||||
/// ID3v1 is a severely limited format, with each field
|
||||
/// being incredibly small in size. All fields have been
|
||||
/// commented with their maximum sizes and any other additional
|
||||
/// restrictions.
|
||||
///
|
||||
/// Attempting to write a field greater than the maximum size
|
||||
/// will **not** error, it will just be shrunk.
|
||||
pub struct Id3v1Tag {
|
||||
/// Track title, 30 bytes max
|
||||
pub title: Option<String>,
|
||||
/// Track artist, 30 bytes max
|
||||
pub artist: Option<String>,
|
||||
/// Album title, 30 bytes max
|
||||
pub album: Option<String>,
|
||||
/// Release year, 4 bytes max
|
||||
pub year: Option<String>,
|
||||
/// A short comment
|
||||
///
|
||||
/// The number of bytes differs between versions, but not much.
|
||||
/// A V1 tag may have been read, which limits this field to 30 bytes.
|
||||
/// A V1.1 tag, however, only has 28 bytes available.
|
||||
///
|
||||
/// Lofty will *always* write a V1.1 tag.
|
||||
pub comment: Option<String>,
|
||||
/// The track number, 1 byte max
|
||||
///
|
||||
/// Issues:
|
||||
///
|
||||
/// * The track number **cannot** be 0. Many readers, including Lofty,
|
||||
/// look for a zeroed byte at the end of the comment to differentiate
|
||||
/// between V1 and V1.1.
|
||||
/// * A V1 tag may have been read, which does *not* have a track number.
|
||||
pub track_number: Option<u8>,
|
||||
/// The track's genre, 1 byte max
|
||||
///
|
||||
/// ID3v1 has a predefined set of genres, see [`GENRES`](crate::id3::v1::GENRES).
|
||||
/// This byte should be an index to a genre.
|
||||
pub genre: Option<u8>,
|
||||
}
|
||||
|
||||
impl Id3v1Tag {
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.title.is_none()
|
||||
&& self.artist.is_none()
|
||||
&& self.album.is_none()
|
||||
&& self.year.is_none()
|
||||
&& self.comment.is_none()
|
||||
&& self.track_number.is_none()
|
||||
&& self.genre.is_none()
|
||||
}
|
||||
|
||||
pub fn write_to(&self, file: &mut File) -> Result<()> {
|
||||
Into::<Id3v1TagRef>::into(self).write_to(file)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Id3v1Tag> for Tag {
|
||||
fn from(input: Id3v1Tag) -> Self {
|
||||
let mut tag = Self::new(TagType::Id3v1);
|
||||
|
||||
input.title.map(|t| tag.insert_text(ItemKey::TrackTitle, t));
|
||||
input
|
||||
.artist
|
||||
.map(|a| tag.insert_text(ItemKey::TrackArtist, a));
|
||||
input.album.map(|a| tag.insert_text(ItemKey::AlbumTitle, a));
|
||||
input.year.map(|y| tag.insert_text(ItemKey::Year, y));
|
||||
input.comment.map(|c| tag.insert_text(ItemKey::Comment, c));
|
||||
|
||||
if let Some(t) = input.track_number {
|
||||
tag.insert_item_unchecked(TagItem::new(
|
||||
ItemKey::TrackNumber,
|
||||
ItemValue::Text(t.to_string()),
|
||||
))
|
||||
}
|
||||
|
||||
if let Some(genre_index) = input.genre {
|
||||
if let Some(genre) = GENRES.get(genre_index as usize) {
|
||||
tag.insert_text(ItemKey::Genre, (*genre).to_string());
|
||||
}
|
||||
}
|
||||
|
||||
tag
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Tag> for Id3v1Tag {
|
||||
fn from(input: Tag) -> Self {
|
||||
Self {
|
||||
title: input.get_string(&ItemKey::TrackTitle).map(str::to_owned),
|
||||
artist: input.get_string(&ItemKey::TrackArtist).map(str::to_owned),
|
||||
album: input.get_string(&ItemKey::AlbumTitle).map(str::to_owned),
|
||||
year: input.get_string(&ItemKey::Year).map(str::to_owned),
|
||||
comment: input.get_string(&ItemKey::Comment).map(str::to_owned),
|
||||
track_number: if let Some(Ok(track_number)) = input
|
||||
.get_string(&ItemKey::TrackNumber)
|
||||
.map(str::parse::<u8>)
|
||||
{
|
||||
Some(track_number)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
genre: input
|
||||
.get_string(&ItemKey::Genre)
|
||||
.and_then(|genre| GENRES.iter().position(|v| v == &genre).map(|pos| pos as u8)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct Id3v1TagRef<'a> {
|
||||
pub title: Option<&'a str>,
|
||||
pub artist: Option<&'a str>,
|
||||
pub album: Option<&'a str>,
|
||||
pub year: Option<&'a str>,
|
||||
pub comment: Option<&'a str>,
|
||||
pub track_number: Option<u8>,
|
||||
pub genre: Option<u8>,
|
||||
}
|
||||
|
||||
impl<'a> Into<Id3v1TagRef<'a>> for &'a Id3v1Tag {
|
||||
fn into(self) -> Id3v1TagRef<'a> {
|
||||
Id3v1TagRef {
|
||||
title: self.title.as_deref(),
|
||||
artist: self.artist.as_deref(),
|
||||
album: self.album.as_deref(),
|
||||
year: self.year.as_deref(),
|
||||
comment: self.comment.as_deref(),
|
||||
track_number: self.track_number,
|
||||
genre: self.genre,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Into<Id3v1TagRef<'a>> for &'a Tag {
|
||||
fn into(self) -> Id3v1TagRef<'a> {
|
||||
Id3v1TagRef {
|
||||
title: self.get_string(&ItemKey::TrackTitle),
|
||||
artist: self.get_string(&ItemKey::TrackArtist),
|
||||
album: self.get_string(&ItemKey::AlbumTitle),
|
||||
year: self.get_string(&ItemKey::Year),
|
||||
comment: self.get_string(&ItemKey::Comment),
|
||||
track_number: if let Some(Ok(track_number)) =
|
||||
self.get_string(&ItemKey::TrackNumber).map(str::parse::<u8>)
|
||||
{
|
||||
Some(track_number)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
genre: self
|
||||
.get_string(&ItemKey::Genre)
|
||||
.and_then(|genre| GENRES.iter().position(|v| v == &genre).map(|pos| pos as u8)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Id3v1TagRef<'a> {
|
||||
pub(crate) fn write_to(&self, file: &mut File) -> Result<()> {
|
||||
super::write::write_id3v1(file, self)
|
||||
}
|
||||
|
||||
pub(super) fn is_empty(&self) -> bool {
|
||||
self.title.is_none()
|
||||
&& self.artist.is_none()
|
||||
&& self.album.is_none()
|
||||
&& self.year.is_none()
|
||||
&& self.comment.is_none()
|
||||
&& self.track_number.is_none()
|
||||
&& self.genre.is_none()
|
||||
}
|
||||
}
|
|
@ -1,17 +1,24 @@
|
|||
use crate::error::Result;
|
||||
use crate::types::item::{ItemKey, ItemValue, TagItem};
|
||||
use crate::types::tag::Tag;
|
||||
use super::tag::Id3v1TagRef;
|
||||
use crate::error::{LoftyError, Result};
|
||||
use crate::logic::id3::find_id3v1;
|
||||
use crate::probe::Probe;
|
||||
use crate::types::file::FileType;
|
||||
|
||||
use std::fs::File;
|
||||
use std::io::{Cursor, Read, Seek, SeekFrom, Write};
|
||||
|
||||
use byteorder::WriteBytesExt;
|
||||
|
||||
pub fn write_id3v1(writer: &mut File, tag: &Tag) -> Result<()> {
|
||||
// This will seek us to the writing position
|
||||
let (exists, _) = super::find_id3v1(writer, false)?;
|
||||
pub(in crate::logic) fn write_id3v1(writer: &mut File, tag: &Id3v1TagRef) -> Result<()> {
|
||||
match Probe::new().file_type(writer) {
|
||||
Some(ft) if ft == FileType::APE || ft == FileType::MP3 => {}
|
||||
_ => return Err(LoftyError::UnsupportedTag),
|
||||
}
|
||||
|
||||
if tag.item_count() == 0 && exists {
|
||||
// This will seek us to the writing position
|
||||
let (exists, _) = find_id3v1(writer, false)?;
|
||||
|
||||
if tag.is_empty() && exists {
|
||||
writer.seek(SeekFrom::Start(0))?;
|
||||
|
||||
let mut file_bytes = Vec::new();
|
||||
|
@ -19,7 +26,7 @@ pub fn write_id3v1(writer: &mut File, tag: &Tag) -> Result<()> {
|
|||
|
||||
writer.seek(SeekFrom::Start(0))?;
|
||||
writer.set_len(0)?;
|
||||
writer.write_all(&file_bytes[..file_bytes.len() - 129])?;
|
||||
writer.write_all(&file_bytes[..file_bytes.len() - 128])?;
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
@ -31,16 +38,16 @@ pub fn write_id3v1(writer: &mut File, tag: &Tag) -> Result<()> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn encode(tag: &Tag) -> Result<Vec<u8>> {
|
||||
fn resize_string(item: Option<&TagItem>, size: usize) -> Result<Vec<u8>> {
|
||||
fn encode(tag: &Id3v1TagRef) -> Result<Vec<u8>> {
|
||||
fn resize_string(value: Option<&str>, size: usize) -> Result<Vec<u8>> {
|
||||
let mut cursor = Cursor::new(vec![0; size]);
|
||||
cursor.seek(SeekFrom::Start(0))?;
|
||||
|
||||
if let Some(ItemValue::Text(text)) = item.map(TagItem::value) {
|
||||
if text.len() > size {
|
||||
cursor.write_all(text.split_at(size).0.as_bytes())?;
|
||||
if let Some(val) = value {
|
||||
if val.len() > size {
|
||||
cursor.write_all(val.split_at(size).0.as_bytes())?;
|
||||
} else {
|
||||
cursor.write_all(text.as_bytes())?;
|
||||
cursor.write_all(val.as_bytes())?;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -51,72 +58,25 @@ fn encode(tag: &Tag) -> Result<Vec<u8>> {
|
|||
|
||||
writer.write_all(&[b'T', b'A', b'G'])?;
|
||||
|
||||
let title = resize_string(tag.get_item_ref(&ItemKey::TrackTitle), 30)?;
|
||||
let title = resize_string(tag.title, 30)?;
|
||||
writer.write_all(&*title)?;
|
||||
|
||||
let artist = resize_string(tag.get_item_ref(&ItemKey::TrackArtist), 30)?;
|
||||
let artist = resize_string(tag.artist, 30)?;
|
||||
writer.write_all(&*artist)?;
|
||||
|
||||
let album = resize_string(tag.get_item_ref(&ItemKey::AlbumTitle), 30)?;
|
||||
let album = resize_string(tag.album, 30)?;
|
||||
writer.write_all(&*album)?;
|
||||
|
||||
let year = resize_string(tag.get_item_ref(&ItemKey::Year), 4)?;
|
||||
let year = resize_string(tag.year, 4)?;
|
||||
writer.write_all(&*year)?;
|
||||
|
||||
let comment = resize_string(tag.get_item_ref(&ItemKey::Comment), 28)?;
|
||||
let comment = resize_string(tag.comment, 28)?;
|
||||
writer.write_all(&*comment)?;
|
||||
|
||||
writer.write_u8(0)?;
|
||||
|
||||
let item_to_byte = |key: &ItemKey, max: u8, empty: u8| {
|
||||
if let Some(track_number) = tag.get_item_ref(key) {
|
||||
match track_number.value() {
|
||||
ItemValue::Text(text) => {
|
||||
if let Ok(parsed) = text.parse::<u8>() {
|
||||
if parsed <= max {
|
||||
return parsed;
|
||||
}
|
||||
}
|
||||
|
||||
empty
|
||||
}
|
||||
ItemValue::UInt(i) => {
|
||||
if *i <= u32::from(max) {
|
||||
*i as u8
|
||||
} else {
|
||||
empty
|
||||
}
|
||||
}
|
||||
ItemValue::UInt64(i) => {
|
||||
if *i <= u64::from(max) {
|
||||
*i as u8
|
||||
} else {
|
||||
empty
|
||||
}
|
||||
}
|
||||
ItemValue::Int(i) => {
|
||||
if i.is_positive() && *i <= i32::from(max) {
|
||||
*i as u8
|
||||
} else {
|
||||
empty
|
||||
}
|
||||
}
|
||||
ItemValue::Int64(i) => {
|
||||
if i.is_positive() && *i <= i64::from(max) {
|
||||
*i as u8
|
||||
} else {
|
||||
empty
|
||||
}
|
||||
}
|
||||
_ => empty,
|
||||
}
|
||||
} else {
|
||||
empty
|
||||
}
|
||||
};
|
||||
|
||||
writer.write_u8(item_to_byte(&ItemKey::TrackNumber, 255, 0))?;
|
||||
writer.write_u8(item_to_byte(&ItemKey::Genre, 191, 255))?;
|
||||
writer.write_u8(tag.track_number.unwrap_or(0))?;
|
||||
writer.write_u8(tag.genre.unwrap_or(255))?;
|
||||
|
||||
Ok(writer)
|
||||
}
|
||||
|
|
|
@ -1,55 +1,39 @@
|
|||
use super::{Id3v2Frame, LanguageSpecificFrame};
|
||||
use crate::error::Result;
|
||||
use crate::error::{LoftyError, Result};
|
||||
use crate::logic::id3::v2::frame::{EncodedTextFrame, FrameID, FrameValue, LanguageFrame};
|
||||
use crate::logic::id3::v2::util::text_utils::{decode_text, TextEncoding};
|
||||
use crate::logic::id3::v2::Id3v2Version;
|
||||
use crate::types::picture::Picture;
|
||||
use crate::{ItemKey, ItemValue, LoftyError, TagItem, TagType};
|
||||
|
||||
use std::io::Read;
|
||||
|
||||
use byteorder::ReadBytesExt;
|
||||
|
||||
pub(crate) enum FrameContent {
|
||||
Picture(Picture),
|
||||
// For values that only apply to an Id3v2Frame
|
||||
Item(TagItem),
|
||||
}
|
||||
|
||||
pub(crate) fn parse_content(
|
||||
content: &mut &[u8],
|
||||
id: &str,
|
||||
version: Id3v2Version,
|
||||
) -> Result<FrameContent> {
|
||||
) -> Result<(FrameID, FrameValue)> {
|
||||
Ok(match id {
|
||||
// The ID was previously upgraded, but the content remains unchanged, so version is necessary
|
||||
"APIC" => FrameContent::Picture(Picture::from_apic_bytes(content, version)?),
|
||||
"TXXX" => FrameContent::Item(parse_user_defined(content, false)?),
|
||||
"WXXX" => FrameContent::Item(parse_user_defined(content, true)?),
|
||||
"COMM" | "USLT" => FrameContent::Item(parse_text_language(id, content)?),
|
||||
"SYLT" => FrameContent::Item({
|
||||
TagItem::new(
|
||||
ItemKey::Id3v2Specific(Id3v2Frame::SyncText),
|
||||
ItemValue::Binary(content.to_vec()),
|
||||
)
|
||||
}),
|
||||
"GEOB" => FrameContent::Item({
|
||||
TagItem::new(
|
||||
ItemKey::Id3v2Specific(Id3v2Frame::EncapsulatedObject),
|
||||
ItemValue::Binary(content.to_vec()),
|
||||
)
|
||||
}),
|
||||
_ if id.starts_with('T') => FrameContent::Item(parse_text(id, content)?),
|
||||
_ if id.starts_with('W') => FrameContent::Item(parse_link(id, content)?),
|
||||
_ => FrameContent::Item(TagItem::new(
|
||||
ItemKey::from_key(&TagType::Id3v2, id)
|
||||
.unwrap_or_else(|| ItemKey::Unknown(id.to_string())),
|
||||
ItemValue::Binary(content.to_vec()),
|
||||
)),
|
||||
"APIC" => (
|
||||
FrameID::Valid(String::from("APIC")),
|
||||
FrameValue::Picture(Picture::from_apic_bytes(content, version)?),
|
||||
),
|
||||
"TXXX" => parse_user_defined(content, false)?,
|
||||
"WXXX" => parse_user_defined(content, true)?,
|
||||
"COMM" | "USLT" => parse_text_language(id, content)?,
|
||||
_ if id.starts_with('T') => parse_text(id, content)?,
|
||||
_ if id.starts_with('W') => parse_link(id, content)?,
|
||||
// SYLT, GEOB, and any unknown frames
|
||||
_ => (
|
||||
FrameID::Valid(String::from(id)),
|
||||
FrameValue::Binary(content.to_vec()),
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
// There are 2 possibilities for the frame's content: text or link.
|
||||
fn parse_user_defined(content: &mut &[u8], link: bool) -> Result<TagItem> {
|
||||
fn parse_user_defined(content: &mut &[u8], link: bool) -> Result<(FrameID, FrameValue)> {
|
||||
if content.len() < 2 {
|
||||
return Err(LoftyError::BadFrameLength);
|
||||
}
|
||||
|
@ -65,21 +49,29 @@ fn parse_user_defined(content: &mut &[u8], link: bool) -> Result<TagItem> {
|
|||
let content =
|
||||
decode_text(content, TextEncoding::Latin1, false)?.unwrap_or_else(String::new);
|
||||
|
||||
TagItem::new(
|
||||
ItemKey::Id3v2Specific(Id3v2Frame::UserURL(encoding, description)),
|
||||
ItemValue::Locator(content),
|
||||
(
|
||||
FrameID::Valid(String::from("WXXX")),
|
||||
FrameValue::UserURL(EncodedTextFrame {
|
||||
encoding,
|
||||
description,
|
||||
content,
|
||||
}),
|
||||
)
|
||||
} else {
|
||||
let content = decode_text(content, encoding, false)?.unwrap_or_else(String::new);
|
||||
|
||||
TagItem::new(
|
||||
ItemKey::Id3v2Specific(Id3v2Frame::UserText(encoding, description)),
|
||||
ItemValue::Text(content),
|
||||
(
|
||||
FrameID::Valid(String::from("TXXX")),
|
||||
FrameValue::UserText(EncodedTextFrame {
|
||||
encoding,
|
||||
description,
|
||||
content,
|
||||
}),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_text_language(id: &str, content: &mut &[u8]) -> Result<TagItem> {
|
||||
fn parse_text_language(id: &str, content: &mut &[u8]) -> Result<(FrameID, FrameValue)> {
|
||||
if content.len() < 5 {
|
||||
return Err(LoftyError::BadFrameLength);
|
||||
}
|
||||
|
@ -98,22 +90,25 @@ fn parse_text_language(id: &str, content: &mut &[u8]) -> Result<TagItem> {
|
|||
let description = decode_text(content, encoding, true)?;
|
||||
let content = decode_text(content, encoding, false)?.unwrap_or_else(String::new);
|
||||
|
||||
let information = LanguageSpecificFrame {
|
||||
let information = LanguageFrame {
|
||||
encoding,
|
||||
language: lang.to_string(),
|
||||
description,
|
||||
description: description.unwrap_or_else(|| String::from("")),
|
||||
content,
|
||||
};
|
||||
|
||||
let item_key = match id {
|
||||
"COMM" => ItemKey::Id3v2Specific(Id3v2Frame::Comment(information)),
|
||||
"USLT" => ItemKey::Id3v2Specific(Id3v2Frame::UnSyncText(information)),
|
||||
let value = match id {
|
||||
"COMM" => FrameValue::Comment(information),
|
||||
"USLT" => FrameValue::UnSyncText(information),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
Ok(TagItem::new(item_key, ItemValue::Text(content)))
|
||||
let id = FrameID::Valid(String::from(id));
|
||||
|
||||
Ok((id, value))
|
||||
}
|
||||
|
||||
fn parse_text(id: &str, content: &mut &[u8]) -> Result<TagItem> {
|
||||
fn parse_text(id: &str, content: &mut &[u8]) -> Result<(FrameID, FrameValue)> {
|
||||
let encoding = match TextEncoding::from_u8(content.read_u8()?) {
|
||||
None => return Err(LoftyError::TextDecode("Found invalid encoding")),
|
||||
Some(e) => e,
|
||||
|
@ -121,17 +116,17 @@ fn parse_text(id: &str, content: &mut &[u8]) -> Result<TagItem> {
|
|||
|
||||
let text = decode_text(content, encoding, false)?.unwrap_or_else(String::new);
|
||||
|
||||
let key = ItemKey::from_key(&TagType::Id3v2, id)
|
||||
.unwrap_or_else(|| ItemKey::Id3v2Specific(Id3v2Frame::Text(id.to_string(), encoding)));
|
||||
|
||||
Ok(TagItem::new(key, ItemValue::Text(text)))
|
||||
Ok((
|
||||
FrameID::Valid(String::from(id)),
|
||||
FrameValue::Text {
|
||||
encoding,
|
||||
value: text,
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
fn parse_link(id: &str, content: &mut &[u8]) -> Result<TagItem> {
|
||||
fn parse_link(id: &str, content: &mut &[u8]) -> Result<(FrameID, FrameValue)> {
|
||||
let link = decode_text(content, TextEncoding::Latin1, false)?.unwrap_or_else(String::new);
|
||||
|
||||
let key = ItemKey::from_key(&TagType::Id3v2, id)
|
||||
.unwrap_or_else(|| ItemKey::Id3v2Specific(Id3v2Frame::URL(id.to_string())));
|
||||
|
||||
Ok(TagItem::new(key, ItemValue::Locator(link)))
|
||||
Ok((FrameID::Valid(String::from(id)), FrameValue::URL(link)))
|
||||
}
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
use super::FrameFlags;
|
||||
use crate::error::{LoftyError, Result};
|
||||
use crate::logic::id3::v2::util::upgrade::{upgrade_v2, upgrade_v3};
|
||||
use crate::types::item::TagItemFlags;
|
||||
|
||||
use std::io::Read;
|
||||
|
||||
pub(crate) fn parse_v2_header<R>(reader: &mut R) -> Result<Option<(String, u32, TagItemFlags)>>
|
||||
pub(crate) fn parse_v2_header<R>(reader: &mut R) -> Result<Option<(String, u32, FrameFlags)>>
|
||||
where
|
||||
R: Read,
|
||||
{
|
||||
|
@ -26,13 +26,13 @@ where
|
|||
let size = u32::from_be_bytes([0, frame_header[3], frame_header[4], frame_header[5]]);
|
||||
|
||||
// V2 doesn't store flags
|
||||
Ok(Some((id.to_string(), size, TagItemFlags::default())))
|
||||
Ok(Some((id.to_string(), size, FrameFlags::default())))
|
||||
}
|
||||
|
||||
pub(crate) fn parse_header<R>(
|
||||
reader: &mut R,
|
||||
synchsafe: bool,
|
||||
) -> Result<Option<(String, u32, TagItemFlags)>>
|
||||
) -> Result<Option<(String, u32, FrameFlags)>>
|
||||
where
|
||||
R: Read,
|
||||
{
|
||||
|
@ -77,8 +77,8 @@ where
|
|||
Ok(Some((id.to_string(), size, flags)))
|
||||
}
|
||||
|
||||
pub(crate) fn parse_flags(flags: u16, v4: bool) -> TagItemFlags {
|
||||
TagItemFlags {
|
||||
pub(crate) fn parse_flags(flags: u16, v4: bool) -> FrameFlags {
|
||||
FrameFlags {
|
||||
tag_alter_preservation: if v4 {
|
||||
flags & 0x4000 == 0x4000
|
||||
} else {
|
||||
|
|
|
@ -3,39 +3,175 @@ mod header;
|
|||
pub(in crate::logic::id3::v2) mod read;
|
||||
|
||||
use super::util::text_utils::TextEncoding;
|
||||
use crate::error::{LoftyError, Result};
|
||||
use crate::logic::id3::v2::util::text_utils::encode_text;
|
||||
use crate::logic::id3::v2::util::upgrade::{upgrade_v2, upgrade_v3};
|
||||
use crate::types::item::{ItemKey, ItemValue, TagItem};
|
||||
use crate::types::picture::Picture;
|
||||
use crate::types::tag::TagType;
|
||||
use std::convert::TryFrom;
|
||||
|
||||
#[derive(PartialEq, Clone, Debug, Eq, Hash)]
|
||||
pub struct Frame {
|
||||
pub(super) id: FrameID,
|
||||
pub(super) value: FrameValue,
|
||||
pub(super) flags: FrameFlags,
|
||||
}
|
||||
|
||||
impl Frame {
|
||||
pub fn new(id: &str, value: FrameValue, flags: FrameFlags) -> Result<Self> {
|
||||
let id = match id.len() {
|
||||
// An ID with a length of 4 could be either V3 or V4.
|
||||
4 => match upgrade_v3(id) {
|
||||
None => FrameID::Valid(id.to_string()),
|
||||
Some(id) => FrameID::Valid(id.to_string()),
|
||||
},
|
||||
3 => match upgrade_v2(id) {
|
||||
None => FrameID::Outdated(id.to_string()),
|
||||
Some(upgraded) => FrameID::Valid(upgraded.to_string()),
|
||||
},
|
||||
_ => {
|
||||
return Err(LoftyError::Id3v2(
|
||||
"Frame ID has a bad length (!= 3 || != 4)",
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
match id {
|
||||
FrameID::Valid(id) | FrameID::Outdated(id) if !id.is_ascii() => {
|
||||
return Err(LoftyError::Id3v2("Frame ID contains non-ascii characters"))
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
Ok(Self { id, value, flags })
|
||||
}
|
||||
|
||||
pub fn id_str(&self) -> &str {
|
||||
match &self.id {
|
||||
FrameID::Valid(id) | FrameID::Outdated(id) => id.as_str(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn content(&self) -> &FrameValue {
|
||||
&self.value
|
||||
}
|
||||
|
||||
/// Returns a reference to the [`FrameFlags`]
|
||||
pub fn flags(&self) -> &FrameFlags {
|
||||
&self.flags
|
||||
}
|
||||
|
||||
/// Set the item's flags
|
||||
pub fn set_flags(&mut self, flags: FrameFlags) {
|
||||
self.flags = flags
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Clone, Debug, Eq, Hash)]
|
||||
/// Information about an ID3v2 frame that requires a language
|
||||
pub struct LanguageSpecificFrame {
|
||||
pub struct LanguageFrame {
|
||||
/// The encoding of the description and comment text
|
||||
pub encoding: TextEncoding,
|
||||
/// ISO-639-2 language code (3 bytes)
|
||||
pub language: String,
|
||||
/// Unique content description
|
||||
pub description: Option<String>,
|
||||
pub description: String,
|
||||
/// The actual frame content
|
||||
pub content: String,
|
||||
}
|
||||
|
||||
impl LanguageFrame {
|
||||
pub fn as_bytes(&self) -> Result<Vec<u8>> {
|
||||
let mut bytes = vec![self.encoding as u8];
|
||||
|
||||
if self.language.len() != 3 || !self.language.is_ascii() {
|
||||
return Err(LoftyError::Id3v2(
|
||||
"Invalid frame language found (expected 3 ascii characters)",
|
||||
));
|
||||
}
|
||||
|
||||
bytes.extend(self.language.as_bytes().iter());
|
||||
bytes.extend(encode_text(&*self.description, self.encoding, true).iter());
|
||||
bytes.extend(encode_text(&*self.content, self.encoding, false));
|
||||
|
||||
Ok(bytes)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Clone, Debug, Eq, Hash)]
|
||||
pub struct EncodedTextFrame {
|
||||
/// The encoding of the description and comment text
|
||||
pub encoding: TextEncoding,
|
||||
/// Unique content description
|
||||
pub description: String,
|
||||
/// The actual frame content
|
||||
pub content: String,
|
||||
}
|
||||
|
||||
impl EncodedTextFrame {
|
||||
pub fn as_bytes(&self) -> Vec<u8> {
|
||||
let mut bytes = vec![self.encoding as u8];
|
||||
|
||||
bytes.extend(encode_text(&*self.description, self.encoding, true).iter());
|
||||
bytes.extend(encode_text(&*self.content, self.encoding, false));
|
||||
|
||||
bytes
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Clone, Debug, Eq, Hash)]
|
||||
/// Different types of ID3v2 frames that require varying amounts of information
|
||||
pub enum Id3v2Frame {
|
||||
pub enum FrameID {
|
||||
Valid(String),
|
||||
/// When an ID3v2.2 key couldn't be upgraded
|
||||
///
|
||||
/// This **will not** be written. It is up to the user to upgrade and store the key as [`Id3v2Frame::Valid`](Self::Valid).
|
||||
///
|
||||
/// The entire frame is stored as [`ItemValue::Binary`](crate::ItemValue::Binary).
|
||||
Outdated(String),
|
||||
}
|
||||
|
||||
impl TryFrom<ItemKey> for FrameID {
|
||||
type Error = LoftyError;
|
||||
|
||||
fn try_from(value: ItemKey) -> std::prelude::rust_2015::Result<Self, Self::Error> {
|
||||
match value {
|
||||
ItemKey::Unknown(unknown) if unknown.len() == 4 && unknown.is_ascii() => {
|
||||
Ok(Self::Valid(unknown.to_ascii_uppercase()))
|
||||
}
|
||||
k => k.map_key(&TagType::Id3v2, false).map_or(
|
||||
Err(LoftyError::Id3v2(
|
||||
"ItemKey does not meet the requirements to be a FrameID",
|
||||
)),
|
||||
|id| Ok(Self::Valid(id.to_string())),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Clone, Debug, Eq, Hash)]
|
||||
pub enum FrameValue {
|
||||
/// Represents a "COMM" frame
|
||||
///
|
||||
/// Due to the amount of information needed, it is contained in a separate struct, [`LanguageSpecificFrame`]
|
||||
Comment(LanguageSpecificFrame),
|
||||
/// Due to the amount of information needed, it is contained in a separate struct, [`LanguageFrame`]
|
||||
Comment(LanguageFrame),
|
||||
/// Represents a "USLT" frame
|
||||
///
|
||||
/// Due to the amount of information needed, it is contained in a separate struct, [`LanguageSpecificFrame`]
|
||||
UnSyncText(LanguageSpecificFrame),
|
||||
/// Due to the amount of information needed, it is contained in a separate struct, [`LanguageFrame`]
|
||||
UnSyncText(LanguageFrame),
|
||||
/// Represents a "T..." (excluding TXXX) frame
|
||||
///
|
||||
/// NOTE: Text frame names **must** be unique
|
||||
///
|
||||
/// This can be thought of as Text(name, encoding)
|
||||
Text(String, TextEncoding),
|
||||
Text {
|
||||
encoding: TextEncoding,
|
||||
value: String,
|
||||
},
|
||||
/// Represents a "TXXX" frame
|
||||
///
|
||||
/// This can be thought of as TXXX(encoding, description), as TXXX frames are often identified by descriptions.
|
||||
UserText(TextEncoding, String),
|
||||
/// Due to the amount of information needed, it is contained in a separate struct, [`EncodedTextFrame`]
|
||||
UserText(EncodedTextFrame),
|
||||
/// Represents a "W..." (excluding WXXX) frame
|
||||
///
|
||||
/// NOTES:
|
||||
|
@ -47,18 +183,161 @@ pub enum Id3v2Frame {
|
|||
URL(String),
|
||||
/// Represents a "WXXX" frame
|
||||
///
|
||||
/// This can be thought of as WXXX(encoding, description), as WXXX frames are often identified by descriptions.
|
||||
UserURL(TextEncoding, String),
|
||||
/// Represents a "SYLT" frame
|
||||
/// Due to the amount of information needed, it is contained in a separate struct, [`EncodedTextFrame`]
|
||||
UserURL(EncodedTextFrame),
|
||||
/// Represents an "APIC" or "PIC" frame
|
||||
Picture(Picture),
|
||||
/// Binary data
|
||||
///
|
||||
/// Nothing is required here, the entire frame is stored as [`ItemValue::Binary`](crate::ItemValue::Binary). For parsing see [`SynchronizedText::parse`](crate::id3::v2::SynchronizedText::parse)
|
||||
SyncText,
|
||||
/// Represents a "GEOB" frame
|
||||
/// NOTES:
|
||||
///
|
||||
/// Nothing is required here, the entire frame is stored as [`ItemValue::Binary`](crate::ItemValue::Binary). For parsing see [`GeneralEncapsulatedObject::parse`](crate::id3::v2::GeneralEncapsulatedObject::parse)
|
||||
EncapsulatedObject,
|
||||
/// When an ID3v2.2 key couldn't be upgraded
|
||||
///
|
||||
/// This **will not** be written. It is up to the user to upgrade and store the key as another variant.
|
||||
Outdated(String),
|
||||
/// * This is used for "GEOB" and "SYLT" frames, see
|
||||
/// [`GeneralEncapsulatedObject::parse`](crate::id3::v2::GeneralEncapsulatedObject::parse) and [`SynchronizedText::parse`](crate::id3::v2::SynchronizedText::parse) respectively
|
||||
/// * This is used for **all** frames with an ID of [`FrameID::Outdated`]
|
||||
/// * This is used for unknown frames
|
||||
Binary(Vec<u8>),
|
||||
}
|
||||
|
||||
impl From<ItemValue> for FrameValue {
|
||||
fn from(input: ItemValue) -> Self {
|
||||
match input {
|
||||
ItemValue::Text(text) => FrameValue::Text {
|
||||
encoding: TextEncoding::UTF8,
|
||||
value: text,
|
||||
},
|
||||
ItemValue::Locator(locator) => FrameValue::URL(locator),
|
||||
ItemValue::Binary(binary) => FrameValue::Binary(binary),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Default)]
|
||||
#[allow(clippy::struct_excessive_bools)]
|
||||
/// Various flags to describe the content of an item
|
||||
pub struct FrameFlags {
|
||||
/// Preserve frame on tag edit
|
||||
pub tag_alter_preservation: bool,
|
||||
/// Preserve frame on file edit
|
||||
pub file_alter_preservation: bool,
|
||||
/// Item cannot be written to
|
||||
pub read_only: bool,
|
||||
/// Frame belongs in a group
|
||||
///
|
||||
/// In addition to setting this flag, a group identifier byte must be added.
|
||||
/// All frames with the same group identifier byte belong to the same group.
|
||||
pub grouping_identity: (bool, u8),
|
||||
/// Frame is zlib compressed
|
||||
///
|
||||
/// It is **required** `data_length_indicator` be set if this is set.
|
||||
pub compression: bool,
|
||||
/// Frame is encrypted
|
||||
///
|
||||
/// NOTE: Since the encryption method is unknown, lofty cannot do anything with these frames
|
||||
///
|
||||
/// In addition to setting this flag, an encryption method symbol must be added.
|
||||
/// The method symbol **must** be > 0x80.
|
||||
pub encryption: (bool, u8),
|
||||
/// Frame is unsynchronised
|
||||
///
|
||||
/// In short, this makes all "0xFF 0x00" combinations into "0xFF 0x00 0x00" to avoid confusion
|
||||
/// with the MPEG frame header, which is often identified by its "frame sync" (11 set bits).
|
||||
/// It is preferred an ID3v2 tag is either *completely* unsynchronised or not unsynchronised at all.
|
||||
pub unsynchronisation: bool,
|
||||
/// Frame has a data length indicator
|
||||
///
|
||||
/// The data length indicator is the size of the frame if the flags were all zeroed out.
|
||||
/// This is usually used in combination with `compression` and `encryption` (depending on encryption method).
|
||||
///
|
||||
/// If using encryption, the final size must be added. It will be ignored if using compression.
|
||||
pub data_length_indicator: (bool, u32),
|
||||
}
|
||||
|
||||
pub(crate) struct FrameRef<'a> {
|
||||
pub id: &'a str,
|
||||
pub value: FrameValueRef<'a>,
|
||||
pub flags: FrameFlags,
|
||||
}
|
||||
|
||||
impl<'a> Frame {
|
||||
pub(crate) fn as_opt_ref(&'a self) -> Option<FrameRef<'a>> {
|
||||
if let FrameID::Valid(id) = &self.id {
|
||||
Some(FrameRef {
|
||||
id,
|
||||
value: (&self.value).into(),
|
||||
flags: self.flags,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> TryFrom<&'a TagItem> for FrameRef<'a> {
|
||||
type Error = LoftyError;
|
||||
|
||||
fn try_from(value: &'a TagItem) -> std::prelude::rust_2015::Result<Self, Self::Error> {
|
||||
let id = match value.key() {
|
||||
ItemKey::Unknown(unknown)
|
||||
if unknown.len() == 4
|
||||
&& unknown.is_ascii()
|
||||
&& unknown.chars().all(|c| c.is_ascii_uppercase()) =>
|
||||
{
|
||||
Ok(unknown.as_str())
|
||||
}
|
||||
k => k.map_key(&TagType::Id3v2, false).ok_or(LoftyError::Id3v2(
|
||||
"ItemKey does not meet the requirements to be a FrameID",
|
||||
)),
|
||||
}?;
|
||||
|
||||
Ok(FrameRef {
|
||||
id,
|
||||
value: Into::<FrameValueRef<'a>>::into(value.value()),
|
||||
flags: FrameFlags::default(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) enum FrameValueRef<'a> {
|
||||
Comment(&'a LanguageFrame),
|
||||
UnSyncText(&'a LanguageFrame),
|
||||
Text {
|
||||
encoding: TextEncoding,
|
||||
value: &'a str,
|
||||
},
|
||||
UserText(&'a EncodedTextFrame),
|
||||
URL(&'a str),
|
||||
UserURL(&'a EncodedTextFrame),
|
||||
Picture(&'a Picture),
|
||||
Binary(&'a [u8]),
|
||||
}
|
||||
|
||||
impl<'a> Into<FrameValueRef<'a>> for &'a FrameValue {
|
||||
fn into(self) -> FrameValueRef<'a> {
|
||||
match self {
|
||||
FrameValue::Comment(lf) => FrameValueRef::Comment(lf),
|
||||
FrameValue::UnSyncText(lf) => FrameValueRef::UnSyncText(lf),
|
||||
FrameValue::Text { encoding, value } => FrameValueRef::Text {
|
||||
encoding: *encoding,
|
||||
value: value.as_str(),
|
||||
},
|
||||
FrameValue::UserText(etf) => FrameValueRef::UserText(etf),
|
||||
FrameValue::URL(url) => FrameValueRef::URL(url.as_str()),
|
||||
FrameValue::UserURL(etf) => FrameValueRef::UserURL(etf),
|
||||
FrameValue::Picture(pic) => FrameValueRef::Picture(pic),
|
||||
FrameValue::Binary(bin) => FrameValueRef::Binary(bin.as_slice()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Into<FrameValueRef<'a>> for &'a ItemValue {
|
||||
fn into(self) -> FrameValueRef<'a> {
|
||||
match self {
|
||||
ItemValue::Text(text) => FrameValueRef::Text {
|
||||
encoding: TextEncoding::UTF8,
|
||||
value: text.as_str(),
|
||||
},
|
||||
ItemValue::Locator(locator) => FrameValueRef::URL(locator.as_str()),
|
||||
ItemValue::Binary(binary) => FrameValueRef::Binary(binary.as_slice()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,18 +1,13 @@
|
|||
use super::header::{parse_header, parse_v2_header};
|
||||
use super::Frame;
|
||||
use crate::error::{LoftyError, Result};
|
||||
use crate::logic::id3::v2::frame::content::{parse_content, FrameContent};
|
||||
use crate::logic::id3::v2::frame::content::parse_content;
|
||||
use crate::logic::id3::v2::Id3v2Version;
|
||||
use crate::types::item::TagItemFlags;
|
||||
|
||||
use std::io::Read;
|
||||
|
||||
use byteorder::{BigEndian, ReadBytesExt};
|
||||
|
||||
pub(crate) struct Frame {
|
||||
pub flags: TagItemFlags,
|
||||
pub content: FrameContent,
|
||||
}
|
||||
|
||||
impl Frame {
|
||||
pub(crate) fn read<R>(reader: &mut R, version: Id3v2Version) -> Result<Option<Self>>
|
||||
where
|
||||
|
@ -59,9 +54,8 @@ impl Frame {
|
|||
flags.data_length_indicator.1 = content_reader.read_u32::<BigEndian>()?;
|
||||
}
|
||||
|
||||
Ok(Some(Self {
|
||||
flags,
|
||||
content: parse_content(&mut content_reader, &*id, version)?,
|
||||
}))
|
||||
let (id, value) = parse_content(&mut content_reader, &*id, version)?;
|
||||
|
||||
Ok(Some(Self { id, value, flags }))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -83,7 +83,7 @@ impl TagRestrictions {
|
|||
/// Read a [`TagRestrictions`] from a byte
|
||||
///
|
||||
/// NOTE: See https://id3.org/id3v2.4.0-structure section 3.2, item d
|
||||
pub fn parse(byte: u8) -> Self {
|
||||
pub fn from_byte(byte: u8) -> Self {
|
||||
let mut restrictions = TagRestrictions::default();
|
||||
|
||||
let restriction_flags = byte;
|
||||
|
@ -134,7 +134,7 @@ impl TagRestrictions {
|
|||
restrictions
|
||||
}
|
||||
|
||||
/// Convert a [`TagRestrictions`] into a byte Vec
|
||||
/// Convert a [`TagRestrictions`] into a `u8`
|
||||
///
|
||||
/// NOTE: This does not include a frame header
|
||||
pub fn as_bytes(&self) -> u8 {
|
||||
|
|
|
@ -1,3 +1,10 @@
|
|||
pub(crate) mod frame;
|
||||
pub(crate) mod items;
|
||||
pub(crate) mod read;
|
||||
pub(crate) mod tag;
|
||||
pub(crate) mod util;
|
||||
pub(in crate::logic) mod write;
|
||||
|
||||
use crate::error::Result;
|
||||
use crate::logic::id3::unsynch_u32;
|
||||
|
||||
|
@ -5,12 +12,6 @@ use std::io::{Read, Seek, SeekFrom};
|
|||
|
||||
use byteorder::{BigEndian, ByteOrder};
|
||||
|
||||
pub(crate) mod frame;
|
||||
pub(crate) mod items;
|
||||
pub(crate) mod read;
|
||||
pub(crate) mod util;
|
||||
pub(in crate::logic) mod write;
|
||||
|
||||
#[derive(PartialEq, Debug, Clone, Copy)]
|
||||
/// The ID3v2 version
|
||||
pub enum Id3v2Version {
|
||||
|
|
|
@ -1,18 +1,17 @@
|
|||
use super::frame::read::Frame;
|
||||
use crate::error::Result;
|
||||
use super::frame::Frame;
|
||||
use super::tag::Id3v2Tag;
|
||||
use super::tag::Id3v2TagFlags;
|
||||
use crate::error::{LoftyError, Result};
|
||||
use crate::logic::id3::unsynch_u32;
|
||||
use crate::logic::id3::v2::frame::content::FrameContent;
|
||||
#[cfg(feature = "id3v2_restrictions")]
|
||||
use crate::logic::id3::v2::items::restrictions::TagRestrictions;
|
||||
use crate::logic::id3::v2::Id3v2Version;
|
||||
use crate::types::tag::{Tag, TagFlags};
|
||||
use crate::{LoftyError, TagType};
|
||||
|
||||
use std::io::Read;
|
||||
|
||||
use byteorder::{BigEndian, ReadBytesExt};
|
||||
|
||||
pub(crate) fn parse_id3v2(bytes: &mut &[u8]) -> Result<Tag> {
|
||||
pub(crate) fn parse_id3v2(bytes: &mut &[u8]) -> Result<Id3v2Tag> {
|
||||
let mut header = [0; 10];
|
||||
bytes.read_exact(&mut header)?;
|
||||
|
||||
|
@ -37,7 +36,7 @@ pub(crate) fn parse_id3v2(bytes: &mut &[u8]) -> Result<Tag> {
|
|||
return Err(LoftyError::Id3v2("Encountered a compressed ID3v2.2 tag"));
|
||||
}
|
||||
|
||||
let mut flags_parsed = TagFlags {
|
||||
let mut flags_parsed = Id3v2TagFlags {
|
||||
unsynchronisation: flags & 0x80 == 0x80,
|
||||
experimental: (version == Id3v2Version::V4 || version == Id3v2Version::V3)
|
||||
&& flags & 0x20 == 0x20,
|
||||
|
@ -82,27 +81,17 @@ pub(crate) fn parse_id3v2(bytes: &mut &[u8]) -> Result<Tag> {
|
|||
// We don't care about the length byte, it is always 1
|
||||
let _data_length = bytes.read_u8()?;
|
||||
|
||||
flags_parsed.restrictions.1 = TagRestrictions::parse(bytes.read_u8()?);
|
||||
flags_parsed.restrictions.1 = TagRestrictions::from_byte(bytes.read_u8()?);
|
||||
}
|
||||
}
|
||||
|
||||
let mut tag = {
|
||||
let mut tag = Tag::new(TagType::Id3v2);
|
||||
tag.set_flags(flags_parsed);
|
||||
|
||||
tag
|
||||
};
|
||||
let mut tag = Id3v2Tag::default();
|
||||
tag.set_flags(flags_parsed);
|
||||
|
||||
loop {
|
||||
match Frame::read(bytes, version)? {
|
||||
None => break,
|
||||
Some(f) => match f.content {
|
||||
FrameContent::Picture(pic) => tag.push_picture(pic),
|
||||
FrameContent::Item(mut item) => {
|
||||
item.set_flags(f.flags);
|
||||
tag.insert_item_unchecked(item)
|
||||
}
|
||||
},
|
||||
Some(f) => drop(tag.insert(f)),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
199
src/logic/id3/v2/tag.rs
Normal file
199
src/logic/id3/v2/tag.rs
Normal file
|
@ -0,0 +1,199 @@
|
|||
use super::frame::{EncodedTextFrame, LanguageFrame};
|
||||
use super::frame::{Frame, FrameFlags, FrameValue};
|
||||
#[cfg(feature = "id3v2_restrictions")]
|
||||
use super::items::restrictions::TagRestrictions;
|
||||
use super::Id3v2Version;
|
||||
use crate::error::Result;
|
||||
use crate::logic::id3::v2::frame::FrameRef;
|
||||
use crate::types::item::{ItemKey, ItemValue, TagItem};
|
||||
use crate::types::tag::{Tag, TagType};
|
||||
|
||||
use std::convert::TryInto;
|
||||
use std::fs::File;
|
||||
|
||||
use byteorder::ByteOrder;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Id3v2Tag {
|
||||
flags: Id3v2TagFlags,
|
||||
frames: Vec<Frame>,
|
||||
}
|
||||
|
||||
impl Id3v2Tag {
|
||||
/// Returns the [`Id3v2TagFlags`]
|
||||
pub fn flags(&self) -> &Id3v2TagFlags {
|
||||
&self.flags
|
||||
}
|
||||
|
||||
/// Restrict the tag's flags
|
||||
pub fn set_flags(&mut self, flags: Id3v2TagFlags) {
|
||||
self.flags = flags
|
||||
}
|
||||
}
|
||||
|
||||
impl Id3v2Tag {
|
||||
pub fn iter(&self) -> impl Iterator<Item = &Frame> {
|
||||
self.frames.iter()
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.frames.len()
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.frames.is_empty()
|
||||
}
|
||||
|
||||
pub fn get(&self, id: &str) -> Option<&Frame> {
|
||||
self.frames.iter().find(|f| f.id_str() == id)
|
||||
}
|
||||
|
||||
pub fn insert(&mut self, frame: Frame) -> Option<Frame> {
|
||||
let replaced = self
|
||||
.frames
|
||||
.iter()
|
||||
.position(|f| f == &frame)
|
||||
.map(|pos| self.frames.remove(pos));
|
||||
|
||||
self.frames.push(frame);
|
||||
replaced
|
||||
}
|
||||
|
||||
pub fn remove(&mut self, id: &str) {
|
||||
self.frames.retain(|f| f.id_str() != id)
|
||||
}
|
||||
}
|
||||
|
||||
impl Id3v2Tag {
|
||||
pub fn write_to(&self, file: &mut File) -> Result<()> {
|
||||
Into::<Id3v2TagRef>::into(self).write_to(file)
|
||||
}
|
||||
|
||||
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 {
|
||||
type Item = Frame;
|
||||
type IntoIter = std::vec::IntoIter<Frame>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.frames.into_iter()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Id3v2Tag> for Tag {
|
||||
fn from(input: Id3v2Tag) -> Self {
|
||||
let mut tag = Self::new(TagType::Id3v2);
|
||||
|
||||
for frame in input.frames {
|
||||
let item_key = ItemKey::from_key(&TagType::Id3v2, frame.id_str());
|
||||
let item_value = match frame.value {
|
||||
FrameValue::Comment(LanguageFrame { content, .. })
|
||||
| FrameValue::UnSyncText(LanguageFrame { content, .. })
|
||||
| FrameValue::Text { value: content, .. }
|
||||
| FrameValue::UserText(EncodedTextFrame { content, .. }) => ItemValue::Text(content),
|
||||
FrameValue::URL(content)
|
||||
| FrameValue::UserURL(EncodedTextFrame { content, .. }) => ItemValue::Locator(content),
|
||||
FrameValue::Picture(pic) => {
|
||||
ItemValue::Binary(if let Ok(bin) = pic.as_apic_bytes(Id3v2Version::V4) {
|
||||
bin
|
||||
} else {
|
||||
continue;
|
||||
})
|
||||
}
|
||||
FrameValue::Binary(binary) => ItemValue::Binary(binary),
|
||||
};
|
||||
|
||||
tag.insert_item_unchecked(TagItem::new(item_key, item_value))
|
||||
}
|
||||
|
||||
tag
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Tag> for Id3v2Tag {
|
||||
fn from(input: Tag) -> Self {
|
||||
let mut id3v2_tag = Self::default();
|
||||
|
||||
for item in input.items {
|
||||
let id = match item.item_key.try_into() {
|
||||
Ok(id) => id,
|
||||
Err(_) => continue,
|
||||
};
|
||||
|
||||
let frame_value: FrameValue = item.item_value.into();
|
||||
|
||||
id3v2_tag.frames.push(Frame {
|
||||
id,
|
||||
value: frame_value,
|
||||
flags: FrameFlags::default(),
|
||||
});
|
||||
}
|
||||
|
||||
id3v2_tag
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Copy, Clone)]
|
||||
#[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>,
|
||||
}
|
||||
|
||||
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<()> {
|
||||
super::write::write_id3v2_to_chunk_file::<B>(file, self)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Into<Id3v2TagRef<'a>> for &'a Tag {
|
||||
fn into(self) -> Id3v2TagRef<'a> {
|
||||
Id3v2TagRef {
|
||||
flags: Id3v2TagFlags::default(),
|
||||
frames: Box::new(
|
||||
self.items()
|
||||
.iter()
|
||||
.map(TryInto::<FrameRef>::try_into)
|
||||
.filter_map(Result::ok),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Into<Id3v2TagRef<'a>> for &'a Id3v2Tag {
|
||||
fn into(self) -> Id3v2TagRef<'a> {
|
||||
Id3v2TagRef {
|
||||
flags: self.flags,
|
||||
frames: Box::new(self.frames.iter().filter_map(Frame::as_opt_ref)),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -7,6 +7,7 @@ use byteorder::ReadBytesExt;
|
|||
|
||||
/// The text encoding for use in ID3v2 frames
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Copy, Hash)]
|
||||
#[repr(u8)]
|
||||
pub enum TextEncoding {
|
||||
/// ISO-8859-1
|
||||
Latin1 = 0,
|
||||
|
|
|
@ -9,6 +9,8 @@ pub(in crate::logic::id3::v2) fn write_to_chunk_file<B>(data: &mut File, tag: &[
|
|||
where
|
||||
B: ByteOrder,
|
||||
{
|
||||
data.seek(SeekFrom::Current(12))?;
|
||||
|
||||
let mut id3v2_chunk = (None, None);
|
||||
|
||||
let mut fourcc = [0; 4];
|
||||
|
@ -22,9 +24,14 @@ where
|
|||
data.seek(SeekFrom::Current(i64::from(size)))?;
|
||||
}
|
||||
|
||||
if let (Some(chunk_start), Some(chunk_size)) = id3v2_chunk {
|
||||
if let (Some(chunk_start), Some(mut chunk_size)) = id3v2_chunk {
|
||||
data.seek(SeekFrom::Start(0))?;
|
||||
|
||||
// We need to remove the padding byte if it exists
|
||||
if chunk_size % 2 != 0 {
|
||||
chunk_size += 1;
|
||||
}
|
||||
|
||||
let mut file_bytes = Vec::new();
|
||||
data.read_to_end(&mut file_bytes)?;
|
||||
|
||||
|
@ -44,7 +51,14 @@ where
|
|||
data.write_u32::<B>(tag.len() as u32)?;
|
||||
data.write_all(tag)?;
|
||||
|
||||
// It is required an odd length chunk be padded with a 0
|
||||
// The 0 isn't included in the chunk size, however
|
||||
if tag.len() % 2 != 0 {
|
||||
data.write_u8(0)?;
|
||||
}
|
||||
|
||||
let total_size = data.seek(SeekFrom::Current(0))? - 8;
|
||||
|
||||
data.seek(SeekFrom::Start(4))?;
|
||||
|
||||
data.write_u32::<B>(total_size as u32)?;
|
||||
|
|
|
@ -1,137 +1,100 @@
|
|||
use crate::error::{LoftyError, Result};
|
||||
use crate::id3::v2::Id3v2Version;
|
||||
use crate::logic::id3::synch_u32;
|
||||
use crate::logic::id3::v2::frame::{Id3v2Frame, LanguageSpecificFrame};
|
||||
use crate::logic::id3::v2::util::text_utils::{encode_text, TextEncoding};
|
||||
use crate::types::item::{ItemKey, ItemValue, TagItem, TagItemFlags};
|
||||
use crate::types::tag::TagType;
|
||||
use crate::logic::id3::v2::frame::{FrameFlags, FrameRef, FrameValueRef};
|
||||
|
||||
use std::io::Write;
|
||||
|
||||
use byteorder::{BigEndian, WriteBytesExt};
|
||||
|
||||
enum FrameType<'a> {
|
||||
EncodedText(TextEncoding),
|
||||
LanguageDependent(&'a LanguageSpecificFrame),
|
||||
UserDefined(TextEncoding, &'a str),
|
||||
Other,
|
||||
}
|
||||
|
||||
pub(in crate::logic::id3::v2) fn create_items<W>(writer: &mut W, items: &[TagItem]) -> Result<()>
|
||||
pub(in crate::logic::id3::v2) fn create_items<'a, W>(
|
||||
writer: &mut W,
|
||||
frames: &mut dyn Iterator<Item = FrameRef<'a>>,
|
||||
) -> Result<()>
|
||||
where
|
||||
W: Write,
|
||||
{
|
||||
// Get rid of any invalid keys
|
||||
let items = items.iter().filter(|i| {
|
||||
(match i.key() {
|
||||
ItemKey::Id3v2Specific(Id3v2Frame::Text(name, _)) => {
|
||||
name.starts_with('T') && name.is_ascii() && name.len() == 4
|
||||
for frame in frames {
|
||||
let value = match frame.value {
|
||||
FrameValueRef::Comment(content) | FrameValueRef::UnSyncText(content) => {
|
||||
content.as_bytes()?
|
||||
}
|
||||
ItemKey::Id3v2Specific(Id3v2Frame::URL(name)) => {
|
||||
name.starts_with('W') && name.is_ascii() && name.len() == 4
|
||||
}
|
||||
ItemKey::Id3v2Specific(id3v2_frame) => {
|
||||
std::mem::discriminant(&Id3v2Frame::Outdated(String::new()))
|
||||
!= std::mem::discriminant(id3v2_frame)
|
||||
}
|
||||
ItemKey::Unknown(_) => false,
|
||||
key => key.map_key(&TagType::Id3v2).is_some(),
|
||||
}) && matches!(
|
||||
i.value(),
|
||||
ItemValue::Text(_) | ItemValue::Locator(_) | ItemValue::Binary(_)
|
||||
)
|
||||
});
|
||||
FrameValueRef::Text { value, encoding } => {
|
||||
let mut v = vec![encoding as u8];
|
||||
|
||||
// Get rid of any invalid keys
|
||||
for item in items {
|
||||
let value = match item.value() {
|
||||
ItemValue::Text(text) => text.as_bytes(),
|
||||
ItemValue::Locator(locator) => locator.as_bytes(),
|
||||
ItemValue::Binary(binary) => binary,
|
||||
_ => unreachable!(),
|
||||
v.extend_from_slice(value.as_bytes());
|
||||
v
|
||||
}
|
||||
FrameValueRef::UserText(content) | FrameValueRef::UserURL(content) => {
|
||||
content.as_bytes()
|
||||
}
|
||||
FrameValueRef::URL(link) => link.as_bytes().to_vec(),
|
||||
FrameValueRef::Picture(pic) => pic.as_apic_bytes(Id3v2Version::V4)?,
|
||||
FrameValueRef::Binary(binary) => binary.to_vec(),
|
||||
};
|
||||
|
||||
let flags = item.flags();
|
||||
|
||||
match item.key() {
|
||||
ItemKey::Id3v2Specific(frame) => match frame {
|
||||
Id3v2Frame::Comment(details) => write_frame(
|
||||
writer,
|
||||
&FrameType::LanguageDependent(details),
|
||||
"COMM",
|
||||
flags,
|
||||
0,
|
||||
value,
|
||||
)?,
|
||||
Id3v2Frame::UnSyncText(details) => write_frame(
|
||||
writer,
|
||||
&FrameType::LanguageDependent(details),
|
||||
"USLT",
|
||||
flags,
|
||||
0,
|
||||
value,
|
||||
)?,
|
||||
Id3v2Frame::Text(name, encoding) => write_frame(
|
||||
writer,
|
||||
&FrameType::EncodedText(*encoding),
|
||||
name,
|
||||
flags,
|
||||
// Encoding
|
||||
1,
|
||||
value,
|
||||
)?,
|
||||
Id3v2Frame::UserText(encoding, descriptor) => write_frame(
|
||||
writer,
|
||||
&FrameType::UserDefined(*encoding, descriptor),
|
||||
"TXXX",
|
||||
flags,
|
||||
// Encoding + descriptor + null terminator
|
||||
2 + descriptor.len() as u32,
|
||||
value,
|
||||
)?,
|
||||
Id3v2Frame::URL(name) => {
|
||||
write_frame(writer, &FrameType::Other, name, flags, 0, value)?
|
||||
}
|
||||
Id3v2Frame::UserURL(encoding, descriptor) => write_frame(
|
||||
writer,
|
||||
&FrameType::UserDefined(*encoding, descriptor),
|
||||
"WXXX",
|
||||
flags,
|
||||
// Encoding + descriptor + null terminator
|
||||
2 + descriptor.len() as u32,
|
||||
value,
|
||||
)?,
|
||||
Id3v2Frame::SyncText => {
|
||||
write_frame(writer, &FrameType::Other, "SYLT", flags, 0, value)?
|
||||
}
|
||||
Id3v2Frame::EncapsulatedObject => {
|
||||
write_frame(writer, &FrameType::Other, "GEOB", flags, 0, value)?
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
key => {
|
||||
let key = key.map_key(&TagType::Id3v2).unwrap();
|
||||
|
||||
if key.starts_with('T') {
|
||||
write_frame(
|
||||
writer,
|
||||
&FrameType::EncodedText(TextEncoding::UTF8),
|
||||
key,
|
||||
flags,
|
||||
// Encoding
|
||||
1,
|
||||
value,
|
||||
)?;
|
||||
} else {
|
||||
write_frame(writer, &FrameType::Other, key, flags, 0, value)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
write_frame(writer, frame.id, frame.flags, &value)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_frame_header<W>(writer: &mut W, name: &str, len: u32, flags: &TagItemFlags) -> Result<()>
|
||||
fn write_frame<W>(writer: &mut W, name: &str, flags: FrameFlags, value: &[u8]) -> Result<()>
|
||||
where
|
||||
W: Write,
|
||||
{
|
||||
if flags.encryption.0 {
|
||||
write_encrypted(writer, name, value, flags)?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let len = value.len() as u32;
|
||||
let is_grouping_identity = flags.grouping_identity.0;
|
||||
|
||||
write_frame_header(
|
||||
writer,
|
||||
name,
|
||||
if is_grouping_identity { len + 1 } else { len },
|
||||
flags,
|
||||
)?;
|
||||
|
||||
if is_grouping_identity {
|
||||
writer.write_u8(flags.grouping_identity.1)?;
|
||||
}
|
||||
|
||||
writer.write_all(value)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_encrypted<W>(writer: &mut W, name: &str, value: &[u8], flags: FrameFlags) -> Result<()>
|
||||
where
|
||||
W: Write,
|
||||
{
|
||||
let method_symbol = flags.encryption.1;
|
||||
let data_length_indicator = flags.data_length_indicator;
|
||||
|
||||
if method_symbol > 0x80 {
|
||||
return Err(LoftyError::Id3v2(
|
||||
"Attempted to write an encrypted frame with an invalid method symbol (> 0x80)",
|
||||
));
|
||||
}
|
||||
|
||||
if data_length_indicator.0 && data_length_indicator.1 > 0 {
|
||||
write_frame_header(writer, name, (value.len() + 1) as u32, flags)?;
|
||||
writer.write_u32::<BigEndian>(synch_u32(data_length_indicator.1)?)?;
|
||||
writer.write_u8(method_symbol)?;
|
||||
writer.write_all(value)?;
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
Err(LoftyError::Id3v2(
|
||||
"Attempted to write an encrypted frame without a data length indicator",
|
||||
))
|
||||
}
|
||||
|
||||
fn write_frame_header<W>(writer: &mut W, name: &str, len: u32, flags: FrameFlags) -> Result<()>
|
||||
where
|
||||
W: Write,
|
||||
{
|
||||
|
@ -142,10 +105,10 @@ where
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn get_flags(tag_flags: &TagItemFlags) -> u16 {
|
||||
fn get_flags(tag_flags: FrameFlags) -> u16 {
|
||||
let mut flags = 0;
|
||||
|
||||
if tag_flags == &TagItemFlags::default() {
|
||||
if tag_flags == FrameFlags::default() {
|
||||
return flags;
|
||||
}
|
||||
|
||||
|
@ -183,97 +146,3 @@ fn get_flags(tag_flags: &TagItemFlags) -> u16 {
|
|||
|
||||
flags
|
||||
}
|
||||
|
||||
fn write_frame<W>(
|
||||
writer: &mut W,
|
||||
frame_type: &FrameType,
|
||||
name: &str,
|
||||
flags: &TagItemFlags,
|
||||
// Any additional bytes, such as encoding or language code
|
||||
additional_len: u32,
|
||||
value: &[u8],
|
||||
) -> Result<()>
|
||||
where
|
||||
W: Write,
|
||||
{
|
||||
if flags.encryption.0 {
|
||||
write_encrypted(writer, name, value, flags)?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let len = value.len() as u32 + additional_len;
|
||||
let is_grouping_identity = flags.grouping_identity.0;
|
||||
|
||||
write_frame_header(
|
||||
writer,
|
||||
name,
|
||||
if is_grouping_identity { len + 1 } else { len },
|
||||
flags,
|
||||
)?;
|
||||
|
||||
if is_grouping_identity {
|
||||
writer.write_u8(flags.grouping_identity.1)?;
|
||||
}
|
||||
|
||||
match frame_type {
|
||||
FrameType::EncodedText(encoding) => {
|
||||
writer.write_u8(*encoding as u8)?;
|
||||
writer.write_all(value)?;
|
||||
}
|
||||
FrameType::LanguageDependent(details) => {
|
||||
writer.write_u8(details.encoding as u8)?;
|
||||
|
||||
if details.language.len() == 3 {
|
||||
writer.write_all(details.language.as_bytes())?;
|
||||
} else {
|
||||
return Err(LoftyError::Id3v2(
|
||||
"Attempted to write a LanguageSpecificFrame with an invalid language String \
|
||||
length (!= 3)",
|
||||
));
|
||||
}
|
||||
|
||||
if let Some(ref descriptor) = details.description {
|
||||
writer.write_all(&encode_text(descriptor, details.encoding, true))?;
|
||||
} else {
|
||||
writer.write_u8(0)?;
|
||||
}
|
||||
|
||||
writer.write_all(value)?;
|
||||
}
|
||||
FrameType::UserDefined(encoding, descriptor) => {
|
||||
writer.write_u8(*encoding as u8)?;
|
||||
writer.write_all(&encode_text(descriptor, *encoding, true))?;
|
||||
writer.write_all(value)?;
|
||||
}
|
||||
FrameType::Other => writer.write_all(value)?,
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_encrypted<W>(writer: &mut W, name: &str, value: &[u8], flags: &TagItemFlags) -> Result<()>
|
||||
where
|
||||
W: Write,
|
||||
{
|
||||
let method_symbol = flags.encryption.1;
|
||||
let data_length_indicator = flags.data_length_indicator;
|
||||
|
||||
if method_symbol > 0x80 {
|
||||
return Err(LoftyError::Id3v2(
|
||||
"Attempted to write an encrypted frame with an invalid method symbol (> 0x80)",
|
||||
));
|
||||
}
|
||||
|
||||
if data_length_indicator.0 && data_length_indicator.1 > 0 {
|
||||
write_frame_header(writer, name, (value.len() + 1) as u32, flags)?;
|
||||
writer.write_u32::<BigEndian>(synch_u32(data_length_indicator.1)?)?;
|
||||
writer.write_u8(method_symbol)?;
|
||||
writer.write_all(value)?;
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
Err(LoftyError::Id3v2(
|
||||
"Attempted to write an encrypted frame without a data length indicator",
|
||||
))
|
||||
}
|
||||
|
|
|
@ -2,29 +2,21 @@ mod chunk_file;
|
|||
mod frame;
|
||||
|
||||
use super::find_id3v2;
|
||||
use crate::error::Result;
|
||||
use crate::error::{LoftyError, Result};
|
||||
use crate::logic::id3::synch_u32;
|
||||
use crate::types::tag::{Tag, TagFlags};
|
||||
use crate::logic::id3::v2::tag::{Id3v2TagFlags, Id3v2TagRef};
|
||||
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};
|
||||
|
||||
pub(in crate::logic) fn write_id3v2(data: &mut File, tag: &Tag) -> Result<()> {
|
||||
if tag.item_count() == 0 {
|
||||
find_id3v2(data, false)?;
|
||||
|
||||
if data.seek(SeekFrom::Current(0))? != 0 {
|
||||
let mut file_bytes = Vec::new();
|
||||
data.read_to_end(&mut file_bytes)?;
|
||||
|
||||
data.seek(SeekFrom::Start(0))?;
|
||||
data.set_len(0)?;
|
||||
data.write_all(&*file_bytes)?;
|
||||
}
|
||||
|
||||
return Ok(());
|
||||
pub(in crate::logic) fn write_id3v2(data: &mut File, tag: &mut Id3v2TagRef) -> Result<()> {
|
||||
match Probe::new().file_type(data) {
|
||||
Some(ft) if ft == FileType::APE || ft == FileType::MP3 => {}
|
||||
_ => return Err(LoftyError::UnsupportedTag),
|
||||
}
|
||||
|
||||
let id3v2 = create_tag(tag)?;
|
||||
|
@ -45,26 +37,38 @@ pub(in crate::logic) fn write_id3v2(data: &mut File, tag: &Tag) -> Result<()> {
|
|||
}
|
||||
|
||||
// Formats such as WAV and AIFF store the ID3v2 tag in an 'ID3 ' chunk rather than at the beginning of the file
|
||||
pub(in crate::logic) fn write_id3v2_to_chunk_file<B>(data: &mut File, tag: &Tag) -> Result<()>
|
||||
pub(in crate::logic) fn write_id3v2_to_chunk_file<B>(
|
||||
data: &mut File,
|
||||
tag: &mut Id3v2TagRef,
|
||||
) -> Result<()>
|
||||
where
|
||||
B: ByteOrder,
|
||||
{
|
||||
let id3v2 = if tag.item_count() == 0 {
|
||||
Vec::new()
|
||||
} else {
|
||||
create_tag(tag)?
|
||||
};
|
||||
match Probe::new().file_type(data) {
|
||||
Some(ft) if ft == FileType::WAV || ft == FileType::AIFF => {}
|
||||
_ => return Err(LoftyError::UnsupportedTag),
|
||||
}
|
||||
|
||||
let id3v2 = create_tag(tag)?;
|
||||
|
||||
chunk_file::write_to_chunk_file::<B>(data, &id3v2)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn create_tag(tag: &Tag) -> Result<Vec<u8>> {
|
||||
let mut id3v2 = create_tag_header(tag.flags())?;
|
||||
fn create_tag(tag: &mut Id3v2TagRef) -> Result<Vec<u8>> {
|
||||
let frames = &mut tag.frames;
|
||||
let mut peek = frames.peekable();
|
||||
|
||||
if peek.peek().is_none() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
let mut id3v2 = create_tag_header(tag.flags)?;
|
||||
let header_len = id3v2.get_ref().len();
|
||||
|
||||
// Write the items
|
||||
frame::create_items(&mut id3v2, tag.items())?;
|
||||
frame::create_items(&mut id3v2, &mut peek)?;
|
||||
|
||||
let len = id3v2.get_ref().len() - header_len;
|
||||
|
||||
|
@ -76,7 +80,7 @@ fn create_tag(tag: &Tag) -> Result<Vec<u8>> {
|
|||
}
|
||||
|
||||
#[allow(clippy::trivially_copy_pass_by_ref)]
|
||||
fn create_tag_header(flags: &TagFlags) -> Result<Cursor<Vec<u8>>> {
|
||||
fn create_tag_header(flags: Id3v2TagFlags) -> Result<Cursor<Vec<u8>>> {
|
||||
let mut header = Cursor::new(Vec::new());
|
||||
|
||||
header.write_all(&[b'I', b'D', b'3'])?;
|
||||
|
|
|
@ -1,11 +1,16 @@
|
|||
mod properties;
|
||||
mod read;
|
||||
mod tag;
|
||||
#[cfg(feature = "aiff_text_chunks")]
|
||||
pub(crate) mod tag;
|
||||
pub(in crate::logic) mod write;
|
||||
|
||||
use crate::error::Result;
|
||||
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::{Tag, TagType};
|
||||
use crate::types::tag::TagType;
|
||||
use tag::AiffTextChunks;
|
||||
|
||||
use std::io::{Read, Seek};
|
||||
|
||||
|
@ -13,10 +18,10 @@ use std::io::{Read, Seek};
|
|||
pub struct AiffFile {
|
||||
#[cfg(feature = "aiff_text_chunks")]
|
||||
/// Any text chunks included in the file
|
||||
pub(crate) text_chunks: Option<Tag>,
|
||||
pub(crate) text_chunks: Option<AiffTextChunks>,
|
||||
#[cfg(feature = "id3v2")]
|
||||
/// An ID3v2 tag
|
||||
pub(crate) id3v2: Option<Tag>,
|
||||
pub(crate) id3v2_tag: Option<Id3v2Tag>,
|
||||
/// The file's audio properties
|
||||
pub(crate) properties: FileProperties,
|
||||
}
|
||||
|
@ -26,10 +31,13 @@ impl From<AiffFile> for TaggedFile {
|
|||
Self {
|
||||
ty: FileType::AIFF,
|
||||
properties: input.properties,
|
||||
tags: vec![input.text_chunks, input.id3v2]
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.collect(),
|
||||
tags: vec![
|
||||
input.text_chunks.map(|tc| tc.into()),
|
||||
input.id3v2_tag.map(|id3| id3.into()),
|
||||
]
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -50,40 +58,18 @@ impl AudioFile for AiffFile {
|
|||
}
|
||||
|
||||
fn contains_tag(&self) -> bool {
|
||||
self.id3v2.is_some() || self.text_chunks.is_some()
|
||||
self.id3v2_tag.is_some() || self.text_chunks.is_some()
|
||||
}
|
||||
|
||||
fn contains_tag_type(&self, tag_type: &TagType) -> bool {
|
||||
match tag_type {
|
||||
TagType::Id3v2 => self.id3v2.is_some(),
|
||||
TagType::Id3v2 => self.id3v2_tag.is_some(),
|
||||
TagType::AiffText => self.text_chunks.is_some(),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AiffFile {
|
||||
#[cfg(feature = "id3v2")]
|
||||
/// Returns a reference to the ID3v2 tag if it exists
|
||||
pub fn id3v2_tag(&self) -> Option<&Tag> {
|
||||
self.id3v2.as_ref()
|
||||
}
|
||||
|
||||
#[cfg(feature = "id3v2")]
|
||||
/// Returns a mutable reference to the ID3v2 tag if it exists
|
||||
pub fn id3v2_tag_mut(&mut self) -> Option<&mut Tag> {
|
||||
self.id3v2.as_mut()
|
||||
}
|
||||
|
||||
#[cfg(feature = "aiff_text_chunks")]
|
||||
/// Returns a reference to the text chunks tag if it exists
|
||||
pub fn text_chunks(&self) -> Option<&Tag> {
|
||||
self.text_chunks.as_ref()
|
||||
}
|
||||
|
||||
#[cfg(feature = "aiff_text_chunks")]
|
||||
/// Returns a mutable reference to the text chunks tag if it exists
|
||||
pub fn text_chunks_mut(&mut self) -> Option<&mut Tag> {
|
||||
self.text_chunks.as_mut()
|
||||
}
|
||||
tag_methods! {
|
||||
AiffFile => ID3v2, id3v2_tag, Id3v2Tag; Text_Chunks, text_chunks, AiffTextChunks
|
||||
}
|
||||
|
|
65
src/logic/iff/aiff/properties.rs
Normal file
65
src/logic/iff/aiff/properties.rs
Normal file
|
@ -0,0 +1,65 @@
|
|||
use crate::error::{LoftyError, Result};
|
||||
use crate::types::properties::FileProperties;
|
||||
|
||||
use std::io::Read;
|
||||
use std::time::Duration;
|
||||
|
||||
use byteorder::{BigEndian, ReadBytesExt};
|
||||
|
||||
pub(super) fn read_properties(comm: &mut &[u8], stream_len: u32) -> Result<FileProperties> {
|
||||
let channels = comm.read_u16::<BigEndian>()? as u8;
|
||||
|
||||
if channels == 0 {
|
||||
return Err(LoftyError::Aiff("File contains 0 channels"));
|
||||
}
|
||||
|
||||
let sample_frames = comm.read_u32::<BigEndian>()?;
|
||||
let _sample_size = comm.read_u16::<BigEndian>()?;
|
||||
|
||||
let mut sample_rate_bytes = [0; 10];
|
||||
comm.read_exact(&mut sample_rate_bytes)?;
|
||||
|
||||
let sign = u64::from(sample_rate_bytes[0] & 0x80);
|
||||
|
||||
sample_rate_bytes[0] &= 0x7f;
|
||||
|
||||
let mut exponent = u16::from(sample_rate_bytes[0]) << 8 | u16::from(sample_rate_bytes[1]);
|
||||
exponent = exponent - 16383 + 1023;
|
||||
|
||||
let fraction = &mut sample_rate_bytes[2..];
|
||||
fraction[0] &= 0x7f;
|
||||
|
||||
let fraction: Vec<u64> = fraction.iter_mut().map(|v| u64::from(*v)).collect();
|
||||
|
||||
let fraction = fraction[0] << 56
|
||||
| fraction[1] << 48
|
||||
| fraction[2] << 40
|
||||
| fraction[3] << 32
|
||||
| fraction[4] << 24
|
||||
| fraction[5] << 16
|
||||
| fraction[6] << 8
|
||||
| fraction[7];
|
||||
|
||||
let f64_bytes = sign << 56 | u64::from(exponent) << 52 | fraction >> 11;
|
||||
let float = f64::from_be_bytes(f64_bytes.to_be_bytes());
|
||||
|
||||
let sample_rate = float.round() as u32;
|
||||
|
||||
let (duration, bitrate) = if sample_rate > 0 && sample_frames > 0 {
|
||||
let length = (u64::from(sample_frames) * 1000) / u64::from(sample_rate);
|
||||
|
||||
(
|
||||
Duration::from_millis(length),
|
||||
(u64::from(stream_len * 8) / length) as u32,
|
||||
)
|
||||
} else {
|
||||
(Duration::ZERO, 0)
|
||||
};
|
||||
|
||||
Ok(FileProperties::new(
|
||||
duration,
|
||||
Some(bitrate),
|
||||
Some(sample_rate),
|
||||
Some(channels),
|
||||
))
|
||||
}
|
|
@ -1,12 +1,13 @@
|
|||
#[cfg(feature = "aiff_text_chunks")]
|
||||
use super::tag::AiffTextChunks;
|
||||
use super::AiffFile;
|
||||
use crate::error::{LoftyError, Result};
|
||||
#[cfg(feature = "id3v2")]
|
||||
use crate::logic::id3::v2::read::parse_id3v2;
|
||||
use crate::types::item::{ItemKey, ItemValue, TagItem};
|
||||
use crate::types::properties::FileProperties;
|
||||
use crate::types::tag::{Tag, TagType};
|
||||
#[cfg(feature = "id3v2")]
|
||||
use crate::logic::id3::v2::tag::Id3v2Tag;
|
||||
|
||||
use std::io::{Read, Seek, SeekFrom};
|
||||
use std::time::Duration;
|
||||
|
||||
use byteorder::{BigEndian, ReadBytesExt};
|
||||
|
||||
|
@ -24,64 +25,6 @@ where
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn read_properties(comm: &mut &[u8], stream_len: u32) -> Result<FileProperties> {
|
||||
let channels = comm.read_u16::<BigEndian>()? as u8;
|
||||
|
||||
if channels == 0 {
|
||||
return Err(LoftyError::Aiff("File contains 0 channels"));
|
||||
}
|
||||
|
||||
let sample_frames = comm.read_u32::<BigEndian>()?;
|
||||
let _sample_size = comm.read_u16::<BigEndian>()?;
|
||||
|
||||
let mut sample_rate_bytes = [0; 10];
|
||||
comm.read_exact(&mut sample_rate_bytes)?;
|
||||
|
||||
let sign = u64::from(sample_rate_bytes[0] & 0x80);
|
||||
|
||||
sample_rate_bytes[0] &= 0x7f;
|
||||
|
||||
let mut exponent = u16::from(sample_rate_bytes[0]) << 8 | u16::from(sample_rate_bytes[1]);
|
||||
exponent = exponent - 16383 + 1023;
|
||||
|
||||
let fraction = &mut sample_rate_bytes[2..];
|
||||
fraction[0] &= 0x7f;
|
||||
|
||||
let fraction: Vec<u64> = fraction.iter_mut().map(|v| u64::from(*v)).collect();
|
||||
|
||||
let fraction = fraction[0] << 56
|
||||
| fraction[1] << 48
|
||||
| fraction[2] << 40
|
||||
| fraction[3] << 32
|
||||
| fraction[4] << 24
|
||||
| fraction[5] << 16
|
||||
| fraction[6] << 8
|
||||
| fraction[7];
|
||||
|
||||
let f64_bytes = sign << 56 | u64::from(exponent) << 52 | fraction >> 11;
|
||||
let float = f64::from_be_bytes(f64_bytes.to_be_bytes());
|
||||
|
||||
let sample_rate = float.round() as u32;
|
||||
|
||||
let (duration, bitrate) = if sample_rate > 0 && sample_frames > 0 {
|
||||
let length = (u64::from(sample_frames) * 1000) / u64::from(sample_rate);
|
||||
|
||||
(
|
||||
Duration::from_millis(length),
|
||||
(u64::from(stream_len * 8) / length) as u32,
|
||||
)
|
||||
} else {
|
||||
(Duration::ZERO, 0)
|
||||
};
|
||||
|
||||
Ok(FileProperties::new(
|
||||
duration,
|
||||
Some(bitrate),
|
||||
Some(sample_rate),
|
||||
Some(channels),
|
||||
))
|
||||
}
|
||||
|
||||
pub(in crate::logic) fn read_from<R>(data: &mut R) -> Result<AiffFile>
|
||||
where
|
||||
R: Read + Seek,
|
||||
|
@ -91,26 +34,30 @@ where
|
|||
let mut comm = None;
|
||||
let mut stream_len = 0;
|
||||
|
||||
let mut text_chunks = Tag::new(TagType::AiffText);
|
||||
let mut id3: Option<Tag> = None;
|
||||
#[cfg(feature = "aiff_text_chunks")]
|
||||
let mut text_chunks = AiffTextChunks::default();
|
||||
#[cfg(feature = "id3v2")]
|
||||
let mut id3v2_tag: Option<Id3v2Tag> = None;
|
||||
|
||||
let mut fourcc = [0; 4];
|
||||
|
||||
while let (Ok(()), Ok(size)) = (data.read_exact(&mut fourcc), data.read_u32::<BigEndian>()) {
|
||||
match &fourcc {
|
||||
#[cfg(feature = "aiff_text_chunks")]
|
||||
b"NAME" | b"AUTH" | b"(c) " => {
|
||||
let mut value = vec![0; size as usize];
|
||||
data.read_exact(&mut value)?;
|
||||
|
||||
// It's safe to unwrap here since this code is unreachable unless the fourcc is valid
|
||||
let item = TagItem::new(
|
||||
ItemKey::from_key(&TagType::AiffText, std::str::from_utf8(&fourcc).unwrap())
|
||||
.unwrap(),
|
||||
ItemValue::Text(String::from_utf8(value)?),
|
||||
);
|
||||
let value = String::from_utf8(value)?;
|
||||
|
||||
text_chunks.insert_item(item);
|
||||
match &fourcc {
|
||||
b"NAME" => text_chunks.name = Some(value),
|
||||
b"AUTH" => text_chunks.author = Some(value),
|
||||
b"(c) " => text_chunks.copyright = Some(value),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
#[cfg(feature = "id3v2")]
|
||||
b"ID3 " | b"id3 " => {
|
||||
let mut value = vec![0; size as usize];
|
||||
data.read_exact(&mut value)?;
|
||||
|
@ -122,7 +69,7 @@ where
|
|||
data.seek(SeekFrom::Current(10))?;
|
||||
}
|
||||
|
||||
id3 = Some(id3v2)
|
||||
id3v2_tag = Some(id3v2);
|
||||
}
|
||||
b"COMM" => {
|
||||
if comm.is_none() {
|
||||
|
@ -146,6 +93,11 @@ where
|
|||
data.seek(SeekFrom::Current(i64::from(size)))?;
|
||||
}
|
||||
}
|
||||
|
||||
// Chunks only start on even boundaries
|
||||
if size % 2 != 0 {
|
||||
data.seek(SeekFrom::Current(1))?;
|
||||
}
|
||||
}
|
||||
|
||||
if comm.is_none() {
|
||||
|
@ -156,11 +108,20 @@ where
|
|||
return Err(LoftyError::Aiff("File does not contain a \"SSND\" chunk"));
|
||||
}
|
||||
|
||||
let properties = read_properties(&mut &*comm.unwrap(), stream_len)?;
|
||||
let properties = super::properties::read_properties(&mut &*comm.unwrap(), stream_len)?;
|
||||
|
||||
Ok(AiffFile {
|
||||
properties,
|
||||
text_chunks: (text_chunks.item_count() > 0).then(|| text_chunks),
|
||||
id3v2: id3,
|
||||
#[cfg(feature = "aiff_text_chunks")]
|
||||
text_chunks: match text_chunks {
|
||||
AiffTextChunks {
|
||||
name: None,
|
||||
author: None,
|
||||
copyright: None,
|
||||
} => None,
|
||||
_ => Some(text_chunks),
|
||||
},
|
||||
#[cfg(feature = "id3v2")]
|
||||
id3v2_tag,
|
||||
})
|
||||
}
|
||||
|
|
|
@ -1,54 +1,122 @@
|
|||
use crate::error::Result;
|
||||
use crate::types::item::{ItemKey, ItemValue};
|
||||
use crate::types::item::{ItemKey, ItemValue, TagItem};
|
||||
use crate::types::tag::{Tag, TagType};
|
||||
|
||||
use std::fs::File;
|
||||
use std::io::{Read, Seek, SeekFrom, Write};
|
||||
|
||||
use byteorder::{BigEndian, LittleEndian, ReadBytesExt};
|
||||
use byteorder::{BigEndian, ReadBytesExt};
|
||||
|
||||
pub(in crate::logic) fn write_aiff_text(data: &mut File, tag: &Tag) -> Result<()> {
|
||||
let mut text_chunks = Vec::new();
|
||||
#[cfg(feature = "aiff_text_chunks")]
|
||||
#[derive(Default)]
|
||||
/// AIFF text chunks
|
||||
///
|
||||
/// AIFF has a few chunks for storing basic metadata, all of
|
||||
/// which can only appear once in a file.
|
||||
pub struct AiffTextChunks {
|
||||
/// The name of the piece
|
||||
pub name: Option<String>,
|
||||
/// The author of the piece
|
||||
pub author: Option<String>,
|
||||
/// A copyright notice consisting of the date followed
|
||||
/// by the copyright owner
|
||||
pub copyright: Option<String>,
|
||||
}
|
||||
|
||||
let items = tag.items().iter().filter(|i| {
|
||||
(i.key() == &ItemKey::TrackTitle
|
||||
|| i.key() == &ItemKey::TrackArtist
|
||||
|| i.key() == &ItemKey::CopyrightMessage)
|
||||
&& std::mem::discriminant(i.value())
|
||||
== std::mem::discriminant(&ItemValue::Text(String::new()))
|
||||
});
|
||||
impl AiffTextChunks {
|
||||
pub fn write_to(&self, file: &mut File) -> Result<()> {
|
||||
Into::<AiffTextChunksRef>::into(self).write_to(file)
|
||||
}
|
||||
}
|
||||
|
||||
for i in items {
|
||||
// Already covered
|
||||
let value = match i.value() {
|
||||
ItemValue::Text(value) => value,
|
||||
_ => unreachable!(),
|
||||
impl From<AiffTextChunks> for Tag {
|
||||
fn from(input: AiffTextChunks) -> Self {
|
||||
let mut tag = Tag::new(TagType::AiffText);
|
||||
|
||||
let push_item = |field: Option<String>, item_key: ItemKey, tag: &mut Tag| {
|
||||
if let Some(text) = field {
|
||||
tag.insert_item_unchecked(TagItem::new(item_key, ItemValue::Text(text)))
|
||||
}
|
||||
};
|
||||
|
||||
let len = (value.len() as u32).to_be_bytes();
|
||||
push_item(input.name, ItemKey::TrackTitle, &mut tag);
|
||||
push_item(input.author, ItemKey::TrackArtist, &mut tag);
|
||||
push_item(input.copyright, ItemKey::CopyrightMessage, &mut tag);
|
||||
|
||||
// Safe to unwrap since we retained the only possible values
|
||||
text_chunks.extend(
|
||||
i.key()
|
||||
.map_key(&TagType::AiffText)
|
||||
.unwrap()
|
||||
.as_bytes()
|
||||
.iter(),
|
||||
);
|
||||
text_chunks.extend(len.iter());
|
||||
text_chunks.extend(value.as_bytes().iter());
|
||||
tag
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Tag> for AiffTextChunks {
|
||||
fn from(input: Tag) -> Self {
|
||||
Self {
|
||||
name: input.get_string(&ItemKey::TrackTitle).map(str::to_owned),
|
||||
author: input.get_string(&ItemKey::TrackArtist).map(str::to_owned),
|
||||
copyright: input
|
||||
.get_string(&ItemKey::CopyrightMessage)
|
||||
.map(str::to_owned),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct AiffTextChunksRef<'a> {
|
||||
pub name: Option<&'a str>,
|
||||
pub author: Option<&'a str>,
|
||||
pub copyright: Option<&'a str>,
|
||||
}
|
||||
|
||||
impl<'a> Into<AiffTextChunksRef<'a>> for &'a AiffTextChunks {
|
||||
fn into(self) -> AiffTextChunksRef<'a> {
|
||||
AiffTextChunksRef {
|
||||
name: self.name.as_deref(),
|
||||
author: self.author.as_deref(),
|
||||
copyright: self.copyright.as_deref(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Into<AiffTextChunksRef<'a>> for &'a Tag {
|
||||
fn into(self) -> AiffTextChunksRef<'a> {
|
||||
AiffTextChunksRef {
|
||||
name: self.get_string(&ItemKey::TrackTitle),
|
||||
author: self.get_string(&ItemKey::TrackArtist),
|
||||
copyright: self.get_string(&ItemKey::CopyrightMessage),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> AiffTextChunksRef<'a> {
|
||||
pub(in crate::logic) fn write_to(&self, file: &mut File) -> Result<()> {
|
||||
write_to(file, self)
|
||||
}
|
||||
}
|
||||
|
||||
pub(in crate::logic) fn write_to(data: &mut File, tag: &AiffTextChunksRef) -> Result<()> {
|
||||
fn write_chunk(writer: &mut Vec<u8>, key: &str, value: Option<&str>) {
|
||||
if let Some(val) = value {
|
||||
let len = (val.len() as u32).to_be_bytes();
|
||||
|
||||
writer.extend(key.as_bytes().iter());
|
||||
writer.extend(len.iter());
|
||||
writer.extend(val.as_bytes().iter());
|
||||
}
|
||||
}
|
||||
|
||||
let mut chunks_remove = Vec::new();
|
||||
super::read::verify_aiff(data)?;
|
||||
|
||||
while let (Ok(fourcc), Ok(size)) = (
|
||||
data.read_u32::<LittleEndian>(),
|
||||
data.read_u32::<BigEndian>(),
|
||||
) {
|
||||
let fourcc_b = &fourcc.to_le_bytes();
|
||||
let mut text_chunks = Vec::new();
|
||||
|
||||
write_chunk(&mut text_chunks, "NAME", tag.name);
|
||||
write_chunk(&mut text_chunks, "AUTH", tag.author);
|
||||
write_chunk(&mut text_chunks, "(c) ", tag.copyright);
|
||||
|
||||
let mut chunks_remove = Vec::new();
|
||||
let mut fourcc = [0; 4];
|
||||
|
||||
while let (Ok(()), Ok(size)) = (data.read_exact(&mut fourcc), data.read_u32::<BigEndian>()) {
|
||||
let pos = (data.seek(SeekFrom::Current(0))? - 8) as usize;
|
||||
|
||||
if fourcc_b == b"NAME" || fourcc_b == b"AUTH" || fourcc_b == b"(c) " {
|
||||
if &fourcc == b"NAME" || &fourcc == b"AUTH" || &fourcc == b"(c) " {
|
||||
chunks_remove.push((pos, (pos + 8 + size as usize)))
|
||||
}
|
||||
|
||||
|
|
|
@ -1,17 +1,18 @@
|
|||
use super::read::verify_aiff;
|
||||
use crate::error::{LoftyError, Result};
|
||||
use crate::logic::id3::v2::tag::Id3v2TagRef;
|
||||
use crate::logic::iff::aiff::tag::AiffTextChunksRef;
|
||||
use crate::types::tag::{Tag, TagType};
|
||||
|
||||
use std::fs::File;
|
||||
|
||||
pub(crate) fn write_to(data: &mut File, tag: &Tag) -> Result<()> {
|
||||
verify_aiff(data)?;
|
||||
use byteorder::BigEndian;
|
||||
|
||||
pub(crate) fn write_to(data: &mut File, tag: &Tag) -> Result<()> {
|
||||
match tag.tag_type() {
|
||||
TagType::AiffText => super::tag::write_aiff_text(data, tag),
|
||||
TagType::Id3v2 => crate::logic::id3::v2::write::write_id3v2_to_chunk_file::<
|
||||
byteorder::BigEndian,
|
||||
>(data, tag),
|
||||
#[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),
|
||||
_ => Err(LoftyError::UnsupportedTag),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,78 +1,28 @@
|
|||
pub(crate) mod properties;
|
||||
mod read;
|
||||
mod tag;
|
||||
#[cfg(feature = "riff_info_list")]
|
||||
pub(crate) mod tag;
|
||||
pub(in crate::logic) mod write;
|
||||
|
||||
use crate::error::Result;
|
||||
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::{Tag, TagType};
|
||||
use crate::types::tag::TagType;
|
||||
use properties::WavProperties;
|
||||
use tag::RiffInfoList;
|
||||
|
||||
use std::io::{Read, Seek};
|
||||
use std::time::Duration;
|
||||
|
||||
#[allow(missing_docs, non_camel_case_types)]
|
||||
/// A WAV file's format
|
||||
pub enum WavFormat {
|
||||
PCM,
|
||||
IEEE_FLOAT,
|
||||
Other(u16),
|
||||
}
|
||||
|
||||
/// A WAV file's audio properties
|
||||
pub struct WavProperties {
|
||||
format: WavFormat,
|
||||
duration: Duration,
|
||||
bitrate: u32,
|
||||
sample_rate: u32,
|
||||
channels: u8,
|
||||
}
|
||||
|
||||
impl From<WavProperties> for FileProperties {
|
||||
fn from(input: WavProperties) -> Self {
|
||||
Self {
|
||||
duration: input.duration,
|
||||
bitrate: Some(input.bitrate),
|
||||
sample_rate: Some(input.sample_rate),
|
||||
channels: Some(input.channels),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl WavProperties {
|
||||
/// Duration
|
||||
pub fn duration(&self) -> Duration {
|
||||
self.duration
|
||||
}
|
||||
|
||||
/// Bitrate (kbps)
|
||||
pub fn bitrate(&self) -> u32 {
|
||||
self.bitrate
|
||||
}
|
||||
|
||||
/// Sample rate (Hz)
|
||||
pub fn sample_rate(&self) -> u32 {
|
||||
self.sample_rate
|
||||
}
|
||||
|
||||
/// Channel count
|
||||
pub fn channels(&self) -> u8 {
|
||||
self.channels
|
||||
}
|
||||
|
||||
/// WAV format
|
||||
pub fn format(&self) -> &WavFormat {
|
||||
&self.format
|
||||
}
|
||||
}
|
||||
|
||||
/// A WAV file
|
||||
pub struct WavFile {
|
||||
#[cfg(feature = "riff_info_list")]
|
||||
/// A RIFF INFO LIST
|
||||
pub(crate) riff_info: Option<Tag>,
|
||||
pub(crate) riff_info: Option<RiffInfoList>,
|
||||
#[cfg(feature = "id3v2")]
|
||||
/// An ID3v2 tag
|
||||
pub(crate) id3v2: Option<Tag>,
|
||||
pub(crate) id3v2_tag: Option<Id3v2Tag>,
|
||||
/// The file's audio properties
|
||||
pub(crate) properties: WavProperties,
|
||||
}
|
||||
|
@ -82,10 +32,13 @@ impl From<WavFile> for TaggedFile {
|
|||
Self {
|
||||
ty: FileType::WAV,
|
||||
properties: FileProperties::from(input.properties),
|
||||
tags: vec![input.riff_info, input.id3v2]
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.collect(),
|
||||
tags: vec![
|
||||
input.riff_info.map(|ri| ri.into()),
|
||||
input.id3v2_tag.map(|id3| id3.into()),
|
||||
]
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -106,40 +59,18 @@ impl AudioFile for WavFile {
|
|||
}
|
||||
|
||||
fn contains_tag(&self) -> bool {
|
||||
self.id3v2.is_some() || self.riff_info.is_some()
|
||||
self.id3v2_tag.is_some() || self.riff_info.is_some()
|
||||
}
|
||||
|
||||
fn contains_tag_type(&self, tag_type: &TagType) -> bool {
|
||||
match tag_type {
|
||||
TagType::Id3v2 => self.id3v2.is_some(),
|
||||
TagType::Id3v2 => self.id3v2_tag.is_some(),
|
||||
TagType::RiffInfo => self.riff_info.is_some(),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl WavFile {
|
||||
#[cfg(feature = "id3v2")]
|
||||
/// Returns a reference to the ID3v2 tag if it exists
|
||||
pub fn id3v2_tag(&self) -> Option<&Tag> {
|
||||
self.id3v2.as_ref()
|
||||
}
|
||||
|
||||
#[cfg(feature = "id3v2")]
|
||||
/// Returns a mutable reference to the ID3v2 tag if it exists
|
||||
pub fn id3v2_tag_mut(&mut self) -> Option<&mut Tag> {
|
||||
self.id3v2.as_mut()
|
||||
}
|
||||
|
||||
#[cfg(feature = "riff_info_list")]
|
||||
/// Returns a reference to the RIFF INFO tag if it exists
|
||||
pub fn riff_info(&self) -> Option<&Tag> {
|
||||
self.riff_info.as_ref()
|
||||
}
|
||||
|
||||
#[cfg(feature = "riff_info_list")]
|
||||
/// Returns a mutable reference to the RIFF INFO tag if it exists
|
||||
pub fn riff_info_mut(&mut self) -> Option<&mut Tag> {
|
||||
self.riff_info.as_mut()
|
||||
}
|
||||
tag_methods! {
|
||||
WavFile => ID3v2, id3v2_tag, Id3v2Tag; RIFF_INFO, riff_info, RiffInfoList
|
||||
}
|
||||
|
|
146
src/logic/iff/wav/properties.rs
Normal file
146
src/logic/iff/wav/properties.rs
Normal file
|
@ -0,0 +1,146 @@
|
|||
use crate::error::{LoftyError, Result};
|
||||
use crate::types::properties::FileProperties;
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
use byteorder::{LittleEndian, ReadBytesExt};
|
||||
|
||||
const PCM: u16 = 0x0001;
|
||||
const IEEE_FLOAT: u16 = 0x0003;
|
||||
const EXTENSIBLE: u16 = 0xfffe;
|
||||
|
||||
#[allow(missing_docs, non_camel_case_types)]
|
||||
/// A WAV file's format
|
||||
pub enum WavFormat {
|
||||
PCM,
|
||||
IEEE_FLOAT,
|
||||
Other(u16),
|
||||
}
|
||||
|
||||
/// A WAV file's audio properties
|
||||
pub struct WavProperties {
|
||||
format: WavFormat,
|
||||
duration: Duration,
|
||||
bitrate: u32,
|
||||
sample_rate: u32,
|
||||
channels: u8,
|
||||
}
|
||||
|
||||
impl From<WavProperties> for FileProperties {
|
||||
fn from(input: WavProperties) -> Self {
|
||||
Self {
|
||||
duration: input.duration,
|
||||
bitrate: Some(input.bitrate),
|
||||
sample_rate: Some(input.sample_rate),
|
||||
channels: Some(input.channels),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl WavProperties {
|
||||
/// Duration
|
||||
pub fn duration(&self) -> Duration {
|
||||
self.duration
|
||||
}
|
||||
|
||||
/// Bitrate (kbps)
|
||||
pub fn bitrate(&self) -> u32 {
|
||||
self.bitrate
|
||||
}
|
||||
|
||||
/// Sample rate (Hz)
|
||||
pub fn sample_rate(&self) -> u32 {
|
||||
self.sample_rate
|
||||
}
|
||||
|
||||
/// Channel count
|
||||
pub fn channels(&self) -> u8 {
|
||||
self.channels
|
||||
}
|
||||
|
||||
/// WAV format
|
||||
pub fn format(&self) -> &WavFormat {
|
||||
&self.format
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn read_properties(
|
||||
fmt: &mut &[u8],
|
||||
total_samples: u32,
|
||||
stream_len: u32,
|
||||
) -> Result<WavProperties> {
|
||||
let mut format_tag = fmt.read_u16::<LittleEndian>()?;
|
||||
let channels = fmt.read_u16::<LittleEndian>()? as u8;
|
||||
|
||||
if channels == 0 {
|
||||
return Err(LoftyError::Wav("File contains 0 channels"));
|
||||
}
|
||||
|
||||
let sample_rate = fmt.read_u32::<LittleEndian>()?;
|
||||
let bytes_per_second = fmt.read_u32::<LittleEndian>()?;
|
||||
|
||||
// Skip 2 bytes
|
||||
// Block align (2)
|
||||
let _ = fmt.read_u16::<LittleEndian>()?;
|
||||
|
||||
let bits_per_sample = fmt.read_u16::<LittleEndian>()?;
|
||||
|
||||
if format_tag == EXTENSIBLE {
|
||||
if fmt.len() < 40 {
|
||||
return Err(LoftyError::Wav(
|
||||
"Extensible format identified, invalid \"fmt \" chunk size found (< 40)",
|
||||
));
|
||||
}
|
||||
|
||||
// Skip 8 bytes
|
||||
// cbSize (Size of extra format information) (2)
|
||||
// Valid bits per sample (2)
|
||||
// Channel mask (4)
|
||||
let _ = fmt.read_u64::<LittleEndian>()?;
|
||||
|
||||
format_tag = fmt.read_u16::<LittleEndian>()?;
|
||||
}
|
||||
|
||||
let non_pcm = format_tag != PCM && format_tag != IEEE_FLOAT;
|
||||
|
||||
if non_pcm && total_samples == 0 {
|
||||
return Err(LoftyError::Wav(
|
||||
"Non-PCM format identified, no \"fact\" chunk found",
|
||||
));
|
||||
}
|
||||
|
||||
let sample_frames = if non_pcm {
|
||||
total_samples
|
||||
} else if bits_per_sample > 0 {
|
||||
stream_len / u32::from(u16::from(channels) * ((bits_per_sample + 7) / 8))
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
let (duration, bitrate) = if sample_rate > 0 && sample_frames > 0 {
|
||||
let length = (u64::from(sample_frames) * 1000) / u64::from(sample_rate);
|
||||
|
||||
(
|
||||
Duration::from_millis(length),
|
||||
(u64::from(stream_len * 8) / length) as u32,
|
||||
)
|
||||
} else if bytes_per_second > 0 {
|
||||
let length = (u64::from(stream_len) * 1000) / u64::from(bytes_per_second);
|
||||
|
||||
(Duration::from_millis(length), (bytes_per_second * 8) / 1000)
|
||||
} else {
|
||||
(Duration::ZERO, 0)
|
||||
};
|
||||
|
||||
Ok(WavProperties {
|
||||
format: match format_tag {
|
||||
PCM => WavFormat::PCM,
|
||||
IEEE_FLOAT => WavFormat::IEEE_FLOAT,
|
||||
other => WavFormat::Other(other),
|
||||
},
|
||||
duration,
|
||||
bitrate,
|
||||
sample_rate,
|
||||
channels,
|
||||
})
|
||||
}
|
|
@ -1,17 +1,15 @@
|
|||
use super::{WavFile, WavFormat, WavProperties};
|
||||
#[cfg(feature = "riff_info_list")]
|
||||
use super::tag::RiffInfoList;
|
||||
use super::WavFile;
|
||||
use crate::error::{LoftyError, Result};
|
||||
#[cfg(feature = "id3v2")]
|
||||
use crate::logic::id3::v2::read::parse_id3v2;
|
||||
use crate::types::tag::{Tag, TagType};
|
||||
|
||||
use std::io::{Read, Seek, SeekFrom};
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::logic::id3::v2::tag::Id3v2Tag;
|
||||
use byteorder::{LittleEndian, ReadBytesExt};
|
||||
|
||||
const PCM: u16 = 0x0001;
|
||||
const IEEE_FLOAT: u16 = 0x0003;
|
||||
const EXTENSIBLE: u16 = 0xfffe;
|
||||
|
||||
pub(in crate::logic::iff) fn verify_wav<T>(data: &mut T) -> Result<()>
|
||||
where
|
||||
T: Read + Seek,
|
||||
|
@ -30,83 +28,6 @@ where
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn read_properties(fmt: &mut &[u8], total_samples: u32, stream_len: u32) -> Result<WavProperties> {
|
||||
let mut format_tag = fmt.read_u16::<LittleEndian>()?;
|
||||
let channels = fmt.read_u16::<LittleEndian>()? as u8;
|
||||
|
||||
if channels == 0 {
|
||||
return Err(LoftyError::Wav("File contains 0 channels"));
|
||||
}
|
||||
|
||||
let sample_rate = fmt.read_u32::<LittleEndian>()?;
|
||||
let bytes_per_second = fmt.read_u32::<LittleEndian>()?;
|
||||
|
||||
// Skip 2 bytes
|
||||
// Block align (2)
|
||||
let _ = fmt.read_u16::<LittleEndian>()?;
|
||||
|
||||
let bits_per_sample = fmt.read_u16::<LittleEndian>()?;
|
||||
|
||||
if format_tag == EXTENSIBLE {
|
||||
if fmt.len() < 40 {
|
||||
return Err(LoftyError::Wav(
|
||||
"Extensible format identified, invalid \"fmt \" chunk size found (< 40)",
|
||||
));
|
||||
}
|
||||
|
||||
// Skip 8 bytes
|
||||
// cbSize (Size of extra format information) (2)
|
||||
// Valid bits per sample (2)
|
||||
// Channel mask (4)
|
||||
let _ = fmt.read_u64::<LittleEndian>()?;
|
||||
|
||||
format_tag = fmt.read_u16::<LittleEndian>()?;
|
||||
}
|
||||
|
||||
let non_pcm = format_tag != PCM && format_tag != IEEE_FLOAT;
|
||||
|
||||
if non_pcm && total_samples == 0 {
|
||||
return Err(LoftyError::Wav(
|
||||
"Non-PCM format identified, no \"fact\" chunk found",
|
||||
));
|
||||
}
|
||||
|
||||
let sample_frames = if non_pcm {
|
||||
total_samples
|
||||
} else if bits_per_sample > 0 {
|
||||
stream_len / u32::from(u16::from(channels) * ((bits_per_sample + 7) / 8))
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
let (duration, bitrate) = if sample_rate > 0 && sample_frames > 0 {
|
||||
let length = (u64::from(sample_frames) * 1000) / u64::from(sample_rate);
|
||||
|
||||
(
|
||||
Duration::from_millis(length),
|
||||
(u64::from(stream_len * 8) / length) as u32,
|
||||
)
|
||||
} else if bytes_per_second > 0 {
|
||||
let length = (u64::from(stream_len) * 1000) / u64::from(bytes_per_second);
|
||||
|
||||
(Duration::from_millis(length), (bytes_per_second * 8) / 1000)
|
||||
} else {
|
||||
(Duration::ZERO, 0)
|
||||
};
|
||||
|
||||
Ok(WavProperties {
|
||||
format: match format_tag {
|
||||
PCM => WavFormat::PCM,
|
||||
IEEE_FLOAT => WavFormat::IEEE_FLOAT,
|
||||
other => WavFormat::Other(other),
|
||||
},
|
||||
duration,
|
||||
bitrate,
|
||||
sample_rate,
|
||||
channels,
|
||||
})
|
||||
}
|
||||
|
||||
pub(in crate::logic) fn read_from<R>(data: &mut R) -> Result<WavFile>
|
||||
where
|
||||
R: Read + Seek,
|
||||
|
@ -117,8 +38,9 @@ where
|
|||
let mut total_samples = 0_u32;
|
||||
let mut fmt = Vec::new();
|
||||
|
||||
let mut riff_info = Tag::new(TagType::RiffInfo);
|
||||
let mut id3: Option<Tag> = None;
|
||||
#[cfg(feature = "riff_info_list")]
|
||||
let mut riff_info = RiffInfoList::default();
|
||||
let mut id3v2_tag: Option<Id3v2Tag> = None;
|
||||
|
||||
let mut fourcc = [0; 4];
|
||||
|
||||
|
@ -133,18 +55,16 @@ where
|
|||
data.read_exact(&mut value)?;
|
||||
|
||||
fmt = value;
|
||||
continue;
|
||||
} else {
|
||||
data.seek(SeekFrom::Current(i64::from(size)))?;
|
||||
}
|
||||
|
||||
data.seek(SeekFrom::Current(i64::from(size)))?;
|
||||
}
|
||||
b"fact" => {
|
||||
if total_samples == 0 {
|
||||
total_samples = data.read_u32::<LittleEndian>()?;
|
||||
continue;
|
||||
} else {
|
||||
data.seek(SeekFrom::Current(4))?;
|
||||
}
|
||||
|
||||
data.seek(SeekFrom::Current(4))?;
|
||||
}
|
||||
b"data" => {
|
||||
if stream_len == 0 {
|
||||
|
@ -157,13 +77,18 @@ where
|
|||
let mut list_type = [0; 4];
|
||||
data.read_exact(&mut list_type)?;
|
||||
|
||||
#[cfg(feature = "riff_info_list")]
|
||||
if &list_type == b"INFO" {
|
||||
let end = data.seek(SeekFrom::Current(0))? + u64::from(size - 4);
|
||||
super::tag::read::parse_riff_info(data, end, &mut riff_info)?;
|
||||
} else {
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "riff_info_list"))]
|
||||
{
|
||||
data.seek(SeekFrom::Current(i64::from(size)))?;
|
||||
}
|
||||
}
|
||||
#[cfg(feature = "id3v2")]
|
||||
b"ID3 " | b"id3 " => {
|
||||
let mut value = vec![0; size as usize];
|
||||
data.read_exact(&mut value)?;
|
||||
|
@ -175,12 +100,17 @@ where
|
|||
data.seek(SeekFrom::Current(10))?;
|
||||
}
|
||||
|
||||
id3 = Some(id3v2);
|
||||
id3v2_tag = Some(id3v2);
|
||||
}
|
||||
_ => {
|
||||
data.seek(SeekFrom::Current(i64::from(size)))?;
|
||||
}
|
||||
}
|
||||
|
||||
// Chunks only start on even boundaries
|
||||
if size % 2 != 0 {
|
||||
data.seek(SeekFrom::Current(1))?;
|
||||
}
|
||||
}
|
||||
|
||||
if fmt.len() < 16 {
|
||||
|
@ -194,8 +124,10 @@ where
|
|||
}
|
||||
|
||||
Ok(WavFile {
|
||||
properties: read_properties(&mut &*fmt, total_samples, stream_len)?,
|
||||
riff_info: (riff_info.item_count() > 0).then(|| riff_info),
|
||||
id3v2: id3,
|
||||
properties: super::properties::read_properties(&mut &*fmt, total_samples, stream_len)?,
|
||||
#[cfg(feature = "riff_info_list")]
|
||||
riff_info: (!riff_info.items.is_empty()).then(|| riff_info),
|
||||
#[cfg(feature = "id3v2")]
|
||||
id3v2_tag,
|
||||
})
|
||||
}
|
||||
|
|
|
@ -1,2 +1,125 @@
|
|||
pub(in crate::logic::iff::wav) mod read;
|
||||
pub(in crate::logic::iff::wav) mod write;
|
||||
|
||||
use crate::error::Result;
|
||||
use crate::types::item::{ItemKey, ItemValue, TagItem};
|
||||
use crate::types::tag::{Tag, TagType};
|
||||
|
||||
use std::fs::File;
|
||||
|
||||
#[derive(Default)]
|
||||
/// A RIFF INFO LIST
|
||||
pub struct RiffInfoList {
|
||||
/// A collection of chunk-value pairs
|
||||
pub(crate) items: Vec<(String, String)>,
|
||||
}
|
||||
|
||||
impl RiffInfoList {
|
||||
pub fn push(&mut self, key: String, value: String) {
|
||||
if valid_key(key.as_str()) {
|
||||
self.items.push((key, value))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn remove(&mut self, key: &str) {
|
||||
self.items
|
||||
.iter()
|
||||
.position(|(k, _)| k == key)
|
||||
.map(|p| self.items.remove(p));
|
||||
}
|
||||
|
||||
pub fn items(&self) -> &[(String, String)] {
|
||||
self.items.as_slice()
|
||||
}
|
||||
}
|
||||
|
||||
impl RiffInfoList {
|
||||
pub fn write_to(&self, file: &mut File) -> Result<()> {
|
||||
Into::<RiffInfoListRef>::into(self).write_to(file)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RiffInfoList> for Tag {
|
||||
fn from(input: RiffInfoList) -> Self {
|
||||
let mut tag = Tag::new(TagType::RiffInfo);
|
||||
|
||||
for (k, v) in input.items {
|
||||
let item_key = ItemKey::from_key(&TagType::RiffInfo, &k);
|
||||
|
||||
tag.insert_item_unchecked(TagItem::new(
|
||||
item_key,
|
||||
ItemValue::Text(v.trim_matches('\0').to_string()),
|
||||
));
|
||||
}
|
||||
|
||||
tag
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Tag> for RiffInfoList {
|
||||
fn from(input: Tag) -> Self {
|
||||
let mut riff_info = RiffInfoList::default();
|
||||
|
||||
for item in input.items {
|
||||
if let ItemValue::Text(val) | ItemValue::Locator(val) = item.item_value {
|
||||
let item_key = match item.item_key {
|
||||
ItemKey::Unknown(unknown) => {
|
||||
if unknown.len() == 4 && unknown.is_ascii() {
|
||||
unknown.to_string()
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
// Safe to unwrap since we already checked ItemKey::Unknown
|
||||
k => k.map_key(&TagType::RiffInfo, false).unwrap().to_string(),
|
||||
};
|
||||
|
||||
riff_info.items.push((item_key, val))
|
||||
}
|
||||
}
|
||||
|
||||
riff_info
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct RiffInfoListRef<'a> {
|
||||
items: Box<dyn Iterator<Item = (&'a str, &'a String)> + 'a>,
|
||||
}
|
||||
|
||||
impl<'a> Into<RiffInfoListRef<'a>> for &'a RiffInfoList {
|
||||
fn into(self) -> RiffInfoListRef<'a> {
|
||||
RiffInfoListRef {
|
||||
items: Box::new(self.items.iter().map(|(k, v)| (k.as_str(), v))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Into<RiffInfoListRef<'a>> for &'a Tag {
|
||||
fn into(self) -> RiffInfoListRef<'a> {
|
||||
RiffInfoListRef {
|
||||
items: Box::new(self.items.iter().filter_map(|i| {
|
||||
if let ItemValue::Text(val) | ItemValue::Locator(val) = &i.item_value {
|
||||
let item_key = i.key().map_key(&TagType::RiffInfo, true).unwrap();
|
||||
|
||||
if item_key.len() == 4 && item_key.is_ascii() {
|
||||
Some((item_key, val))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> RiffInfoListRef<'a> {
|
||||
pub(crate) fn write_to(&mut self, file: &mut File) -> Result<()> {
|
||||
write::write_riff_info(file, self)
|
||||
}
|
||||
}
|
||||
|
||||
fn valid_key(key: &str) -> bool {
|
||||
key.len() == 4 && key.is_ascii()
|
||||
}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
use super::RiffInfoList;
|
||||
use crate::error::{LoftyError, Result};
|
||||
use crate::types::item::{ItemKey, ItemValue, TagItem};
|
||||
use crate::types::tag::{Tag, TagType};
|
||||
|
||||
use std::io::{Read, Seek, SeekFrom};
|
||||
|
||||
|
@ -9,25 +8,22 @@ use byteorder::{LittleEndian, ReadBytesExt};
|
|||
pub(in crate::logic::iff::wav) fn parse_riff_info<R>(
|
||||
data: &mut R,
|
||||
end: u64,
|
||||
tag: &mut Tag,
|
||||
tag: &mut RiffInfoList,
|
||||
) -> Result<()>
|
||||
where
|
||||
R: Read + Seek,
|
||||
{
|
||||
while data.seek(SeekFrom::Current(0))? != end {
|
||||
let mut key = [0; 4];
|
||||
let mut key = vec![0; 4];
|
||||
data.read_exact(&mut key)?;
|
||||
|
||||
let key_str = std::str::from_utf8(&key)
|
||||
let key_str = String::from_utf8(key)
|
||||
.map_err(|_| LoftyError::Wav("Non UTF-8 key found in RIFF INFO"))?;
|
||||
|
||||
if !key_str.is_ascii() {
|
||||
return Err(LoftyError::Wav("Non ascii key found in RIFF INFO"));
|
||||
return Err(LoftyError::Wav("Non-ascii key found in RIFF INFO"));
|
||||
}
|
||||
|
||||
let item_key = ItemKey::from_key(&TagType::RiffInfo, key_str)
|
||||
.unwrap_or_else(|| ItemKey::Unknown(key_str.to_string()));
|
||||
|
||||
let size = data.read_u32::<LittleEndian>()?;
|
||||
|
||||
let mut value = vec![0; size as usize];
|
||||
|
@ -41,9 +37,9 @@ where
|
|||
let value_str = std::str::from_utf8(&value)
|
||||
.map_err(|_| LoftyError::Wav("Non UTF-8 value found in RIFF INFO"))?;
|
||||
|
||||
tag.insert_item_unchecked(TagItem::new(
|
||||
item_key,
|
||||
ItemValue::Text(value_str.trim_matches('\0').to_string()),
|
||||
tag.items.push((
|
||||
key_str.to_string(),
|
||||
value_str.trim_matches('\0').to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
|
|
|
@ -1,22 +1,23 @@
|
|||
use super::RiffInfoListRef;
|
||||
use crate::error::{LoftyError, Result};
|
||||
use crate::types::item::ItemValue;
|
||||
use crate::types::tag::{Tag, TagType};
|
||||
|
||||
use std::fs::File;
|
||||
use std::io::{Read, Seek, SeekFrom, Write};
|
||||
|
||||
use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
|
||||
|
||||
pub(in crate::logic::iff::wav) fn write_riff_info(data: &mut File, tag: &Tag) -> Result<()> {
|
||||
pub(in crate::logic::iff::wav) fn write_riff_info(
|
||||
data: &mut File,
|
||||
tag: &mut RiffInfoListRef,
|
||||
) -> Result<()> {
|
||||
let mut riff_info_bytes = Vec::new();
|
||||
create_riff_info(tag, &mut riff_info_bytes)?;
|
||||
create_riff_info(&mut tag.items, &mut riff_info_bytes)?;
|
||||
|
||||
if find_info_list(data)? {
|
||||
let info_list_size = data.read_u32::<LittleEndian>()? as usize;
|
||||
data.seek(SeekFrom::Current(-8))?;
|
||||
let (info_list, info_list_size) = find_info_list(data)?;
|
||||
|
||||
let info_list_start = data.seek(SeekFrom::Current(0))? as usize;
|
||||
let info_list_end = info_list_start + 8 + info_list_size;
|
||||
if info_list {
|
||||
let info_list_start = data.seek(SeekFrom::Current(-12))? as usize;
|
||||
let info_list_end = info_list_start + 8 + info_list_size as usize;
|
||||
|
||||
data.seek(SeekFrom::Start(0))?;
|
||||
|
||||
|
@ -45,15 +46,15 @@ pub(in crate::logic::iff::wav) fn write_riff_info(data: &mut File, tag: &Tag) ->
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn find_info_list<T>(data: &mut T) -> Result<bool>
|
||||
fn find_info_list<T>(data: &mut T) -> Result<(bool, u32)>
|
||||
where
|
||||
T: Read + Seek,
|
||||
{
|
||||
let mut fourcc = [0; 4];
|
||||
|
||||
let mut found_info = false;
|
||||
let mut info = (false, 0);
|
||||
|
||||
while let (Ok(()), Ok(size)) = (
|
||||
while let (Ok(()), Ok(mut size)) = (
|
||||
data.read_exact(&mut fourcc),
|
||||
data.read_u32::<LittleEndian>(),
|
||||
) {
|
||||
|
@ -62,54 +63,52 @@ where
|
|||
data.read_exact(&mut list_type)?;
|
||||
|
||||
if &list_type == b"INFO" {
|
||||
data.seek(SeekFrom::Current(-8))?;
|
||||
found_info = true;
|
||||
info = (true, size);
|
||||
break;
|
||||
}
|
||||
|
||||
data.seek(SeekFrom::Current(-8))?;
|
||||
}
|
||||
|
||||
if size % 2 != 0 {
|
||||
size += 1;
|
||||
}
|
||||
|
||||
data.seek(SeekFrom::Current(i64::from(size)))?;
|
||||
}
|
||||
|
||||
Ok(found_info)
|
||||
Ok(info)
|
||||
}
|
||||
|
||||
fn create_riff_info(tag: &Tag, bytes: &mut Vec<u8>) -> Result<()> {
|
||||
if tag.item_count() == 0 {
|
||||
fn create_riff_info(
|
||||
items: &mut dyn Iterator<Item = (&str, &String)>,
|
||||
bytes: &mut Vec<u8>,
|
||||
) -> Result<()> {
|
||||
let mut items = items.peekable();
|
||||
|
||||
if items.peek().is_none() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
bytes.extend(b"LIST".iter());
|
||||
bytes.extend(b"INFO".iter());
|
||||
|
||||
for item in tag.items() {
|
||||
if let Some(key) = item.key().map_key(&TagType::RiffInfo) {
|
||||
if key.len() == 4 && key.is_ascii() {
|
||||
if let ItemValue::Text(value) = item.value() {
|
||||
if value.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let val_b = value.as_bytes();
|
||||
// Account for null terminator
|
||||
let len = val_b.len() + 1;
|
||||
|
||||
// Each value has to be null terminated and have an even length
|
||||
let (size, terminator): (u32, &[u8]) = if len % 2 == 0 {
|
||||
(len as u32, &[0])
|
||||
} else {
|
||||
((len + 1) as u32, &[0, 0])
|
||||
};
|
||||
|
||||
bytes.extend(key.as_bytes().iter());
|
||||
bytes.extend(size.to_le_bytes().iter());
|
||||
bytes.extend(val_b.iter());
|
||||
bytes.extend(terminator.iter());
|
||||
}
|
||||
}
|
||||
for (k, v) in items {
|
||||
if v.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let val_b = v.as_bytes();
|
||||
// Account for null terminator
|
||||
let len = val_b.len() + 1;
|
||||
|
||||
// Each value has to be null terminated and have an even length
|
||||
let terminator: &[u8] = if len % 2 == 0 { &[0] } else { &[0, 0] };
|
||||
|
||||
bytes.extend(k.as_bytes().iter());
|
||||
bytes.extend((len as u32).to_le_bytes().iter());
|
||||
bytes.extend(val_b.iter());
|
||||
bytes.extend(terminator.iter());
|
||||
}
|
||||
|
||||
let packet_size = bytes.len() - 4;
|
||||
|
|
|
@ -1,17 +1,19 @@
|
|||
use super::read::verify_wav;
|
||||
use crate::error::{LoftyError, Result};
|
||||
use crate::logic::id3::v2::tag::Id3v2TagRef;
|
||||
use crate::logic::iff::wav::tag::RiffInfoListRef;
|
||||
use crate::types::tag::{Tag, TagType};
|
||||
|
||||
use std::fs::File;
|
||||
|
||||
use byteorder::LittleEndian;
|
||||
|
||||
pub(crate) fn write_to(data: &mut File, tag: &Tag) -> Result<()> {
|
||||
verify_wav(data)?;
|
||||
|
||||
match tag.tag_type() {
|
||||
TagType::RiffInfo => super::tag::write::write_riff_info(data, tag),
|
||||
TagType::Id3v2 => crate::logic::id3::v2::write::write_id3v2_to_chunk_file::<
|
||||
byteorder::LittleEndian,
|
||||
>(data, tag),
|
||||
TagType::RiffInfo => Into::<RiffInfoListRef>::into(tag).write_to(data),
|
||||
TagType::Id3v2 => Into::<Id3v2TagRef>::into(tag).write_to_chunk_file::<LittleEndian>(data),
|
||||
_ => Err(LoftyError::UnsupportedTag),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,16 +1,16 @@
|
|||
pub(crate) mod ape;
|
||||
pub(crate) mod id3;
|
||||
pub(crate) mod iff;
|
||||
pub(crate) mod mp3;
|
||||
pub(crate) mod mp4;
|
||||
pub(crate) mod ogg;
|
||||
use ogg::constants::{OPUSTAGS, VORBIS_COMMENT_HEAD};
|
||||
|
||||
#[cfg(any(feature = "id3v1", feature = "id3v2"))]
|
||||
pub(crate) mod id3;
|
||||
|
||||
use crate::error::Result;
|
||||
use crate::logic::mp4::ilst::IlstRef;
|
||||
use crate::logic::ogg::tag::VorbisCommentsRef;
|
||||
use crate::types::file::FileType;
|
||||
use crate::types::tag::Tag;
|
||||
use ogg::constants::{OPUSTAGS, VORBIS_COMMENT_HEAD};
|
||||
|
||||
use std::fs::File;
|
||||
|
||||
|
@ -18,11 +18,35 @@ pub(crate) fn write_tag(tag: &Tag, file: &mut File, file_type: FileType) -> Resu
|
|||
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, tag),
|
||||
FileType::FLAC => {
|
||||
ogg::flac::write::write_to(file, &mut Into::<VorbisCommentsRef>::into(tag))
|
||||
}
|
||||
FileType::MP3 => mp3::write::write_to(file, tag),
|
||||
FileType::MP4 => mp4::ilst::write::write_to(file, tag),
|
||||
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),
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
#[doc = "Sets the " $display_name]
|
||||
pub fn [<set_ $name>](&mut self, tag: $ty) {
|
||||
self.$name = Some(tag)
|
||||
}
|
||||
)*
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(in crate::logic) use tag_methods;
|
||||
|
|
|
@ -3,8 +3,12 @@ pub(crate) mod header;
|
|||
pub(crate) mod read;
|
||||
pub(in crate::logic) mod write;
|
||||
|
||||
use crate::logic::ape::tag::ApeTag;
|
||||
use crate::logic::id3::v1::tag::Id3v1Tag;
|
||||
use crate::logic::id3::v2::tag::Id3v2Tag;
|
||||
use crate::logic::tag_methods;
|
||||
use crate::types::file::{AudioFile, FileType, TaggedFile};
|
||||
use crate::{FileProperties, Result, Tag, TagType};
|
||||
use crate::{FileProperties, Result, TagType};
|
||||
use header::{ChannelMode, Layer, MpegVersion};
|
||||
|
||||
use std::io::{Read, Seek};
|
||||
|
@ -73,13 +77,13 @@ impl Mp3Properties {
|
|||
pub struct Mp3File {
|
||||
#[cfg(feature = "id3v2")]
|
||||
/// An ID3v2 tag
|
||||
pub(crate) id3v2: Option<Tag>,
|
||||
pub(crate) id3v2_tag: Option<Id3v2Tag>,
|
||||
#[cfg(feature = "id3v1")]
|
||||
/// An ID3v1 tag
|
||||
pub(crate) id3v1: Option<Tag>,
|
||||
pub(crate) id3v1_tag: Option<Id3v1Tag>,
|
||||
#[cfg(feature = "ape")]
|
||||
/// An APEv1/v2 tag
|
||||
pub(crate) ape: Option<Tag>,
|
||||
pub(crate) ape_tag: Option<ApeTag>,
|
||||
/// The file's audio properties
|
||||
pub(crate) properties: Mp3Properties,
|
||||
}
|
||||
|
@ -89,10 +93,14 @@ impl From<Mp3File> for TaggedFile {
|
|||
Self {
|
||||
ty: FileType::MP3,
|
||||
properties: FileProperties::from(input.properties),
|
||||
tags: vec![input.id3v1, input.id3v2, input.ape]
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.collect(),
|
||||
tags: vec![
|
||||
input.id3v2_tag.map(|id3v2| id3v2.into()),
|
||||
input.id3v1_tag.map(|id3v1| id3v1.into()),
|
||||
input.ape_tag.map(|at| at.into()),
|
||||
]
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -112,53 +120,19 @@ impl AudioFile for Mp3File {
|
|||
}
|
||||
|
||||
fn contains_tag(&self) -> bool {
|
||||
self.id3v2.is_some() || self.id3v1.is_some() || self.ape.is_some()
|
||||
self.id3v2_tag.is_some() || self.id3v1_tag.is_some() || self.ape_tag.is_some()
|
||||
}
|
||||
|
||||
fn contains_tag_type(&self, tag_type: &TagType) -> bool {
|
||||
match tag_type {
|
||||
TagType::Ape => self.ape.is_some(),
|
||||
TagType::Id3v2 => self.id3v2.is_some(),
|
||||
TagType::Id3v1 => self.id3v1.is_some(),
|
||||
TagType::Ape => self.ape_tag.is_some(),
|
||||
TagType::Id3v2 => self.id3v2_tag.is_some(),
|
||||
TagType::Id3v1 => self.id3v1_tag.is_some(),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Mp3File {
|
||||
#[cfg(feature = "id3v2")]
|
||||
/// Returns a reference to the ID3v2 tag if it exists
|
||||
pub fn id3v2_tag(&self) -> Option<&Tag> {
|
||||
self.id3v2.as_ref()
|
||||
}
|
||||
|
||||
#[cfg(feature = "id3v2")]
|
||||
/// Returns a mutable reference to the ID3v2 tag if it exists
|
||||
pub fn id3v2_tag_mut(&mut self) -> Option<&mut Tag> {
|
||||
self.id3v2.as_mut()
|
||||
}
|
||||
|
||||
#[cfg(feature = "id3v1")]
|
||||
/// Returns a reference to the ID3v1 tag if it exists
|
||||
pub fn id3v1_tag(&self) -> Option<&Tag> {
|
||||
self.id3v1.as_ref()
|
||||
}
|
||||
|
||||
#[cfg(feature = "id3v1")]
|
||||
/// Returns a mutable reference to the ID3v1 tag if it exists
|
||||
pub fn id3v1_tag_mut(&mut self) -> Option<&mut Tag> {
|
||||
self.id3v1.as_mut()
|
||||
}
|
||||
|
||||
#[cfg(feature = "ape")]
|
||||
/// Returns a reference to the APEv1/2 tag if it exists
|
||||
pub fn ape_tag(&self) -> Option<&Tag> {
|
||||
self.ape.as_ref()
|
||||
}
|
||||
|
||||
#[cfg(feature = "ape")]
|
||||
/// Returns a mutable reference to the APEv1/2 tag if it exists
|
||||
pub fn ape_tag_mut(&mut self) -> Option<&mut Tag> {
|
||||
self.ape.as_mut()
|
||||
}
|
||||
tag_methods! {
|
||||
Mp3File => ID3v2, id3v2_tag, Id3v2Tag; ID3v1, id3v1_tag, Id3v1Tag; APE, ape_tag, ApeTag
|
||||
}
|
||||
|
|
|
@ -2,12 +2,14 @@ use super::header::{verify_frame_sync, Header, XingHeader};
|
|||
use super::{Mp3File, Mp3Properties};
|
||||
use crate::error::{LoftyError, Result};
|
||||
use crate::logic::id3::unsynch_u32;
|
||||
use crate::logic::id3::v1::tag::Id3v1Tag;
|
||||
use crate::logic::id3::v2::read::parse_id3v2;
|
||||
use crate::types::tag::Tag;
|
||||
|
||||
use std::io::{Read, Seek, SeekFrom};
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::id3::v2::Id3v2Tag;
|
||||
use crate::logic::ape::tag::ApeTag;
|
||||
use byteorder::{BigEndian, ByteOrder, ReadBytesExt};
|
||||
|
||||
fn read_properties(
|
||||
|
@ -62,9 +64,9 @@ pub(crate) fn read_from<R>(data: &mut R) -> Result<Mp3File>
|
|||
where
|
||||
R: Read + Seek,
|
||||
{
|
||||
let mut id3v2: Option<Tag> = None;
|
||||
let mut id3v1: Option<Tag> = None;
|
||||
let mut ape: Option<Tag> = None;
|
||||
let mut id3v2_tag: Option<Id3v2Tag> = None;
|
||||
let mut id3v1_tag: Option<Id3v1Tag> = None;
|
||||
let mut ape_tag: Option<ApeTag> = None;
|
||||
|
||||
let mut first_mpeg_frame = (None, 0);
|
||||
let mut last_mpeg_frame = (None, 0);
|
||||
|
@ -100,14 +102,14 @@ where
|
|||
let mut id3v2_read = vec![0; size];
|
||||
data.read_exact(&mut id3v2_read)?;
|
||||
|
||||
let id3v2_tag = parse_id3v2(&mut &*id3v2_read)?;
|
||||
let id3v2 = parse_id3v2(&mut &*id3v2_read)?;
|
||||
|
||||
// Skip over the footer
|
||||
if id3v2_tag.flags().footer {
|
||||
if id3v2.flags().footer {
|
||||
data.seek(SeekFrom::Current(10))?;
|
||||
}
|
||||
|
||||
id3v2 = Some(id3v2_tag);
|
||||
id3v2_tag = Some(id3v2);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
@ -117,7 +119,7 @@ where
|
|||
let mut id3v1_read = [0; 128];
|
||||
data.read_exact(&mut id3v1_read)?;
|
||||
|
||||
id3v1 = Some(crate::logic::id3::v1::read::parse_id3v1(id3v1_read));
|
||||
id3v1_tag = Some(crate::logic::id3::v1::read::parse_id3v1(id3v1_read));
|
||||
continue;
|
||||
}
|
||||
[b'A', b'P', b'E', b'T'] => {
|
||||
|
@ -125,7 +127,7 @@ where
|
|||
data.read_exact(&mut header_remaining)?;
|
||||
|
||||
if &header_remaining == b"AGEX" {
|
||||
ape = Some(crate::logic::ape::tag::read::read_ape_tag(data, false)?.0);
|
||||
ape_tag = Some(crate::logic::ape::tag::read::read_ape_tag(data, false)?.0);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
@ -150,9 +152,12 @@ where
|
|||
let xing_header = XingHeader::read(&mut &xing_reader[..]).ok();
|
||||
|
||||
Ok(Mp3File {
|
||||
id3v2,
|
||||
id3v1,
|
||||
ape,
|
||||
#[cfg(feature = "id3v2")]
|
||||
id3v2_tag,
|
||||
#[cfg(feature = "id3v1")]
|
||||
id3v1_tag,
|
||||
#[cfg(feature = "ape")]
|
||||
ape_tag,
|
||||
properties: read_properties(first_mpeg_frame, last_mpeg_frame, xing_header),
|
||||
})
|
||||
}
|
||||
|
|
|
@ -1,13 +1,16 @@
|
|||
use crate::error::{LoftyError, Result};
|
||||
use crate::logic::ape::tag::ApeTagRef;
|
||||
use crate::logic::id3::v1::tag::Id3v1TagRef;
|
||||
use crate::logic::id3::v2::tag::Id3v2TagRef;
|
||||
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() {
|
||||
TagType::Ape => crate::logic::ape::tag::write::write_to(data, tag),
|
||||
TagType::Id3v1 => crate::logic::id3::v1::write::write_id3v1(data, tag),
|
||||
TagType::Id3v2 => crate::logic::id3::v2::write::write_id3v2(data, tag),
|
||||
TagType::Ape => Into::<ApeTagRef>::into(tag).write_to(data),
|
||||
TagType::Id3v1 => Into::<Id3v1TagRef>::into(tag).write_to(data),
|
||||
TagType::Id3v2 => Into::<Id3v2TagRef>::into(tag).write_to(data),
|
||||
_ => Err(LoftyError::UnsupportedTag),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,61 +0,0 @@
|
|||
use crate::error::{LoftyError, Result};
|
||||
|
||||
use std::io::{Read, Seek, SeekFrom};
|
||||
|
||||
use byteorder::{BigEndian, ReadBytesExt};
|
||||
|
||||
pub(crate) struct Atom {
|
||||
pub(crate) start: u64,
|
||||
pub(crate) len: u64,
|
||||
pub(crate) extended: bool,
|
||||
pub(crate) ident: String,
|
||||
}
|
||||
|
||||
impl Atom {
|
||||
pub(crate) fn read<R>(data: &mut R) -> Result<Self>
|
||||
where
|
||||
R: Read + Seek,
|
||||
{
|
||||
let start = data.seek(SeekFrom::Current(0))?;
|
||||
|
||||
let len = data.read_u32::<BigEndian>()?;
|
||||
|
||||
let mut ident = [0; 4];
|
||||
data.read_exact(&mut ident)?;
|
||||
|
||||
let (len, extended) = match len {
|
||||
// The atom extends to the end of the file
|
||||
0 => {
|
||||
let pos = data.seek(SeekFrom::Current(0))?;
|
||||
let end = data.seek(SeekFrom::End(0))?;
|
||||
|
||||
data.seek(SeekFrom::Start(pos))?;
|
||||
|
||||
(end - pos, false)
|
||||
}
|
||||
// There's an extended length
|
||||
1 => (data.read_u64::<BigEndian>()?, true),
|
||||
_ if len < 8 => return Err(LoftyError::BadAtom("Found an invalid length (< 8)")),
|
||||
_ => (u64::from(len), false),
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
start,
|
||||
len,
|
||||
extended,
|
||||
ident: if ident[0] == 0xA9 {
|
||||
let end = simdutf8::basic::from_utf8(&ident[1..])
|
||||
.map_err(|_| LoftyError::BadAtom("Encountered a non UTF-8 atom identifier"))?;
|
||||
|
||||
let mut ident = String::from('\u{a9}');
|
||||
ident.push_str(end);
|
||||
|
||||
ident
|
||||
} else {
|
||||
simdutf8::basic::from_utf8(&ident)
|
||||
.map_err(|_| LoftyError::BadAtom("Encountered a non UTF-8 atom identifier"))?
|
||||
.to_string()
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
93
src/logic/mp4/atom_info.rs
Normal file
93
src/logic/mp4/atom_info.rs
Normal file
|
@ -0,0 +1,93 @@
|
|||
use crate::error::{LoftyError, Result};
|
||||
|
||||
use std::io::{Read, Seek, SeekFrom};
|
||||
|
||||
use crate::mp4::AtomIdent;
|
||||
use byteorder::{BigEndian, ReadBytesExt};
|
||||
|
||||
pub(crate) struct AtomInfo {
|
||||
pub(crate) start: u64,
|
||||
pub(crate) len: u64,
|
||||
pub(crate) extended: bool,
|
||||
pub(crate) ident: AtomIdent,
|
||||
}
|
||||
|
||||
impl AtomInfo {
|
||||
pub(crate) fn read<R>(data: &mut R) -> Result<Self>
|
||||
where
|
||||
R: Read + Seek,
|
||||
{
|
||||
let start = data.seek(SeekFrom::Current(0))?;
|
||||
|
||||
let len = data.read_u32::<BigEndian>()?;
|
||||
|
||||
let mut ident = [0; 4];
|
||||
data.read_exact(&mut ident)?;
|
||||
|
||||
let mut atom_ident = AtomIdent::Fourcc(ident);
|
||||
|
||||
// Encountered a freeform identifier
|
||||
if &ident == b"----" {
|
||||
atom_ident = parse_freeform(data)?;
|
||||
}
|
||||
|
||||
let (len, extended) = match len {
|
||||
// The atom extends to the end of the file
|
||||
0 => {
|
||||
let pos = data.seek(SeekFrom::Current(0))?;
|
||||
let end = data.seek(SeekFrom::End(0))?;
|
||||
|
||||
data.seek(SeekFrom::Start(pos))?;
|
||||
|
||||
(end - pos, false)
|
||||
}
|
||||
// There's an extended length
|
||||
1 => (data.read_u64::<BigEndian>()?, true),
|
||||
_ if len < 8 => return Err(LoftyError::BadAtom("Found an invalid length (< 8)")),
|
||||
_ => (u64::from(len), false),
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
start,
|
||||
len,
|
||||
extended,
|
||||
ident: atom_ident,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_freeform<R>(data: &mut R) -> Result<AtomIdent>
|
||||
where
|
||||
R: Read + Seek,
|
||||
{
|
||||
let mean = freeform_chunk(data, b"mean")?;
|
||||
let name = freeform_chunk(data, b"name")?;
|
||||
|
||||
Ok(AtomIdent::Freeform { mean, name })
|
||||
}
|
||||
|
||||
fn freeform_chunk<R>(data: &mut R, name: &[u8]) -> Result<String>
|
||||
where
|
||||
R: Read + Seek,
|
||||
{
|
||||
let atom = AtomInfo::read(data)?;
|
||||
|
||||
match atom.ident {
|
||||
AtomIdent::Fourcc(ref fourcc) if fourcc == name => {
|
||||
// Version (1)
|
||||
// Flags (3)
|
||||
data.seek(SeekFrom::Current(4))?;
|
||||
|
||||
// Already read the size, identifier, and version/flags (12 bytes)
|
||||
let mut content = vec![0; (atom.len - 12) as usize];
|
||||
data.read_exact(&mut content)?;
|
||||
|
||||
String::from_utf8(content).map_err(|_| {
|
||||
LoftyError::BadAtom("Found a non UTF-8 string while reading freeform identifier")
|
||||
})
|
||||
}
|
||||
_ => Err(LoftyError::BadAtom(
|
||||
"Found freeform identifier \"----\" with no trailing \"mean\" or \"name\" atoms",
|
||||
)),
|
||||
}
|
||||
}
|
|
@ -1,2 +1,246 @@
|
|||
pub(in crate::logic::mp4) mod read;
|
||||
pub(in crate::logic) mod write;
|
||||
|
||||
use crate::picture::Picture;
|
||||
use crate::types::item::{ItemKey, ItemValue, TagItem};
|
||||
use crate::types::tag::{Tag, TagType};
|
||||
use std::convert::TryInto;
|
||||
|
||||
#[derive(Default)]
|
||||
/// An Mp4
|
||||
pub struct Ilst {
|
||||
pub(crate) atoms: Vec<Atom>,
|
||||
}
|
||||
|
||||
impl From<Ilst> for Tag {
|
||||
fn from(input: Ilst) -> Self {
|
||||
let mut tag = Self::new(TagType::Mp4Atom);
|
||||
|
||||
for atom in input.atoms {
|
||||
let value = match atom.data {
|
||||
AtomData::UTF8(text) | AtomData::UTF16(text) => ItemValue::Text(text),
|
||||
AtomData::Picture(pic) => {
|
||||
tag.pictures.push(pic);
|
||||
continue;
|
||||
}
|
||||
_ => continue,
|
||||
};
|
||||
|
||||
let key = ItemKey::from_key(
|
||||
&TagType::Mp4Atom,
|
||||
&match atom.ident {
|
||||
AtomIdent::Fourcc(fourcc) => {
|
||||
fourcc.iter().map(|b| *b as char).collect::<String>()
|
||||
}
|
||||
AtomIdent::Freeform { mean, name } => {
|
||||
format!("----:{}:{}", mean, name)
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
tag.items.push(TagItem::new(key, value));
|
||||
}
|
||||
|
||||
tag
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Tag> for Ilst {
|
||||
fn from(input: Tag) -> Self {
|
||||
let mut ilst = Self::default();
|
||||
|
||||
for item in input.items {
|
||||
if let Some(ident) = item_key_to_ident(item.key()).map(|k| k.into()) {
|
||||
let data = match item.item_value {
|
||||
ItemValue::Text(text) => AtomData::UTF8(text),
|
||||
_ => continue,
|
||||
};
|
||||
|
||||
ilst.atoms.push(Atom { ident, data })
|
||||
}
|
||||
}
|
||||
|
||||
ilst
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Atom {
|
||||
ident: AtomIdent,
|
||||
data: AtomData,
|
||||
}
|
||||
|
||||
#[derive(Eq, PartialEq)]
|
||||
pub enum AtomIdent {
|
||||
/// A four byte identifier
|
||||
///
|
||||
/// Many FOURCCs start with `0xA9` (©), and should be a UTF-8 string.
|
||||
Fourcc([u8; 4]),
|
||||
/// A freeform identifier
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```text
|
||||
/// ----:com.apple.iTunes:SUBTITLE
|
||||
/// ─┬── ────────┬─────── ───┬────
|
||||
/// ╰freeform identifier ╰name
|
||||
/// |
|
||||
/// ╰mean
|
||||
/// ```
|
||||
///
|
||||
/// * `mean`: A string using a reverse DNS naming convention
|
||||
/// * `name`: A string identifying the atom
|
||||
Freeform { mean: String, name: String },
|
||||
}
|
||||
|
||||
/// The data of an atom
|
||||
///
|
||||
/// NOTES:
|
||||
///
|
||||
/// * This only covers the most common data types.
|
||||
/// See the list of [well-known data types](https://developer.apple.com/library/archive/documentation/QuickTime/QTFF/Metadata/Metadata.html#//apple_ref/doc/uid/TP40000939-CH1-SW34)
|
||||
/// for codes.
|
||||
/// * There are only two variants for integers, which
|
||||
/// will come from codes `21` and `22`. All other integer
|
||||
/// types will be stored as [`AtomData::Unknown`], refer
|
||||
/// to the link above for codes.
|
||||
pub enum AtomData {
|
||||
UTF8(String),
|
||||
UTF16(String),
|
||||
/// A JPEG, PNG, GIF *(Deprecated)*, or BMP image
|
||||
///
|
||||
/// The type is read from the picture itself
|
||||
Picture(Picture),
|
||||
/// A big endian signed integer (1-4 bytes)
|
||||
SignedInteger(i32),
|
||||
/// A big endian unsigned integer (1-4 bytes)
|
||||
UnsignedInteger(u32),
|
||||
/// Unknown data
|
||||
///
|
||||
/// Due to the number of possible types, there are many
|
||||
/// **specified** types that are going to fall into this
|
||||
/// variant.
|
||||
Unknown {
|
||||
code: u32,
|
||||
data: Vec<u8>,
|
||||
},
|
||||
}
|
||||
|
||||
pub(crate) struct IlstRef<'a> {
|
||||
atoms: Box<dyn Iterator<Item = AtomRef<'a>> + 'a>,
|
||||
}
|
||||
|
||||
pub(crate) struct AtomRef<'a> {
|
||||
ident: AtomIdentRef<'a>,
|
||||
data: AtomDataRef<'a>,
|
||||
}
|
||||
|
||||
impl<'a> Into<AtomRef<'a>> for &'a Atom {
|
||||
fn into(self) -> AtomRef<'a> {
|
||||
AtomRef {
|
||||
ident: (&self.ident).into(),
|
||||
data: (&self.data).into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) enum AtomIdentRef<'a> {
|
||||
Fourcc([u8; 4]),
|
||||
Freeform { mean: &'a str, name: &'a str },
|
||||
}
|
||||
|
||||
impl<'a> Into<AtomIdentRef<'a>> for &'a AtomIdent {
|
||||
fn into(self) -> AtomIdentRef<'a> {
|
||||
match self {
|
||||
AtomIdent::Fourcc(fourcc) => AtomIdentRef::Fourcc(*fourcc),
|
||||
AtomIdent::Freeform { mean, name } => AtomIdentRef::Freeform { mean, name },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<AtomIdentRef<'a>> for AtomIdent {
|
||||
fn from(input: AtomIdentRef<'a>) -> Self {
|
||||
match input {
|
||||
AtomIdentRef::Fourcc(fourcc) => AtomIdent::Fourcc(fourcc),
|
||||
AtomIdentRef::Freeform { mean, name } => AtomIdent::Freeform {
|
||||
mean: mean.to_string(),
|
||||
name: name.to_string(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) enum AtomDataRef<'a> {
|
||||
UTF8(&'a str),
|
||||
UTF16(&'a str),
|
||||
Picture(&'a Picture),
|
||||
SignedInteger(i32),
|
||||
UnsignedInteger(u32),
|
||||
Unknown { code: u32, data: &'a [u8] },
|
||||
}
|
||||
|
||||
impl<'a> Into<AtomDataRef<'a>> for &'a AtomData {
|
||||
fn into(self) -> AtomDataRef<'a> {
|
||||
match self {
|
||||
AtomData::UTF8(utf8) => AtomDataRef::UTF8(utf8),
|
||||
AtomData::UTF16(utf16) => AtomDataRef::UTF16(utf16),
|
||||
AtomData::Picture(pic) => AtomDataRef::Picture(pic),
|
||||
AtomData::SignedInteger(int) => AtomDataRef::SignedInteger(*int),
|
||||
AtomData::UnsignedInteger(uint) => AtomDataRef::UnsignedInteger(*uint),
|
||||
AtomData::Unknown { code, data } => AtomDataRef::Unknown { code: *code, data },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Into<IlstRef<'a>> for &'a Ilst {
|
||||
fn into(self) -> IlstRef<'a> {
|
||||
IlstRef {
|
||||
atoms: Box::new(self.atoms.iter().map(|a| a.into())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Into<IlstRef<'a>> for &'a Tag {
|
||||
fn into(self) -> IlstRef<'a> {
|
||||
let iter = self.items.iter().filter_map(|i| {
|
||||
if let (Some(ident), ItemValue::Text(text)) = (item_key_to_ident(i.key()), i.value()) {
|
||||
Some(AtomRef {
|
||||
ident,
|
||||
data: AtomDataRef::UTF8(text),
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
IlstRef {
|
||||
atoms: Box::new(iter),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn item_key_to_ident(key: &ItemKey) -> Option<AtomIdentRef> {
|
||||
key.map_key(&TagType::Mp4Atom, true).and_then(|ident| {
|
||||
if ident.starts_with("----") {
|
||||
let mut split = ident.split(':');
|
||||
|
||||
split.next();
|
||||
|
||||
let mean = split.next();
|
||||
let name = split.next();
|
||||
|
||||
if let (Some(mean), Some(name)) = (mean, name) {
|
||||
Some(AtomIdentRef::Freeform { mean, name })
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
let fourcc = ident.chars().map(|c| c as u8).collect::<Vec<_>>();
|
||||
|
||||
if let Ok(fourcc) = TryInto::<[u8; 4]>::try_into(fourcc) {
|
||||
Some(AtomIdentRef::Fourcc(fourcc))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -1,18 +1,16 @@
|
|||
use super::{Atom, AtomData, AtomIdent, Ilst};
|
||||
use crate::error::{LoftyError, Result};
|
||||
use crate::logic::id3::v2::util::text_utils::utf16_decode;
|
||||
use crate::logic::id3::v2::util::text_utils::TextEncoding;
|
||||
use crate::logic::mp4::atom::Atom;
|
||||
use crate::logic::id3::v2::util::text_utils::{utf16_decode, TextEncoding};
|
||||
use crate::logic::mp4::atom_info::AtomInfo;
|
||||
use crate::logic::mp4::read::skip_unneeded;
|
||||
use crate::types::item::{ItemKey, ItemValue, TagItem};
|
||||
use crate::types::picture::{MimeType, Picture, PictureInformation, PictureType};
|
||||
use crate::types::tag::{Tag, TagType};
|
||||
|
||||
use std::borrow::Cow;
|
||||
use std::io::{Cursor, Read, Seek, SeekFrom};
|
||||
|
||||
use byteorder::{BigEndian, ReadBytesExt};
|
||||
use byteorder::ReadBytesExt;
|
||||
|
||||
pub(crate) fn parse_ilst<R>(data: &mut R, len: u64) -> Result<Option<Tag>>
|
||||
pub(crate) fn parse_ilst<R>(data: &mut R, len: u64) -> Result<Option<Ilst>>
|
||||
where
|
||||
R: Read + Seek,
|
||||
{
|
||||
|
@ -21,96 +19,77 @@ where
|
|||
|
||||
let mut cursor = Cursor::new(contents);
|
||||
|
||||
let mut tag = Tag::new(TagType::Mp4Atom);
|
||||
let mut tag = Ilst::default();
|
||||
|
||||
while let Ok(atom) = Atom::read(&mut cursor) {
|
||||
// Safe to unwrap here since ItemKey::Unknown exists
|
||||
let key = match &*atom.ident {
|
||||
"free" | "skip" => {
|
||||
skip_unneeded(&mut cursor, atom.extended, atom.len)?;
|
||||
continue;
|
||||
}
|
||||
"covr" => {
|
||||
let (mime_type, picture) = match parse_data(&mut cursor)? {
|
||||
(ItemValue::Binary(picture), 13) => (MimeType::Jpeg, picture),
|
||||
(ItemValue::Binary(picture), 14) => (MimeType::Png, picture),
|
||||
(ItemValue::Binary(picture), 27) => (MimeType::Bmp, picture),
|
||||
// GIF is deprecated
|
||||
(ItemValue::Binary(picture), 12) => (MimeType::Gif, picture),
|
||||
// Type 0 is implicit
|
||||
(ItemValue::Binary(picture), 0) => (MimeType::None, picture),
|
||||
_ => return Err(LoftyError::BadAtom("\"covr\" atom has an unknown type")),
|
||||
};
|
||||
|
||||
tag.push_picture(Picture {
|
||||
pic_type: PictureType::Other,
|
||||
text_encoding: TextEncoding::UTF8,
|
||||
mime_type,
|
||||
description: None,
|
||||
information: PictureInformation {
|
||||
width: 0,
|
||||
height: 0,
|
||||
color_depth: 0,
|
||||
num_colors: 0,
|
||||
},
|
||||
data: Cow::from(picture),
|
||||
});
|
||||
|
||||
continue;
|
||||
}
|
||||
"----" => ItemKey::from_key(&TagType::Mp4Atom, &*parse_freeform(&mut cursor)?),
|
||||
other => ItemKey::from_key(&TagType::Mp4Atom, other),
|
||||
}
|
||||
.unwrap();
|
||||
|
||||
let data = parse_data(&mut cursor)?.0;
|
||||
|
||||
match key {
|
||||
ItemKey::TrackNumber | ItemKey::DiscNumber => {
|
||||
if let ItemValue::Binary(pair) = data {
|
||||
let pair = &mut &pair[2..6];
|
||||
|
||||
let number = u32::from(pair.read_u16::<BigEndian>()?);
|
||||
let total = u32::from(pair.read_u16::<BigEndian>()?);
|
||||
|
||||
if total == 0 {
|
||||
match key {
|
||||
ItemKey::TrackNumber => tag.insert_item_unchecked(TagItem::new(
|
||||
ItemKey::TrackTotal,
|
||||
ItemValue::UInt(total),
|
||||
)),
|
||||
ItemKey::DiscNumber => tag.insert_item_unchecked(TagItem::new(
|
||||
ItemKey::DiscTotal,
|
||||
ItemValue::UInt(total),
|
||||
)),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
if number == 0 {
|
||||
tag.insert_item_unchecked(TagItem::new(key, ItemValue::UInt(number)))
|
||||
}
|
||||
} else {
|
||||
return Err(LoftyError::BadAtom(
|
||||
"Expected atom data to include integer pair",
|
||||
));
|
||||
while let Ok(atom) = AtomInfo::read(&mut cursor) {
|
||||
let ident = match &atom.ident {
|
||||
AtomIdent::Fourcc(ref fourcc) => match fourcc {
|
||||
b"free" | b"skip" => {
|
||||
skip_unneeded(&mut cursor, atom.extended, atom.len)?;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
_ => tag.insert_item_unchecked(TagItem::new(key, data)),
|
||||
}
|
||||
b"covr" => {
|
||||
let value = parse_data(&mut cursor)?;
|
||||
|
||||
let (mime_type, data) = match value {
|
||||
AtomData::Unknown { code, data } => match code {
|
||||
// Type 0 is implicit
|
||||
0 => (MimeType::None, data),
|
||||
// GIF is deprecated
|
||||
12 => (MimeType::Gif, data),
|
||||
13 => (MimeType::Jpeg, data),
|
||||
14 => (MimeType::Png, data),
|
||||
27 => (MimeType::Bmp, data),
|
||||
_ => {
|
||||
return Err(LoftyError::BadAtom(
|
||||
"\"covr\" atom has an unknown type",
|
||||
))
|
||||
}
|
||||
},
|
||||
_ => return Err(LoftyError::BadAtom("\"covr\" atom has an unknown type")),
|
||||
};
|
||||
|
||||
tag.atoms.push(Atom {
|
||||
ident: atom.ident,
|
||||
data: AtomData::Picture(Picture {
|
||||
pic_type: PictureType::Other,
|
||||
text_encoding: TextEncoding::UTF8,
|
||||
mime_type,
|
||||
description: None,
|
||||
information: PictureInformation {
|
||||
width: 0,
|
||||
height: 0,
|
||||
color_depth: 0,
|
||||
num_colors: 0,
|
||||
},
|
||||
data: Cow::from(data),
|
||||
}),
|
||||
});
|
||||
|
||||
continue;
|
||||
}
|
||||
_ => atom.ident,
|
||||
},
|
||||
_ => atom.ident,
|
||||
};
|
||||
|
||||
let data = parse_data(&mut cursor)?;
|
||||
|
||||
tag.atoms.push(Atom { ident, data })
|
||||
}
|
||||
|
||||
Ok(Some(tag))
|
||||
}
|
||||
|
||||
fn parse_data<R>(data: &mut R) -> Result<(ItemValue, u32)>
|
||||
fn parse_data<R>(data: &mut R) -> Result<AtomData>
|
||||
where
|
||||
R: Read + Seek,
|
||||
{
|
||||
let atom = Atom::read(data)?;
|
||||
let atom = AtomInfo::read(data)?;
|
||||
|
||||
if atom.ident != "data" {
|
||||
return Err(LoftyError::BadAtom("Expected atom \"data\" to follow name"));
|
||||
match atom.ident {
|
||||
AtomIdent::Fourcc(ref name) if name == b"data" => {}
|
||||
_ => return Err(LoftyError::BadAtom("Expected atom \"data\" to follow name")),
|
||||
}
|
||||
|
||||
// We don't care about the version
|
||||
|
@ -129,26 +108,25 @@ where
|
|||
|
||||
// https://developer.apple.com/library/archive/documentation/QuickTime/QTFF/Metadata/Metadata.html#//apple_ref/doc/uid/TP40000939-CH1-SW35
|
||||
let value = match flags {
|
||||
1 => ItemValue::Text(String::from_utf8(content)?),
|
||||
2 => ItemValue::Text(utf16_decode(&*content, u16::from_be_bytes)?),
|
||||
15 => ItemValue::Locator(String::from_utf8(content)?),
|
||||
22 | 76 | 77 | 78 => parse_uint(&*content)?,
|
||||
21 | 66 | 67 | 74 => parse_int(&*content)?,
|
||||
_ => ItemValue::Binary(content),
|
||||
1 => AtomData::UTF8(String::from_utf8(content)?),
|
||||
2 => AtomData::UTF16(utf16_decode(&*content, u16::from_be_bytes)?),
|
||||
21 => AtomData::SignedInteger(parse_int(&content)?),
|
||||
22 => AtomData::UnsignedInteger(parse_uint(&content)?),
|
||||
code => AtomData::Unknown {
|
||||
code,
|
||||
data: content,
|
||||
},
|
||||
};
|
||||
|
||||
Ok((value, flags))
|
||||
Ok(value)
|
||||
}
|
||||
|
||||
fn parse_uint(bytes: &[u8]) -> Result<ItemValue> {
|
||||
fn parse_uint(bytes: &[u8]) -> Result<u32> {
|
||||
Ok(match bytes.len() {
|
||||
1 => ItemValue::UInt(u32::from(bytes[0])),
|
||||
2 => ItemValue::UInt(u32::from(u16::from_be_bytes([bytes[0], bytes[1]]))),
|
||||
3 => ItemValue::UInt(u32::from_be_bytes([0, bytes[0], bytes[1], bytes[2]])),
|
||||
4 => ItemValue::UInt(u32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]])),
|
||||
8 => ItemValue::UInt64(u64::from_be_bytes([
|
||||
bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7],
|
||||
])),
|
||||
1 => u32::from(bytes[0]),
|
||||
2 => u32::from(u16::from_be_bytes([bytes[0], bytes[1]])),
|
||||
3 => u32::from_be_bytes([0, bytes[0], bytes[1], bytes[2]]),
|
||||
4 => u32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]),
|
||||
_ => {
|
||||
return Err(LoftyError::BadAtom(
|
||||
"Unexpected atom size for type \"BE unsigned integer\"",
|
||||
|
@ -157,15 +135,12 @@ fn parse_uint(bytes: &[u8]) -> Result<ItemValue> {
|
|||
})
|
||||
}
|
||||
|
||||
fn parse_int(bytes: &[u8]) -> Result<ItemValue> {
|
||||
fn parse_int(bytes: &[u8]) -> Result<i32> {
|
||||
Ok(match bytes.len() {
|
||||
1 => ItemValue::Int(i32::from(bytes[0])),
|
||||
2 => ItemValue::Int(i32::from(i16::from_be_bytes([bytes[0], bytes[1]]))),
|
||||
3 => ItemValue::Int(i32::from_be_bytes([0, bytes[0], bytes[1], bytes[2]]) as i32),
|
||||
4 => ItemValue::Int(i32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]) as i32),
|
||||
8 => ItemValue::Int64(i64::from_be_bytes([
|
||||
bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7],
|
||||
])),
|
||||
1 => i32::from(bytes[0]),
|
||||
2 => i32::from(i16::from_be_bytes([bytes[0], bytes[1]])),
|
||||
3 => i32::from_be_bytes([0, bytes[0], bytes[1], bytes[2]]) as i32,
|
||||
4 => i32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]) as i32,
|
||||
_ => {
|
||||
return Err(LoftyError::BadAtom(
|
||||
"Unexpected atom size for type \"BE signed integer\"",
|
||||
|
@ -173,45 +148,3 @@ fn parse_int(bytes: &[u8]) -> Result<ItemValue> {
|
|||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_freeform<R>(data: &mut R) -> Result<String>
|
||||
where
|
||||
R: Read + Seek,
|
||||
{
|
||||
// ----:????:????
|
||||
let mut freeform = String::new();
|
||||
freeform.push_str("----:");
|
||||
|
||||
freeform_chunk(data, "mean", &mut freeform)?;
|
||||
freeform.push(':');
|
||||
freeform_chunk(data, "name", &mut freeform)?;
|
||||
|
||||
Ok(freeform)
|
||||
}
|
||||
|
||||
fn freeform_chunk<R>(data: &mut R, name: &str, freeform: &mut String) -> Result<()>
|
||||
where
|
||||
R: Read + Seek,
|
||||
{
|
||||
let atom = Atom::read(data)?;
|
||||
|
||||
if atom.ident != name {
|
||||
return Err(LoftyError::BadAtom(
|
||||
"Found freeform identifier \"----\" with no trailing \"mean\" or \"name\" atoms",
|
||||
));
|
||||
}
|
||||
|
||||
// Version (1)
|
||||
// Flags (3)
|
||||
data.seek(SeekFrom::Current(4))?;
|
||||
|
||||
// Already read the size, identifier, and version/flags (12 bytes)
|
||||
let mut content = vec![0; (atom.len - 12) as usize];
|
||||
data.read_exact(&mut content)?;
|
||||
|
||||
freeform.push_str(std::str::from_utf8(&*content).map_err(|_| {
|
||||
LoftyError::BadAtom("Found a non UTF-8 string while reading freeform identifier")
|
||||
})?);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -1,23 +1,18 @@
|
|||
use super::{AtomDataRef, IlstRef};
|
||||
use crate::error::{LoftyError, Result};
|
||||
use crate::logic::mp4::ilst::{AtomIdentRef, AtomRef};
|
||||
use crate::logic::mp4::moov::Moov;
|
||||
use crate::logic::mp4::read::nested_atom;
|
||||
use crate::logic::mp4::read::verify_mp4;
|
||||
use crate::picture::MimeType;
|
||||
use crate::types::item::ItemValue;
|
||||
use crate::types::picture::Picture;
|
||||
use crate::types::tag::{Tag, TagType};
|
||||
|
||||
use std::convert::TryInto;
|
||||
use std::fs::File;
|
||||
use std::io::{Cursor, Read, Seek, SeekFrom, Write};
|
||||
|
||||
use byteorder::{BigEndian, WriteBytesExt};
|
||||
|
||||
pub(in crate::logic) fn write_to(data: &mut File, tag: &Tag) -> Result<()> {
|
||||
if tag.tag_type() != &TagType::Mp4Atom {
|
||||
return Err(LoftyError::UnsupportedTag);
|
||||
}
|
||||
|
||||
pub(in crate::logic) fn write_to(data: &mut File, tag: &mut IlstRef) -> Result<()> {
|
||||
verify_mp4(data)?;
|
||||
|
||||
let moov = Moov::find(data)?;
|
||||
|
@ -31,10 +26,10 @@ pub(in crate::logic) fn write_to(data: &mut File, tag: &Tag) -> Result<()> {
|
|||
let mut cursor = Cursor::new(file_bytes);
|
||||
cursor.seek(SeekFrom::Start(pos))?;
|
||||
|
||||
let ilst = build_ilst(tag)?;
|
||||
let ilst = build_ilst(&mut tag.atoms)?;
|
||||
let remove_tag = ilst.is_empty();
|
||||
|
||||
let udta = nested_atom(&mut cursor, moov.len, "udta")?;
|
||||
let udta = nested_atom(&mut cursor, moov.len, b"udta")?;
|
||||
|
||||
// Nothing to do
|
||||
if remove_tag && udta.is_none() {
|
||||
|
@ -48,11 +43,11 @@ pub(in crate::logic) fn write_to(data: &mut File, tag: &Tag) -> Result<()> {
|
|||
|
||||
// ilst is nested in udta.meta, so we need to check what atoms actually exist
|
||||
if let Some(udta) = udta {
|
||||
if let Some(meta) = nested_atom(&mut cursor, udta.len, "meta")? {
|
||||
if let Some(meta) = nested_atom(&mut cursor, udta.len, b"meta")? {
|
||||
// Skip version and flags
|
||||
cursor.seek(SeekFrom::Current(4))?;
|
||||
let (replacement, range, existing_ilst_size) =
|
||||
if let Some(ilst_existing) = nested_atom(&mut cursor, meta.len - 4, "ilst")? {
|
||||
if let Some(ilst_existing) = nested_atom(&mut cursor, meta.len - 4, b"ilst")? {
|
||||
let ilst_existing_size = ilst_existing.len;
|
||||
|
||||
let replacement = if remove_tag { Vec::new() } else { ilst };
|
||||
|
@ -184,55 +179,28 @@ fn write_size(start: u64, size: u64, extended: bool, writer: &mut Cursor<Vec<u8>
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn build_ilst(tag: &Tag) -> Result<Vec<u8>> {
|
||||
if tag.item_count() == 0 && tag.picture_count() == 0 {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
fn build_ilst(atoms: &mut dyn Iterator<Item = AtomRef>) -> Result<Vec<u8>> {
|
||||
let mut peek = atoms.peekable();
|
||||
|
||||
let items = tag
|
||||
.items()
|
||||
.iter()
|
||||
.filter_map(|i| {
|
||||
let key = i.key().map_key(&TagType::Mp4Atom).unwrap();
|
||||
let valid_value = std::mem::discriminant(&ItemValue::Binary(Vec::new()))
|
||||
!= std::mem::discriminant(i.value());
|
||||
|
||||
((key.chars().count() == 4 || key.starts_with("----")) && valid_value)
|
||||
.then(|| (key, i.value()))
|
||||
})
|
||||
.collect::<Vec<(&str, &ItemValue)>>();
|
||||
|
||||
if items.is_empty() {
|
||||
if peek.peek().is_none() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
let mut writer = Cursor::new(vec![0, 0, 0, 0, b'i', b'l', b's', b't']);
|
||||
writer.seek(SeekFrom::End(0))?;
|
||||
|
||||
for (key, value) in items {
|
||||
for atom in peek {
|
||||
let start = writer.seek(SeekFrom::Current(0))?;
|
||||
|
||||
// Empty size, we get it later
|
||||
writer.write_all(&[0; 4])?;
|
||||
|
||||
if key.starts_with("----") {
|
||||
write_freeform(key, &mut writer)?;
|
||||
} else {
|
||||
// "©" is 2 bytes, we only want to write the second one
|
||||
writer.write_all(&if key.starts_with('©') {
|
||||
let key_bytes = key.as_bytes();
|
||||
|
||||
[key_bytes[1], key_bytes[2], key_bytes[3], key_bytes[4]]
|
||||
} else if key.len() > 4 {
|
||||
return Err(LoftyError::BadAtom(
|
||||
"Attempted to write an atom identifier bigger than 4 bytes",
|
||||
));
|
||||
} else {
|
||||
key.as_bytes().try_into().unwrap()
|
||||
})?;
|
||||
match atom.ident {
|
||||
AtomIdentRef::Fourcc(ref fourcc) => writer.write_all(fourcc)?,
|
||||
AtomIdentRef::Freeform { mean, name } => write_freeform(mean, name, &mut writer)?,
|
||||
}
|
||||
|
||||
write_item(value, &mut writer)?;
|
||||
write_atom_data(&atom.data, &mut writer)?;
|
||||
|
||||
let end = writer.seek(SeekFrom::Current(0))?;
|
||||
|
||||
|
@ -245,10 +213,6 @@ fn build_ilst(tag: &Tag) -> Result<Vec<u8>> {
|
|||
writer.seek(SeekFrom::Start(end))?;
|
||||
}
|
||||
|
||||
for pic in tag.pictures() {
|
||||
write_picture(pic, &mut writer)?;
|
||||
}
|
||||
|
||||
let size = writer.get_ref().len();
|
||||
|
||||
write_size(
|
||||
|
@ -261,29 +225,18 @@ fn build_ilst(tag: &Tag) -> Result<Vec<u8>> {
|
|||
Ok(writer.into_inner())
|
||||
}
|
||||
|
||||
fn write_freeform(freeform: &str, writer: &mut Cursor<Vec<u8>>) -> Result<()> {
|
||||
fn write_freeform(mean: &str, name: &str, writer: &mut Cursor<Vec<u8>>) -> Result<()> {
|
||||
// ---- : ???? : ????
|
||||
let freeform_split = freeform.splitn(3, ':').collect::<Vec<&str>>();
|
||||
|
||||
if freeform_split.len() != 3 {
|
||||
return Err(LoftyError::BadAtom(
|
||||
"Attempted to write an incomplete freeform identifier",
|
||||
));
|
||||
}
|
||||
|
||||
// ----
|
||||
writer.write_all(freeform_split[0].as_bytes())?;
|
||||
writer.write_all(b"----")?;
|
||||
|
||||
// .... MEAN 0000 ????
|
||||
let mean = freeform_split[1];
|
||||
|
||||
writer.write_u32::<BigEndian>((12 + mean.len()) as u32)?;
|
||||
writer.write_all(&[b'm', b'e', b'a', b'n', 0, 0, 0, 0])?;
|
||||
writer.write_all(mean.as_bytes())?;
|
||||
|
||||
// .... NAME 0000 ????
|
||||
let name = freeform_split[2];
|
||||
|
||||
writer.write_u32::<BigEndian>((12 + name.len()) as u32)?;
|
||||
writer.write_all(&[b'n', b'a', b'm', b'e', 0, 0, 0, 0])?;
|
||||
writer.write_all(name.as_bytes())?;
|
||||
|
@ -291,15 +244,14 @@ fn write_freeform(freeform: &str, writer: &mut Cursor<Vec<u8>>) -> Result<()> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn write_item(value: &ItemValue, writer: &mut Cursor<Vec<u8>>) -> Result<()> {
|
||||
fn write_atom_data(value: &AtomDataRef, writer: &mut Cursor<Vec<u8>>) -> Result<()> {
|
||||
match value {
|
||||
ItemValue::Text(text) => write_data(1, text.as_bytes(), writer),
|
||||
ItemValue::Locator(locator) => write_data(2, locator.as_bytes(), writer),
|
||||
ItemValue::UInt(uint) => write_data(22, uint.to_be_bytes().as_ref(), writer),
|
||||
ItemValue::UInt64(uint64) => write_data(78, uint64.to_be_bytes().as_ref(), writer),
|
||||
ItemValue::Int(int) => write_data(21, int.to_be_bytes().as_ref(), writer),
|
||||
ItemValue::Int64(int64) => write_data(74, int64.to_be_bytes().as_ref(), writer),
|
||||
_ => unreachable!(),
|
||||
AtomDataRef::UTF8(text) => write_data(1, text.as_bytes(), writer),
|
||||
AtomDataRef::UTF16(text) => write_data(2, text.as_bytes(), writer),
|
||||
AtomDataRef::Picture(pic) => write_picture(pic, writer),
|
||||
AtomDataRef::SignedInteger(int) => write_data(21, int.to_be_bytes().as_ref(), writer),
|
||||
AtomDataRef::UnsignedInteger(uint) => write_data(22, uint.to_be_bytes().as_ref(), writer),
|
||||
AtomDataRef::Unknown { code, data } => write_data(*code, data, writer),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -318,8 +270,14 @@ fn write_picture(picture: &Picture, writer: &mut Cursor<Vec<u8>>) -> Result<()>
|
|||
}
|
||||
}
|
||||
|
||||
fn write_data(flags: u8, data: &[u8], writer: &mut Cursor<Vec<u8>>) -> Result<()> {
|
||||
// .... DATA (flags) 0000 (data)
|
||||
fn write_data(flags: u32, data: &[u8], writer: &mut Cursor<Vec<u8>>) -> Result<()> {
|
||||
if flags > 16_777_215 {
|
||||
return Err(LoftyError::BadAtom(
|
||||
"Attempted to write a code that cannot fit in 24 bits",
|
||||
));
|
||||
}
|
||||
|
||||
// .... DATA (version = 0) (flags) (locale = 0000) (data)
|
||||
let size = 16_u64 + data.len() as u64;
|
||||
|
||||
writer.write_all(&[0, 0, 0, 0, b'd', b'a', b't', b'a'])?;
|
||||
|
@ -328,7 +286,9 @@ fn write_data(flags: u8, data: &[u8], writer: &mut Cursor<Vec<u8>>) -> Result<()
|
|||
// Version
|
||||
writer.write_u8(0)?;
|
||||
|
||||
writer.write_all(&[0, 0, flags])?;
|
||||
writer.write_uint::<BigEndian>(u64::from(flags), 3)?;
|
||||
|
||||
// Locale
|
||||
writer.write_all(&[0; 4])?;
|
||||
writer.write_all(data)?;
|
||||
|
||||
|
|
|
@ -1,12 +1,16 @@
|
|||
mod atom;
|
||||
pub(in crate::logic) mod ilst;
|
||||
mod atom_info;
|
||||
#[cfg(feature = "mp4_atoms")]
|
||||
pub(crate) mod ilst;
|
||||
mod moov;
|
||||
mod properties;
|
||||
mod read;
|
||||
mod trak;
|
||||
|
||||
use crate::logic::tag_methods;
|
||||
use crate::types::file::{AudioFile, FileType, TaggedFile};
|
||||
use crate::{FileProperties, Result, Tag, TagType};
|
||||
use crate::{FileProperties, Result, TagType};
|
||||
#[cfg(feature = "mp4_atoms")]
|
||||
use ilst::Ilst;
|
||||
|
||||
use std::io::{Read, Seek};
|
||||
use std::time::Duration;
|
||||
|
@ -72,7 +76,7 @@ pub struct Mp4File {
|
|||
pub(crate) ftyp: String,
|
||||
#[cfg(feature = "mp4_atoms")]
|
||||
/// The [`Tag`] parsed from the ilst atom, not guaranteed
|
||||
pub(crate) ilst: Option<Tag>,
|
||||
pub(crate) ilst: Option<Ilst>,
|
||||
/// The file's audio properties
|
||||
pub(crate) properties: Mp4Properties,
|
||||
}
|
||||
|
@ -83,7 +87,7 @@ impl From<Mp4File> for TaggedFile {
|
|||
ty: FileType::MP4,
|
||||
properties: FileProperties::from(input.properties),
|
||||
tags: if let Some(ilst) = input.ilst {
|
||||
vec![ilst]
|
||||
vec![ilst.into()]
|
||||
} else {
|
||||
Vec::new()
|
||||
},
|
||||
|
@ -122,16 +126,8 @@ impl Mp4File {
|
|||
pub fn ftyp(&self) -> &str {
|
||||
self.ftyp.as_ref()
|
||||
}
|
||||
|
||||
#[cfg(feature = "mp4_atoms")]
|
||||
/// Returns a reference to the "ilst" tag if it exists
|
||||
pub fn ilst(&self) -> Option<&Tag> {
|
||||
self.ilst.as_ref()
|
||||
}
|
||||
|
||||
#[cfg(feature = "mp4_atoms")]
|
||||
/// Returns a mutable reference to the "ilst" tag if it exists
|
||||
pub fn ilst_mut(&mut self) -> Option<&mut Tag> {
|
||||
self.ilst.as_mut()
|
||||
}
|
||||
}
|
||||
|
||||
tag_methods! {
|
||||
Mp4File => ilst, ilst, Ilst
|
||||
}
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
use super::atom::Atom;
|
||||
use super::atom_info::AtomInfo;
|
||||
#[cfg(feature = "mp4_atoms")]
|
||||
use super::ilst::read::parse_ilst;
|
||||
use super::ilst::{AtomIdent, Ilst};
|
||||
use super::read::skip_unneeded;
|
||||
use super::trak::Trak;
|
||||
use crate::error::{LoftyError, Result};
|
||||
use crate::types::tag::Tag;
|
||||
|
||||
use std::io::{Read, Seek};
|
||||
|
||||
|
@ -12,18 +13,18 @@ use byteorder::{BigEndian, ReadBytesExt};
|
|||
pub(crate) struct Moov {
|
||||
pub(crate) traks: Vec<Trak>,
|
||||
// Represents a parsed moov.udta.meta.ilst since we don't need anything else
|
||||
pub(crate) meta: Option<Tag>,
|
||||
pub(crate) meta: Option<Ilst>,
|
||||
}
|
||||
|
||||
impl Moov {
|
||||
pub(crate) fn find<R>(data: &mut R) -> Result<Atom>
|
||||
pub(crate) fn find<R>(data: &mut R) -> Result<AtomInfo>
|
||||
where
|
||||
R: Read + Seek,
|
||||
{
|
||||
let mut moov = (false, None);
|
||||
|
||||
while let Ok(atom) = Atom::read(data) {
|
||||
if &*atom.ident == "moov" {
|
||||
while let Ok(atom) = AtomInfo::read(data) {
|
||||
if atom.ident == AtomIdent::Fourcc(*b"moov") {
|
||||
moov = (true, Some(atom));
|
||||
break;
|
||||
}
|
||||
|
@ -45,21 +46,27 @@ impl Moov {
|
|||
let mut traks = Vec::new();
|
||||
let mut meta = None;
|
||||
|
||||
while let Ok(atom) = Atom::read(data) {
|
||||
match &*atom.ident {
|
||||
"trak" => traks.push(Trak::parse(data, &atom)?),
|
||||
"udta" => {
|
||||
meta = meta_from_udta(data, atom.len - 8)?;
|
||||
while let Ok(atom) = AtomInfo::read(data) {
|
||||
if let AtomIdent::Fourcc(fourcc) = atom.ident {
|
||||
match &fourcc {
|
||||
b"trak" => traks.push(Trak::parse(data, &atom)?),
|
||||
b"udta" => {
|
||||
meta = meta_from_udta(data, atom.len - 8)?;
|
||||
}
|
||||
_ => skip_unneeded(data, atom.extended, atom.len)?,
|
||||
}
|
||||
_ => skip_unneeded(data, atom.extended, atom.len)?,
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
skip_unneeded(data, atom.extended, atom.len)?
|
||||
}
|
||||
|
||||
Ok(Self { traks, meta })
|
||||
}
|
||||
}
|
||||
|
||||
fn meta_from_udta<R>(data: &mut R, len: u64) -> Result<Option<Tag>>
|
||||
fn meta_from_udta<R>(data: &mut R, len: u64) -> Result<Option<Ilst>>
|
||||
where
|
||||
R: Read + Seek,
|
||||
{
|
||||
|
@ -67,9 +74,9 @@ where
|
|||
let mut meta = (false, 0_u64);
|
||||
|
||||
while read < len {
|
||||
let atom = Atom::read(data)?;
|
||||
let atom = AtomInfo::read(data)?;
|
||||
|
||||
if &*atom.ident == "meta" {
|
||||
if atom.ident == AtomIdent::Fourcc(*b"meta") {
|
||||
meta = (true, atom.len);
|
||||
break;
|
||||
}
|
||||
|
@ -91,9 +98,9 @@ where
|
|||
let mut islt = (false, 0_u64);
|
||||
|
||||
while read < meta.1 {
|
||||
let atom = Atom::read(data)?;
|
||||
let atom = AtomInfo::read(data)?;
|
||||
|
||||
if &*atom.ident == "ilst" {
|
||||
if atom.ident == AtomIdent::Fourcc(*b"ilst") {
|
||||
islt = (true, atom.len);
|
||||
break;
|
||||
}
|
||||
|
@ -102,6 +109,7 @@ where
|
|||
skip_unneeded(data, atom.extended, atom.len)?;
|
||||
}
|
||||
|
||||
#[cfg(feature = "mp4_atoms")]
|
||||
if islt.0 {
|
||||
return parse_ilst(data, islt.1 - 8);
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
use super::atom::Atom;
|
||||
use super::atom_info::AtomInfo;
|
||||
use super::ilst::AtomIdent;
|
||||
use super::read::nested_atom;
|
||||
use super::read::skip_unneeded;
|
||||
use super::trak::Trak;
|
||||
|
@ -30,32 +31,39 @@ where
|
|||
let mut read = 8;
|
||||
|
||||
while read < mdia.len {
|
||||
let atom = Atom::read(data)?;
|
||||
let atom = AtomInfo::read(data)?;
|
||||
|
||||
match &*atom.ident {
|
||||
"mdhd" => {
|
||||
skip_unneeded(data, atom.extended, atom.len)?;
|
||||
mdhd = Some(atom)
|
||||
}
|
||||
"hdlr" => {
|
||||
// The hdlr atom is followed by 8 zeros
|
||||
data.seek(SeekFrom::Current(8))?;
|
||||
|
||||
let mut handler_type = [0; 4];
|
||||
data.read_exact(&mut handler_type)?;
|
||||
|
||||
if &handler_type == b"soun" {
|
||||
audio_track = true
|
||||
if let AtomIdent::Fourcc(fourcc) = atom.ident {
|
||||
match &fourcc {
|
||||
b"mdhd" => {
|
||||
skip_unneeded(data, atom.extended, atom.len)?;
|
||||
mdhd = Some(atom)
|
||||
}
|
||||
b"hdlr" => {
|
||||
// The hdlr atom is followed by 8 zeros
|
||||
data.seek(SeekFrom::Current(8))?;
|
||||
|
||||
skip_unneeded(data, atom.extended, atom.len - 12)?;
|
||||
}
|
||||
"minf" => minf = Some(atom),
|
||||
_ => {
|
||||
skip_unneeded(data, atom.extended, atom.len)?;
|
||||
read += atom.len
|
||||
let mut handler_type = [0; 4];
|
||||
data.read_exact(&mut handler_type)?;
|
||||
|
||||
if &handler_type == b"soun" {
|
||||
audio_track = true
|
||||
}
|
||||
|
||||
skip_unneeded(data, atom.extended, atom.len - 12)?;
|
||||
}
|
||||
b"minf" => minf = Some(atom),
|
||||
_ => {
|
||||
skip_unneeded(data, atom.extended, atom.len)?;
|
||||
read += atom.len;
|
||||
}
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
skip_unneeded(data, atom.extended, atom.len)?;
|
||||
read += atom.len;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -106,8 +114,8 @@ where
|
|||
if let Some(minf) = minf {
|
||||
data.seek(SeekFrom::Start(minf.start + 8))?;
|
||||
|
||||
if let Some(stbl) = nested_atom(data, minf.len, "stbl")? {
|
||||
if let Some(stsd) = nested_atom(data, stbl.len, "stsd")? {
|
||||
if let Some(stbl) = nested_atom(data, minf.len, b"stbl")? {
|
||||
if let Some(stsd) = nested_atom(data, stbl.len, b"stsd")? {
|
||||
let mut stsd = vec![0; (stsd.len - 8) as usize];
|
||||
data.read_exact(&mut stsd)?;
|
||||
|
||||
|
@ -119,12 +127,18 @@ where
|
|||
// Number of entries (4)
|
||||
stsd_reader.seek(SeekFrom::Start(8))?;
|
||||
|
||||
let atom = Atom::read(&mut stsd_reader)?;
|
||||
let atom = AtomInfo::read(&mut stsd_reader)?;
|
||||
|
||||
match &*atom.ident {
|
||||
"mp4a" => mp4a_properties(&mut stsd_reader, &mut properties)?,
|
||||
"alac" => alac_properties(&mut stsd_reader, &mut properties)?,
|
||||
unknown => properties.codec = Mp4Codec::Unknown(unknown.to_string()),
|
||||
if let AtomIdent::Fourcc(ref fourcc) = atom.ident {
|
||||
match fourcc {
|
||||
b"mp4a" => mp4a_properties(&mut stsd_reader, &mut properties)?,
|
||||
b"alac" => alac_properties(&mut stsd_reader, &mut properties)?,
|
||||
unknown => {
|
||||
if let Ok(codec) = std::str::from_utf8(unknown) {
|
||||
properties.codec = Mp4Codec::Unknown(codec.to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -157,11 +171,11 @@ where
|
|||
data.seek(SeekFrom::Current(2))?;
|
||||
|
||||
// This information is often followed by an esds (elementary stream descriptor) atom containing the bitrate
|
||||
if let Ok(esds) = Atom::read(data) {
|
||||
if let Ok(esds) = AtomInfo::read(data) {
|
||||
// There are 4 bytes we expect to be zeroed out
|
||||
// Version (1)
|
||||
// Flags (3)
|
||||
if &*esds.ident == "esds" && data.read_u32::<BigEndian>()? == 0 {
|
||||
if esds.ident == AtomIdent::Fourcc(*b"esds") && data.read_u32::<BigEndian>()? == 0 {
|
||||
let mut descriptor = [0; 4];
|
||||
data.read_exact(&mut descriptor)?;
|
||||
|
||||
|
@ -220,8 +234,8 @@ where
|
|||
// First alac atom's content (28)
|
||||
data.seek(SeekFrom::Start(44))?;
|
||||
|
||||
if let Ok(alac) = Atom::read(data) {
|
||||
if &*alac.ident == "alac" {
|
||||
if let Ok(alac) = AtomInfo::read(data) {
|
||||
if alac.ident == AtomIdent::Fourcc(*b"alac") {
|
||||
// Skipping 13 bytes
|
||||
// Version (4)
|
||||
// Samples per frame (4)
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
use super::atom::Atom;
|
||||
use super::atom_info::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};
|
||||
|
||||
|
@ -10,9 +11,9 @@ pub(in crate::logic::mp4) fn verify_mp4<R>(data: &mut R) -> Result<String>
|
|||
where
|
||||
R: Read + Seek,
|
||||
{
|
||||
let atom = Atom::read(data)?;
|
||||
let atom = AtomInfo::read(data)?;
|
||||
|
||||
if atom.ident != "ftyp" {
|
||||
if atom.ident != AtomIdent::Fourcc(*b"ftyp") {
|
||||
return Err(LoftyError::UnknownFormat);
|
||||
}
|
||||
|
||||
|
@ -61,7 +62,7 @@ where
|
|||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn nested_atom<R>(data: &mut R, len: u64, expected: &str) -> Result<Option<Atom>>
|
||||
pub(crate) fn nested_atom<R>(data: &mut R, len: u64, expected: &[u8]) -> Result<Option<AtomInfo>>
|
||||
where
|
||||
R: Read + Seek,
|
||||
{
|
||||
|
@ -69,10 +70,10 @@ where
|
|||
let mut ret = None;
|
||||
|
||||
while read < len {
|
||||
let atom = Atom::read(data)?;
|
||||
let atom = AtomInfo::read(data)?;
|
||||
|
||||
match &*atom.ident {
|
||||
ident if ident == expected => {
|
||||
match atom.ident {
|
||||
AtomIdent::Fourcc(ref fourcc) if fourcc == expected => {
|
||||
ret = Some(atom);
|
||||
break;
|
||||
}
|
||||
|
|
|
@ -1,15 +1,16 @@
|
|||
use super::atom::Atom;
|
||||
use super::atom_info::AtomInfo;
|
||||
use super::ilst::AtomIdent;
|
||||
use super::read::skip_unneeded;
|
||||
use crate::error::Result;
|
||||
|
||||
use std::io::{Read, Seek, SeekFrom};
|
||||
|
||||
pub(crate) struct Trak {
|
||||
pub(crate) mdia: Option<Atom>,
|
||||
pub(crate) mdia: Option<AtomInfo>,
|
||||
}
|
||||
|
||||
impl Trak {
|
||||
pub(crate) fn parse<R>(data: &mut R, trak: &Atom) -> Result<Self>
|
||||
pub(crate) fn parse<R>(data: &mut R, trak: &AtomInfo) -> Result<Self>
|
||||
where
|
||||
R: Read + Seek,
|
||||
{
|
||||
|
@ -18,9 +19,9 @@ impl Trak {
|
|||
let mut read = 8;
|
||||
|
||||
while read < trak.len {
|
||||
let atom = Atom::read(data)?;
|
||||
let atom = AtomInfo::read(data)?;
|
||||
|
||||
if &*atom.ident == "mdia" {
|
||||
if atom.ident == AtomIdent::Fourcc(*b"mdia") {
|
||||
mdia = Some(atom);
|
||||
data.seek(SeekFrom::Current((trak.len - read - 8) as i64))?;
|
||||
break;
|
||||
|
|
|
@ -2,50 +2,34 @@ mod block;
|
|||
mod read;
|
||||
pub(crate) mod write;
|
||||
|
||||
use super::tag::VorbisComments;
|
||||
use crate::error::Result;
|
||||
use crate::logic::tag_methods;
|
||||
use crate::types::file::{AudioFile, FileType, TaggedFile};
|
||||
use crate::types::item::{ItemKey, ItemValue, TagItem};
|
||||
use crate::types::properties::FileProperties;
|
||||
use crate::types::tag::{Tag, TagType};
|
||||
use crate::types::tag::TagType;
|
||||
|
||||
use std::io::{Read, Seek};
|
||||
|
||||
/// A FLAC file
|
||||
pub struct FlacFile {
|
||||
/// The file's audio properties
|
||||
pub(crate) properties: FileProperties,
|
||||
#[cfg(feature = "vorbis_comments")]
|
||||
/// The file vendor's name found in the vorbis comments (if it exists)
|
||||
pub(crate) vendor: Option<String>,
|
||||
#[cfg(feature = "vorbis_comments")]
|
||||
/// The vorbis comments contained in the file
|
||||
///
|
||||
/// NOTE: This field being `Some` does not mean the file has vorbis comments, as Picture blocks exist.
|
||||
pub(crate) vorbis_comments: Option<Tag>,
|
||||
pub(crate) vorbis_comments: Option<VorbisComments>,
|
||||
/// The file's audio properties
|
||||
pub(crate) properties: FileProperties,
|
||||
}
|
||||
|
||||
impl From<FlacFile> for TaggedFile {
|
||||
fn from(input: FlacFile) -> Self {
|
||||
// Preserve vendor string
|
||||
let tags = {
|
||||
if let Some(mut tag) = input.vorbis_comments {
|
||||
if let Some(vendor) = input.vendor {
|
||||
tag.insert_item_unchecked(TagItem::new(
|
||||
ItemKey::EncoderSoftware,
|
||||
ItemValue::Text(vendor),
|
||||
))
|
||||
}
|
||||
|
||||
vec![tag]
|
||||
} else {
|
||||
Vec::new()
|
||||
}
|
||||
};
|
||||
|
||||
Self {
|
||||
ty: FileType::FLAC,
|
||||
properties: input.properties,
|
||||
tags,
|
||||
tags: input
|
||||
.vorbis_comments
|
||||
.map_or_else(Vec::new, |t| vec![t.into()]),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -77,16 +61,6 @@ impl AudioFile for FlacFile {
|
|||
}
|
||||
}
|
||||
|
||||
impl FlacFile {
|
||||
#[cfg(feature = "vorbis_comments")]
|
||||
/// Returns a reference to the Vorbis comments tag if it exists
|
||||
pub fn vorbis_comments(&self) -> Option<&Tag> {
|
||||
self.vorbis_comments.as_ref()
|
||||
}
|
||||
|
||||
#[cfg(feature = "vorbis_comments")]
|
||||
/// Returns a mutable reference to the Vorbis comments tag if it exists
|
||||
pub fn vorbis_comments_mut(&mut self) -> Option<&mut Tag> {
|
||||
self.vorbis_comments.as_mut()
|
||||
}
|
||||
tag_methods! {
|
||||
FlacFile => Vorbis_Comments, vorbis_comments, VorbisComments
|
||||
}
|
||||
|
|
|
@ -2,9 +2,9 @@ use super::block::Block;
|
|||
use super::FlacFile;
|
||||
use crate::error::{LoftyError, Result};
|
||||
use crate::logic::ogg::read::read_comments;
|
||||
use crate::logic::ogg::tag::VorbisComments;
|
||||
use crate::picture::Picture;
|
||||
use crate::types::properties::FileProperties;
|
||||
use crate::types::tag::{Tag, TagType};
|
||||
|
||||
use std::io::{Read, Seek, SeekFrom};
|
||||
use std::time::Duration;
|
||||
|
@ -92,16 +92,21 @@ where
|
|||
|
||||
let mut last_block = stream_info.last;
|
||||
|
||||
let mut vendor = None;
|
||||
let mut tag = Tag::new(TagType::VorbisComments);
|
||||
let mut tag = VorbisComments {
|
||||
vendor: String::new(),
|
||||
items: vec![],
|
||||
pictures: vec![],
|
||||
};
|
||||
|
||||
while !last_block {
|
||||
let block = Block::read(data)?;
|
||||
last_block = block.last;
|
||||
|
||||
match block.ty {
|
||||
4 => vendor = Some(read_comments(&mut &*block.content, &mut tag)?),
|
||||
6 => tag.push_picture(Picture::from_flac_bytes(&*block.content)?),
|
||||
4 => read_comments(&mut &*block.content, &mut tag)?,
|
||||
6 => tag
|
||||
.pictures
|
||||
.push(Picture::from_flac_bytes(&*block.content)?),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
@ -116,7 +121,6 @@ where
|
|||
|
||||
Ok(FlacFile {
|
||||
properties,
|
||||
vendor,
|
||||
vorbis_comments: (!(tag.picture_count() == 0 && tag.item_count() == 0)).then(|| tag),
|
||||
vorbis_comments: (!(tag.items.is_empty() && tag.pictures.is_empty())).then(|| tag),
|
||||
})
|
||||
}
|
||||
|
|
|
@ -1,21 +1,16 @@
|
|||
use super::block::Block;
|
||||
use super::read::verify_flac;
|
||||
use crate::error::{LoftyError, Result};
|
||||
use crate::logic::ogg::tag::VorbisCommentsRef;
|
||||
use crate::logic::ogg::write::create_comments;
|
||||
use crate::picture::Picture;
|
||||
use crate::types::item::{ItemKey, ItemValue, TagItem};
|
||||
use crate::types::tag::{Tag, TagType};
|
||||
|
||||
use std::fs::File;
|
||||
use std::io::{Cursor, Read, Seek, SeekFrom, Write};
|
||||
|
||||
use byteorder::{LittleEndian, WriteBytesExt};
|
||||
|
||||
pub(in crate::logic) fn write_to(data: &mut File, tag: &Tag) -> Result<()> {
|
||||
if tag.tag_type() != &TagType::VorbisComments {
|
||||
return Err(LoftyError::UnsupportedTag);
|
||||
}
|
||||
|
||||
pub(in crate::logic) fn write_to(data: &mut File, tag: &mut VorbisCommentsRef) -> Result<()> {
|
||||
let stream_info = verify_flac(data)?;
|
||||
let stream_info_end = stream_info.end as usize;
|
||||
|
||||
|
@ -78,22 +73,13 @@ pub(in crate::logic) fn write_to(data: &mut File, tag: &Tag) -> Result<()> {
|
|||
);
|
||||
}
|
||||
|
||||
let vendor = if let Some(ItemValue::Text(vendor)) = tag
|
||||
.get_item_ref(&ItemKey::EncoderSoftware)
|
||||
.map(TagItem::value)
|
||||
{
|
||||
Some(vendor)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let mut comment_blocks = Cursor::new(Vec::new());
|
||||
|
||||
let mut comment_blocks = Vec::new();
|
||||
create_comment_block(
|
||||
&mut comment_blocks,
|
||||
vendor.unwrap_or(&String::new()),
|
||||
tag.items(),
|
||||
)?;
|
||||
create_picture_blocks(&mut comment_blocks, tag.pictures())?;
|
||||
create_comment_block(&mut comment_blocks, tag.vendor, &mut tag.items)?;
|
||||
|
||||
let mut comment_blocks = comment_blocks.into_inner();
|
||||
|
||||
create_picture_blocks(&mut comment_blocks, tag.pictures)?;
|
||||
|
||||
if blocks_remove.is_empty() {
|
||||
file_bytes.splice(0..0, comment_blocks);
|
||||
|
@ -117,25 +103,43 @@ pub(in crate::logic) fn write_to(data: &mut File, tag: &Tag) -> Result<()> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn create_comment_block(writer: &mut Vec<u8>, vendor: &str, items: &[TagItem]) -> Result<()> {
|
||||
if !items.is_empty() {
|
||||
fn create_comment_block(
|
||||
writer: &mut Cursor<Vec<u8>>,
|
||||
vendor: &str,
|
||||
items: &mut dyn Iterator<Item = (&str, &String)>,
|
||||
) -> Result<()> {
|
||||
let mut peek = items.peekable();
|
||||
|
||||
if peek.peek().is_some() {
|
||||
let mut byte = 0_u8;
|
||||
byte |= 4 & 0x7f;
|
||||
|
||||
writer.write_u8(byte)?;
|
||||
writer.write_u32::<LittleEndian>(vendor.len() as u32)?;
|
||||
writer.write_all(vendor.as_bytes())?;
|
||||
writer.write_u32::<LittleEndian>(items.len() as u32)?;
|
||||
|
||||
create_comments(writer, items);
|
||||
let item_count_pos = writer.seek(SeekFrom::Current(0))?;
|
||||
let mut count = 0;
|
||||
|
||||
let len = (writer.len() - 1) as u32;
|
||||
writer.write_u32::<LittleEndian>(count)?;
|
||||
|
||||
create_comments(writer, &mut count, items)?;
|
||||
|
||||
let len = (writer.get_ref().len() - 1) as u32;
|
||||
|
||||
if len > 65535 {
|
||||
return Err(LoftyError::TooMuchData);
|
||||
}
|
||||
|
||||
writer.splice(1..1, len.to_be_bytes()[1..].to_vec());
|
||||
let comment_end = writer.seek(SeekFrom::Current(0))?;
|
||||
|
||||
writer.seek(SeekFrom::Start(item_count_pos))?;
|
||||
writer.write_u32::<LittleEndian>(count)?;
|
||||
|
||||
writer.seek(SeekFrom::Start(comment_end))?;
|
||||
writer
|
||||
.get_mut()
|
||||
.splice(1..1, len.to_be_bytes()[1..].to_vec());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
|
|
@ -6,10 +6,13 @@ use ogg_pager::Page;
|
|||
|
||||
pub(crate) mod constants;
|
||||
pub(crate) mod read;
|
||||
#[cfg(feature = "vorbis_comments")]
|
||||
pub(crate) mod write;
|
||||
|
||||
pub(crate) mod flac;
|
||||
pub(crate) mod opus;
|
||||
#[cfg(feature = "vorbis_comments")]
|
||||
pub(crate) mod tag;
|
||||
pub(crate) mod vorbis;
|
||||
|
||||
pub fn page_from_packet(packet: &mut [u8]) -> Result<Vec<Page>> {
|
||||
|
|
|
@ -1,94 +1,34 @@
|
|||
pub(in crate::logic::ogg) mod properties;
|
||||
pub(in crate::logic::ogg) mod write;
|
||||
pub(crate) mod properties;
|
||||
pub(super) mod write;
|
||||
|
||||
use super::find_last_page;
|
||||
use super::tag::VorbisComments;
|
||||
use crate::error::Result;
|
||||
use crate::logic::ogg::constants::{OPUSHEAD, OPUSTAGS};
|
||||
use crate::types::file::{AudioFile, FileType, TaggedFile};
|
||||
use crate::types::item::{ItemKey, ItemValue, TagItem};
|
||||
use crate::types::properties::FileProperties;
|
||||
use crate::types::tag::{Tag, TagType};
|
||||
use crate::types::tag::TagType;
|
||||
use properties::OpusProperties;
|
||||
|
||||
use std::io::{Read, Seek};
|
||||
use std::time::Duration;
|
||||
|
||||
/// An Opus file's audio properties
|
||||
pub struct OpusProperties {
|
||||
duration: Duration,
|
||||
bitrate: u32,
|
||||
channels: u8,
|
||||
version: u8,
|
||||
input_sample_rate: u32,
|
||||
}
|
||||
|
||||
impl From<OpusProperties> for FileProperties {
|
||||
fn from(input: OpusProperties) -> Self {
|
||||
Self {
|
||||
duration: input.duration,
|
||||
bitrate: Some(input.bitrate),
|
||||
sample_rate: Some(input.input_sample_rate),
|
||||
channels: Some(input.channels),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl OpusProperties {
|
||||
/// Duration
|
||||
pub fn duration(&self) -> Duration {
|
||||
self.duration
|
||||
}
|
||||
|
||||
/// Bitrate (kbps)
|
||||
pub fn bitrate(&self) -> u32 {
|
||||
self.bitrate
|
||||
}
|
||||
|
||||
/// Channel count
|
||||
pub fn channels(&self) -> u8 {
|
||||
self.channels
|
||||
}
|
||||
|
||||
/// Opus version
|
||||
pub fn version(&self) -> u8 {
|
||||
self.version
|
||||
}
|
||||
|
||||
/// Input sample rate
|
||||
pub fn input_sample_rate(&self) -> u32 {
|
||||
self.input_sample_rate
|
||||
}
|
||||
}
|
||||
|
||||
/// An OGG Opus file
|
||||
pub struct OpusFile {
|
||||
#[cfg(feature = "vorbis_comments")]
|
||||
/// The file vendor's name
|
||||
pub(crate) vendor: String,
|
||||
#[cfg(feature = "vorbis_comments")]
|
||||
/// The vorbis comments contained in the file
|
||||
///
|
||||
/// NOTE: While a metadata packet is required, it isn't required to actually have any data.
|
||||
pub(crate) vorbis_comments: Tag,
|
||||
pub(crate) vorbis_comments: VorbisComments,
|
||||
/// The file's audio properties
|
||||
pub(crate) properties: OpusProperties,
|
||||
}
|
||||
|
||||
impl From<OpusFile> for TaggedFile {
|
||||
fn from(input: OpusFile) -> Self {
|
||||
// Preserve vendor string
|
||||
let mut tag = input.vorbis_comments;
|
||||
|
||||
if !input.vendor.is_empty() {
|
||||
tag.insert_item_unchecked(TagItem::new(
|
||||
ItemKey::EncoderSoftware,
|
||||
ItemValue::Text(input.vendor),
|
||||
))
|
||||
}
|
||||
|
||||
Self {
|
||||
ty: FileType::Opus,
|
||||
properties: FileProperties::from(input.properties),
|
||||
tags: vec![tag],
|
||||
tags: vec![input.vorbis_comments.into()],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -103,9 +43,10 @@ impl AudioFile for OpusFile {
|
|||
let file_information = super::read::read_from(reader, OPUSHEAD, OPUSTAGS)?;
|
||||
|
||||
Ok(Self {
|
||||
properties: properties::read_properties(reader, &file_information.2)?,
|
||||
vendor: file_information.0,
|
||||
vorbis_comments: file_information.1,
|
||||
properties: properties::read_properties(reader, &file_information.1)?,
|
||||
#[cfg(feature = "vorbis_comments")]
|
||||
// Safe to unwrap, a metadata packet is mandatory in Opus
|
||||
vorbis_comments: file_information.0.unwrap(),
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -129,13 +70,13 @@ impl AudioFile for OpusFile {
|
|||
impl OpusFile {
|
||||
#[cfg(feature = "vorbis_comments")]
|
||||
/// Returns a reference to the Vorbis comments tag
|
||||
pub fn vorbis_comments(&self) -> &Tag {
|
||||
pub fn vorbis_comments(&self) -> &VorbisComments {
|
||||
&self.vorbis_comments
|
||||
}
|
||||
|
||||
#[cfg(feature = "vorbis_comments")]
|
||||
/// Returns a mutable reference to the Vorbis comments tag
|
||||
pub fn vorbis_comments_mut(&mut self) -> &mut Tag {
|
||||
pub fn vorbis_comments_mut(&mut self) -> &mut VorbisComments {
|
||||
&mut self.vorbis_comments
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
use super::{find_last_page, OpusProperties};
|
||||
use super::find_last_page;
|
||||
use crate::error::{LoftyError, Result};
|
||||
use crate::types::properties::FileProperties;
|
||||
|
||||
use std::io::{Read, Seek, SeekFrom};
|
||||
use std::time::Duration;
|
||||
|
@ -7,6 +8,53 @@ use std::time::Duration;
|
|||
use byteorder::{LittleEndian, ReadBytesExt};
|
||||
use ogg_pager::Page;
|
||||
|
||||
/// An Opus file's audio properties
|
||||
pub struct OpusProperties {
|
||||
duration: Duration,
|
||||
bitrate: u32,
|
||||
channels: u8,
|
||||
version: u8,
|
||||
input_sample_rate: u32,
|
||||
}
|
||||
|
||||
impl From<OpusProperties> for FileProperties {
|
||||
fn from(input: OpusProperties) -> Self {
|
||||
Self {
|
||||
duration: input.duration,
|
||||
bitrate: Some(input.bitrate),
|
||||
sample_rate: Some(input.input_sample_rate),
|
||||
channels: Some(input.channels),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl OpusProperties {
|
||||
/// Duration
|
||||
pub fn duration(&self) -> Duration {
|
||||
self.duration
|
||||
}
|
||||
|
||||
/// Bitrate (kbps)
|
||||
pub fn bitrate(&self) -> u32 {
|
||||
self.bitrate
|
||||
}
|
||||
|
||||
/// Channel count
|
||||
pub fn channels(&self) -> u8 {
|
||||
self.channels
|
||||
}
|
||||
|
||||
/// Opus version
|
||||
pub fn version(&self) -> u8 {
|
||||
self.version
|
||||
}
|
||||
|
||||
/// Input sample rate
|
||||
pub fn input_sample_rate(&self) -> u32 {
|
||||
self.input_sample_rate
|
||||
}
|
||||
}
|
||||
|
||||
pub(in crate::logic::ogg) fn read_properties<R>(
|
||||
data: &mut R,
|
||||
first_page: &Page,
|
||||
|
|
|
@ -1,17 +1,16 @@
|
|||
use super::tag::VorbisComments;
|
||||
use super::verify_signature;
|
||||
use crate::error::{LoftyError, Result};
|
||||
use crate::picture::Picture;
|
||||
use crate::types::item::{ItemKey, ItemValue, TagItem};
|
||||
use crate::types::tag::{Tag, TagType};
|
||||
|
||||
use std::io::{Read, Seek, SeekFrom};
|
||||
|
||||
use byteorder::{LittleEndian, ReadBytesExt};
|
||||
use ogg_pager::Page;
|
||||
|
||||
pub type OGGTags = (String, Tag, Page);
|
||||
pub type OGGTags = (Option<VorbisComments>, Page);
|
||||
|
||||
pub(crate) fn read_comments<R>(data: &mut R, tag: &mut Tag) -> Result<String>
|
||||
pub(super) fn read_comments<R>(data: &mut R, tag: &mut VorbisComments) -> Result<()>
|
||||
where
|
||||
R: Read,
|
||||
{
|
||||
|
@ -25,6 +24,8 @@ where
|
|||
Err(_) => return Err(LoftyError::Ogg("File has an invalid vendor string")),
|
||||
};
|
||||
|
||||
tag.vendor = vendor;
|
||||
|
||||
let comments_total_len = data.read_u32::<LittleEndian>()?;
|
||||
|
||||
for _ in 0..comments_total_len {
|
||||
|
@ -37,18 +38,15 @@ where
|
|||
|
||||
let split: Vec<&str> = comment.splitn(2, '=').collect();
|
||||
|
||||
if split[0] == "METADATA_BLOCK_PICTURE" {
|
||||
tag.push_picture(Picture::from_flac_bytes(split[1].as_bytes())?)
|
||||
} else {
|
||||
// It's safe to unwrap here since any unknown key is wrapped in ItemKey::Unknown
|
||||
tag.insert_item(TagItem::new(
|
||||
ItemKey::from_key(&TagType::VorbisComments, split[0]).unwrap(),
|
||||
ItemValue::Text(split[1].to_string()),
|
||||
));
|
||||
match &*split[0] {
|
||||
"METADATA_BLOCK_PICTURE" => tag
|
||||
.pictures
|
||||
.push(Picture::from_flac_bytes(split[1].as_bytes())?),
|
||||
_ => tag.items.push((split[0].to_string(), split[1].to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(vendor)
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn read_from<T>(data: &mut T, header_sig: &[u8], comment_sig: &[u8]) -> Result<OGGTags>
|
||||
|
@ -78,10 +76,20 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
let mut tag = Tag::new(TagType::VorbisComments);
|
||||
#[cfg(feature = "vorbis_comments")]
|
||||
{
|
||||
let mut tag = VorbisComments {
|
||||
vendor: String::new(),
|
||||
items: vec![],
|
||||
pictures: vec![],
|
||||
};
|
||||
|
||||
let reader = &mut &md_pages[..];
|
||||
let vendor = read_comments(reader, &mut tag)?;
|
||||
let reader = &mut &md_pages[..];
|
||||
read_comments(reader, &mut tag)?;
|
||||
|
||||
Ok((vendor, tag, first_page))
|
||||
Ok((Some(tag), first_page))
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "vorbis_comments"))]
|
||||
Ok((None, first_page))
|
||||
}
|
||||
|
|
124
src/logic/ogg/tag.rs
Normal file
124
src/logic/ogg/tag.rs
Normal file
|
@ -0,0 +1,124 @@
|
|||
use crate::error::{LoftyError, Result};
|
||||
use crate::logic::ogg::constants::{OPUSHEAD, VORBIS_IDENT_HEAD};
|
||||
use crate::probe::Probe;
|
||||
use crate::types::file::FileType;
|
||||
use crate::types::item::{ItemKey, ItemValue, TagItem};
|
||||
use crate::types::picture::Picture;
|
||||
use crate::types::tag::{Tag, TagType};
|
||||
|
||||
use std::fs::File;
|
||||
|
||||
#[derive(Default)]
|
||||
/// Vorbis comments
|
||||
pub struct VorbisComments {
|
||||
/// An identifier for the encoding software
|
||||
pub vendor: String,
|
||||
/// A collection of key-value pairs
|
||||
pub items: Vec<(String, String)>,
|
||||
/// A collection of all pictures
|
||||
pub pictures: Vec<Picture>,
|
||||
}
|
||||
|
||||
impl VorbisComments {
|
||||
pub fn write_to(&self, file: &mut File) -> Result<()> {
|
||||
Into::<VorbisCommentsRef>::into(self).write_to(file)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<VorbisComments> for Tag {
|
||||
fn from(input: VorbisComments) -> Self {
|
||||
let mut tag = Tag::new(TagType::VorbisComments);
|
||||
|
||||
tag.insert_item_unchecked(TagItem::new(
|
||||
ItemKey::EncoderSoftware,
|
||||
ItemValue::Text(input.vendor),
|
||||
));
|
||||
|
||||
for (k, v) in input.items {
|
||||
tag.insert_item_unchecked(TagItem::new(
|
||||
ItemKey::from_key(&TagType::VorbisComments, &k),
|
||||
ItemValue::Text(v),
|
||||
));
|
||||
}
|
||||
|
||||
for pic in input.pictures {
|
||||
tag.push_picture(pic)
|
||||
}
|
||||
|
||||
tag
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Tag> for VorbisComments {
|
||||
fn from(input: Tag) -> Self {
|
||||
let mut vorbis_comments = Self::default();
|
||||
|
||||
if let Some(vendor) = input.get_string(&ItemKey::EncoderSoftware) {
|
||||
vorbis_comments.vendor = vendor.to_string()
|
||||
}
|
||||
|
||||
for item in input.items {
|
||||
// Discard binary items, as they are not allowed in Vorbis comments
|
||||
let val = match item.value() {
|
||||
ItemValue::Text(text) | ItemValue::Locator(text) => text,
|
||||
_ => continue,
|
||||
};
|
||||
|
||||
// Safe to unwrap since all ItemKeys map in Vorbis comments
|
||||
let key = item.key().map_key(&TagType::VorbisComments, true).unwrap();
|
||||
|
||||
vorbis_comments
|
||||
.items
|
||||
.push((key.to_string(), val.to_string()));
|
||||
}
|
||||
|
||||
vorbis_comments
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct VorbisCommentsRef<'a> {
|
||||
pub vendor: &'a str,
|
||||
pub items: Box<dyn Iterator<Item = (&'a str, &'a String)> + 'a>,
|
||||
pub pictures: &'a [Picture],
|
||||
}
|
||||
|
||||
impl<'a> VorbisCommentsRef<'a> {
|
||||
fn write_to(&mut self, file: &mut File) -> Result<()> {
|
||||
match Probe::new().file_type(file) {
|
||||
Some(FileType::FLAC) => super::flac::write::write_to(file, self),
|
||||
Some(FileType::Opus) => super::write::write(file, self, OPUSHEAD),
|
||||
Some(FileType::Vorbis) => super::write::write(file, self, VORBIS_IDENT_HEAD),
|
||||
_ => Err(LoftyError::UnsupportedTag),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Into<VorbisCommentsRef<'a>> for &'a VorbisComments {
|
||||
fn into(self) -> VorbisCommentsRef<'a> {
|
||||
VorbisCommentsRef {
|
||||
vendor: self.vendor.as_str(),
|
||||
items: Box::new(self.items.as_slice().iter().map(|(k, v)| (k.as_str(), v))),
|
||||
pictures: self.pictures.as_slice(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Into<VorbisCommentsRef<'a>> for &'a Tag {
|
||||
fn into(self) -> VorbisCommentsRef<'a> {
|
||||
let vendor = self.get_string(&ItemKey::EncoderSoftware).unwrap_or("");
|
||||
|
||||
let items = self.items.iter().filter_map(|i| match i.value() {
|
||||
ItemValue::Text(val) | ItemValue::Locator(val) => Some((
|
||||
i.key().map_key(&TagType::VorbisComments, true).unwrap(),
|
||||
val,
|
||||
)),
|
||||
_ => None,
|
||||
});
|
||||
|
||||
VorbisCommentsRef {
|
||||
vendor,
|
||||
items: Box::new(items),
|
||||
pictures: self.pictures(),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,112 +1,34 @@
|
|||
pub(in crate::logic::ogg) mod properties;
|
||||
pub(crate) mod properties;
|
||||
pub(in crate::logic::ogg) mod write;
|
||||
|
||||
use super::find_last_page;
|
||||
use super::tag::VorbisComments;
|
||||
use crate::error::Result;
|
||||
use crate::logic::ogg::constants::{VORBIS_COMMENT_HEAD, VORBIS_IDENT_HEAD};
|
||||
use crate::types::file::{AudioFile, FileType, TaggedFile};
|
||||
use crate::types::item::{ItemKey, ItemValue, TagItem};
|
||||
use crate::types::properties::FileProperties;
|
||||
use crate::types::tag::{Tag, TagType};
|
||||
use crate::types::tag::TagType;
|
||||
use properties::VorbisProperties;
|
||||
|
||||
use std::io::{Read, Seek};
|
||||
use std::time::Duration;
|
||||
|
||||
/// An OGG Vorbis file's audio properties
|
||||
pub struct VorbisProperties {
|
||||
duration: Duration,
|
||||
bitrate: u32,
|
||||
sample_rate: u32,
|
||||
channels: u8,
|
||||
version: u32,
|
||||
bitrate_maximum: u32,
|
||||
bitrate_nominal: u32,
|
||||
bitrate_minimum: u32,
|
||||
}
|
||||
|
||||
impl From<VorbisProperties> for FileProperties {
|
||||
fn from(input: VorbisProperties) -> Self {
|
||||
Self {
|
||||
duration: input.duration,
|
||||
bitrate: Some(input.bitrate),
|
||||
sample_rate: Some(input.sample_rate),
|
||||
channels: Some(input.channels),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl VorbisProperties {
|
||||
/// Duration
|
||||
pub fn duration(&self) -> Duration {
|
||||
self.duration
|
||||
}
|
||||
|
||||
/// Bitrate (kbps)
|
||||
pub fn bitrate(&self) -> u32 {
|
||||
self.bitrate
|
||||
}
|
||||
|
||||
/// Sample rate (Hz)
|
||||
pub fn sample_rate(&self) -> u32 {
|
||||
self.sample_rate
|
||||
}
|
||||
|
||||
/// Channel count
|
||||
pub fn channels(&self) -> u8 {
|
||||
self.channels
|
||||
}
|
||||
|
||||
/// Vorbis version
|
||||
pub fn version(&self) -> u32 {
|
||||
self.version
|
||||
}
|
||||
|
||||
/// Maximum bitrate
|
||||
pub fn bitrate_max(&self) -> u32 {
|
||||
self.bitrate_maximum
|
||||
}
|
||||
|
||||
/// Nominal bitrate
|
||||
pub fn bitrate_nominal(&self) -> u32 {
|
||||
self.bitrate_nominal
|
||||
}
|
||||
|
||||
/// Minimum bitrate
|
||||
pub fn bitrate_min(&self) -> u32 {
|
||||
self.bitrate_minimum
|
||||
}
|
||||
}
|
||||
|
||||
/// An OGG Vorbis file
|
||||
pub struct VorbisFile {
|
||||
#[cfg(feature = "vorbis_comments")]
|
||||
/// The file vendor's name
|
||||
pub(crate) vendor: String,
|
||||
#[cfg(feature = "vorbis_comments")]
|
||||
/// The vorbis comments contained in the file
|
||||
///
|
||||
/// NOTE: While a metadata packet is required, it isn't required to actually have any data.
|
||||
pub(crate) vorbis_comments: Tag,
|
||||
pub(crate) vorbis_comments: VorbisComments,
|
||||
/// The file's audio properties
|
||||
pub(crate) properties: VorbisProperties,
|
||||
}
|
||||
|
||||
impl From<VorbisFile> for TaggedFile {
|
||||
fn from(input: VorbisFile) -> Self {
|
||||
// Preserve vendor string
|
||||
let mut tag = input.vorbis_comments;
|
||||
|
||||
if !input.vendor.is_empty() {
|
||||
tag.insert_item_unchecked(TagItem::new(
|
||||
ItemKey::EncoderSoftware,
|
||||
ItemValue::Text(input.vendor),
|
||||
))
|
||||
}
|
||||
|
||||
Self {
|
||||
ty: FileType::Vorbis,
|
||||
properties: FileProperties::from(input.properties),
|
||||
tags: vec![tag],
|
||||
tags: vec![input.vorbis_comments.into()],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -122,9 +44,10 @@ impl AudioFile for VorbisFile {
|
|||
super::read::read_from(reader, VORBIS_IDENT_HEAD, VORBIS_COMMENT_HEAD)?;
|
||||
|
||||
Ok(Self {
|
||||
properties: properties::read_properties(reader, &file_information.2)?,
|
||||
vendor: file_information.0,
|
||||
vorbis_comments: file_information.1,
|
||||
properties: properties::read_properties(reader, &file_information.1)?,
|
||||
#[cfg(feature = "vorbis_comments")]
|
||||
// Safe to unwrap, a metadata packet is mandatory in OGG Vorbis
|
||||
vorbis_comments: file_information.0.unwrap(),
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -148,13 +71,13 @@ impl AudioFile for VorbisFile {
|
|||
impl VorbisFile {
|
||||
#[cfg(feature = "vorbis_comments")]
|
||||
/// Returns a reference to the Vorbis comments tag
|
||||
pub fn vorbis_comments(&self) -> &Tag {
|
||||
pub fn vorbis_comments(&self) -> &VorbisComments {
|
||||
&self.vorbis_comments
|
||||
}
|
||||
|
||||
#[cfg(feature = "vorbis_comments")]
|
||||
/// Returns a mutable reference to the Vorbis comments tag
|
||||
pub fn vorbis_comments_mut(&mut self) -> &mut Tag {
|
||||
pub fn vorbis_comments_mut(&mut self) -> &mut VorbisComments {
|
||||
&mut self.vorbis_comments
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
use super::{find_last_page, VorbisProperties};
|
||||
use super::find_last_page;
|
||||
use crate::error::{LoftyError, Result};
|
||||
use crate::types::properties::FileProperties;
|
||||
|
||||
use std::io::{Read, Seek};
|
||||
use std::time::Duration;
|
||||
|
@ -7,6 +8,71 @@ use std::time::Duration;
|
|||
use byteorder::{LittleEndian, ReadBytesExt};
|
||||
use ogg_pager::Page;
|
||||
|
||||
/// An OGG Vorbis file's audio properties
|
||||
pub struct VorbisProperties {
|
||||
duration: Duration,
|
||||
bitrate: u32,
|
||||
sample_rate: u32,
|
||||
channels: u8,
|
||||
version: u32,
|
||||
bitrate_maximum: u32,
|
||||
bitrate_nominal: u32,
|
||||
bitrate_minimum: u32,
|
||||
}
|
||||
|
||||
impl From<VorbisProperties> for FileProperties {
|
||||
fn from(input: VorbisProperties) -> Self {
|
||||
Self {
|
||||
duration: input.duration,
|
||||
bitrate: Some(input.bitrate),
|
||||
sample_rate: Some(input.sample_rate),
|
||||
channels: Some(input.channels),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl VorbisProperties {
|
||||
/// Duration
|
||||
pub fn duration(&self) -> Duration {
|
||||
self.duration
|
||||
}
|
||||
|
||||
/// Bitrate (kbps)
|
||||
pub fn bitrate(&self) -> u32 {
|
||||
self.bitrate
|
||||
}
|
||||
|
||||
/// Sample rate (Hz)
|
||||
pub fn sample_rate(&self) -> u32 {
|
||||
self.sample_rate
|
||||
}
|
||||
|
||||
/// Channel count
|
||||
pub fn channels(&self) -> u8 {
|
||||
self.channels
|
||||
}
|
||||
|
||||
/// Vorbis version
|
||||
pub fn version(&self) -> u32 {
|
||||
self.version
|
||||
}
|
||||
|
||||
/// Maximum bitrate
|
||||
pub fn bitrate_max(&self) -> u32 {
|
||||
self.bitrate_maximum
|
||||
}
|
||||
|
||||
/// Nominal bitrate
|
||||
pub fn bitrate_nominal(&self) -> u32 {
|
||||
self.bitrate_nominal
|
||||
}
|
||||
|
||||
/// Minimum bitrate
|
||||
pub fn bitrate_min(&self) -> u32 {
|
||||
self.bitrate_minimum
|
||||
}
|
||||
}
|
||||
|
||||
pub(in crate::logic::ogg) fn read_properties<R>(
|
||||
data: &mut R,
|
||||
first_page: &Page,
|
||||
|
|
|
@ -2,64 +2,83 @@ use super::{page_from_packet, verify_signature};
|
|||
use crate::error::{LoftyError, Result};
|
||||
use crate::logic::ogg::constants::OPUSTAGS;
|
||||
use crate::logic::ogg::constants::VORBIS_COMMENT_HEAD;
|
||||
use crate::types::item::{ItemValue, TagItem};
|
||||
use crate::logic::ogg::tag::VorbisCommentsRef;
|
||||
use crate::types::tag::{Tag, TagType};
|
||||
|
||||
use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
|
||||
use std::convert::TryFrom;
|
||||
use std::fs::File;
|
||||
use std::io::{Read, Seek, SeekFrom, Write};
|
||||
use std::io::{Cursor, Read, Seek, SeekFrom, Write};
|
||||
|
||||
use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
|
||||
use ogg_pager::Page;
|
||||
|
||||
pub(crate) fn create_comments(packet: &mut Vec<u8>, items: &[TagItem]) {
|
||||
for item in items {
|
||||
if let ItemValue::Text(value) = item.value() {
|
||||
let comment = format!(
|
||||
"{}={}",
|
||||
item.key().map_key(&TagType::VorbisComments).unwrap(),
|
||||
value
|
||||
);
|
||||
let comment_b = comment.as_bytes();
|
||||
|
||||
let bytes_len = comment_b.len();
|
||||
|
||||
if u32::try_from(bytes_len as u64).is_ok() {
|
||||
packet.extend((bytes_len as u32).to_le_bytes().iter());
|
||||
packet.extend(comment_b.iter());
|
||||
}
|
||||
}
|
||||
pub(in crate::logic) fn write_to(data: &mut File, tag: &Tag, sig: &[u8]) -> Result<()> {
|
||||
match tag.tag_type() {
|
||||
TagType::VorbisComments => write(data, &mut Into::<VorbisCommentsRef>::into(tag), sig),
|
||||
_ => Err(LoftyError::UnsupportedTag),
|
||||
}
|
||||
}
|
||||
|
||||
fn create_pages(tag: &Tag, writer: &mut Vec<u8>) -> Result<Vec<Page>> {
|
||||
let item_count = tag.item_count() + tag.picture_count();
|
||||
#[cfg(feature = "vorbis_comments")]
|
||||
pub(crate) fn create_comments(
|
||||
packet: &mut impl Write,
|
||||
count: &mut u32,
|
||||
items: &mut dyn Iterator<Item = (&str, &String)>,
|
||||
) -> Result<()> {
|
||||
for (k, v) in items {
|
||||
let comment = format!("{}={}", k, v);
|
||||
|
||||
writer.write_u32::<LittleEndian>(item_count)?;
|
||||
create_comments(writer, tag.items());
|
||||
let comment_b = comment.as_bytes();
|
||||
let bytes_len = comment_b.len();
|
||||
|
||||
for pic in tag.pictures() {
|
||||
if u32::try_from(bytes_len as u64).is_ok() {
|
||||
*count += 1;
|
||||
|
||||
packet.write_all(&(bytes_len as u32).to_le_bytes())?;
|
||||
packet.write_all(comment_b)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(feature = "vorbis_comments")]
|
||||
fn create_pages(tag: &mut VorbisCommentsRef, writer: &mut Cursor<Vec<u8>>) -> Result<Vec<Page>> {
|
||||
let item_count_pos = writer.seek(SeekFrom::Current(0))?;
|
||||
|
||||
writer.write_u32::<LittleEndian>(0)?;
|
||||
|
||||
let mut count = 0;
|
||||
create_comments(writer, &mut count, &mut tag.items)?;
|
||||
|
||||
for pic in tag.pictures {
|
||||
let picture = format!(
|
||||
"METADATA_BLOCK_PICTURE={}",
|
||||
base64::encode(pic.as_flac_bytes())
|
||||
);
|
||||
|
||||
let picture_b = picture.as_bytes();
|
||||
let bytes_len = picture_b.len();
|
||||
|
||||
if u32::try_from(bytes_len as u64).is_ok() {
|
||||
count += 1;
|
||||
|
||||
writer.write_u32::<LittleEndian>(bytes_len as u32)?;
|
||||
writer.write_all(picture_b)?;
|
||||
}
|
||||
}
|
||||
|
||||
page_from_packet(writer)
|
||||
let packet_end = writer.seek(SeekFrom::Current(0))?;
|
||||
|
||||
writer.seek(SeekFrom::Start(item_count_pos))?;
|
||||
writer.write_u32::<LittleEndian>(count)?;
|
||||
writer.seek(SeekFrom::Start(packet_end))?;
|
||||
|
||||
page_from_packet(writer.get_mut())
|
||||
}
|
||||
|
||||
pub(in crate::logic) fn write_to(data: &mut File, tag: &Tag, sig: &[u8]) -> Result<()> {
|
||||
if tag.tag_type() != &TagType::VorbisComments {
|
||||
return Err(LoftyError::UnsupportedTag);
|
||||
}
|
||||
|
||||
#[cfg(feature = "vorbis_comments")]
|
||||
pub(super) fn write(data: &mut File, tag: &mut VorbisCommentsRef, sig: &[u8]) -> Result<()> {
|
||||
let first_page = Page::read(data, false)?;
|
||||
|
||||
let ser = first_page.serial;
|
||||
|
@ -77,7 +96,8 @@ pub(in crate::logic) fn write_to(data: &mut File, tag: &Tag, sig: &[u8]) -> Resu
|
|||
let mut vendor = vec![0; vendor_len as usize];
|
||||
md_reader.read_exact(&mut vendor)?;
|
||||
|
||||
let mut packet = Vec::new();
|
||||
let mut packet = Cursor::new(Vec::new());
|
||||
|
||||
packet.write_all(sig)?;
|
||||
packet.write_u32::<LittleEndian>(vendor_len)?;
|
||||
packet.write_all(&vendor)?;
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
use crate::error::{LoftyError, Result};
|
||||
use crate::logic::ape::ApeFile;
|
||||
use crate::logic::iff::aiff::AiffFile;
|
||||
use crate::logic::iff::wav::WavFile;
|
||||
|
@ -6,8 +7,7 @@ use crate::logic::mp4::Mp4File;
|
|||
use crate::logic::ogg::flac::FlacFile;
|
||||
use crate::logic::ogg::opus::OpusFile;
|
||||
use crate::logic::ogg::vorbis::VorbisFile;
|
||||
use crate::types::file::AudioFile;
|
||||
use crate::{FileType, LoftyError, Result, TaggedFile};
|
||||
use crate::types::file::{AudioFile, FileType, TaggedFile};
|
||||
|
||||
use std::io::{Cursor, Read, Seek};
|
||||
use std::path::Path;
|
||||
|
|
|
@ -51,28 +51,27 @@ impl TaggedFile {
|
|||
/// | `FLAC`, `Opus`, `Vorbis` | `VorbisComments` |
|
||||
/// | `MP4` | `Mp4Atom` |
|
||||
pub fn primary_tag(&self) -> Option<&Tag> {
|
||||
let tag_type = match self.ty {
|
||||
FileType::AIFF | FileType::MP3 | FileType::WAV => &TagType::Id3v2,
|
||||
FileType::APE => &TagType::Ape,
|
||||
FileType::FLAC | FileType::Opus | FileType::Vorbis => &TagType::VorbisComments,
|
||||
FileType::MP4 => &TagType::Mp4Atom,
|
||||
};
|
||||
|
||||
self.tag(tag_type)
|
||||
self.tag(&Self::primary_tag_type(self.ty))
|
||||
}
|
||||
|
||||
/// Gets a mutable reference to the file's "Primary tag"
|
||||
///
|
||||
/// See [`primary_tag`](Self::primary_tag) for an explanation
|
||||
pub fn primary_tag_mut(&mut self) -> Option<&mut Tag> {
|
||||
let tag_type = match self.ty {
|
||||
FileType::AIFF | FileType::MP3 | FileType::WAV => &TagType::Id3v2,
|
||||
FileType::APE => &TagType::Ape,
|
||||
FileType::FLAC | FileType::Opus | FileType::Vorbis => &TagType::VorbisComments,
|
||||
FileType::MP4 => &TagType::Mp4Atom,
|
||||
};
|
||||
self.tag_mut(&Self::primary_tag_type(self.ty))
|
||||
}
|
||||
|
||||
self.tag_mut(tag_type)
|
||||
fn primary_tag_type(f_ty: FileType) -> TagType {
|
||||
match f_ty {
|
||||
#[cfg(feature = "id3v2")]
|
||||
FileType::AIFF | FileType::MP3 | FileType::WAV => TagType::Id3v2,
|
||||
#[cfg(feature = "ape")]
|
||||
FileType::APE => TagType::Ape,
|
||||
#[cfg(feature = "vorbis_comments")]
|
||||
FileType::FLAC | FileType::Opus | FileType::Vorbis => TagType::VorbisComments,
|
||||
#[cfg(feature = "mp4_atoms")]
|
||||
FileType::MP4 => TagType::Mp4Atom,
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets the first tag, if there are any
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
use crate::logic::id3::v1::constants::VALID_ITEMKEYS;
|
||||
use crate::TagType;
|
||||
|
||||
#[cfg(feature = "id3v2")]
|
||||
use crate::logic::id3::v2::frame::Id3v2Frame;
|
||||
|
||||
macro_rules! first_key {
|
||||
($key:tt $(| $remaining:expr)*) => {
|
||||
$key
|
||||
|
@ -21,7 +19,7 @@ macro_rules! first_key {
|
|||
// The standard key(s) **must** come before any popular non-standard keys.
|
||||
// Keys should appear in order of popularity.
|
||||
macro_rules! item_keys {
|
||||
(ALLOWED_UNKNOWN => [$($unknown_tag_type:pat),+]; $($variant:ident => [$($($tag_type:pat)|* => $($key:tt)|+),+]),+) => {
|
||||
($($variant:ident => [$($($tag_type:pat)|* => $($key:tt)|+),+]),+) => {
|
||||
#[derive(PartialEq, Clone, Debug, Eq, Hash)]
|
||||
#[allow(missing_docs)]
|
||||
#[non_exhaustive]
|
||||
|
@ -30,9 +28,6 @@ macro_rules! item_keys {
|
|||
$(
|
||||
$variant,
|
||||
)+
|
||||
#[cfg(feature = "id3v2")]
|
||||
/// An item that only exists in ID3v2
|
||||
Id3v2Specific(Id3v2Frame),
|
||||
/// When a key couldn't be mapped to another variant
|
||||
///
|
||||
/// This **will not** allow writing keys that are out of spec (Eg. ID3v2.4 frame IDs **must** be 4 characters)
|
||||
|
@ -44,37 +39,29 @@ macro_rules! item_keys {
|
|||
///
|
||||
/// NOTE: If used with ID3v2, this will only check against the ID3v2.4 keys.
|
||||
/// If you wish to use a V2 or V3 key, see [`upgrade_v2`](crate::id3::v2::upgrade_v2) and [`upgrade_v3`](crate::id3::v2::upgrade_v3)
|
||||
pub fn from_key(tag_type: &TagType, key: &str) -> Option<Self> {
|
||||
pub fn from_key(tag_type: &TagType, key: &str) -> Self {
|
||||
match tag_type {
|
||||
$(
|
||||
$(
|
||||
$($tag_type)|* if $(key.eq_ignore_ascii_case($key))||* => Some(ItemKey::$variant),
|
||||
$($tag_type)|* if $(key.eq_ignore_ascii_case($key))||* => ItemKey::$variant,
|
||||
)+
|
||||
)+
|
||||
$(
|
||||
$unknown_tag_type => Some(ItemKey::Unknown(key.to_string())),
|
||||
)+
|
||||
_ => None,
|
||||
_ => Self::Unknown(key.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Maps the variant to a format-specific key
|
||||
///
|
||||
/// NOTE: Since all ID3v2 tags are upgraded to [`Id3v2Version::V4`](crate::id3::v2::Id3v2Version), the
|
||||
/// version provided does not matter. They cannot be downgraded.
|
||||
pub fn map_key(&self, tag_type: &TagType) -> Option<&str> {
|
||||
/// 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) {
|
||||
$(
|
||||
$(
|
||||
($($tag_type)|*, ItemKey::$variant) => Some(first_key!($($key)|*)),
|
||||
)+
|
||||
)+
|
||||
$(
|
||||
($unknown_tag_type, ItemKey::Unknown(unknown)) => Some(&*unknown),
|
||||
)+
|
||||
// Need a special case here to allow for checked insertion, the result isn't actually used.
|
||||
#[cfg(feature = "id3v2")]
|
||||
(TagType::Id3v2, ItemKey::Id3v2Specific(_)) => Some(""),
|
||||
(_, ItemKey::Unknown(unknown)) if allow_unknown => Some(&*unknown),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
@ -83,7 +70,6 @@ macro_rules! item_keys {
|
|||
}
|
||||
|
||||
item_keys!(
|
||||
ALLOWED_UNKNOWN => [TagType::Ape, TagType::VorbisComments, TagType::Mp4Atom];
|
||||
// Titles
|
||||
AlbumTitle => [
|
||||
TagType::Id3v2 => "TALB", TagType::Mp4Atom => "\u{a9}alb",
|
||||
|
@ -432,84 +418,29 @@ item_keys!(
|
|||
pub enum ItemValue {
|
||||
/// Any UTF-8 encoded text
|
||||
Text(String),
|
||||
/// **(APE/ID3v2 ONLY)** Any UTF-8 encoded locator of external information
|
||||
/// Any UTF-8 encoded locator of external information
|
||||
///
|
||||
/// This is only gets special treatment in ID3v2 and APE tags, being written
|
||||
/// as a normal string in other tags
|
||||
Locator(String),
|
||||
/// **(APE/ID3v2/MP4 ONLY)** Binary information
|
||||
///
|
||||
/// In the case of ID3v2, this is the type of a [`Id3v2Frame::EncapsulatedObject`](crate::id3::v2::Id3v2Frame::EncapsulatedObject),
|
||||
/// [`Id3v2Frame::SyncText`](crate::id3::v2::Id3v2Frame::SyncText), and any unknown frame.
|
||||
///
|
||||
/// For APEv2 and MP4, the only use is for unknown items.
|
||||
/// Binary information
|
||||
Binary(Vec<u8>),
|
||||
/// Any 32 bit unsigned integer
|
||||
///
|
||||
/// This is most commonly used for items such as track and disc numbers
|
||||
UInt(u32),
|
||||
/// **(MP4 ONLY)** Any 64 bit unsigned integer
|
||||
///
|
||||
/// There are no common [`ItemKey`]s that use this
|
||||
UInt64(u64),
|
||||
/// Any 32 bit signed integer
|
||||
///
|
||||
/// There are no common [`ItemKey`]s that use this
|
||||
Int(i32),
|
||||
/// **(MP4 ONLY)** Any 64 bit signed integer
|
||||
///
|
||||
/// There are no common [`ItemKey`]s that use this
|
||||
Int64(i64),
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "id3v2", feature = "ape"))]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Hash, Default)]
|
||||
#[allow(clippy::struct_excessive_bools)]
|
||||
/// **(ID3v2/APEv2 ONLY)** Various flags to describe the content of an item
|
||||
///
|
||||
/// It is not an error to attempt to write flags to a format that doesn't support them.
|
||||
/// They will just be ignored.
|
||||
pub struct TagItemFlags {
|
||||
#[cfg(feature = "id3v2")]
|
||||
/// **(ID3v2 ONLY)** Preserve frame on tag edit
|
||||
pub tag_alter_preservation: bool,
|
||||
#[cfg(feature = "id3v2")]
|
||||
/// **(ID3v2 ONLY)** Preserve frame on file edit
|
||||
pub file_alter_preservation: bool,
|
||||
#[cfg(any(feature = "id3v2", feature = "ape"))]
|
||||
/// **(ID3v2/APEv2 ONLY)** Item cannot be written to
|
||||
pub read_only: bool,
|
||||
#[cfg(feature = "id3v2")]
|
||||
/// **(ID3v2 ONLY)** Frame belongs in a group
|
||||
///
|
||||
/// In addition to setting this flag, a group identifier byte must be added.
|
||||
/// All frames with the same group identifier byte belong to the same group.
|
||||
pub grouping_identity: (bool, u8),
|
||||
#[cfg(feature = "id3v2")]
|
||||
/// **(ID3v2 ONLY)** Frame is zlib compressed
|
||||
///
|
||||
/// It is **required** `data_length_indicator` be set if this is set.
|
||||
pub compression: bool,
|
||||
#[cfg(feature = "id3v2")]
|
||||
/// **(ID3v2 ONLY)** Frame is encrypted
|
||||
///
|
||||
/// NOTE: Since the encryption method is unknown, lofty cannot do anything with these frames
|
||||
///
|
||||
/// In addition to setting this flag, an encryption method symbol must be added.
|
||||
/// The method symbol **must** be > 0x80.
|
||||
pub encryption: (bool, u8),
|
||||
#[cfg(feature = "id3v2")]
|
||||
/// **(ID3v2 ONLY)** Frame is unsynchronised
|
||||
///
|
||||
/// In short, this makes all "0xFF 0x00" combinations into "0xFF 0x00 0x00" to avoid confusion
|
||||
/// with the MPEG frame header, which is often identified by its "frame sync" (11 set bits).
|
||||
/// It is preferred an ID3v2 tag is either *completely* unsynchronised or not unsynchronised at all.
|
||||
pub unsynchronisation: bool,
|
||||
#[cfg(feature = "id3v2")]
|
||||
/// **(ID3v2 ONLY)** Frame has a data length indicator
|
||||
///
|
||||
/// The data length indicator is the size of the frame if the flags were all zeroed out.
|
||||
/// This is usually used in combination with `compression` and `encryption` (depending on encryption method).
|
||||
///
|
||||
/// If using encryption, the final size must be added. It will be ignored if using compression.
|
||||
pub data_length_indicator: (bool, u32),
|
||||
pub(crate) enum ItemValueRef<'a> {
|
||||
Text(&'a str),
|
||||
Locator(&'a str),
|
||||
Binary(&'a [u8]),
|
||||
}
|
||||
|
||||
impl<'a> Into<ItemValueRef<'a>> for &'a ItemValue {
|
||||
fn into(self) -> ItemValueRef<'a> {
|
||||
match self {
|
||||
ItemValue::Text(text) => ItemValueRef::Text(text),
|
||||
ItemValue::Locator(locator) => ItemValueRef::Locator(locator),
|
||||
ItemValue::Binary(binary) => ItemValueRef::Binary(binary),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
||||
|
@ -517,8 +448,6 @@ pub struct TagItemFlags {
|
|||
pub struct TagItem {
|
||||
pub(crate) item_key: ItemKey,
|
||||
pub(crate) item_value: ItemValue,
|
||||
#[cfg(any(feature = "id3v2", feature = "ape"))]
|
||||
pub(crate) flags: TagItemFlags,
|
||||
}
|
||||
|
||||
impl TagItem {
|
||||
|
@ -534,10 +463,9 @@ impl TagItem {
|
|||
item_key: ItemKey,
|
||||
item_value: ItemValue,
|
||||
) -> Option<Self> {
|
||||
item_key.map_key(tag_type).is_some().then(|| Self {
|
||||
item_key.map_key(tag_type, false).is_some().then(|| Self {
|
||||
item_key,
|
||||
item_value,
|
||||
flags: TagItemFlags::default(),
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -546,22 +474,9 @@ impl TagItem {
|
|||
Self {
|
||||
item_key,
|
||||
item_value,
|
||||
flags: TagItemFlags::default(),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "id3v2", feature = "ape"))]
|
||||
/// Returns a reference to the [`TagItemFlags`]
|
||||
pub fn flags(&self) -> &TagItemFlags {
|
||||
&self.flags
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "id3v2", feature = "ape"))]
|
||||
/// Set the item's flags
|
||||
pub fn set_flags(&mut self, flags: TagItemFlags) {
|
||||
self.flags = flags
|
||||
}
|
||||
|
||||
/// Returns a reference to the [`ItemKey`]
|
||||
pub fn key(&self) -> &ItemKey {
|
||||
&self.item_key
|
||||
|
@ -573,13 +488,10 @@ impl TagItem {
|
|||
}
|
||||
|
||||
pub(crate) fn re_map(&self, tag_type: &TagType) -> Option<()> {
|
||||
#[cfg(any(feature = "id3v2", feature = "ape"))]
|
||||
{
|
||||
(!self.flags().read_only && self.item_key.map_key(tag_type).is_some()).then(|| ())
|
||||
}
|
||||
#[cfg(not(any(feature = "id3v2", feature = "ape")))]
|
||||
{
|
||||
self.item_key.map_key(tag_type).is_some().then(|| ())
|
||||
if tag_type == &TagType::Id3v1 {
|
||||
return VALID_ITEMKEYS.contains(&self.item_key).then(|| ());
|
||||
}
|
||||
|
||||
self.item_key.map_key(tag_type, false).is_some().then(|| ())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use crate::logic::id3::v2::util::text_utils::TextEncoding;
|
||||
use crate::logic::id3::v2::Id3v2Version;
|
||||
use crate::{LoftyError, Result};
|
||||
#[cfg(feature = "id3v2")]
|
||||
use {crate::logic::id3::v2::util::text_utils::TextEncoding, crate::logic::id3::v2::Id3v2Version};
|
||||
|
||||
use std::borrow::Cow;
|
||||
use std::io::{Cursor, Read};
|
||||
|
@ -10,8 +10,8 @@ use byteorder::WriteBytesExt;
|
|||
#[cfg(any(feature = "vorbis_comments", feature = "id3v2",))]
|
||||
use byteorder::{BigEndian, ReadBytesExt};
|
||||
|
||||
/// Mime types for covers.
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
/// Mime types for pictures.
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
|
||||
pub enum MimeType {
|
||||
/// PNG image
|
||||
Png,
|
||||
|
@ -74,7 +74,7 @@ impl MimeType {
|
|||
|
||||
/// The picture type
|
||||
#[allow(missing_docs)]
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
|
||||
pub enum PictureType {
|
||||
Other,
|
||||
Icon,
|
||||
|
@ -165,30 +165,30 @@ impl PictureType {
|
|||
|
||||
/// Get an APE item key from a PictureType
|
||||
#[cfg(feature = "ape")]
|
||||
pub fn as_ape_key(&self) -> &str {
|
||||
pub fn as_ape_key(&self) -> Option<&str> {
|
||||
match self {
|
||||
Self::Other => "Cover Art (Other)",
|
||||
Self::Icon => "Cover Art (Png Icon)",
|
||||
Self::OtherIcon => "Cover Art (Icon)",
|
||||
Self::CoverFront => "Cover Art (Front)",
|
||||
Self::CoverBack => "Cover Art (Back)",
|
||||
Self::Leaflet => "Cover Art (Leaflet)",
|
||||
Self::Media => "Cover Art (Media)",
|
||||
Self::LeadArtist => "Cover Art (Lead Artist)",
|
||||
Self::Artist => "Cover Art (Artist)",
|
||||
Self::Conductor => "Cover Art (Conductor)",
|
||||
Self::Band => "Cover Art (Band)",
|
||||
Self::Composer => "Cover Art (Composer)",
|
||||
Self::Lyricist => "Cover Art (Lyricist)",
|
||||
Self::RecordingLocation => "Cover Art (Recording Location)",
|
||||
Self::DuringRecording => "Cover Art (During Recording)",
|
||||
Self::DuringPerformance => "Cover Art (During Performance)",
|
||||
Self::ScreenCapture => "Cover Art (Video Capture)",
|
||||
Self::BrightFish => "Cover Art (Fish)",
|
||||
Self::Illustration => "Cover Art (Illustration)",
|
||||
Self::BandLogo => "Cover Art (Band Logotype)",
|
||||
Self::PublisherLogo => "Cover Art (Publisher Logotype)",
|
||||
Self::Undefined(_) => "",
|
||||
Self::Other => Some("Cover Art (Other)"),
|
||||
Self::Icon => Some("Cover Art (Png Icon)"),
|
||||
Self::OtherIcon => Some("Cover Art (Icon)"),
|
||||
Self::CoverFront => Some("Cover Art (Front)"),
|
||||
Self::CoverBack => Some("Cover Art (Back)"),
|
||||
Self::Leaflet => Some("Cover Art (Leaflet)"),
|
||||
Self::Media => Some("Cover Art (Media)"),
|
||||
Self::LeadArtist => Some("Cover Art (Lead Artist)"),
|
||||
Self::Artist => Some("Cover Art (Artist)"),
|
||||
Self::Conductor => Some("Cover Art (Conductor)"),
|
||||
Self::Band => Some("Cover Art (Band)"),
|
||||
Self::Composer => Some("Cover Art (Composer)"),
|
||||
Self::Lyricist => Some("Cover Art (Lyricist)"),
|
||||
Self::RecordingLocation => Some("Cover Art (Recording Location)"),
|
||||
Self::DuringRecording => Some("Cover Art (During Recording)"),
|
||||
Self::DuringPerformance => Some("Cover Art (During Performance)"),
|
||||
Self::ScreenCapture => Some("Cover Art (Video Capture)"),
|
||||
Self::BrightFish => Some("Cover Art (Fish)"),
|
||||
Self::Illustration => Some("Cover Art (Illustration)"),
|
||||
Self::BandLogo => Some("Cover Art (Band Logotype)"),
|
||||
Self::PublisherLogo => Some("Cover Art (Publisher Logotype)"),
|
||||
Self::Undefined(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -223,7 +223,7 @@ impl PictureType {
|
|||
}
|
||||
|
||||
/// Information about a [`Picture`]
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct PictureInformation {
|
||||
/// The picture's width in pixels
|
||||
pub width: u32,
|
||||
|
@ -236,10 +236,11 @@ pub struct PictureInformation {
|
|||
}
|
||||
|
||||
/// Represents a picture.
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct Picture {
|
||||
/// The picture type according to ID3v2 APIC
|
||||
pub pic_type: PictureType,
|
||||
#[cfg(feature = "id3v2")]
|
||||
/// **(ONLY APPLICABLE TO ID3v2)** The text encoding
|
||||
pub text_encoding: TextEncoding,
|
||||
/// The picture's mimetype
|
||||
|
@ -272,179 +273,123 @@ impl Picture {
|
|||
}
|
||||
|
||||
#[cfg(feature = "id3v2")]
|
||||
#[allow(clippy::single_match_else)]
|
||||
/// Convert a [`Picture`] to a ID3v2 A/PIC byte Vec
|
||||
///
|
||||
/// NOTE: This does not include the frame header
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// * Too much data was provided
|
||||
///
|
||||
/// ID3v2.2:
|
||||
///
|
||||
/// * The mimetype is not [`MimeType::Png`] or [`MimeType::Jpeg`]
|
||||
/// * Too much data was provided
|
||||
///
|
||||
/// ID3v2.3/4:
|
||||
///
|
||||
/// * Too much data was provided
|
||||
pub fn as_apic_bytes(&self, version: Id3v2Version) -> Result<Vec<u8>> {
|
||||
match version {
|
||||
Id3v2Version::V2 => {
|
||||
// ID3v2.2 PIC is pretty limited with formats
|
||||
let format = match self.mime_type {
|
||||
MimeType::Png => "PNG",
|
||||
MimeType::Jpeg => "JPG",
|
||||
_ => return Err(LoftyError::BadPictureFormat(self.mime_type.to_string())),
|
||||
};
|
||||
let mut data = vec![self.text_encoding as u8];
|
||||
|
||||
let mut data = vec![self.text_encoding as u8];
|
||||
let max_size = if version == Id3v2Version::V2 {
|
||||
// ID3v2.2 PIC is pretty limited with formats
|
||||
let format = match self.mime_type {
|
||||
MimeType::Png => "PNG",
|
||||
MimeType::Jpeg => "JPG",
|
||||
_ => return Err(LoftyError::BadPictureFormat(self.mime_type.to_string())),
|
||||
};
|
||||
|
||||
data.write_all(format.as_bytes())?;
|
||||
data.write_u8(self.pic_type.as_u8())?;
|
||||
data.write_all(format.as_bytes())?;
|
||||
|
||||
if let Some(description) = &self.description {
|
||||
data.write_all(&*crate::logic::id3::v2::util::text_utils::encode_text(
|
||||
description,
|
||||
self.text_encoding,
|
||||
true,
|
||||
))?;
|
||||
}
|
||||
// ID3v2.2 uses a 24-bit number for sizes
|
||||
0xFFFF_FF16_u64
|
||||
} else {
|
||||
data.write_all(self.mime_type.as_str().as_bytes())?;
|
||||
|
||||
data.write_u8(0)?;
|
||||
data.write_all(&*self.data)?;
|
||||
u64::from(u32::MAX)
|
||||
};
|
||||
|
||||
let size = data.len() - 6;
|
||||
data.write_u8(self.pic_type.as_u8())?;
|
||||
|
||||
if size as u64 > u64::from(u32::MAX) {
|
||||
return Err(LoftyError::TooMuchData);
|
||||
}
|
||||
|
||||
let size = (size as u32).to_be_bytes();
|
||||
|
||||
if size[0] != 0 {
|
||||
return Err(LoftyError::TooMuchData);
|
||||
}
|
||||
|
||||
Ok(data)
|
||||
}
|
||||
_ => {
|
||||
let mut data = vec![self.text_encoding as u8];
|
||||
|
||||
data.write_all(self.mime_type.as_str().as_bytes())?;
|
||||
data.write_u8(self.pic_type.as_u8())?;
|
||||
|
||||
if let Some(description) = &self.description {
|
||||
data.write_all(&*crate::logic::id3::v2::util::text_utils::encode_text(
|
||||
description,
|
||||
self.text_encoding,
|
||||
true,
|
||||
))?;
|
||||
}
|
||||
|
||||
data.write_u8(0)?;
|
||||
data.write_all(&*self.data)?;
|
||||
|
||||
let size = data.len();
|
||||
|
||||
if size as u64 > u64::from(u32::MAX) {
|
||||
return Err(LoftyError::TooMuchData);
|
||||
}
|
||||
|
||||
Ok(data)
|
||||
match &self.description {
|
||||
Some(description) => {
|
||||
data.write_all(&*crate::logic::id3::v2::util::text_utils::encode_text(
|
||||
description,
|
||||
self.text_encoding,
|
||||
true,
|
||||
))?
|
||||
}
|
||||
None => data.write_u8(0)?,
|
||||
}
|
||||
|
||||
data.write_all(&*self.data)?;
|
||||
|
||||
let size = data.len();
|
||||
|
||||
if size as u64 > max_size {
|
||||
return Err(LoftyError::TooMuchData);
|
||||
}
|
||||
|
||||
Ok(data)
|
||||
}
|
||||
|
||||
#[cfg(feature = "id3v2")]
|
||||
#[allow(clippy::single_match_else)]
|
||||
/// Get a [`Picture`] from ID3v2 A/PIC bytes:
|
||||
///
|
||||
/// NOTE: This expects the frame header to have already been skipped
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// This function will return [`NotAPicture`][LoftyError::NotAPicture] if at any point it's unable to parse the data
|
||||
/// * There isn't enough data present
|
||||
/// * The data isn't a picture
|
||||
///
|
||||
/// ID3v2.2:
|
||||
///
|
||||
/// * The format is not "PNG" or "JPG"
|
||||
pub fn from_apic_bytes(bytes: &[u8], version: Id3v2Version) -> Result<Self> {
|
||||
let mut cursor = Cursor::new(bytes);
|
||||
|
||||
if let Some(encoding) = TextEncoding::from_u8(cursor.read_u8()?) {
|
||||
return match version {
|
||||
Id3v2Version::V2 => {
|
||||
let mut format = [0; 3];
|
||||
cursor.read_exact(&mut format)?;
|
||||
let encoding = match TextEncoding::from_u8(cursor.read_u8()?) {
|
||||
Some(encoding) => encoding,
|
||||
None => return Err(LoftyError::NotAPicture),
|
||||
};
|
||||
|
||||
let mime_type = match format {
|
||||
[b'P', b'N', b'G'] => MimeType::Png,
|
||||
[b'J', b'P', b'G'] => MimeType::Jpeg,
|
||||
_ => {
|
||||
return Err(LoftyError::BadPictureFormat(
|
||||
String::from_utf8_lossy(&format).to_string(),
|
||||
))
|
||||
}
|
||||
};
|
||||
let mime_type = if version == Id3v2Version::V2 {
|
||||
let mut format = [0; 3];
|
||||
cursor.read_exact(&mut format)?;
|
||||
|
||||
let picture_type = PictureType::from_u8(cursor.read_u8()?);
|
||||
let description = crate::logic::id3::v2::util::text_utils::decode_text(
|
||||
&mut cursor,
|
||||
encoding,
|
||||
true,
|
||||
)?
|
||||
.map(Cow::from);
|
||||
|
||||
let mut data = Vec::new();
|
||||
cursor.read_to_end(&mut data)?;
|
||||
|
||||
Ok(Picture {
|
||||
pic_type: picture_type,
|
||||
text_encoding: encoding,
|
||||
mime_type,
|
||||
description,
|
||||
information: PictureInformation {
|
||||
width: 0,
|
||||
height: 0,
|
||||
color_depth: 0,
|
||||
num_colors: 0,
|
||||
},
|
||||
data: Cow::from(data),
|
||||
})
|
||||
}
|
||||
match format {
|
||||
[b'P', b'N', b'G'] => MimeType::Png,
|
||||
[b'J', b'P', b'G'] => MimeType::Jpeg,
|
||||
_ => {
|
||||
let mime_type = (crate::logic::id3::v2::util::text_utils::decode_text(
|
||||
&mut cursor,
|
||||
encoding,
|
||||
true,
|
||||
)?)
|
||||
.map_or(MimeType::None, |mime_type| MimeType::from_str(&*mime_type));
|
||||
|
||||
let picture_type = PictureType::from_u8(cursor.read_u8()?);
|
||||
let description = crate::logic::id3::v2::util::text_utils::decode_text(
|
||||
&mut cursor,
|
||||
encoding,
|
||||
true,
|
||||
)?
|
||||
.map(Cow::from);
|
||||
|
||||
let mut data = Vec::new();
|
||||
cursor.read_to_end(&mut data)?;
|
||||
|
||||
Ok(Picture {
|
||||
pic_type: picture_type,
|
||||
text_encoding: encoding,
|
||||
mime_type,
|
||||
description,
|
||||
information: PictureInformation {
|
||||
width: 0,
|
||||
height: 0,
|
||||
color_depth: 0,
|
||||
num_colors: 0,
|
||||
},
|
||||
data: Cow::from(data),
|
||||
})
|
||||
return Err(LoftyError::BadPictureFormat(
|
||||
String::from_utf8_lossy(&format).to_string(),
|
||||
))
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
} else {
|
||||
(crate::logic::id3::v2::util::text_utils::decode_text(&mut cursor, encoding, true)?)
|
||||
.map_or(MimeType::None, |mime_type| MimeType::from_str(&*mime_type))
|
||||
};
|
||||
|
||||
Err(LoftyError::NotAPicture)
|
||||
let picture_type = PictureType::from_u8(cursor.read_u8()?);
|
||||
|
||||
let description =
|
||||
crate::logic::id3::v2::util::text_utils::decode_text(&mut cursor, encoding, true)?
|
||||
.map(Cow::from);
|
||||
|
||||
let mut data = Vec::new();
|
||||
cursor.read_to_end(&mut data)?;
|
||||
|
||||
Ok(Picture {
|
||||
pic_type: picture_type,
|
||||
text_encoding: encoding,
|
||||
mime_type,
|
||||
description,
|
||||
information: PictureInformation {
|
||||
width: 0,
|
||||
height: 0,
|
||||
color_depth: 0,
|
||||
num_colors: 0,
|
||||
},
|
||||
data: Cow::from(data),
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(feature = "vorbis_comments")]
|
||||
|
@ -497,10 +442,7 @@ impl Picture {
|
|||
///
|
||||
/// This function will return [`NotAPicture`][LoftyError::NotAPicture] if at any point it's unable to parse the data
|
||||
pub fn from_flac_bytes(bytes: &[u8]) -> Result<Self> {
|
||||
let data = match base64::decode(bytes) {
|
||||
Ok(o) => o,
|
||||
Err(_) => bytes.to_vec(),
|
||||
};
|
||||
let data = base64::decode(bytes).unwrap_or_else(|_| bytes.to_vec());
|
||||
|
||||
let mut cursor = Cursor::new(data);
|
||||
|
||||
|
|
115
src/types/tag.rs
115
src/types/tag.rs
|
@ -1,17 +1,13 @@
|
|||
use super::item::{ItemKey, ItemValue, TagItem};
|
||||
use super::picture::{Picture, PictureType};
|
||||
use crate::error::{LoftyError, Result};
|
||||
#[cfg(feature = "id3v2_restrictions")]
|
||||
use crate::logic::id3::v2::items::restrictions::TagRestrictions;
|
||||
use crate::probe::Probe;
|
||||
|
||||
use std::fs::{File, OpenOptions};
|
||||
use std::path::Path;
|
||||
|
||||
#[cfg(feature = "quick_tag_accessors")]
|
||||
use paste::paste;
|
||||
|
||||
#[cfg(feature = "quick_tag_accessors")]
|
||||
macro_rules! common_items {
|
||||
($($item_key:ident => $name:tt),+) => {
|
||||
paste! {
|
||||
|
@ -41,38 +37,14 @@ macro_rules! common_items {
|
|||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "id3v2")]
|
||||
#[derive(Default, Copy, Clone)]
|
||||
#[allow(clippy::struct_excessive_bools)]
|
||||
/// **(ID3v2 ONLY)** Flags that apply to the entire tag
|
||||
pub struct TagFlags {
|
||||
/// Whether or not all frames are unsynchronised. See [`TagItemFlags::unsynchronisation`](crate::TagItemFlags::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),
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
/// Represents a parsed tag
|
||||
///
|
||||
/// NOTE: Items and pictures are separated
|
||||
pub struct Tag {
|
||||
tag_type: TagType,
|
||||
pictures: Vec<Picture>,
|
||||
items: Vec<TagItem>,
|
||||
#[cfg(feature = "id3v2")]
|
||||
flags: TagFlags,
|
||||
pub(crate) pictures: Vec<Picture>,
|
||||
pub(crate) items: Vec<TagItem>,
|
||||
}
|
||||
|
||||
impl IntoIterator for Tag {
|
||||
|
@ -84,33 +56,6 @@ impl IntoIterator for Tag {
|
|||
}
|
||||
}
|
||||
|
||||
impl Tag {
|
||||
/// The tag's items as a slice
|
||||
pub fn as_slice(&self) -> &[TagItem] {
|
||||
&*self.items
|
||||
}
|
||||
|
||||
/// Retain tag items based on the predicate
|
||||
///
|
||||
/// See [`Vec::retain`](std::vec::Vec::retain)
|
||||
pub fn retain<F>(&mut self, f: F)
|
||||
where
|
||||
F: FnMut(&TagItem) -> bool,
|
||||
{
|
||||
self.items.retain(f)
|
||||
}
|
||||
|
||||
/// Find the first TagItem matching the predicate
|
||||
///
|
||||
/// See [`Iterator::find`](std::iter::Iterator::find)
|
||||
pub fn find<P>(&mut self, predicate: P) -> Option<&TagItem>
|
||||
where
|
||||
P: for<'a> FnMut(&'a &TagItem) -> bool,
|
||||
{
|
||||
self.items.iter().find(predicate)
|
||||
}
|
||||
}
|
||||
|
||||
impl Tag {
|
||||
/// Initialize a new tag with a certain [`TagType`]
|
||||
pub fn new(tag_type: TagType) -> Self {
|
||||
|
@ -118,15 +63,6 @@ impl Tag {
|
|||
tag_type,
|
||||
pictures: vec![],
|
||||
items: vec![],
|
||||
flags: TagFlags::default(),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "id3v2")]
|
||||
/// **(ID3v2 ONLY)** Restrict the tag's flags
|
||||
pub fn set_flags(&mut self, flags: TagFlags) {
|
||||
if TagType::Id3v2 == self.tag_type {
|
||||
self.flags = flags
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -154,12 +90,6 @@ impl Tag {
|
|||
pub fn item_count(&self) -> u32 {
|
||||
self.items.len() as u32
|
||||
}
|
||||
|
||||
#[cfg(feature = "id3v2")]
|
||||
/// Returns the [`TagFlags`]
|
||||
pub fn flags(&self) -> &TagFlags {
|
||||
&self.flags
|
||||
}
|
||||
}
|
||||
|
||||
impl Tag {
|
||||
|
@ -225,10 +155,7 @@ impl Tag {
|
|||
|
||||
/// Insert a [`TagItem`], replacing any existing one of the same type
|
||||
///
|
||||
/// NOTES:
|
||||
///
|
||||
/// * This **will** respect [`TagItemFlags::read_only`](crate::TagItemFlags::read_only)
|
||||
/// * This **will** verify an [`ItemKey`] mapping exists for the target [`TagType`]
|
||||
/// NOTE: This **will** verify an [`ItemKey`] mapping exists for the target [`TagType`]
|
||||
///
|
||||
/// # Warning
|
||||
///
|
||||
|
@ -247,17 +174,21 @@ impl Tag {
|
|||
///
|
||||
/// Notes:
|
||||
///
|
||||
/// * This **will not** respect [`TagItemFlags::read_only`](crate::TagItemFlags::read_only)
|
||||
/// * This **will not** verify an [`ItemKey`] mapping exists
|
||||
/// * This **will not** allow writing item keys that are out of spec (keys are verified before writing)
|
||||
///
|
||||
/// This is only necessary if using [`ItemKey::Unknown`] or single [`ItemKey`]s that are parts of larger lists.
|
||||
/// This is only necessary if dealing with [`ItemKey::Unknown`].
|
||||
pub fn insert_item_unchecked(&mut self, item: TagItem) {
|
||||
match self.items.iter_mut().find(|i| i.item_key == item.item_key) {
|
||||
None => self.items.push(item),
|
||||
Some(i) => *i = item,
|
||||
};
|
||||
}
|
||||
|
||||
/// An alias for [`Tag::insert_item`] that doesn't require the user to create a [`TagItem`]
|
||||
pub fn insert_text(&mut self, item_key: ItemKey, text: String) -> bool {
|
||||
self.insert_item(TagItem::new(item_key, ItemValue::Text(text)))
|
||||
}
|
||||
}
|
||||
|
||||
impl Tag {
|
||||
|
@ -302,7 +233,33 @@ impl Tag {
|
|||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "quick_tag_accessors")]
|
||||
impl Tag {
|
||||
/// The tag's items as a slice
|
||||
pub fn as_slice(&self) -> &[TagItem] {
|
||||
&*self.items
|
||||
}
|
||||
|
||||
/// Retain tag items based on the predicate
|
||||
///
|
||||
/// See [`Vec::retain`](std::vec::Vec::retain)
|
||||
pub fn retain<F>(&mut self, f: F)
|
||||
where
|
||||
F: FnMut(&TagItem) -> bool,
|
||||
{
|
||||
self.items.retain(f)
|
||||
}
|
||||
|
||||
/// Find the first TagItem matching the predicate
|
||||
///
|
||||
/// See [`Iterator::find`](std::iter::Iterator::find)
|
||||
pub fn find<P>(&mut self, predicate: P) -> Option<&TagItem>
|
||||
where
|
||||
P: for<'a> FnMut(&'a &TagItem) -> bool,
|
||||
{
|
||||
self.items.iter().find(predicate)
|
||||
}
|
||||
}
|
||||
|
||||
common_items!(TrackArtist => artist, TrackTitle => title, AlbumTitle => album_title, AlbumArtist => album_artist);
|
||||
|
||||
/// The tag's format
|
||||
|
|
|
@ -1,14 +1,12 @@
|
|||
mod util;
|
||||
|
||||
use lofty::{FileType, ItemKey, ItemValue, Probe, TagItem, TagType};
|
||||
use std::io::Write;
|
||||
use std::io::{Seek, Write};
|
||||
|
||||
#[test]
|
||||
fn read() {
|
||||
// Here we have an AIFF file with both an ID3v2 chunk and text chunks
|
||||
let file = Probe::new()
|
||||
.read_from_path("tests/assets/a_mixed.aiff")
|
||||
.unwrap();
|
||||
let file = Probe::new().read_from_path("tests/assets/a.aiff").unwrap();
|
||||
|
||||
assert_eq!(file.file_type(), &FileType::AIFF);
|
||||
|
||||
|
@ -21,10 +19,8 @@ fn read() {
|
|||
|
||||
#[test]
|
||||
fn write() {
|
||||
let mut file = std::fs::OpenOptions::new()
|
||||
.read(true)
|
||||
.write(true)
|
||||
.open("tests/assets/a_mixed.aiff")
|
||||
let mut file = tempfile::tempfile().unwrap();
|
||||
file.write_all(&std::fs::read("tests/assets/a.aiff").unwrap())
|
||||
.unwrap();
|
||||
|
||||
let mut tagged_file = Probe::new().read_from(&mut file).unwrap();
|
||||
|
@ -37,6 +33,8 @@ fn write() {
|
|||
// Text chunks
|
||||
crate::set_artist!(tagged_file, tag_mut, TagType::AiffText, "Bar artist", 1 => file, "Baz artist");
|
||||
|
||||
drop(tagged_file);
|
||||
|
||||
// Now reread the file
|
||||
let mut tagged_file = Probe::new().read_from(&mut file).unwrap();
|
||||
|
||||
|
@ -47,10 +45,10 @@ fn write() {
|
|||
|
||||
#[test]
|
||||
fn remove_text_chunks() {
|
||||
crate::remove_tag!("tests/assets/a_mixed.aiff", TagType::AiffText);
|
||||
crate::remove_tag!("tests/assets/a.aiff", TagType::AiffText);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remove_id3v2() {
|
||||
crate::remove_tag!("tests/assets/a_mixed.aiff", TagType::Id3v2);
|
||||
crate::remove_tag!("tests/assets/a.aiff", TagType::Id3v2);
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
mod util;
|
||||
|
||||
use lofty::{FileType, ItemKey, ItemValue, Probe, TagItem, TagType};
|
||||
use std::io::Write;
|
||||
use std::io::{Seek, Write};
|
||||
|
||||
#[test]
|
||||
fn read() {
|
||||
|
@ -23,11 +23,8 @@ fn read() {
|
|||
#[test]
|
||||
fn write() {
|
||||
// We don't write an ID3v2 tag here since it's against the spec
|
||||
|
||||
let mut file = std::fs::OpenOptions::new()
|
||||
.read(true)
|
||||
.write(true)
|
||||
.open("tests/assets/a.ape")
|
||||
let mut file = tempfile::tempfile().unwrap();
|
||||
file.write_all(&std::fs::read("tests/assets/a.ape").unwrap())
|
||||
.unwrap();
|
||||
|
||||
let mut tagged_file = Probe::new().read_from(&mut file).unwrap();
|
||||
|
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -1,7 +1,7 @@
|
|||
mod util;
|
||||
|
||||
use lofty::{FileType, ItemKey, ItemValue, Probe, TagItem, TagType};
|
||||
use std::io::Write;
|
||||
use std::io::{Seek, Write};
|
||||
|
||||
#[test]
|
||||
fn read() {
|
||||
|
@ -16,10 +16,8 @@ fn read() {
|
|||
|
||||
#[test]
|
||||
fn write() {
|
||||
let mut file = std::fs::OpenOptions::new()
|
||||
.read(true)
|
||||
.write(true)
|
||||
.open("tests/assets/a.m4a")
|
||||
let mut file = tempfile::tempfile().unwrap();
|
||||
file.write_all(&std::fs::read("tests/assets/a.m4a").unwrap())
|
||||
.unwrap();
|
||||
|
||||
let mut tagged_file = Probe::new().read_from(&mut file).unwrap();
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
mod util;
|
||||
|
||||
use lofty::{FileType, ItemKey, ItemValue, Probe, TagItem, TagType};
|
||||
use std::io::Write;
|
||||
use std::io::{Seek, Write};
|
||||
|
||||
#[test]
|
||||
fn read() {
|
||||
|
@ -22,10 +22,8 @@ fn read() {
|
|||
|
||||
#[test]
|
||||
fn write() {
|
||||
let mut file = std::fs::OpenOptions::new()
|
||||
.read(true)
|
||||
.write(true)
|
||||
.open("tests/assets/a.mp3")
|
||||
let mut file = tempfile::tempfile().unwrap();
|
||||
file.write_all(&std::fs::read("tests/assets/a.mp3").unwrap())
|
||||
.unwrap();
|
||||
|
||||
let mut tagged_file = Probe::new().read_from(&mut file).unwrap();
|
||||
|
|
11
tests/ogg.rs
11
tests/ogg.rs
|
@ -1,7 +1,7 @@
|
|||
mod util;
|
||||
|
||||
use lofty::{FileType, ItemKey, ItemValue, Probe, TagItem, TagType};
|
||||
use std::io::Write;
|
||||
use std::io::{Seek, Write};
|
||||
|
||||
// The tests for OGG Opus/Vorbis are nearly identical
|
||||
// We have the vendor string and a title stored in the tag
|
||||
|
@ -61,11 +61,8 @@ fn read(path: &str, file_type: &FileType) {
|
|||
}
|
||||
|
||||
fn write(path: &str, file_type: &FileType) {
|
||||
let mut file = std::fs::OpenOptions::new()
|
||||
.read(true)
|
||||
.write(true)
|
||||
.open(path)
|
||||
.unwrap();
|
||||
let mut file = tempfile::tempfile().unwrap();
|
||||
file.write_all(&std::fs::read(path).unwrap()).unwrap();
|
||||
|
||||
let mut tagged_file = Probe::new().read_from(&mut file).unwrap();
|
||||
|
||||
|
@ -73,6 +70,8 @@ fn write(path: &str, file_type: &FileType) {
|
|||
|
||||
crate::set_artist!(tagged_file, primary_tag_mut, "Foo artist", 2 => file, "Bar artist");
|
||||
|
||||
drop(tagged_file);
|
||||
|
||||
// Now reread the file
|
||||
let mut tagged_file = Probe::new().read_from(&mut file).unwrap();
|
||||
|
||||
|
|
|
@ -70,6 +70,8 @@ macro_rules! remove_tag {
|
|||
|
||||
assert!($tag_type.remove_from(&mut file));
|
||||
|
||||
file.seek(std::io::SeekFrom::Start(0)).unwrap();
|
||||
|
||||
let tagged_file = Probe::new().read_from(&mut file).unwrap();
|
||||
assert!(tagged_file.tag(&$tag_type).is_none());
|
||||
};
|
||||
|
|
10
tests/wav.rs
10
tests/wav.rs
|
@ -1,7 +1,7 @@
|
|||
mod util;
|
||||
|
||||
use lofty::{FileType, ItemKey, ItemValue, Probe, TagItem, TagType};
|
||||
use std::io::Write;
|
||||
use std::io::{Seek, Write};
|
||||
|
||||
#[test]
|
||||
fn read() {
|
||||
|
@ -21,10 +21,8 @@ fn read() {
|
|||
|
||||
#[test]
|
||||
fn write() {
|
||||
let mut file = std::fs::OpenOptions::new()
|
||||
.read(true)
|
||||
.write(true)
|
||||
.open("tests/assets/a_mixed.wav")
|
||||
let mut file = tempfile::tempfile().unwrap();
|
||||
file.write_all(&std::fs::read("tests/assets/a_mixed.wav").unwrap())
|
||||
.unwrap();
|
||||
|
||||
let mut tagged_file = Probe::new().read_from(&mut file).unwrap();
|
||||
|
@ -37,6 +35,8 @@ fn write() {
|
|||
// RIFF INFO
|
||||
crate::set_artist!(tagged_file, tag_mut, TagType::RiffInfo, "Bar artist", 1 => file, "Baz artist");
|
||||
|
||||
drop(tagged_file);
|
||||
|
||||
// Now reread the file
|
||||
let mut tagged_file = Probe::new().read_from(&mut file).unwrap();
|
||||
|
||||
|
|
Loading…
Reference in a new issue