Add ID3v2 writing

This commit is contained in:
Serial 2021-09-26 22:36:20 -04:00
parent 8bf22a0115
commit c2b76462ff
31 changed files with 984 additions and 459 deletions

View file

@ -4,7 +4,7 @@ pub(crate) mod read;
pub(crate) mod tag;
pub(crate) mod write;
use crate::types::file::AudioFile;
use crate::types::file::{AudioFile, FileType, TaggedFile};
use crate::{FileProperties, Result, Tag, TagType};
use std::io::{Read, Seek};
@ -24,6 +24,19 @@ pub struct ApeFile {
pub(crate) properties: FileProperties,
}
impl From<ApeFile> for TaggedFile {
fn from(input: ApeFile) -> Self {
Self {
ty: FileType::APE,
properties: input.properties,
tags: vec![input.id3v1, input.id3v2, input.ape]
.into_iter()
.flatten()
.collect(),
}
}
}
impl AudioFile for ApeFile {
fn read_from<R>(reader: &mut R) -> Result<Self>
where

View file

@ -7,7 +7,6 @@ pub(crate) 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::Id3v2 => todo!(),
_ => Err(LoftyError::UnsupportedTag),
}
}

View file

@ -9,6 +9,7 @@ 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
@ -30,18 +31,18 @@ where
let mut id3_header = [0; 10];
data.read_exact(&mut id3_header)?;
data.seek(SeekFrom::Current(-10))?;
if &id3_header[..3] == b"ID3" {
let size = unsynch_u32(BigEndian::read_u32(&id3_header[6..]));
if read {
data.seek(SeekFrom::Current(-10))?;
let mut tag = vec![0; (size + 10) as usize];
data.read_exact(&mut tag)?;
id3v2 = Some(tag)
} else {
data.seek(SeekFrom::Current(i64::from(size)))?;
data.seek(SeekFrom::Current(i64::from(size + 10)))?;
}
}

View file

@ -0,0 +1,52 @@
use crate::error::Result;
use std::fs::File;
use std::io::{Read, Seek, SeekFrom, Write};
use byteorder::{ByteOrder, ReadBytesExt, WriteBytesExt};
pub(in crate::logic::id3::v2) fn write_to_chunk_file<B>(data: &mut File, tag: &[u8]) -> Result<()>
where
B: ByteOrder,
{
let mut id3v2_chunk = (None, None);
let mut fourcc = [0; 4];
while let (Ok(()), Ok(size)) = (data.read_exact(&mut fourcc), data.read_u32::<B>()) {
if &fourcc == b"ID3 " || &fourcc == b"id3 " {
id3v2_chunk = (Some(data.seek(SeekFrom::Current(0))? - 8), Some(size));
break;
}
data.seek(SeekFrom::Current(i64::from(size)))?;
}
if let (Some(chunk_start), Some(chunk_size)) = id3v2_chunk {
data.seek(SeekFrom::Start(0))?;
let mut file_bytes = Vec::new();
data.read_to_end(&mut file_bytes)?;
file_bytes.splice(
chunk_start as usize..(chunk_start + u64::from(chunk_size) + 8) as usize,
[],
);
data.seek(SeekFrom::Start(0))?;
data.set_len(0)?;
data.write_all(&*file_bytes)?;
}
data.seek(SeekFrom::End(0))?;
data.write_all(&[b'I', b'D', b'3', b' '])?;
data.write_u32::<B>(tag.len() as u32)?;
data.write_all(tag)?;
let total_size = data.seek(SeekFrom::Current(0))? - 8;
data.seek(SeekFrom::Start(4))?;
data.write_u32::<B>(total_size as u32)?;
Ok(())
}

View file

