mirror of
https://github.com/Serial-ATA/lofty-rs
synced 2024-12-12 21:52:33 +00:00
Read AIFF ANNO
chunks
This commit is contained in:
parent
d7872c671e
commit
a5039e4198
4 changed files with 232 additions and 66 deletions
|
@ -36,6 +36,8 @@ where
|
||||||
|
|
||||||
#[cfg(feature = "aiff_text_chunks")]
|
#[cfg(feature = "aiff_text_chunks")]
|
||||||
let mut text_chunks = AiffTextChunks::default();
|
let mut text_chunks = AiffTextChunks::default();
|
||||||
|
#[cfg(feature = "aiff_text_chunks")]
|
||||||
|
let mut annotations = Vec::new();
|
||||||
#[cfg(feature = "id3v2")]
|
#[cfg(feature = "id3v2")]
|
||||||
let mut id3v2_tag: Option<Id3v2Tag> = None;
|
let mut id3v2_tag: Option<Id3v2Tag> = None;
|
||||||
|
|
||||||
|
@ -75,6 +77,11 @@ where
|
||||||
let value = String::from_utf8(chunks.content(data)?)?;
|
let value = String::from_utf8(chunks.content(data)?)?;
|
||||||
text_chunks.copyright = Some(value);
|
text_chunks.copyright = Some(value);
|
||||||
},
|
},
|
||||||
|
#[cfg(feature = "aiff_text_chunks")]
|
||||||
|
b"ANNO" => {
|
||||||
|
let value = String::from_utf8(chunks.content(data)?)?;
|
||||||
|
annotations.push(value);
|
||||||
|
},
|
||||||
_ => {
|
_ => {
|
||||||
data.seek(SeekFrom::Current(i64::from(chunks.size)))?;
|
data.seek(SeekFrom::Current(i64::from(chunks.size)))?;
|
||||||
},
|
},
|
||||||
|
@ -83,6 +90,11 @@ where
|
||||||
chunks.correct_position(data)?;
|
chunks.correct_position(data)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "aiff_text_chunks")]
|
||||||
|
if !annotations.is_empty() {
|
||||||
|
text_chunks.annotations = Some(annotations);
|
||||||
|
}
|
||||||
|
|
||||||
let properties = if read_properties {
|
let properties = if read_properties {
|
||||||
if comm.is_none() {
|
if comm.is_none() {
|
||||||
return Err(LoftyError::Aiff("File does not contain a \"COMM\" chunk"));
|
return Err(LoftyError::Aiff("File does not contain a \"COMM\" chunk"));
|
||||||
|
@ -109,6 +121,7 @@ where
|
||||||
name: None,
|
name: None,
|
||||||
author: None,
|
author: None,
|
||||||
copyright: None,
|
copyright: None,
|
||||||
|
annotations: None,
|
||||||
} => None,
|
} => None,
|
||||||
_ => Some(text_chunks),
|
_ => Some(text_chunks),
|
||||||
},
|
},
|
||||||
|
|
|
@ -40,8 +40,11 @@ pub struct AiffTextChunks {
|
||||||
/// A copyright notice consisting of the date followed
|
/// A copyright notice consisting of the date followed
|
||||||
/// by the copyright owner
|
/// by the copyright owner
|
||||||
pub copyright: Option<String>,
|
pub copyright: Option<String>,
|
||||||
// TODO: ANNO chunks
|
/// Basic comments
|
||||||
// pub annotations: Option<Vec<String>>,
|
///
|
||||||
|
/// The use of these chunks is discouraged, as the `comments`
|
||||||
|
/// field is more powerful.
|
||||||
|
pub annotations: Option<Vec<String>>,
|
||||||
// TODO: COMT chunk
|
// TODO: COMT chunk
|
||||||
// pub comments: Option<Vec<Comment>>
|
// pub comments: Option<Vec<Comment>>
|
||||||
}
|
}
|
||||||
|
@ -100,7 +103,7 @@ impl AiffTextChunks {
|
||||||
///
|
///
|
||||||
/// * Attempting to write the tag to a format that does not support it
|
/// * Attempting to write the tag to a format that does not support it
|
||||||
pub fn write_to(&self, file: &mut File) -> Result<()> {
|
pub fn write_to(&self, file: &mut File) -> Result<()> {
|
||||||
Into::<AiffTextChunksRef>::into(self).write_to(file)
|
Into::<AiffTextChunksRef<&[String]>>::into(self).write_to(file)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -119,120 +122,270 @@ impl From<AiffTextChunks> for Tag {
|
||||||
push_item(input.author, ItemKey::TrackArtist, &mut tag);
|
push_item(input.author, ItemKey::TrackArtist, &mut tag);
|
||||||
push_item(input.copyright, ItemKey::CopyrightMessage, &mut tag);
|
push_item(input.copyright, ItemKey::CopyrightMessage, &mut tag);
|
||||||
|
|
||||||
|
if let Some(annotations) = input.annotations {
|
||||||
|
for anno in annotations {
|
||||||
|
tag.items
|
||||||
|
.push(TagItem::new(ItemKey::Comment, ItemValue::Text(anno)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
tag
|
tag
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<Tag> for AiffTextChunks {
|
impl From<Tag> for AiffTextChunks {
|
||||||
fn from(input: Tag) -> Self {
|
fn from(mut input: Tag) -> Self {
|
||||||
Self {
|
Self {
|
||||||
name: input.get_string(&ItemKey::TrackTitle).map(str::to_owned),
|
name: input.get_string(&ItemKey::TrackTitle).map(str::to_owned),
|
||||||
author: input.get_string(&ItemKey::TrackArtist).map(str::to_owned),
|
author: input.get_string(&ItemKey::TrackArtist).map(str::to_owned),
|
||||||
copyright: input
|
copyright: input
|
||||||
.get_string(&ItemKey::CopyrightMessage)
|
.get_string(&ItemKey::CopyrightMessage)
|
||||||
.map(str::to_owned),
|
.map(str::to_owned),
|
||||||
|
annotations: {
|
||||||
|
let anno = input
|
||||||
|
.take(&ItemKey::Comment)
|
||||||
|
.filter_map(|i| match i.item_value {
|
||||||
|
ItemValue::Text(text) => Some(text),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
if anno.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(anno)
|
||||||
|
}
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) struct AiffTextChunksRef<'a> {
|
pub(crate) struct AiffTextChunksRef<'a, T: AsRef<[String]>> {
|
||||||
pub name: Option<&'a str>,
|
pub name: Option<&'a str>,
|
||||||
pub author: Option<&'a str>,
|
pub author: Option<&'a str>,
|
||||||
pub copyright: Option<&'a str>,
|
pub copyright: Option<&'a str>,
|
||||||
|
pub annotations: Option<T>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Into<AiffTextChunksRef<'a>> for &'a AiffTextChunks {
|
impl<'a> Into<AiffTextChunksRef<'a, &'a [String]>> for &'a AiffTextChunks {
|
||||||
fn into(self) -> AiffTextChunksRef<'a> {
|
fn into(self) -> AiffTextChunksRef<'a, &'a [String]> {
|
||||||
AiffTextChunksRef {
|
AiffTextChunksRef {
|
||||||
name: self.name.as_deref(),
|
name: self.name.as_deref(),
|
||||||
author: self.author.as_deref(),
|
author: self.author.as_deref(),
|
||||||
copyright: self.copyright.as_deref(),
|
copyright: self.copyright.as_deref(),
|
||||||
|
annotations: self.annotations.as_deref(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Into<AiffTextChunksRef<'a>> for &'a Tag {
|
impl<'a> Into<AiffTextChunksRef<'a, Vec<String>>> for &'a Tag {
|
||||||
fn into(self) -> AiffTextChunksRef<'a> {
|
fn into(self) -> AiffTextChunksRef<'a, Vec<String>> {
|
||||||
AiffTextChunksRef {
|
AiffTextChunksRef {
|
||||||
name: self.get_string(&ItemKey::TrackTitle),
|
name: self.get_string(&ItemKey::TrackTitle),
|
||||||
author: self.get_string(&ItemKey::TrackArtist),
|
author: self.get_string(&ItemKey::TrackArtist),
|
||||||
copyright: self.get_string(&ItemKey::CopyrightMessage),
|
copyright: self.get_string(&ItemKey::CopyrightMessage),
|
||||||
|
annotations: {
|
||||||
|
let anno = self
|
||||||
|
.get_items(&ItemKey::Comment)
|
||||||
|
.filter_map(|i| match i.value() {
|
||||||
|
ItemValue::Text(text) => Some(text.clone()),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
if anno.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(anno)
|
||||||
|
}
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> AiffTextChunksRef<'a> {
|
impl<'a, T: AsRef<[String]>> AiffTextChunksRef<'a, T> {
|
||||||
pub(crate) fn write_to(&self, file: &mut File) -> Result<()> {
|
pub(crate) fn write_to(&self, file: &mut File) -> Result<()> {
|
||||||
write_to(file, self)
|
AiffTextChunksRef::write_to_inner(file, self)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn write_to(data: &mut File, tag: &AiffTextChunksRef) -> Result<()> {
|
fn write_to_inner(data: &mut File, tag: &AiffTextChunksRef<T>) -> Result<()> {
|
||||||
fn write_chunk(writer: &mut Vec<u8>, key: &str, value: Option<&str>) {
|
fn write_chunk(writer: &mut Vec<u8>, key: &str, value: Option<&str>) {
|
||||||
if let Some(val) = value {
|
if let Some(val) = value {
|
||||||
if let Ok(len) = u32::try_from(val.len()) {
|
if let Ok(len) = u32::try_from(val.len()) {
|
||||||
writer.extend(key.as_bytes().iter());
|
writer.extend(key.as_bytes().iter());
|
||||||
writer.extend(len.to_be_bytes().iter());
|
writer.extend(len.to_be_bytes().iter());
|
||||||
writer.extend(val.as_bytes().iter());
|
writer.extend(val.as_bytes().iter());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
super::read::verify_aiff(data)?;
|
super::read::verify_aiff(data)?;
|
||||||
|
|
||||||
let mut text_chunks = Vec::new();
|
let mut text_chunks = Vec::new();
|
||||||
|
|
||||||
write_chunk(&mut text_chunks, "NAME", tag.name);
|
write_chunk(&mut text_chunks, "NAME", tag.name);
|
||||||
write_chunk(&mut text_chunks, "AUTH", tag.author);
|
write_chunk(&mut text_chunks, "AUTH", tag.author);
|
||||||
write_chunk(&mut text_chunks, "(c) ", tag.copyright);
|
write_chunk(&mut text_chunks, "(c) ", tag.copyright);
|
||||||
|
|
||||||
let mut chunks_remove = Vec::new();
|
if let Some(annotations) = &tag.annotations {
|
||||||
|
for anno in annotations.as_ref() {
|
||||||
let mut chunks = Chunks::<BigEndian>::new();
|
write_chunk(&mut text_chunks, "ANNO", Some(anno.as_str()));
|
||||||
|
}
|
||||||
while chunks.next(data).is_ok() {
|
|
||||||
let pos = (data.seek(SeekFrom::Current(0))? - 8) as usize;
|
|
||||||
|
|
||||||
if &chunks.fourcc == b"NAME" || &chunks.fourcc == b"AUTH" || &chunks.fourcc == b"(c) " {
|
|
||||||
chunks_remove.push((pos, (pos + 8 + chunks.size as usize)))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
data.seek(SeekFrom::Current(i64::from(chunks.size)))?;
|
let mut chunks_remove = Vec::new();
|
||||||
chunks.correct_position(data)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
data.seek(SeekFrom::Start(0))?;
|
let mut chunks = Chunks::<BigEndian>::new();
|
||||||
|
|
||||||
let mut file_bytes = Vec::new();
|
while chunks.next(data).is_ok() {
|
||||||
data.read_to_end(&mut file_bytes)?;
|
let pos = (data.seek(SeekFrom::Current(0))? - 8) as usize;
|
||||||
|
|
||||||
if chunks_remove.is_empty() {
|
match &chunks.fourcc {
|
||||||
data.seek(SeekFrom::Start(16))?;
|
b"NAME" | b"AUTH" | b"(c) " | b"ANNO" => {
|
||||||
|
chunks_remove.push((pos, (pos + 8 + chunks.size as usize)))
|
||||||
|
},
|
||||||
|
_ => {},
|
||||||
|
}
|
||||||
|
|
||||||
let mut size = [0; 4];
|
data.seek(SeekFrom::Current(i64::from(chunks.size)))?;
|
||||||
data.read_exact(&mut size)?;
|
chunks.correct_position(data)?;
|
||||||
|
|
||||||
let comm_end = (20 + u32::from_le_bytes(size)) as usize;
|
|
||||||
file_bytes.splice(comm_end..comm_end, text_chunks);
|
|
||||||
} else {
|
|
||||||
chunks_remove.sort_unstable();
|
|
||||||
chunks_remove.reverse();
|
|
||||||
|
|
||||||
let first = chunks_remove.pop().unwrap();
|
|
||||||
|
|
||||||
for (s, e) in &chunks_remove {
|
|
||||||
file_bytes.drain(*s as usize..*e as usize);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
file_bytes.splice(first.0 as usize..first.1 as usize, text_chunks);
|
data.seek(SeekFrom::Start(0))?;
|
||||||
|
|
||||||
|
let mut file_bytes = Vec::new();
|
||||||
|
data.read_to_end(&mut file_bytes)?;
|
||||||
|
|
||||||
|
if chunks_remove.is_empty() {
|
||||||
|
data.seek(SeekFrom::Start(16))?;
|
||||||
|
|
||||||
|
let mut size = [0; 4];
|
||||||
|
data.read_exact(&mut size)?;
|
||||||
|
|
||||||
|
let comm_end = (20 + u32::from_le_bytes(size)) as usize;
|
||||||
|
file_bytes.splice(comm_end..comm_end, text_chunks);
|
||||||
|
} else {
|
||||||
|
chunks_remove.sort_unstable();
|
||||||
|
chunks_remove.reverse();
|
||||||
|
|
||||||
|
let first = chunks_remove.pop().unwrap();
|
||||||
|
|
||||||
|
for (s, e) in &chunks_remove {
|
||||||
|
file_bytes.drain(*s as usize..*e as usize);
|
||||||
|
}
|
||||||
|
|
||||||
|
file_bytes.splice(first.0 as usize..first.1 as usize, text_chunks);
|
||||||
|
}
|
||||||
|
|
||||||
|
let total_size = ((file_bytes.len() - 8) as u32).to_be_bytes();
|
||||||
|
file_bytes.splice(4..8, total_size.to_vec());
|
||||||
|
|
||||||
|
data.seek(SeekFrom::Start(0))?;
|
||||||
|
data.set_len(0)?;
|
||||||
|
data.write_all(&*file_bytes)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::{ItemKey, ItemValue, Tag, TagItem, TagType};
|
||||||
|
|
||||||
|
use crate::iff::AiffTextChunks;
|
||||||
|
use std::io::{Cursor, Read};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_aiff_text() {
|
||||||
|
let expected_tag = AiffTextChunks {
|
||||||
|
name: Some(String::from("Foo title")),
|
||||||
|
author: Some(String::from("Bar artist")),
|
||||||
|
copyright: Some(String::from("Baz copyright")),
|
||||||
|
annotations: Some(vec![
|
||||||
|
String::from("Qux annotation"),
|
||||||
|
String::from("Quux annotation"),
|
||||||
|
]),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut tag = Vec::new();
|
||||||
|
std::fs::File::open("tests/tags/assets/test.aiff_text")
|
||||||
|
.unwrap()
|
||||||
|
.read_to_end(&mut tag)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let parsed_tag = super::super::read::read_from(&mut Cursor::new(tag), false)
|
||||||
|
.unwrap()
|
||||||
|
.text_chunks
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(expected_tag, parsed_tag);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn aiff_text_to_tag() {
|
||||||
|
let mut tag_bytes = Vec::new();
|
||||||
|
std::fs::File::open("tests/tags/assets/test.aiff_text")
|
||||||
|
.unwrap()
|
||||||
|
.read_to_end(&mut tag_bytes)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let aiff_text = super::super::read::read_from(&mut Cursor::new(tag_bytes), false)
|
||||||
|
.unwrap()
|
||||||
|
.text_chunks
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let tag: Tag = aiff_text.into();
|
||||||
|
|
||||||
|
assert_eq!(tag.get_string(&ItemKey::TrackTitle), Some("Foo title"));
|
||||||
|
assert_eq!(tag.get_string(&ItemKey::TrackArtist), Some("Bar artist"));
|
||||||
|
assert_eq!(
|
||||||
|
tag.get_string(&ItemKey::CopyrightMessage),
|
||||||
|
Some("Baz copyright")
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut comments = tag.get_items(&ItemKey::Comment);
|
||||||
|
assert_eq!(
|
||||||
|
comments.next().map(TagItem::value),
|
||||||
|
Some(&ItemValue::Text(String::from("Qux annotation")))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
comments.next().map(TagItem::value),
|
||||||
|
Some(&ItemValue::Text(String::from("Quux annotation")))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tag_to_aiff_text() {
|
||||||
|
let mut tag = Tag::new(TagType::AiffText);
|
||||||
|
tag.insert_text(ItemKey::TrackTitle, String::from("Foo title"));
|
||||||
|
tag.insert_text(ItemKey::TrackArtist, String::from("Bar artist"));
|
||||||
|
tag.insert_text(ItemKey::CopyrightMessage, String::from("Baz copyright"));
|
||||||
|
tag.insert_item_unchecked(
|
||||||
|
TagItem::new(
|
||||||
|
ItemKey::Comment,
|
||||||
|
ItemValue::Text(String::from("Qux annotation")),
|
||||||
|
),
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
tag.insert_item_unchecked(
|
||||||
|
TagItem::new(
|
||||||
|
ItemKey::Comment,
|
||||||
|
ItemValue::Text(String::from("Quux annotation")),
|
||||||
|
),
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
|
||||||
|
let aiff_text: AiffTextChunks = tag.into();
|
||||||
|
|
||||||
|
assert_eq!(aiff_text.name, Some(String::from("Foo title")));
|
||||||
|
assert_eq!(aiff_text.author, Some(String::from("Bar artist")));
|
||||||
|
assert_eq!(aiff_text.copyright, Some(String::from("Baz copyright")));
|
||||||
|
assert_eq!(
|
||||||
|
aiff_text.annotations,
|
||||||
|
Some(vec![
|
||||||
|
String::from("Qux annotation"),
|
||||||
|
String::from("Quux annotation")
|
||||||
|
])
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let total_size = ((file_bytes.len() - 8) as u32).to_be_bytes();
|
|
||||||
file_bytes.splice(4..8, total_size.to_vec());
|
|
||||||
|
|
||||||
data.seek(SeekFrom::Start(0))?;
|
|
||||||
data.set_len(0)?;
|
|
||||||
data.write_all(&*file_bytes)?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,7 +12,7 @@ use std::fs::File;
|
||||||
pub(crate) fn write_to(data: &mut File, tag: &Tag) -> Result<()> {
|
pub(crate) fn write_to(data: &mut File, tag: &Tag) -> Result<()> {
|
||||||
match tag.tag_type() {
|
match tag.tag_type() {
|
||||||
#[cfg(feature = "aiff_text_chunks")]
|
#[cfg(feature = "aiff_text_chunks")]
|
||||||
TagType::AiffText => Into::<AiffTextChunksRef>::into(tag).write_to(data),
|
TagType::AiffText => Into::<AiffTextChunksRef<Vec<String>>>::into(tag).write_to(data),
|
||||||
#[cfg(feature = "id3v2")]
|
#[cfg(feature = "id3v2")]
|
||||||
TagType::Id3v2 => Into::<Id3v2TagRef>::into(tag).write_to(data),
|
TagType::Id3v2 => Into::<Id3v2TagRef>::into(tag).write_to(data),
|
||||||
_ => Err(LoftyError::UnsupportedTag),
|
_ => Err(LoftyError::UnsupportedTag),
|
||||||
|
|
BIN
tests/tags/assets/test.aiff_text
Normal file
BIN
tests/tags/assets/test.aiff_text
Normal file
Binary file not shown.
Loading…
Reference in a new issue