From c2b76462ff46917923716d4e7f652d62f34be119 Mon Sep 17 00:00:00 2001 From: Serial <69764315+Serial-ATA@users.noreply.github.com> Date: Sun, 26 Sep 2021 22:36:20 -0400 Subject: [PATCH] Add ID3v2 writing --- src/logic/ape/mod.rs | 15 +- src/logic/ape/write.rs | 1 - src/logic/id3/v2/mod.rs | 7 +- src/logic/id3/v2/write/chunk_file.rs | 52 +++++ src/logic/id3/v2/write/frame.rs | 279 +++++++++++++++++++++++++++ src/logic/id3/v2/write/mod.rs | 127 ++++++++++++ src/logic/iff/aiff/mod.rs | 16 +- src/logic/iff/aiff/tag.rs | 92 +++++++++ src/logic/iff/aiff/write.rs | 97 +--------- src/logic/iff/wav/mod.rs | 16 +- src/logic/iff/wav/read.rs | 43 +---- src/logic/iff/wav/tag/mod.rs | 2 + src/logic/iff/wav/tag/read.rs | 51 +++++ src/logic/iff/wav/tag/write.rs | 129 +++++++++++++ src/logic/iff/wav/write.rs | 132 +------------ src/logic/mod.rs | 2 +- src/logic/mp4/mod.rs | 16 +- src/logic/mpeg/mod.rs | 16 +- src/logic/mpeg/write.rs | 13 ++ src/logic/ogg/flac/mod.rs | 29 ++- src/logic/ogg/opus/mod.rs | 23 ++- src/logic/ogg/vorbis/mod.rs | 23 ++- src/types/file.rs | 187 ++---------------- tests/aiff_read_write.rs | 6 +- tests/ape_read_write.rs | 12 +- tests/assets/a.ape | Bin 65570 -> 64546 bytes tests/assets/a.mp3 | Bin 12627 -> 11744 bytes tests/assets/a_mixed.aiff | Bin 275320 -> 274295 bytes tests/assets/a_mixed.wav | Bin 275322 -> 274299 bytes tests/mpeg_read_write.rs | 51 +++++ tests/wav_read_write.rs | 6 +- 31 files changed, 984 insertions(+), 459 deletions(-) create mode 100644 src/logic/id3/v2/write/chunk_file.rs create mode 100644 src/logic/id3/v2/write/frame.rs create mode 100644 src/logic/id3/v2/write/mod.rs create mode 100644 src/logic/iff/aiff/tag.rs create mode 100644 src/logic/iff/wav/tag/mod.rs create mode 100644 src/logic/iff/wav/tag/read.rs create mode 100644 src/logic/iff/wav/tag/write.rs create mode 100644 src/logic/mpeg/write.rs create mode 100644 tests/mpeg_read_write.rs diff --git a/src/logic/ape/mod.rs b/src/logic/ape/mod.rs index 4350829b..828d75e4 100644 --- a/src/logic/ape/mod.rs +++ b/src/logic/ape/mod.rs @@ -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 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(reader: &mut R) -> Result where diff --git a/src/logic/ape/write.rs b/src/logic/ape/write.rs index 4f0e603c..cb43429e 100644 --- a/src/logic/ape/write.rs +++ b/src/logic/ape/write.rs @@ -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), } } diff --git a/src/logic/id3/v2/mod.rs b/src/logic/id3/v2/mod.rs index 383f1a0e..3c130619 100644 --- a/src/logic/id3/v2/mod.rs +++ b/src/logic/id3/v2/mod.rs @@ -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)))?; } } diff --git a/src/logic/id3/v2/write/chunk_file.rs b/src/logic/id3/v2/write/chunk_file.rs new file mode 100644 index 00000000..6d1fb4a7 --- /dev/null +++ b/src/logic/id3/v2/write/chunk_file.rs @@ -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(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::()) { + 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::(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::(total_size as u32)?; + + Ok(()) +} diff --git a/src/logic/id3/v2/write/frame.rs b/src/logic/id3/v2/write/frame.rs new file mode 100644 index 00000000..332caf29 --- /dev/null +++ b/src/logic/id3/v2/write/frame.rs @@ -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(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(writer: &mut W, name: &str, len: u32, flags: &TagItemFlags) -> Result<()> +where + W: Write, +{ + writer.write_all(name.as_bytes())?; + writer.write_u32::(synch_u32(len)?)?; + writer.write_u16::(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( + 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(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::(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", + )) +} diff --git a/src/logic/id3/v2/write/mod.rs b/src/logic/id3/v2/write/mod.rs new file mode 100644 index 00000000..6d65bbc0 --- /dev/null +++ b/src/logic/id3/v2/write/mod.rs @@ -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(data: &mut File, tag: &Tag) -> Result<()> +where + B: ByteOrder, +{ + let id3v2 = create_tag(tag)?; + chunk_file::write_to_chunk_file::(data, &id3v2)?; + + Ok(()) +} + +fn create_tag(tag: &Tag) -> Result> { + 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::(synch_u32(len as u32)?)?; + + Ok(id3v2.into_inner()) +} + +#[allow(clippy::trivially_copy_pass_by_ref)] +fn create_tag_header(flags: &TagFlags) -> Result>> { + 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::(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::(synch_u32(size)?)?; + header.seek(SeekFrom::Current(1))?; + header.write_u8(ext_flags)?; + + header.seek(SeekFrom::End(0))?; + } + + Ok(header) +} diff --git a/src/logic/iff/aiff/mod.rs b/src/logic/iff/aiff/mod.rs index f395fb2f..fe86e88f 100644 --- a/src/logic/iff/aiff/mod.rs +++ b/src/logic/iff/aiff/mod.rs @@ -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, } +impl From 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(reader: &mut R) -> Result where diff --git a/src/logic/iff/aiff/tag.rs b/src/logic/iff/aiff/tag.rs new file mode 100644 index 00000000..39a019b3 --- /dev/null +++ b/src/logic/iff/aiff/tag.rs @@ -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::(), + data.read_u32::(), + ) { + 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(()) +} diff --git a/src/logic/iff/aiff/write.rs b/src/logic/iff/aiff/write.rs index bd69c6dc..223e6259 100644 --- a/src/logic/iff/aiff/write.rs +++ b/src/logic/iff/aiff/write.rs @@ -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::(), - data.read_u32::(), - ) { - 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(()) } diff --git a/src/logic/iff/wav/mod.rs b/src/logic/iff/wav/mod.rs index 39a11a06..905c7288 100644 --- a/src/logic/iff/wav/mod.rs +++ b/src/logic/iff/wav/mod.rs @@ -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, } +impl From 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(reader: &mut R) -> Result where diff --git a/src/logic/iff/wav/read.rs b/src/logic/iff/wav/read.rs index b3a494f4..dfe7397d 100644 --- a/src/logic/iff/wav/read.rs +++ b/src/logic/iff/wav/read.rs @@ -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(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::()?; - - 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(()) -} diff --git a/src/logic/iff/wav/tag/mod.rs b/src/logic/iff/wav/tag/mod.rs new file mode 100644 index 00000000..b9839531 --- /dev/null +++ b/src/logic/iff/wav/tag/mod.rs @@ -0,0 +1,2 @@ +pub(in crate::logic::iff::wav) mod read; +pub(in crate::logic::iff::wav) mod write; diff --git a/src/logic/iff/wav/tag/read.rs b/src/logic/iff/wav/tag/read.rs new file mode 100644 index 00000000..537b8d74 --- /dev/null +++ b/src/logic/iff/wav/tag/read.rs @@ -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( + 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::()?; + + 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(()) +} diff --git a/src/logic/iff/wav/tag/write.rs b/src/logic/iff/wav/tag/write.rs new file mode 100644 index 00000000..de50b8d2 --- /dev/null +++ b/src/logic/iff/wav/tag/write.rs @@ -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::()? 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::(len)?; + } + + Ok(()) +} + +fn find_info_list(data: &mut T) -> Result +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::(), + ) { + 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) -> 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(()) +} diff --git a/src/logic/iff/wav/write.rs b/src/logic/iff/wav/write.rs index 521f5ab3..77767379 100644 --- a/src/logic/iff/wav/write.rs +++ b/src/logic/iff/wav/write.rs @@ -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(data: &mut T) -> Result -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::(), - ) { - 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::()? 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::(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) -> 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(()) } diff --git a/src/logic/mod.rs b/src/logic/mod.rs index c397b7ad..97eba4fc 100644 --- a/src/logic/mod.rs +++ b/src/logic/mod.rs @@ -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), diff --git a/src/logic/mp4/mod.rs b/src/logic/mp4/mod.rs index 15aef5da..33f6303d 100644 --- a/src/logic/mp4/mod.rs +++ b/src/logic/mp4/mod.rs @@ -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 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(reader: &mut R) -> Result where diff --git a/src/logic/mpeg/mod.rs b/src/logic/mpeg/mod.rs index e688f8a9..684601b1 100644 --- a/src/logic/mpeg/mod.rs +++ b/src/logic/mpeg/mod.rs @@ -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 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(reader: &mut R) -> Result where diff --git a/src/logic/mpeg/write.rs b/src/logic/mpeg/write.rs new file mode 100644 index 00000000..5f9a6f00 --- /dev/null +++ b/src/logic/mpeg/write.rs @@ -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), + } +} diff --git a/src/logic/ogg/flac/mod.rs b/src/logic/ogg/flac/mod.rs index ed4b6160..0f147cec 100644 --- a/src/logic/ogg/flac/mod.rs +++ b/src/logic/ogg/flac/mod.rs @@ -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, } +impl From 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(reader: &mut R) -> Result where diff --git a/src/logic/ogg/opus/mod.rs b/src/logic/ogg/opus/mod.rs index 31010555..968c88aa 100644 --- a/src/logic/ogg/opus/mod.rs +++ b/src/logic/ogg/opus/mod.rs @@ -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 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(reader: &mut R) -> Result where diff --git a/src/logic/ogg/vorbis/mod.rs b/src/logic/ogg/vorbis/mod.rs index d2ae32e2..00d82d7b 100644 --- a/src/logic/ogg/vorbis/mod.rs +++ b/src/logic/ogg/vorbis/mod.rs @@ -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 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(reader: &mut R) -> Result where diff --git a/src/types/file.rs b/src/types/file.rs index bd98a746..0da48300 100644 --- a/src/types/file.rs +++ b/src/types/file.rs @@ -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 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 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 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 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 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 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 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 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, } } diff --git a/tests/aiff_read_write.rs b/tests/aiff_read_write.rs index 8cfb0d97..5783fd1a 100644 --- a/tests/aiff_read_write.rs +++ b/tests/aiff_read_write.rs @@ -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"); } diff --git a/tests/ape_read_write.rs b/tests/ape_read_write.rs index aaef2b2e..075c7e6b 100644 --- a/tests/ape_read_write.rs +++ b/tests/ape_read_write.rs @@ -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 } diff --git a/tests/assets/a.ape b/tests/assets/a.ape index cd8ad03a6bacc3ab6e72ff97f54f073ec0f84c4a..05ab95cad47531b58b11b2d6f7759f21634323e5 100644 GIT binary patch delta 43 ucmZ3~z_REEv%IH^F$)6-h=v5X8Ukr<1_oxQ#43fvqLR$wlFjlg%zpsd5(6vzCvPANoH}$Mn^^#0D5f@M*si- diff --git a/tests/assets/a_mixed.wav b/tests/assets/a_mixed.wav index 9597de94e64159c882dcff34f788edbaad785194..4eccb8dc003ccf80634659732a5995b27d786ca3 100644 GIT binary patch delta 74 zcmezMOyKuD0k$AdH@9N_Mz&Tq##T0_RyO9XY%F^E)1!=86x2Okj1}Y=7#M&w3y@-9 Z5Df`%H3ZV!3=GU}`S}WoMJ1WVB>>~l5j_9^ delta 78 zcmex;PvF-x0k$AdH@70=Mz&Tq##T0_RyO9XY%F^E>Ygsf3UVwA3_zNh0SGumLIPY3 cfedaScFWIKNGvMJEH2q-_@8;Yi9U-003%fs00000 diff --git a/tests/mpeg_read_write.rs b/tests/mpeg_read_write.rs new file mode 100644 index 00000000..063228b7 --- /dev/null +++ b/tests/mpeg_read_write.rs @@ -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"); +} diff --git a/tests/wav_read_write.rs b/tests/wav_read_write.rs index a2c8a1e0..860db623 100644 --- a/tests/wav_read_write.rs +++ b/tests/wav_read_write.rs @@ -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"); }