@ -0,0 +1,279 @@
use crate::error::{LoftyError, Result};
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 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<()>
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
},
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(_)
)
});
// 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!(),
};
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)?;
}
},
}
}
Ok(())
}
fn write_frame_header<W>(writer: &mut W, name: &str, len: u32, flags: &TagItemFlags) -> Result<()>
where
W: Write,
{
writer.write_all(name.as_bytes())?;
writer.write_u32::<BigEndian>(synch_u32(len)?)?;
writer.write_u16::<BigEndian>(get_flags(flags))?;
Ok(())
}
fn get_flags(tag_flags: &TagItemFlags) -> u16 {
let mut flags = 0;
if tag_flags == &TagItemFlags::default() {
return flags;
}
if tag_flags.tag_alter_preservation {
flags |= 0x4000
}
if tag_flags.file_alter_preservation {
flags |= 0x2000
}
if tag_flags.read_only {
flags |= 0x1000
}
if tag_flags.grouping_identity.0 {
flags |= 0x0040
}
if tag_flags.compression {
flags |= 0x0008
}
if tag_flags.encryption.0 {
flags |= 0x0004
}
if tag_flags.unsynchronisation {
flags |= 0x0002
}
if tag_flags.data_length_indicator.0 {
flags |= 0x0001
}
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",
))
}

View file

@ -0,0 +1,127 @@
mod chunk_file;
mod frame;
use super::find_id3v2;
use crate::error::Result;
use crate::logic::id3::synch_u32;
use crate::types::tag::{Tag, TagFlags};
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<()> {
let id3v2 = create_tag(tag)?;
// find_id3v2 will seek us to the end of the tag
find_id3v2(data, false)?;
let mut file_bytes = Vec::new();
data.read_to_end(&mut file_bytes)?;
file_bytes.splice(0..0, id3v2);
data.seek(SeekFrom::Start(0))?;
data.set_len(0)?;
data.write_all(&*file_bytes)?;
Ok(())
}
// 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<()>
where
B: ByteOrder,
{
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())?;
let header_len = id3v2.get_ref().len();
// Write the items
frame::create_items(&mut id3v2, tag.items())?;
let len = id3v2.get_ref().len() - header_len;
// Go back to the start and write the final size
id3v2.seek(SeekFrom::Start(6))?;
id3v2.write_u32::<BigEndian>(synch_u32(len as u32)?)?;
Ok(id3v2.into_inner())
}
#[allow(clippy::trivially_copy_pass_by_ref)]
fn create_tag_header(flags: &TagFlags) -> Result<Cursor<Vec<u8>>> {
let mut header = Cursor::new(Vec::new());
header.write_all(&[b'I', b'D', b'3'])?;
let mut tag_flags = 0;
// Version 4, rev 0
header.write_all(&[4, 0])?;
#[cfg(not(feature = "id3v2_restrictions"))]
let extended_header = flags.crc;
#[cfg(feature = "id3v2_restrictions")]
let extended_header = flags.crc || flags.restrictions.0;
if flags.experimental {
tag_flags |= 0x20
}
if extended_header {
tag_flags |= 0x40
}
if flags.unsynchronisation {
tag_flags |= 0x80
}
header.write_u8(tag_flags)?;
header.write_u32::<BigEndian>(0)?;
if extended_header {
// Size (4)
// Number of flag bytes (1)
// Flags (1)
header.write_all(&[0, 0, 0, 0, 1, 0])?;
let mut size = 6_u32;
let mut ext_flags = 0_u8;
if flags.crc {
// TODO
ext_flags |= 0x20;
size += 5;
header.write_all(&[5, 0, 0, 0, 0, 0])?;
}
#[cfg(feature = "id3v2_restrictions")]
if flags.restrictions.0 {
ext_flags |= 0x10;
size += 2;
header.write_u8(1)?;
header.write_u8(flags.restrictions.1.as_bytes())?;
}
header.seek(SeekFrom::Start(10))?;
header.write_u32::<BigEndian>(synch_u32(size)?)?;
header.seek(SeekFrom::Current(1))?;
header.write_u8(ext_flags)?;
header.seek(SeekFrom::End(0))?;
}
Ok(header)
}

View file

