mirror of
https://github.com/Serial-ATA/lofty-rs
synced 2024-11-14 16:37:12 +00:00
Add ID3v2 writing
This commit is contained in:
parent
8bf22a0115
commit
c2b76462ff
31 changed files with 984 additions and 459 deletions
|
@ -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
|
||||
|
|
|
@ -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),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)))?;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
52
src/logic/id3/v2/write/chunk_file.rs
Normal file
52
src/logic/id3/v2/write/chunk_file.rs
Normal 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(())
|
||||
}
|
279
src/logic/id3/v2/write/frame.rs
Normal file
279
src/logic/id3/v2/write/frame.rs
Normal 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",
|
||||
))
|
||||
}
|
127
src/logic/id3/v2/write/mod.rs
Normal file
127
src/logic/id3/v2/write/mod.rs
Normal 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)
|
||||
}
|
|
@ -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
92
src/logic/iff/aiff/tag.rs
Normal 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(())
|
||||
}
|
|
@ -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(())
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
|
|
2
src/logic/iff/wav/tag/mod.rs
Normal file
2
src/logic/iff/wav/tag/mod.rs
Normal file
|
@ -0,0 +1,2 @@
|
|||
pub(in crate::logic::iff::wav) mod read;
|
||||
pub(in crate::logic::iff::wav) mod write;
|
51
src/logic/iff/wav/tag/read.rs
Normal file
51
src/logic/iff/wav/tag/read.rs
Normal 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(())
|
||||
}
|
129
src/logic/iff/wav/tag/write.rs
Normal file
129
src/logic/iff/wav/tag/write.rs
Normal 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(())
|
||||
}
|
|
@ -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(())
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
13
src/logic/mpeg/write.rs
Normal 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),
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
|
|
|
@ -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
51
tests/mpeg_read_write.rs
Normal 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");
|
||||
}
|
|
@ -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");
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue