mirror of
https://github.com/Serial-ATA/lofty-rs
synced 2024-11-10 06:34:18 +00:00
MP4: Support atoms with multiple values
This commit is contained in:
parent
3788a436af
commit
b5478d1f1d
9 changed files with 426 additions and 249 deletions
|
@ -6,6 +6,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
- **MP4**:
|
||||
- Support atoms with multiple values ([issue](https://github.com/Serial-ATA/lofty-rs/issues/48))
|
||||
- `Atom::from_collection`
|
||||
|
||||
### Changed
|
||||
- **ID3v2**: Discard empty frames, rather than error
|
||||
- **APE**: Allow empty tag items
|
||||
|
|
|
@ -162,7 +162,8 @@
|
|||
clippy::similar_names,
|
||||
clippy::tabs_in_doc_comments,
|
||||
clippy::len_without_is_empty,
|
||||
clippy::needless_late_init
|
||||
clippy::needless_late_init,
|
||||
clippy::type_complexity
|
||||
)]
|
||||
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
||||
|
||||
|
|
|
@ -1,17 +1,107 @@
|
|||
use crate::mp4::AtomIdent;
|
||||
use crate::picture::Picture;
|
||||
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
use std::fmt::{Debug, Formatter};
|
||||
|
||||
// Atoms with multiple values aren't all that common,
|
||||
// so there's no need to create a bunch of single-element Vecs
|
||||
#[derive(PartialEq, Clone)]
|
||||
pub(super) enum AtomDataStorage {
|
||||
Single(AtomData),
|
||||
Multiple(Vec<AtomData>),
|
||||
}
|
||||
|
||||
impl AtomDataStorage {
|
||||
pub(super) fn take_first(self) -> AtomData {
|
||||
match self {
|
||||
AtomDataStorage::Single(val) => val,
|
||||
AtomDataStorage::Multiple(mut data) => data.swap_remove(0),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for AtomDataStorage {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match &self {
|
||||
AtomDataStorage::Single(v) => write!(f, "{:?}", v),
|
||||
AtomDataStorage::Multiple(v) => f.debug_list().entries(v.iter()).finish(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> IntoIterator for &'a AtomDataStorage {
|
||||
type Item = &'a AtomData;
|
||||
type IntoIter = AtomDataStorageIter<'a>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
let cap = match self {
|
||||
AtomDataStorage::Single(_) => 0,
|
||||
AtomDataStorage::Multiple(v) => v.len(),
|
||||
};
|
||||
|
||||
Self::IntoIter {
|
||||
storage: Some(self),
|
||||
idx: 0,
|
||||
cap,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) struct AtomDataStorageIter<'a> {
|
||||
storage: Option<&'a AtomDataStorage>,
|
||||
idx: usize,
|
||||
cap: usize,
|
||||
}
|
||||
|
||||
impl<'a> Iterator for AtomDataStorageIter<'a> {
|
||||
type Item = &'a AtomData;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
match self.storage {
|
||||
Some(AtomDataStorage::Single(data)) => {
|
||||
self.storage = None;
|
||||
Some(data)
|
||||
},
|
||||
Some(AtomDataStorage::Multiple(data)) => {
|
||||
if self.idx == self.cap {
|
||||
self.storage = None;
|
||||
}
|
||||
|
||||
self.idx += 1;
|
||||
Some(&data[self.idx])
|
||||
},
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Clone)]
|
||||
/// Represents an `MP4` atom
|
||||
pub struct Atom {
|
||||
pub(crate) ident: AtomIdent,
|
||||
pub(crate) data: AtomData,
|
||||
pub(super) data: AtomDataStorage,
|
||||
}
|
||||
|
||||
impl Atom {
|
||||
/// Create a new [`Atom`]
|
||||
pub fn new(ident: AtomIdent, data: AtomData) -> Self {
|
||||
Self { ident, data }
|
||||
Self {
|
||||
ident,
|
||||
data: AtomDataStorage::Single(data),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new [`Atom`] from a collection of [`AtomData`]s
|
||||
///
|
||||
/// This will return `None` if `data` is empty, as empty atoms are useless.
|
||||
pub fn from_collection(ident: AtomIdent, mut data: Vec<AtomData>) -> Option<Self> {
|
||||
let data = match data.len() {
|
||||
0 => return None,
|
||||
1 => AtomDataStorage::Single(data.swap_remove(0)),
|
||||
_ => AtomDataStorage::Multiple(data),
|
||||
};
|
||||
|
||||
Some(Self { ident, data })
|
||||
}
|
||||
|
||||
/// Returns the atom's [`AtomIdent`]
|
||||
|
@ -20,8 +110,24 @@ impl Atom {
|
|||
}
|
||||
|
||||
/// Returns the atom's [`AtomData`]
|
||||
// TODO: Do this properly to return all values
|
||||
pub fn data(&self) -> &AtomData {
|
||||
&self.data
|
||||
match &self.data {
|
||||
AtomDataStorage::Single(val) => val,
|
||||
// There must be at least 1 element in here
|
||||
AtomDataStorage::Multiple(data) => &data[0],
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: push_data
|
||||
}
|
||||
|
||||
impl Debug for Atom {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("Atom")
|
||||
.field("ident", &self.ident)
|
||||
.field("data", &self.data)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -105,65 +211,3 @@ impl From<u8> for AdvisoryRating {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct AtomRef<'a> {
|
||||
pub(crate) ident: AtomIdentRef<'a>,
|
||||
pub(crate) 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 },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,17 +1,19 @@
|
|||
pub(super) mod atom;
|
||||
pub(super) mod constants;
|
||||
pub(super) mod read;
|
||||
mod r#ref;
|
||||
pub(crate) mod write;
|
||||
|
||||
use super::AtomIdent;
|
||||
use crate::error::{LoftyError, Result};
|
||||
use crate::error::LoftyError;
|
||||
use crate::mp4::ilst::atom::AtomDataStorage;
|
||||
use crate::picture::{Picture, PictureType};
|
||||
use crate::tag::item::{ItemKey, ItemValue, TagItem};
|
||||
use crate::tag::{Tag, TagType};
|
||||
use crate::traits::{Accessor, TagExt};
|
||||
use atom::{AdvisoryRating, Atom, AtomData, AtomDataRef, AtomIdentRef, AtomRef};
|
||||
use atom::{AdvisoryRating, Atom, AtomData};
|
||||
use r#ref::AtomIdentRef;
|
||||
|
||||
use std::convert::TryInto;
|
||||
use std::fs::{File, OpenOptions};
|
||||
use std::io::Write;
|
||||
use std::path::Path;
|
||||
|
@ -39,7 +41,7 @@ macro_rules! impl_accessor {
|
|||
fn [<set_ $name>](&mut self, value: String) {
|
||||
self.replace_atom(Atom {
|
||||
ident: $const,
|
||||
data: AtomData::UTF8(value),
|
||||
data: AtomDataStorage::Single(AtomData::UTF8(value)),
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -134,11 +136,14 @@ impl Ilst {
|
|||
pub fn pictures(&self) -> impl Iterator<Item = &Picture> {
|
||||
const COVR: AtomIdent = AtomIdent::Fourcc(*b"covr");
|
||||
|
||||
self.atoms.iter().filter_map(|a| match a {
|
||||
Atom {
|
||||
ident: COVR,
|
||||
data: AtomData::Picture(pic),
|
||||
} => Some(pic),
|
||||
self.atoms.iter().filter_map(|a| match a.ident {
|
||||
COVR => {
|
||||
if let AtomData::Picture(pic) = a.data() {
|
||||
Some(pic)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
},
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
|
@ -150,7 +155,7 @@ impl Ilst {
|
|||
|
||||
self.atoms.push(Atom {
|
||||
ident: AtomIdent::Fourcc(*b"covr"),
|
||||
data: AtomData::Picture(picture),
|
||||
data: AtomDataStorage::Single(AtomData::Picture(picture)),
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -162,8 +167,8 @@ impl Ilst {
|
|||
|
||||
/// Returns the parental advisory rating according to the `rtng` atom
|
||||
pub fn advisory_rating(&self) -> Option<AdvisoryRating> {
|
||||
if let Some(Atom { data, .. }) = self.atom(&AtomIdent::Fourcc(*b"rtng")) {
|
||||
let rating = match data {
|
||||
if let Some(atom) = self.atom(&AtomIdent::Fourcc(*b"rtng")) {
|
||||
let rating = match atom.data() {
|
||||
AtomData::SignedInteger(si) => *si as u8,
|
||||
AtomData::Unknown { data: c, .. } if !c.is_empty() => c[0],
|
||||
_ => return None,
|
||||
|
@ -181,7 +186,7 @@ impl Ilst {
|
|||
|
||||
self.replace_atom(Atom {
|
||||
ident: AtomIdent::Fourcc(*b"rtng"),
|
||||
data: AtomData::SignedInteger(i32::from(byte)),
|
||||
data: AtomDataStorage::Single(AtomData::SignedInteger(i32::from(byte))),
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -236,11 +241,11 @@ impl TagExt for Ilst {
|
|||
}
|
||||
|
||||
fn save_to(&self, file: &mut File) -> std::result::Result<(), Self::Err> {
|
||||
Into::<IlstRef<'_>>::into(self).write_to(file)
|
||||
self.as_ref().write_to(file)
|
||||
}
|
||||
|
||||
fn dump_to<W: Write>(&self, writer: &mut W) -> std::result::Result<(), Self::Err> {
|
||||
Into::<IlstRef<'_>>::into(self).dump_to(writer)
|
||||
self.as_ref().dump_to(writer)
|
||||
}
|
||||
|
||||
fn remove_from_path<P: AsRef<Path>>(&self, path: P) -> std::result::Result<(), Self::Err> {
|
||||
|
@ -261,7 +266,8 @@ impl From<Ilst> for Tag {
|
|||
let mut tag = Self::new(TagType::Mp4Ilst);
|
||||
|
||||
for atom in input.atoms {
|
||||
let value = match atom.data {
|
||||
let Atom { ident, data } = atom;
|
||||
let value = match data.take_first() {
|
||||
AtomData::UTF8(text) | AtomData::UTF16(text) => ItemValue::Text(text),
|
||||
AtomData::Picture(pic) => {
|
||||
tag.pictures.push(pic);
|
||||
|
@ -269,7 +275,7 @@ impl From<Ilst> for Tag {
|
|||
},
|
||||
// We have to special case track/disc numbers since they are stored together
|
||||
AtomData::Unknown { code: 0, data } if data.len() >= 6 => {
|
||||
if let AtomIdent::Fourcc(ref fourcc) = atom.ident {
|
||||
if let AtomIdent::Fourcc(ref fourcc) = ident {
|
||||
match fourcc {
|
||||
b"trkn" => {
|
||||
let current = u16::from_be_bytes([data[2], data[3]]);
|
||||
|
@ -296,7 +302,7 @@ impl From<Ilst> for Tag {
|
|||
|
||||
let key = ItemKey::from_key(
|
||||
TagType::Mp4Ilst,
|
||||
&match atom.ident {
|
||||
&match ident {
|
||||
AtomIdent::Fourcc(fourcc) => {
|
||||
fourcc.iter().map(|b| *b as char).collect::<String>()
|
||||
},
|
||||
|
@ -330,10 +336,10 @@ impl From<Tag> for Ilst {
|
|||
|
||||
tag.atoms.push(Atom {
|
||||
ident: AtomIdent::Fourcc(ident),
|
||||
data: AtomData::Unknown {
|
||||
data: AtomDataStorage::Single(AtomData::Unknown {
|
||||
code: 0,
|
||||
data: vec![0, 0, current[0], current[1], total[0], total[1], 0, 0],
|
||||
},
|
||||
}),
|
||||
})
|
||||
},
|
||||
}
|
||||
|
@ -361,7 +367,7 @@ impl From<Tag> for Ilst {
|
|||
ItemKey::DiscTotal => convert_to_uint(&mut discs.1, data.as_str()),
|
||||
_ => ilst.atoms.push(Atom {
|
||||
ident,
|
||||
data: AtomData::UTF8(data),
|
||||
data: AtomDataStorage::Single(AtomData::UTF8(data)),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
@ -374,7 +380,7 @@ impl From<Tag> for Ilst {
|
|||
|
||||
ilst.atoms.push(Atom {
|
||||
ident: AtomIdent::Fourcc([b'c', b'o', b'v', b'r']),
|
||||
data: AtomData::Picture(picture),
|
||||
data: AtomDataStorage::Single(AtomData::Picture(picture)),
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -385,50 +391,6 @@ impl From<Tag> for Ilst {
|
|||
}
|
||||
}
|
||||
|
||||
pub(crate) struct IlstRef<'a> {
|
||||
atoms: Box<dyn Iterator<Item = AtomRef<'a>> + 'a>,
|
||||
}
|
||||
|
||||
impl<'a> IlstRef<'a> {
|
||||
pub(crate) fn write_to(&mut self, file: &mut File) -> Result<()> {
|
||||
write::write_to(file, self)
|
||||
}
|
||||
|
||||
pub(crate) fn dump_to<W: Write>(&mut self, writer: &mut W) -> Result<()> {
|
||||
let temp = write::build_ilst(&mut self.atoms)?;
|
||||
writer.write_all(&*temp)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Into<IlstRef<'a>> for &'a Ilst {
|
||||
fn into(self) -> IlstRef<'a> {
|
||||
IlstRef {
|
||||
atoms: Box::new(self.atoms.iter().map(Into::into)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Into<IlstRef<'a>> for &'a Tag {
|
||||
fn into(self) -> IlstRef<'a> {
|
||||
let iter =
|
||||
self.items
|
||||
.iter()
|
||||
.filter_map(|i| match (item_key_to_ident(i.key()), i.value()) {
|
||||
(Some(ident), ItemValue::Text(text)) => Some(AtomRef {
|
||||
ident,
|
||||
data: AtomDataRef::UTF8(text),
|
||||
}),
|
||||
_ => None,
|
||||
});
|
||||
|
||||
IlstRef {
|
||||
atoms: Box::new(iter),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn item_key_to_ident(key: &ItemKey) -> Option<AtomIdentRef<'_>> {
|
||||
key.map_key(TagType::Mp4Ilst, true).and_then(|ident| {
|
||||
if ident.starts_with("----") {
|
||||
|
@ -458,6 +420,7 @@ fn item_key_to_ident(key: &ItemKey) -> Option<AtomIdentRef<'_>> {
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::mp4::ilst::atom::AtomDataStorage;
|
||||
use crate::mp4::{AdvisoryRating, Atom, AtomData, AtomIdent, Ilst, Mp4File};
|
||||
use crate::tag::utils::test_utils::read_path;
|
||||
use crate::{Accessor, AudioFile, ItemKey, Tag, TagExt, TagType};
|
||||
|
@ -717,7 +680,7 @@ mod tests {
|
|||
let mut tag = Ilst::default();
|
||||
tag.insert_atom(Atom {
|
||||
ident: AtomIdent::Fourcc(*b"\xa9ART"),
|
||||
data: AtomData::UTF8(String::from("Foo artist")),
|
||||
data: AtomDataStorage::Single(AtomData::UTF8(String::from("Foo artist"))),
|
||||
});
|
||||
|
||||
assert!(tag.save_to(&mut file).is_ok());
|
||||
|
@ -732,4 +695,25 @@ mod tests {
|
|||
&AtomData::UTF8(String::from("Foo artist")),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multi_value_atom() {
|
||||
let ilst = read_ilst("tests/tags/assets/ilst/multi_value_atom.ilst");
|
||||
let artist_atom = ilst.atom(&AtomIdent::Fourcc(*b"\xa9ART")).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
artist_atom.data,
|
||||
AtomDataStorage::Multiple(vec![
|
||||
AtomData::UTF8(String::from("Foo artist")),
|
||||
AtomData::UTF8(String::from("Bar artist")),
|
||||
])
|
||||
);
|
||||
|
||||
// Sanity single value atom
|
||||
verify_atom(
|
||||
&ilst,
|
||||
*b"\xa9gen",
|
||||
&AtomData::UTF8(String::from("Classical")),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,6 +14,7 @@ use crate::picture::{MimeType, Picture, PictureType};
|
|||
use std::borrow::Cow;
|
||||
use std::io::{Cursor, Read, Seek, SeekFrom};
|
||||
|
||||
use crate::mp4::ilst::atom::AtomDataStorage;
|
||||
use byteorder::ReadBytesExt;
|
||||
|
||||
pub(in crate::mp4) fn parse_ilst<R>(reader: &mut R, len: u64) -> Result<Ilst>
|
||||
|
@ -28,35 +29,34 @@ where
|
|||
let mut tag = Ilst::default();
|
||||
|
||||
while let Ok(atom) = AtomInfo::read(&mut cursor) {
|
||||
let ident = match atom.ident {
|
||||
AtomIdent::Fourcc(ref fourcc) => match fourcc {
|
||||
if let AtomIdent::Fourcc(ref fourcc) = atom.ident {
|
||||
match fourcc {
|
||||
b"free" | b"skip" => {
|
||||
skip_unneeded(&mut cursor, atom.extended, atom.len)?;
|
||||
continue;
|
||||
},
|
||||
b"covr" => {
|
||||
handle_covr(&mut cursor, &mut tag)?;
|
||||
handle_covr(&mut cursor, &mut tag, &atom)?;
|
||||
continue;
|
||||
},
|
||||
// Upgrade this to a \xa9gen atom
|
||||
b"gnre" => {
|
||||
let content = parse_data(&mut cursor)?;
|
||||
if let Some(atom_data) = parse_data_inner(&mut cursor, &atom)? {
|
||||
let mut data = Vec::new();
|
||||
|
||||
if let Some(AtomData::Unknown {
|
||||
code: BE_UNSIGNED_INTEGER | 0,
|
||||
data,
|
||||
}) = content
|
||||
{
|
||||
if data.len() >= 2 {
|
||||
let index = data[1] as usize;
|
||||
|
||||
if index > 0 && index <= GENRES.len() {
|
||||
tag.atoms.push(Atom {
|
||||
ident: AtomIdent::Fourcc(*b"\xa9gen"),
|
||||
data: AtomData::UTF8(String::from(GENRES[index - 1])),
|
||||
})
|
||||
for (flags, content) in atom_data {
|
||||
if (flags == BE_SIGNED_INTEGER || flags == 0) && content.len() >= 2 {
|
||||
let index = content[1] as usize;
|
||||
if index > 0 && index <= GENRES.len() {
|
||||
data.push(AtomData::UTF8(String::from(GENRES[index - 1])));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tag.atoms.push(Atom {
|
||||
ident: AtomIdent::Fourcc(*b"\xa9gen"),
|
||||
data: AtomDataStorage::Multiple(data),
|
||||
})
|
||||
}
|
||||
|
||||
continue;
|
||||
|
@ -64,94 +64,117 @@ where
|
|||
// Special case the "Album ID", as it has the code "BE signed integer" (21), but
|
||||
// must be interpreted as a "BE 64-bit Signed Integer" (74)
|
||||
b"plID" => {
|
||||
if let Some((code, content)) = parse_data_inner(&mut cursor)? {
|
||||
if (code == BE_SIGNED_INTEGER || code == BE_64BIT_SIGNED_INTEGER)
|
||||
&& content.len() == 8
|
||||
{
|
||||
tag.atoms.push(Atom {
|
||||
ident: AtomIdent::Fourcc(*b"plID"),
|
||||
data: AtomData::Unknown {
|
||||
if let Some(atom_data) = parse_data_inner(&mut cursor, &atom)? {
|
||||
let mut data = Vec::new();
|
||||
|
||||
for (code, content) in atom_data {
|
||||
if (code == BE_SIGNED_INTEGER || code == BE_64BIT_SIGNED_INTEGER)
|
||||
&& content.len() == 8
|
||||
{
|
||||
data.push(AtomData::Unknown {
|
||||
code,
|
||||
data: content,
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
tag.atoms.push(Atom {
|
||||
ident: AtomIdent::Fourcc(*b"plID"),
|
||||
data: AtomDataStorage::Multiple(data),
|
||||
})
|
||||
}
|
||||
|
||||
continue;
|
||||
},
|
||||
_ => atom.ident,
|
||||
},
|
||||
ident => ident,
|
||||
};
|
||||
|
||||
if let Some(data) = parse_data(&mut cursor)? {
|
||||
tag.atoms.push(Atom { ident, data })
|
||||
_ => {},
|
||||
}
|
||||
}
|
||||
|
||||
parse_data(&mut cursor, &mut tag, atom)?;
|
||||
}
|
||||
|
||||
Ok(tag)
|
||||
}
|
||||
|
||||
fn parse_data<R>(data: &mut R) -> Result<Option<AtomData>>
|
||||
fn parse_data<R>(data: &mut R, tag: &mut Ilst, atom_info: AtomInfo) -> Result<()>
|
||||
where
|
||||
R: Read + Seek,
|
||||
{
|
||||
if let Some((flags, content)) = parse_data_inner(data)? {
|
||||
// https://developer.apple.com/library/archive/documentation/QuickTime/QTFF/Metadata/Metadata.html#//apple_ref/doc/uid/TP40000939-CH1-SW35
|
||||
let value = match flags {
|
||||
UTF8 => AtomData::UTF8(String::from_utf8(content)?),
|
||||
UTF16 => AtomData::UTF16(utf16_decode(&*content, u16::from_be_bytes)?),
|
||||
BE_SIGNED_INTEGER => AtomData::SignedInteger(parse_int(&content)?),
|
||||
BE_UNSIGNED_INTEGER => AtomData::UnsignedInteger(parse_uint(&content)?),
|
||||
code => AtomData::Unknown {
|
||||
code,
|
||||
data: content,
|
||||
},
|
||||
};
|
||||
if let Some(mut atom_data) = parse_data_inner(data, &atom_info)? {
|
||||
// Most atoms we encounter are only going to have 1 value, so store them as such
|
||||
if atom_data.len() == 1 {
|
||||
let (flags, content) = atom_data.remove(0);
|
||||
let data = interpret_atom_content(flags, content)?;
|
||||
|
||||
return Ok(Some(value));
|
||||
tag.atoms.push(Atom {
|
||||
ident: atom_info.ident,
|
||||
data: AtomDataStorage::Single(data),
|
||||
});
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut data = Vec::new();
|
||||
for (flags, content) in atom_data {
|
||||
let value = interpret_atom_content(flags, content)?;
|
||||
data.push(value);
|
||||
}
|
||||
|
||||
tag.atoms.push(Atom {
|
||||
ident: atom_info.ident,
|
||||
data: AtomDataStorage::Multiple(data),
|
||||
});
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn parse_data_inner<R>(data: &mut R) -> Result<Option<(u32, Vec<u8>)>>
|
||||
fn parse_data_inner<R>(data: &mut R, atom_info: &AtomInfo) -> Result<Option<Vec<(u32, Vec<u8>)>>>
|
||||
where
|
||||
R: Read + Seek,
|
||||
{
|
||||
let atom = AtomInfo::read(data)?;
|
||||
// An atom can contain multiple data atoms
|
||||
let mut ret = Vec::new();
|
||||
|
||||
match atom.ident {
|
||||
AtomIdent::Fourcc(ref name) if name == b"data" => {},
|
||||
_ => {
|
||||
return Err(LoftyError::new(ErrorKind::BadAtom(
|
||||
"Expected atom \"data\" to follow name",
|
||||
)))
|
||||
},
|
||||
let to_read = (atom_info.start + atom_info.len) - data.stream_position()?;
|
||||
let mut pos = 0;
|
||||
while pos < to_read {
|
||||
let data_atom = AtomInfo::read(data)?;
|
||||
match data_atom.ident {
|
||||
AtomIdent::Fourcc(ref name) if name == b"data" => {},
|
||||
_ => {
|
||||
return Err(LoftyError::new(ErrorKind::BadAtom(
|
||||
"Expected atom \"data\" to follow name",
|
||||
)))
|
||||
},
|
||||
}
|
||||
|
||||
// We don't care about the version
|
||||
let _version = data.read_u8()?;
|
||||
|
||||
let mut flags = [0; 3];
|
||||
data.read_exact(&mut flags)?;
|
||||
|
||||
let flags = u32::from_be_bytes([0, flags[0], flags[1], flags[2]]);
|
||||
|
||||
// We don't care about the locale
|
||||
data.seek(SeekFrom::Current(4))?;
|
||||
|
||||
let content_len = (data_atom.len - 16) as usize;
|
||||
if content_len == 0 {
|
||||
// We won't add empty atoms
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let mut content = try_vec![0; content_len];
|
||||
data.read_exact(&mut content)?;
|
||||
|
||||
pos += data_atom.len;
|
||||
ret.push((flags, content));
|
||||
}
|
||||
|
||||
// We don't care about the version
|
||||
let _version = data.read_u8()?;
|
||||
|
||||
let mut flags = [0; 3];
|
||||
data.read_exact(&mut flags)?;
|
||||
|
||||
let flags = u32::from_be_bytes([0, flags[0], flags[1], flags[2]]);
|
||||
|
||||
// We don't care about the locale
|
||||
data.seek(SeekFrom::Current(4))?;
|
||||
|
||||
let content_len = (atom.len - 16) as usize;
|
||||
if content_len == 0 {
|
||||
// We won't add empty atoms
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let mut content = try_vec![0; content_len];
|
||||
data.read_exact(&mut content)?;
|
||||
|
||||
Ok(Some((flags, content)))
|
||||
let ret = if ret.is_empty() { None } else { Some(ret) };
|
||||
Ok(ret)
|
||||
}
|
||||
|
||||
fn parse_uint(bytes: &[u8]) -> Result<u32> {
|
||||
|
@ -182,40 +205,65 @@ fn parse_int(bytes: &[u8]) -> Result<i32> {
|
|||
})
|
||||
}
|
||||
|
||||
fn handle_covr(reader: &mut Cursor<Vec<u8>>, tag: &mut Ilst) -> Result<()> {
|
||||
if let Some(value) = parse_data(reader)? {
|
||||
let (mime_type, data) = match value {
|
||||
AtomData::Unknown { code, data } => match code {
|
||||
fn handle_covr(reader: &mut Cursor<Vec<u8>>, tag: &mut Ilst, atom_info: &AtomInfo) -> Result<()> {
|
||||
if let Some(atom_data) = parse_data_inner(reader, atom_info)? {
|
||||
let mut data = Vec::new();
|
||||
|
||||
let len = atom_data.len();
|
||||
for (flags, value) in atom_data {
|
||||
let mime_type = match flags {
|
||||
// Type 0 is implicit
|
||||
RESERVED => (MimeType::None, data),
|
||||
RESERVED => MimeType::None,
|
||||
// GIF is deprecated
|
||||
12 => (MimeType::Gif, data),
|
||||
JPEG => (MimeType::Jpeg, data),
|
||||
PNG => (MimeType::Png, data),
|
||||
BMP => (MimeType::Bmp, data),
|
||||
12 => MimeType::Gif,
|
||||
JPEG => MimeType::Jpeg,
|
||||
PNG => MimeType::Png,
|
||||
BMP => MimeType::Bmp,
|
||||
_ => {
|
||||
return Err(LoftyError::new(ErrorKind::BadAtom(
|
||||
"\"covr\" atom has an unknown type",
|
||||
)))
|
||||
},
|
||||
},
|
||||
_ => {
|
||||
return Err(LoftyError::new(ErrorKind::BadAtom(
|
||||
"\"covr\" atom has an unknown type",
|
||||
)))
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
tag.atoms.push(Atom {
|
||||
ident: AtomIdent::Fourcc(*b"covr"),
|
||||
data: AtomData::Picture(Picture {
|
||||
let picture_data = AtomData::Picture(Picture {
|
||||
pic_type: PictureType::Other,
|
||||
mime_type,
|
||||
description: None,
|
||||
data: Cow::from(data),
|
||||
}),
|
||||
data: Cow::from(value),
|
||||
});
|
||||
|
||||
if len == 1 {
|
||||
tag.atoms.push(Atom {
|
||||
ident: AtomIdent::Fourcc(*b"covr"),
|
||||
data: AtomDataStorage::Single(picture_data),
|
||||
});
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
data.push(picture_data);
|
||||
}
|
||||
|
||||
tag.atoms.push(Atom {
|
||||
ident: AtomIdent::Fourcc(*b"covr"),
|
||||
data: AtomDataStorage::Multiple(data),
|
||||
});
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn interpret_atom_content(flags: u32, content: Vec<u8>) -> Result<AtomData> {
|
||||
// https://developer.apple.com/library/archive/documentation/QuickTime/QTFF/Metadata/Metadata.html#//apple_ref/doc/uid/TP40000939-CH1-SW35
|
||||
Ok(match flags {
|
||||
UTF8 => AtomData::UTF8(String::from_utf8(content)?),
|
||||
UTF16 => AtomData::UTF16(utf16_decode(&*content, u16::from_be_bytes)?),
|
||||
BE_SIGNED_INTEGER => AtomData::SignedInteger(parse_int(&content)?),
|
||||
BE_UNSIGNED_INTEGER => AtomData::UnsignedInteger(parse_uint(&content)?),
|
||||
code => AtomData::Unknown {
|
||||
code,
|
||||
data: content,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
|
77
src/mp4/ilst/ref.rs
Normal file
77
src/mp4/ilst/ref.rs
Normal file
|
@ -0,0 +1,77 @@
|
|||
// *********************
|
||||
// Reference Conversions
|
||||
// *********************
|
||||
|
||||
use crate::error::Result;
|
||||
use crate::mp4::{Atom, AtomData, AtomIdent, Ilst};
|
||||
|
||||
use std::fs::File;
|
||||
use std::io::Write;
|
||||
|
||||
impl Ilst {
|
||||
pub(crate) fn as_ref(&self) -> IlstRef<'_, impl IntoIterator<Item = &AtomData>> {
|
||||
IlstRef {
|
||||
atoms: Box::new(self.atoms.iter().map(Atom::as_ref)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct IlstRef<'a, I> {
|
||||
pub(super) atoms: Box<dyn Iterator<Item = AtomRef<'a, I>> + 'a>,
|
||||
}
|
||||
|
||||
impl<'a, I: 'a> IlstRef<'a, I>
|
||||
where
|
||||
I: IntoIterator<Item = &'a AtomData>,
|
||||
{
|
||||
pub(crate) fn write_to(&mut self, file: &mut File) -> Result<()> {
|
||||
super::write::write_to(file, self)
|
||||
}
|
||||
|
||||
pub(crate) fn dump_to<W: Write>(&mut self, writer: &mut W) -> Result<()> {
|
||||
let temp = super::write::build_ilst(&mut self.atoms)?;
|
||||
writer.write_all(&*temp)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Atom {
|
||||
pub(super) fn as_ref(&self) -> AtomRef<'_, impl IntoIterator<Item = &AtomData>> {
|
||||
AtomRef {
|
||||
ident: (&self.ident).into(),
|
||||
data: (&self.data).into_iter(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct AtomRef<'a, I> {
|
||||
pub(crate) ident: AtomIdentRef<'a>,
|
||||
pub(crate) data: I,
|
||||
}
|
||||
|
||||
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(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,11 +1,12 @@
|
|||
use super::{AtomDataRef, IlstRef};
|
||||
use super::r#ref::IlstRef;
|
||||
use crate::error::{ErrorKind, FileEncodingError, LoftyError, Result};
|
||||
use crate::file::FileType;
|
||||
use crate::macros::try_vec;
|
||||
use crate::mp4::atom_info::{AtomIdent, AtomInfo};
|
||||
use crate::mp4::ilst::{AtomIdentRef, AtomRef};
|
||||
use crate::mp4::ilst::r#ref::{AtomIdentRef, AtomRef};
|
||||
use crate::mp4::moov::Moov;
|
||||
use crate::mp4::read::{atom_tree, meta_is_full, nested_atom, verify_mp4};
|
||||
use crate::mp4::AtomData;
|
||||
use crate::picture::{MimeType, Picture};
|
||||
|
||||
use std::fs::File;
|
||||
|
@ -13,7 +14,10 @@ use std::io::{Cursor, Read, Seek, SeekFrom, Write};
|
|||
|
||||
use byteorder::{BigEndian, WriteBytesExt};
|
||||
|
||||
pub(in crate) fn write_to(data: &mut File, tag: &mut IlstRef<'_>) -> Result<()> {
|
||||
pub(in crate) fn write_to<'a, I: 'a>(data: &mut File, tag: &mut IlstRef<'a, I>) -> Result<()>
|
||||
where
|
||||
I: IntoIterator<Item = &'a AtomData>,
|
||||
{
|
||||
verify_mp4(data)?;
|
||||
|
||||
let moov = Moov::find(data)?;
|
||||
|
@ -292,7 +296,12 @@ fn write_size(start: u64, size: u64, extended: bool, writer: &mut Cursor<Vec<u8>
|
|||
Ok(())
|
||||
}
|
||||
|
||||
pub(super) fn build_ilst(atoms: &mut dyn Iterator<Item = AtomRef<'_>>) -> Result<Vec<u8>> {
|
||||
pub(super) fn build_ilst<'a, I: 'a>(
|
||||
atoms: &mut dyn Iterator<Item = AtomRef<'a, I>>,
|
||||
) -> Result<Vec<u8>>
|
||||
where
|
||||
I: IntoIterator<Item = &'a AtomData>,
|
||||
{
|
||||
let mut peek = atoms.peekable();
|
||||
|
||||
if peek.peek().is_none() {
|
||||
|
@ -313,7 +322,7 @@ pub(super) fn build_ilst(atoms: &mut dyn Iterator<Item = AtomRef<'_>>) -> Result
|
|||
AtomIdentRef::Freeform { mean, name } => write_freeform(mean, name, &mut writer)?,
|
||||
}
|
||||
|
||||
write_atom_data(&atom.data, &mut writer)?;
|
||||
write_atom_data(atom.data, &mut writer)?;
|
||||
|
||||
let end = writer.stream_position()?;
|
||||
|
||||
|
@ -357,15 +366,22 @@ fn write_freeform(mean: &str, name: &str, writer: &mut Cursor<Vec<u8>>) -> Resul
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn write_atom_data(value: &AtomDataRef<'_>, writer: &mut Cursor<Vec<u8>>) -> Result<()> {
|
||||
match value {
|
||||
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_signed_int(*int, writer),
|
||||
AtomDataRef::UnsignedInteger(uint) => write_unsigned_int(*uint, writer),
|
||||
AtomDataRef::Unknown { code, data } => write_data(*code, data, writer),
|
||||
fn write_atom_data<'a, I: 'a>(data: I, writer: &mut Cursor<Vec<u8>>) -> Result<()>
|
||||
where
|
||||
I: IntoIterator<Item = &'a AtomData>,
|
||||
{
|
||||
for value in data {
|
||||
match value {
|
||||
AtomData::UTF8(text) => write_data(1, text.as_bytes(), writer)?,
|
||||
AtomData::UTF16(text) => write_data(2, text.as_bytes(), writer)?,
|
||||
AtomData::Picture(ref pic) => write_picture(pic, writer)?,
|
||||
AtomData::SignedInteger(int) => write_signed_int(*int, writer)?,
|
||||
AtomData::UnsignedInteger(uint) => write_unsigned_int(*uint, writer)?,
|
||||
AtomData::Unknown { code, ref data } => write_data(*code, data, writer)?,
|
||||
};
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_signed_int(int: i32, writer: &mut Cursor<Vec<u8>>) -> Result<()> {
|
||||
|
|
|
@ -8,7 +8,7 @@ use crate::id3::v1::tag::Id3v1TagRef;
|
|||
#[cfg(feature = "id3v2")]
|
||||
use crate::id3::v2::{self, tag::Id3v2TagRef, Id3v2TagFlags};
|
||||
#[cfg(feature = "mp4_ilst")]
|
||||
use crate::mp4::ilst::IlstRef;
|
||||
use crate::mp4::Ilst;
|
||||
#[cfg(feature = "vorbis_comments")]
|
||||
use crate::ogg::tag::{create_vorbis_comments_ref, VorbisCommentsRef};
|
||||
#[cfg(feature = "ape")]
|
||||
|
@ -32,7 +32,9 @@ pub(crate) fn write_tag(tag: &Tag, file: &mut File, file_type: FileType) -> Resu
|
|||
},
|
||||
FileType::MP3 => mp3::write::write_to(file, tag),
|
||||
#[cfg(feature = "mp4_ilst")]
|
||||
FileType::MP4 => crate::mp4::ilst::write::write_to(file, &mut Into::<IlstRef<'_>>::into(tag)),
|
||||
FileType::MP4 => {
|
||||
crate::mp4::ilst::write::write_to(file, &mut Into::<Ilst>::into(tag.clone()).as_ref())
|
||||
},
|
||||
FileType::WAV => iff::wav::write::write_to(file, tag),
|
||||
_ => Err(LoftyError::new(ErrorKind::UnsupportedTag)),
|
||||
}
|
||||
|
@ -56,7 +58,7 @@ pub(crate) fn dump_tag<W: Write>(tag: &Tag, writer: &mut W) -> Result<()> {
|
|||
}
|
||||
.dump_to(writer),
|
||||
#[cfg(feature = "mp4_ilst")]
|
||||
TagType::Mp4Ilst => Into::<IlstRef<'_>>::into(tag).dump_to(writer),
|
||||
TagType::Mp4Ilst => Into::<Ilst>::into(tag.clone()).as_ref().dump_to(writer),
|
||||
#[cfg(feature = "vorbis_comments")]
|
||||
TagType::VorbisComments => {
|
||||
let (vendor, items, pictures) = create_vorbis_comments_ref(tag);
|
||||
|
|
BIN
tests/tags/assets/ilst/multi_value_atom.ilst
Normal file
BIN
tests/tags/assets/ilst/multi_value_atom.ilst
Normal file
Binary file not shown.
Loading…
Reference in a new issue