@ -1,8 +1,9 @@
mod read;
mod tag;
pub(in crate::logic) mod write;
use crate::error::Result;
use crate::types::file::AudioFile;
use crate::types::file::{AudioFile, FileType, TaggedFile};
use crate::types::properties::FileProperties;
use crate::types::tag::{Tag, TagType};
@ -20,6 +21,19 @@ pub struct AiffFile {
pub(crate) id3v2: Option<Tag>,
}
impl From<AiffFile> for TaggedFile {
fn from(input: AiffFile) -> Self {
Self {
ty: FileType::AIFF,
properties: input.properties,
tags: vec![input.text_chunks, input.id3v2]
.into_iter()
.flatten()
.collect(),
}
}
}
impl AudioFile for AiffFile {
fn read_from<R>(reader: &mut R) -> Result<Self>
where

92
src/logic/iff/aiff/tag.rs Normal file
View file

@ -0,0 +1,92 @@
use crate::error::Result;
use crate::types::item::{ItemKey, ItemValue};
use crate::types::tag::{Tag, TagType};
use std::fs::File;
use std::io::{Read, Seek, SeekFrom, Write};
use byteorder::{BigEndian, LittleEndian, ReadBytesExt};
pub(in crate::logic) fn write_aiff_text(data: &mut File, tag: &Tag) -> Result<()> {
let mut text_chunks = Vec::new();
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()))
});
for i in items {
// Already covered
let value = match i.value() {
ItemValue::Text(value) => value,
_ => unreachable!(),
};
let len = (value.len() as u32).to_be_bytes();
// 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());
}
let mut chunks_remove = Vec::new();
while let (Ok(fourcc), Ok(size)) = (
data.read_u32::<LittleEndian>(),
data.read_u32::<BigEndian>(),
) {
let fourcc_b = &fourcc.to_le_bytes();
let pos = (data.seek(SeekFrom::Current(0))? - 8) as usize;
if fourcc_b == b"NAME" || fourcc_b == b"AUTH" || fourcc_b == b"(c) " {
chunks_remove.push((pos, (pos + 8 + size as usize)))
}
data.seek(SeekFrom::Current(i64::from(size)))?;
}
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(())
}

View file

@ -1,100 +1,17 @@
use super::read::verify_aiff;
use crate::error::{LoftyError, Result};
use crate::types::item::{ItemKey, ItemValue};
use crate::types::tag::{Tag, TagType};
use std::fs::File;
use std::io::{Read, Seek, SeekFrom, Write};
use byteorder::{BigEndian, LittleEndian, ReadBytesExt};
// TODO: support ID3v2
pub(in crate::logic) fn write_to(data: &mut File, tag: &Tag) -> Result<()> {
if tag.tag_type() != &TagType::AiffText {
return Err(LoftyError::UnsupportedTag);
}
pub(crate) fn write_to(data: &mut File, tag: &Tag) -> Result<()> {
verify_aiff(data)?;
let mut text_chunks = Vec::new();
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()))
});
for i in items {
// Already covered
let value = match i.value() {
ItemValue::Text(value) => value,
_ => unreachable!(),
};
let len = (value.len() as u32).to_be_bytes();
// 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());
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),
_ => Err(LoftyError::UnsupportedTag),
}
let mut chunks_remove = Vec::new();
while let (Ok(fourcc), Ok(size)) = (
data.read_u32::<LittleEndian>(),
data.read_u32::<BigEndian>(),
) {
let fourcc_b = &fourcc.to_le_bytes();
let pos = (data.seek(SeekFrom::Current(0))? - 8) as usize;
if fourcc_b == b"NAME" || fourcc_b == b"AUTH" || fourcc_b == b"(c) " {
chunks_remove.push((pos, (pos + 8 + size as usize)))
}
data.seek(SeekFrom::Current(i64::from(size)))?;
}
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(())
}

View file

@ -1,8 +1,9 @@
mod read;
mod tag;
pub(in crate::logic) mod write;
use crate::error::Result;
use crate::types::file::AudioFile;
use crate::types::file::{AudioFile, FileType, TaggedFile};
use crate::types::properties::FileProperties;
use crate::types::tag::{Tag, TagType};
@ -20,6 +21,19 @@ pub struct WavFile {
pub(crate) id3v2: Option<Tag>,
}
impl From<WavFile> for TaggedFile {
fn from(input: WavFile) -> Self {
Self {
ty: FileType::WAV,
properties: input.properties,
tags: vec![input.riff_info, input.id3v2]
.into_iter()
.flatten()
.collect(),
}
}
}
impl AudioFile for WavFile {
fn read_from<R>(reader: &mut R) -> Result<Self>
where

View file

@ -1,7 +1,6 @@
use super::WavFile;
use crate::error::{LoftyError, Result};
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};
@ -156,7 +155,7 @@ where
if &list_type == b"INFO" {
let end = data.seek(SeekFrom::Current(0))? + u64::from(size - 4);
parse_riff_info(data, end, &mut riff_info)?;
super::tag::read::parse_riff_info(data, end, &mut riff_info)?;
} else {
data.seek(SeekFrom::Current(i64::from(size)))?;
}
@ -198,43 +197,3 @@ where
id3v2: id3,
})
}
fn parse_riff_info<R>(data: &mut R, end: u64, tag: &mut Tag) -> Result<()>
where
R: Read + Seek,
{
while data.seek(SeekFrom::Current(0))? != end {
let mut key = [0; 4];
data.read_exact(&mut key)?;
let key_str = std::str::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"));
}
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];
data.read_exact(&mut value)?;
// Values are expected to have an even size, and are padded with a 0 if necessary
if size % 2 != 0 {
data.read_u8()?;
}
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()),
));
}
Ok(())
}

View file

@ -0,0 +1,2 @@
pub(in crate::logic::iff::wav) mod read;
pub(in crate::logic::iff::wav) mod write;

View file

@ -0,0 +1,51 @@
use crate::error::{LoftyError, Result};
use crate::types::item::{ItemKey, ItemValue, TagItem};
use crate::types::tag::{Tag, TagType};
use std::io::{Read, Seek, SeekFrom};
use byteorder::{LittleEndian, ReadBytesExt};
pub(in crate::logic::iff::wav) fn parse_riff_info<R>(
data: &mut R,
end: u64,
tag: &mut Tag,
) -> Result<()>
where
R: Read + Seek,
{
while data.seek(SeekFrom::Current(0))? != end {
let mut key = [0; 4];
data.read_exact(&mut key)?;
let key_str = std::str::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"));
}
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];
data.read_exact(&mut value)?;
// Values are expected to have an even size, and are padded with a 0 if necessary
if size % 2 != 0 {
data.read_u8()?;
}
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()),
));
}
Ok(())
}

View file

@ -0,0 +1,129 @@
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<()> {
let mut riff_info_bytes = Vec::new();
create_riff_info(tag, &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_start = data.seek(SeekFrom::Current(0))? as usize;
let info_list_end = info_list_start + 8 + info_list_size;
data.seek(SeekFrom::Start(0))?;
let mut file_bytes = Vec::new();
data.read_to_end(&mut file_bytes)?;
let _ = file_bytes.splice(info_list_start..info_list_end, riff_info_bytes);
let total_size = (file_bytes.len() - 8) as u32;
let _ = file_bytes.splice(4..8, total_size.to_le_bytes());
data.seek(SeekFrom::Start(0))?;
data.set_len(0)?;
data.write_all(&*file_bytes)?;
} else {
data.seek(SeekFrom::End(0))?;
data.write_all(&riff_info_bytes)?;
let len = (data.seek(SeekFrom::Current(0))? - 8) as u32;
data.seek(SeekFrom::Start(4))?;
data.write_u32::<LittleEndian>(len)?;
}
Ok(())
}
fn find_info_list<T>(data: &mut T) -> Result<bool>
where
T: Read + Seek,
{
let mut fourcc = [0; 4];
let mut found_info = false;
while let (Ok(()), Ok(size)) = (
data.read_exact(&mut fourcc),
data.read_u32::<LittleEndian>(),
) {
if &fourcc == b"LIST" {
let mut list_type = [0; 4];
data.read_exact(&mut list_type)?;
if &list_type == b"INFO" {
data.seek(SeekFrom::Current(-8))?;
found_info = true;
break;
}
data.seek(SeekFrom::Current(-8))?;
}
data.seek(SeekFrom::Current(i64::from(size)))?;
}
Ok(found_info)
}
fn create_riff_info(tag: &Tag, bytes: &mut Vec<u8>) -> Result<()> {
if tag.item_count() == 0 {
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());
}
}
}
}
let packet_size = bytes.len() - 4;
if packet_size > u32::MAX as usize {
return Err(LoftyError::TooMuchData);
}
let size = (packet_size as u32).to_le_bytes();
#[allow(clippy::needless_range_loop)]
for i in 0..4 {
bytes.insert(i + 4, size[i]);
}
Ok(())
}

View file

@ -1,137 +1,17 @@
use super::read::verify_wav;
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};
fn find_info_list<T>(data: &mut T) -> Result<bool>
where
T: Read + Seek,
{
let mut fourcc = [0; 4];
let mut found_info = false;
while let (Ok(()), Ok(size)) = (
data.read_exact(&mut fourcc),
data.read_u32::<LittleEndian>(),
) {
if &fourcc == b"LIST" {
let mut list_type = [0; 4];
data.read_exact(&mut list_type)?;
if &list_type == b"INFO" {
data.seek(SeekFrom::Current(-8))?;
found_info = true;
break;
}
data.seek(SeekFrom::Current(-8))?;
}
data.seek(SeekFrom::Current(i64::from(size)))?;
}
Ok(found_info)
}
// TODO: ID3v2
pub(crate) fn write_to(data: &mut File, tag: &Tag) -> Result<()> {
if tag.tag_type() != &TagType::RiffInfo {
return Err(LoftyError::UnsupportedTag);
}
verify_wav(data)?;
let mut riff_info_bytes = Vec::new();
create_riff_info(tag, &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_start = data.seek(SeekFrom::Current(0))? as usize;
let info_list_end = info_list_start + 8 + info_list_size;
data.seek(SeekFrom::Start(0))?;
let mut file_bytes = Vec::new();
data.read_to_end(&mut file_bytes)?;
let _ = file_bytes.splice(info_list_start..info_list_end, riff_info_bytes);
let total_size = (file_bytes.len() - 8) as u32;
let _ = file_bytes.splice(4..8, total_size.to_le_bytes());
data.seek(SeekFrom::Start(0))?;
data.set_len(0)?;
data.write_all(&*file_bytes)?;
} else {
data.seek(SeekFrom::End(0))?;
data.write_all(&riff_info_bytes)?;
let len = (data.seek(SeekFrom::Current(0))? - 8) as u32;
data.seek(SeekFrom::Start(4))?;
data.write_u32::<LittleEndian>(len)?;
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),
_ => Err(LoftyError::UnsupportedTag),
}
Ok(())
}
fn create_riff_info(tag: &Tag, bytes: &mut Vec<u8>) -> Result<()> {
if tag.item_count() == 0 {
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());
}
}
}
}
let packet_size = bytes.len() - 4;
if packet_size > u32::MAX as usize {
return Err(LoftyError::TooMuchData);
}
let size = (packet_size as u32).to_le_bytes();
#[allow(clippy::needless_range_loop)]
for i in 0..4 {
bytes.insert(i + 4, size[i]);
}
Ok(())
}

View file

@ -19,7 +19,7 @@ pub(crate) fn write_tag(tag: &Tag, file: &mut File, file_type: FileType) -> Resu
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::MP3 => Ok(()), // TODO
FileType::MP3 => mpeg::write::write_to(file, tag),
FileType::MP4 => mp4::ilst::write::write_to(file, tag),
FileType::Opus => ogg::write::create_pages(file, OPUSTAGS, tag),
FileType::Vorbis => ogg::write::create_pages(file, VORBIS_COMMENT_HEAD, tag),

View file

@ -5,7 +5,7 @@ mod properties;
mod read;
mod trak;
use crate::types::file::AudioFile;
use crate::types::file::{AudioFile, FileType, TaggedFile};
use crate::{FileProperties, Result, Tag, TagType};
use std::io::{Read, Seek};
@ -21,6 +21,20 @@ pub struct Mp4File {
pub(crate) properties: FileProperties,
}
impl From<Mp4File> for TaggedFile {
fn from(input: Mp4File) -> Self {
Self {
ty: FileType::MP4,
properties: input.properties,
tags: if let Some(ilst) = input.ilst {
vec![ilst]
} else {
Vec::new()
},
}
}
}
impl AudioFile for Mp4File {
fn read_from<R>(reader: &mut R) -> Result<Self>
where

View file

@ -1,8 +1,9 @@
mod constants;
pub(crate) mod header;
pub(crate) mod read;
pub(in crate::logic) mod write;
use crate::types::file::AudioFile;
use crate::types::file::{AudioFile, FileType, TaggedFile};
use crate::{FileProperties, Result, Tag, TagType};
use std::io::{Read, Seek};
@ -22,6 +23,19 @@ pub struct MpegFile {
pub(crate) properties: FileProperties,
}
impl From<MpegFile> for TaggedFile {
fn from(input: MpegFile) -> Self {
Self {
ty: FileType::MP3,
properties: input.properties,
tags: vec![input.id3v1, input.id3v2, input.ape]
.into_iter()
.flatten()
.collect(),
}
}
}
impl AudioFile for MpegFile {
fn read_from<R>(reader: &mut R) -> Result<Self>
where

13
src/logic/mpeg/write.rs Normal file
View file

@ -0,0 +1,13 @@
use crate::error::{LoftyError, Result};
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),
_ => Err(LoftyError::UnsupportedTag),
}
}

View file

@ -3,7 +3,8 @@ mod read;
pub(crate) mod write;
use crate::error::Result;
use crate::types::file::AudioFile;
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};
@ -23,6 +24,32 @@ pub struct FlacFile {
pub(crate) vorbis_comments: Option<Tag>,
}
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,
}
}
}
impl AudioFile for FlacFile {
fn read_from<R>(reader: &mut R) -> Result<Self>
where

View file

@ -4,7 +4,8 @@ pub(in crate::logic::ogg) mod write;
use super::find_last_page;
use crate::error::Result;
use crate::logic::ogg::constants::{OPUSHEAD, OPUSTAGS};
use crate::types::file::AudioFile;
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};
@ -24,6 +25,26 @@ pub struct OpusFile {
pub(crate) vorbis_comments: Tag,
}
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: input.properties,
tags: vec![tag],
}
}
}
impl AudioFile for OpusFile {
fn read_from<R>(reader: &mut R) -> Result<Self>
where

View file

@ -4,7 +4,8 @@ pub(in crate::logic::ogg) mod write;
use super::find_last_page;
use crate::error::Result;
use crate::logic::ogg::constants::{VORBIS_COMMENT_HEAD, VORBIS_IDENT_HEAD};
use crate::types::file::AudioFile;
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};
@ -24,6 +25,26 @@ pub struct VorbisFile {
pub(crate) vorbis_comments: Tag,
}
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: input.properties,
tags: vec![tag],
}
}
}
impl AudioFile for VorbisFile {
fn read_from<R>(reader: &mut R) -> Result<Self>
where

View file

@ -1,15 +1,6 @@
use super::item::{ItemKey, ItemValue, TagItem};
use super::properties::FileProperties;
use super::tag::{Tag, TagType};
use crate::error::{LoftyError, Result};
use crate::logic::ape::ApeFile;
use crate::logic::iff::aiff::AiffFile;
use crate::logic::iff::wav::WavFile;
use crate::logic::mp4::Mp4File;
use crate::logic::mpeg::MpegFile;
use crate::logic::ogg::flac::FlacFile;
use crate::logic::ogg::opus::OpusFile;
use crate::logic::ogg::vorbis::VorbisFile;
use std::convert::TryInto;
use std::io::{Read, Seek, SeekFrom};
@ -55,36 +46,28 @@ impl TaggedFile {
/// | `FLAC`, `Opus`, `Vorbis` | `VorbisComments` |
/// | `MP4` | `Mp4Atom` |
pub fn primary_tag(&self) -> Option<&Tag> {
let pred = match self.ty {
FileType::AIFF | FileType::MP3 | FileType::WAV => {
|t: &&Tag| t.tag_type() == &TagType::Id3v2
},
FileType::APE => |t: &&Tag| t.tag_type() == &TagType::Ape,
FileType::FLAC | FileType::Opus | FileType::Vorbis => {
|t: &&Tag| t.tag_type() == &TagType::VorbisComments
},
FileType::MP4 => |t: &&Tag| t.tag_type() == &TagType::Mp4Atom,
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.tags.iter().find(pred)
self.tag(tag_type)
}
/// 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 pred = match self.ty {
FileType::AIFF | FileType::MP3 | FileType::WAV => {
|t: &&mut Tag| t.tag_type() == &TagType::Id3v2
},
FileType::APE => |t: &&mut Tag| t.tag_type() == &TagType::Ape,
FileType::FLAC | FileType::Opus | FileType::Vorbis => {
|t: &&mut Tag| t.tag_type() == &TagType::VorbisComments
},
FileType::MP4 => |t: &&mut Tag| t.tag_type() == &TagType::Mp4Atom,
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.tags.iter_mut().find(pred)
self.tag_mut(tag_type)
}
/// Gets the first tag, if there are any
@ -118,138 +101,6 @@ impl TaggedFile {
}
}
impl From<AiffFile> for TaggedFile {
fn from(input: AiffFile) -> Self {
Self {
ty: FileType::AIFF,
properties: input.properties,
tags: vec![input.text_chunks, input.id3v2]
.into_iter()
.flatten()
.collect(),
}
}
}
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: input.properties,
tags: vec![tag],
}
}
}
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: input.properties,
tags: vec![tag],
}
}
}
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,
}
}
}
impl From<WavFile> for TaggedFile {
fn from(input: WavFile) -> Self {
Self {
ty: FileType::WAV,
properties: input.properties,
tags: vec![input.riff_info, input.id3v2]
.into_iter()
.flatten()
.collect(),
}
}
}
impl From<MpegFile> for TaggedFile {
fn from(input: MpegFile) -> Self {
Self {
ty: FileType::MP3,
properties: input.properties,
tags: vec![input.id3v1, input.id3v2, input.ape]
.into_iter()
.flatten()
.collect(),
}
}
}
impl From<Mp4File> for TaggedFile {
fn from(input: Mp4File) -> Self {
Self {
ty: FileType::MP4,
properties: input.properties,
tags: if let Some(ilst) = input.ilst {
vec![ilst]
} else {
Vec::new()
},
}
}
}
impl From<ApeFile> for TaggedFile {
fn from(input: ApeFile) -> Self {
Self {
ty: FileType::APE,
properties: input.properties,
tags: vec![input.id3v1, input.id3v2, input.ape]
.into_iter()
.flatten()
.collect(),
}
}
}
#[derive(PartialEq, Copy, Clone, Debug)]
#[allow(missing_docs)]
/// The type of file read
@ -268,17 +119,14 @@ impl FileType {
/// Returns if the target FileType supports a [`TagType`]
pub fn supports_tag_type(&self, tag_type: &TagType) -> bool {
match self {
FileType::AIFF => {
std::mem::discriminant(tag_type) == std::mem::discriminant(&TagType::Id3v2)
|| tag_type == &TagType::AiffText
},
FileType::AIFF => tag_type == &TagType::Id3v2 || tag_type == &TagType::AiffText,
FileType::APE => {
tag_type == &TagType::Ape
|| tag_type == &TagType::Id3v1
|| std::mem::discriminant(tag_type) == std::mem::discriminant(&TagType::Id3v2)
|| tag_type == &TagType::Id3v2
},
FileType::MP3 => {
std::mem::discriminant(tag_type) == std::mem::discriminant(&TagType::Id3v2)
tag_type == &TagType::Id3v2
|| tag_type == &TagType::Ape
|| tag_type == &TagType::Id3v1
},
@ -286,10 +134,7 @@ impl FileType {
tag_type == &TagType::VorbisComments
},
FileType::MP4 => tag_type == &TagType::Mp4Atom,
FileType::WAV => {
std::mem::discriminant(tag_type) == std::mem::discriminant(&TagType::Id3v2)
|| tag_type == &TagType::RiffInfo
},
FileType::WAV => tag_type == &TagType::Id3v2 || tag_type == &TagType::RiffInfo,
}
}

View file

@ -31,8 +31,7 @@ fn aiff_write() {
assert_eq!(tagged_file.file_type(), &FileType::AIFF);
// ID3v2
// TODO
// crate::set_artist!(tagged_file, primary_tag_mut, "Foo artist", 1 => file, "Bar artist");
crate::set_artist!(tagged_file, primary_tag_mut, "Foo artist", 1 => file, "Bar artist");
// Text chunks
crate::set_artist!(tagged_file, tag_mut, TagType::AiffText, "Bar artist", 1 => file, "Baz artist");
@ -40,8 +39,7 @@ fn aiff_write() {
// Now reread the file
let mut tagged_file = Probe::new().read_from(&mut file).unwrap();
// TODO
// crate::set_artist!(tagged_file, primary_tag_mut, "Bar artist", 1 => file, "Foo artist");
crate::set_artist!(tagged_file, primary_tag_mut, "Bar artist", 1 => file, "Foo artist");
crate::set_artist!(tagged_file, tag_mut, TagType::AiffText, "Baz artist", 1 => file, "Bar artist");
}

View file

@ -15,13 +15,14 @@ fn ape_read() {
// Now verify ID3v1
crate::verify_artist!(file, tag, TagType::Id3v1, "Bar artist", 1);
// TODO
// Finally, verify ID3v2
// crate::verify_artist!(file, tag, TagType::Id3v2, "Baz artist", 1);
crate::verify_artist!(file, tag, TagType::Id3v2, "Baz artist", 1);
}
#[test]
fn ape_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)
@ -38,17 +39,10 @@ fn ape_write() {
// ID3v1
crate::set_artist!(tagged_file, tag_mut, TagType::Id3v1, "Bar artist", 1 => file, "Baz artist");
// ID3v2
// crate::set_artist!(tagged_file, tag_mut, TagType::Id3v2, "Baz artist", 1 => file, "Qux artist");
// TODO
// Now reread the file
let mut tagged_file = Probe::new().read_from(&mut file).unwrap();
crate::set_artist!(tagged_file, primary_tag_mut, "Bar artist", 1 => file, "Foo artist");
crate::set_artist!(tagged_file, tag_mut, TagType::Id3v1, "Baz artist", 1 => file, "Bar artist");
// crate::set_artist!(tagged_file, tag_mut, TagType::Id3v2, "Qux artist", 1 => file, "Baz artist");
// TODO
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

51
tests/mpeg_read_write.rs Normal file
View file

@ -0,0 +1,51 @@
mod util;
use lofty::{FileType, ItemKey, ItemValue, Probe, TagItem, TagType};
#[test]
fn mpeg_read() {
// Here we have an MP3 file with an ID3v2, ID3v1, and an APEv2 tag
let file = Probe::new().read_from_path("tests/assets/a.mp3").unwrap();
assert_eq!(file.file_type(), &FileType::MP3);
// Verify the ID3v2 tag first
crate::verify_artist!(file, primary_tag, "Foo artist", 1);
// Now verify ID3v1
crate::verify_artist!(file, tag, TagType::Id3v1, "Bar artist", 1);
// Finally, verify APEv2
crate::verify_artist!(file, tag, TagType::Ape, "Baz artist", 1);
}
#[test]
fn mpeg_write() {
let mut file = std::fs::OpenOptions::new()
.read(true)
.write(true)
.open("tests/assets/a.mp3")
.unwrap();
let mut tagged_file = Probe::new().read_from(&mut file).unwrap();
assert_eq!(tagged_file.file_type(), &FileType::MP3);
// ID3v2
crate::set_artist!(tagged_file, primary_tag_mut, "Foo artist", 1 => file, "Bar artist");
// ID3v1
crate::set_artist!(tagged_file, tag_mut, TagType::Id3v1, "Bar artist", 1 => file, "Baz artist");
// APEv2
crate::set_artist!(tagged_file, tag_mut, TagType::Ape, "Baz artist", 1 => file, "Qux artist");
// Now reread the file
let mut tagged_file = Probe::new().read_from(&mut file).unwrap();
crate::set_artist!(tagged_file, primary_tag_mut, "Bar artist", 1 => file, "Foo artist");
crate::set_artist!(tagged_file, tag_mut, TagType::Id3v1, "Baz artist", 1 => file, "Bar artist");
crate::set_artist!(tagged_file, tag_mut, TagType::Ape, "Qux artist", 1 => file, "Baz artist");
}

View file

@ -31,8 +31,7 @@ fn wav_write() {
assert_eq!(tagged_file.file_type(), &FileType::WAV);
// ID3v2
// TODO
// crate::set_artist!(tagged_file, primary_tag_mut, "Foo artist", 1 => file, "Bar artist");
crate::set_artist!(tagged_file, primary_tag_mut, "Foo artist", 1 => file, "Bar artist");
// RIFF INFO
crate::set_artist!(tagged_file, tag_mut, TagType::RiffInfo, "Bar artist", 1 => file, "Baz artist");
@ -40,8 +39,7 @@ fn wav_write() {
// Now reread the file
let mut tagged_file = Probe::new().read_from(&mut file).unwrap();
// TODO
// crate::set_artist!(tagged_file, primary_tag_mut, "Bar artist", 1 => file, "Foo artist");
crate::set_artist!(tagged_file, primary_tag_mut, "Bar artist", 1 => file, "Foo artist");
crate::set_artist!(tagged_file, tag_mut, TagType::RiffInfo, "Baz artist", 1 => file, "Bar artist");
}