From a09568cef6d43a9d5e05f911a5f44e582e43de7a Mon Sep 17 00:00:00 2001 From: Serial <69764315+Serial-ATA@users.noreply.github.com> Date: Wed, 3 Apr 2024 01:40:04 -0400 Subject: [PATCH] misc: Introduce WriteOptions This allows the caller to tweak how Lofty writes their tags in various ways. As this is just a dumping ground for all sorts of format-specific settings, this is best used as an application global config that gets set once. In its current state, it will only respect `uppercase_id3v2_chunk` and `preferred_padding` (for some formats). `respect_read_only` and `remove_others` are defined for later use. closes #228 --- CHANGELOG.md | 6 ++ Cargo.toml | 2 +- examples/tag_writer.rs | 4 +- lofty_attr/src/internal.rs | 14 ++-- lofty_attr/src/lofty_file.rs | 8 +- src/ape/tag/mod.rs | 33 ++++++--- src/ape/tag/write.rs | 8 +- src/file.rs | 46 +++++++----- src/flac/mod.rs | 9 ++- src/flac/write.rs | 9 ++- src/id3/v1/tag.rs | 33 ++++++--- src/id3/v1/write.rs | 7 +- src/id3/v2/tag.rs | 29 ++++++-- src/id3/v2/tag/tests.rs | 14 ++-- src/id3/v2/write/chunk_file.rs | 20 ++++- src/id3/v2/write/mod.rs | 41 +++++++++-- src/iff/aiff/tag.rs | 31 ++++++-- src/iff/wav/tag/mod.rs | 33 ++++++--- src/iff/wav/tag/write.rs | 2 + src/lib.rs | 2 + src/mp4/ilst/mod.rs | 34 +++++---- src/mp4/ilst/ref.rs | 11 ++- src/mp4/ilst/write.rs | 48 ++++++++---- src/ogg/tag.rs | 35 ++++++--- src/ogg/write.rs | 17 ++++- src/tag/mod.rs | 23 ++++-- src/tag/utils.rs | 56 ++++++++------ src/traits.rs | 29 ++++++-- src/write_options.rs | 131 +++++++++++++++++++++++++++++++++ tests/files/mpeg.rs | 12 +-- tests/files/ogg.rs | 6 +- tests/files/util/mod.rs | 3 +- 32 files changed, 566 insertions(+), 190 deletions(-) create mode 100644 src/write_options.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 307573ece..6e4f9a965 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +- `WriteOptions` ([issue](https://github.com/Serial-ATA/lofty-rs/issues/228)) ([PR](https://github.com/Serial-ATA/lofty-rs/pull/361)): + - ⚠️ Important ⚠️: This update introduces `WriteOptions` to allow for finer grained control over how + Lofty writes tags. These are best used as global user-configurable options, as most options will + not apply to all files. The defaults are set to be as safe as possible, + see [here](https://docs.rs/lofty/latest/lofty/struct.WriteOptions.html#impl-Default-for-WriteOptions). + ## [0.18.2] - 2024-01-23 ### Fixed diff --git a/Cargo.toml b/Cargo.toml index 43275cf8b..bc04b052d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,7 +18,7 @@ byteorder = "1.5.0" # ID3 compressed frames flate2 = { version = "1.0.28", optional = true } # Proc macros -lofty_attr = "0.9.0" +lofty_attr = { path = "lofty_attr" } # Debug logging log = "0.4.20" # OGG Vorbis/Opus diff --git a/examples/tag_writer.rs b/examples/tag_writer.rs index f02e1514b..51e10d061 100644 --- a/examples/tag_writer.rs +++ b/examples/tag_writer.rs @@ -1,4 +1,4 @@ -use lofty::{Accessor, Probe, Tag, TagExt, TaggedFileExt}; +use lofty::{Accessor, Probe, Tag, TagExt, TaggedFileExt, WriteOptions}; use structopt::StructOpt; @@ -75,7 +75,7 @@ fn main() { tag.set_genre(genre) } - tag.save_to_path(&opt.path) + tag.save_to_path(&opt.path, WriteOptions::new()) .expect("ERROR: Failed to write the tag!"); println!("INFO: Tag successfully updated!"); diff --git a/lofty_attr/src/internal.rs b/lofty_attr/src/internal.rs index 8bfc98fdd..d0f9aaf5e 100644 --- a/lofty_attr/src/internal.rs +++ b/lofty_attr/src/internal.rs @@ -48,16 +48,16 @@ pub(crate) fn init_write_lookup( read_only: false, items: lofty::ape::tag::tagitems_into_ape(tag), } - .write_to(data) + .write_to(data, write_options) }); insert!(map, Id3v1, { - Into::>::into(tag).write_to(data) + Into::>::into(tag).write_to(data, write_options) }); if id3v2_strippable { insert!(map, Id3v2, { - lofty::id3::v2::tag::Id3v2TagRef::empty().write_to(data) + lofty::id3::v2::tag::Id3v2TagRef::empty().write_to(data, write_options) }); } else { insert!(map, Id3v2, { @@ -65,7 +65,7 @@ pub(crate) fn init_write_lookup( flags: lofty::id3::v2::Id3v2TagFlags::default(), frames: lofty::id3::v2::tag::tag_frames(tag), } - .write_to(data) + .write_to(data, write_options) }); } @@ -73,7 +73,7 @@ pub(crate) fn init_write_lookup( lofty::iff::wav::tag::RIFFInfoListRef::new(lofty::iff::wav::tag::tagitems_into_riff( tag.items(), )) - .write_to(data) + .write_to(data, write_options) }); insert!(map, AiffText, { @@ -84,7 +84,7 @@ pub(crate) fn init_write_lookup( annotations: Some(tag.get_strings(&lofty::tag::item::ItemKey::Comment)), comments: None, } - .write_to(data) + .write_to(data, write_options) }); map @@ -111,7 +111,7 @@ pub(crate) fn write_module( quote! { pub(crate) mod write { #[allow(unused_variables)] - pub(crate) fn write_to(data: &mut ::std::fs::File, tag: &::lofty::Tag) -> ::lofty::error::Result<()> { + pub(crate) fn write_to(data: &mut ::std::fs::File, tag: &::lofty::Tag, write_options: ::lofty::WriteOptions) -> ::lofty::error::Result<()> { match tag.tag_type() { #( #applicable_formats )* _ => crate::macros::err!(UnsupportedTag), diff --git a/lofty_attr/src/lofty_file.rs b/lofty_attr/src/lofty_file.rs index d9a520cb5..9da3d2ea5 100644 --- a/lofty_attr/src/lofty_file.rs +++ b/lofty_attr/src/lofty_file.rs @@ -445,7 +445,7 @@ fn generate_audiofile_impl(file: &LoftyFile) -> syn::Result ::lofty::error::Result<()> { + fn save_to(&self, file: &mut ::std::fs::File, write_options: ::lofty::WriteOptions) -> ::lofty::error::Result<()> { use ::lofty::TagExt as _; use ::std::io::Seek as _; #save_to_body @@ -480,7 +480,7 @@ fn get_save_to_body( // Custom write fn if let Some(write_fn) = write_fn { return quote! { - #write_fn(&self, file) + #write_fn(&self, file, write_options) }; } @@ -490,13 +490,13 @@ fn get_save_to_body( quote! { if let Some(ref tag) = self.#name { file.rewind()?; - tag.save_to(file)?; + tag.save_to(file, write_options)?; } } } else { quote! { file.rewind()?; - self.#name.save_to(file)?; + self.#name.save_to(file, write_options)?; } } }); diff --git a/src/ape/tag/mod.rs b/src/ape/tag/mod.rs index be4e25847..416aa213c 100644 --- a/src/ape/tag/mod.rs +++ b/src/ape/tag/mod.rs @@ -8,6 +8,7 @@ use crate::id3::v2::util::pairs::{format_number_pair, set_number, NUMBER_PAIR_KE use crate::tag::item::{ItemKey, ItemValue, ItemValueRef, TagItem}; use crate::tag::{try_parse_year, Tag, TagType}; use crate::traits::{Accessor, MergeTag, SplitTag, TagExt}; +use crate::write_options::WriteOptions; use std::borrow::Cow; use std::fs::File; @@ -320,12 +321,16 @@ impl TagExt for ApeTag { /// /// * Attempting to write the tag to a format that does not support it /// * An existing tag has an invalid size - fn save_to(&self, file: &mut File) -> std::result::Result<(), Self::Err> { + fn save_to( + &self, + file: &mut File, + write_options: WriteOptions, + ) -> std::result::Result<(), Self::Err> { ApeTagRef { read_only: self.read_only, items: self.items.iter().map(Into::into), } - .write_to(file) + .write_to(file, write_options) } /// Dumps the tag to a writer @@ -333,12 +338,16 @@ impl TagExt for ApeTag { /// # Errors /// /// * [`std::io::Error`] - fn dump_to(&self, writer: &mut W) -> std::result::Result<(), Self::Err> { + fn dump_to( + &self, + writer: &mut W, + write_options: WriteOptions, + ) -> std::result::Result<(), Self::Err> { ApeTagRef { read_only: self.read_only, items: self.items.iter().map(Into::into), } - .dump_to(writer) + .dump_to(writer, write_options) } fn remove_from_path>(&self, path: P) -> std::result::Result<(), Self::Err> { @@ -482,11 +491,15 @@ impl<'a, I> ApeTagRef<'a, I> where I: Iterator>, { - pub(crate) fn write_to(&mut self, file: &mut File) -> Result<()> { - write::write_to(file, self) + pub(crate) fn write_to(&mut self, file: &mut File, write_options: WriteOptions) -> Result<()> { + write::write_to(file, self, write_options) } - pub(crate) fn dump_to(&mut self, writer: &mut W) -> Result<()> { + pub(crate) fn dump_to( + &mut self, + writer: &mut W, + _write_options: WriteOptions, + ) -> Result<()> { let temp = write::create_ape_tag(self)?; writer.write_all(&temp)?; @@ -531,7 +544,7 @@ pub(crate) fn tagitems_into_ape(tag: &Tag) -> impl Iterator(data: &mut File, tag: &mut ApeTagRef<'a, I>) -> Result<()> +pub(crate) fn write_to<'a, I>( + data: &mut File, + tag: &mut ApeTagRef<'a, I>, + _write_options: WriteOptions, +) -> Result<()> where I: Iterator>, { @@ -40,6 +45,7 @@ where // If one is found, it'll be removed and rewritten at the bottom, where it should be let mut header_ape_tag = (false, (0, 0)); + // TODO: Respect read only let start = data.stream_position()?; match read::read_ape_tag(data, false)? { Some((mut existing_tag, header)) => { diff --git a/src/file.rs b/src/file.rs index ed6ca53b0..a32a571f8 100644 --- a/src/file.rs +++ b/src/file.rs @@ -5,6 +5,7 @@ use crate::properties::FileProperties; use crate::resolve::custom_resolvers; use crate::tag::{Tag, TagType}; use crate::traits::TagExt; +use crate::write_options::WriteOptions; use std::ffi::OsStr; use std::fs::{File, OpenOptions}; @@ -39,7 +40,7 @@ pub trait AudioFile: Into { /// # Examples /// /// ```rust,no_run - /// use lofty::{AudioFile, TaggedFileExt}; + /// use lofty::{AudioFile, TaggedFileExt, WriteOptions}; /// /// # fn main() -> lofty::Result<()> { /// # let path = "tests/files/assets/minimal/full_test.mp3"; @@ -47,11 +48,14 @@ pub trait AudioFile: Into { /// /// // Edit the tags /// - /// tagged_file.save_to_path(path)?; + /// tagged_file.save_to_path(path, WriteOptions::new())?; /// # Ok(()) } /// ``` - fn save_to_path(&self, path: impl AsRef) -> Result<()> { - self.save_to(&mut OpenOptions::new().read(true).write(true).open(path)?) + fn save_to_path(&self, path: impl AsRef, write_options: WriteOptions) -> Result<()> { + self.save_to( + &mut OpenOptions::new().read(true).write(true).open(path)?, + write_options, + ) } /// Attempts to write all tags to a file @@ -63,7 +67,7 @@ pub trait AudioFile: Into { /// # Examples /// /// ```rust,no_run - /// use lofty::{AudioFile, TaggedFileExt}; + /// use lofty::{AudioFile, TaggedFileExt, WriteOptions}; /// use std::fs::OpenOptions; /// /// # fn main() -> lofty::Result<()> { @@ -73,10 +77,10 @@ pub trait AudioFile: Into { /// // Edit the tags /// /// let mut file = OpenOptions::new().read(true).write(true).open(path)?; - /// tagged_file.save_to(&mut file)?; + /// tagged_file.save_to(&mut file, WriteOptions::new())?; /// # Ok(()) } /// ``` - fn save_to(&self, file: &mut File) -> Result<()>; + fn save_to(&self, file: &mut File, write_options: WriteOptions) -> Result<()>; /// Returns a reference to the file's properties fn properties(&self) -> &Self::Properties; @@ -492,12 +496,12 @@ impl AudioFile for TaggedFile { .read() } - fn save_to(&self, file: &mut File) -> Result<()> { + fn save_to(&self, file: &mut File, write_options: WriteOptions) -> Result<()> { for tag in &self.tags { // TODO: This is a temporary solution. Ideally we should probe once and use // the format-specific writing to avoid these rewinds. file.rewind()?; - tag.save_to(file)?; + tag.save_to(file, write_options)?; } Ok(()) @@ -528,7 +532,7 @@ impl From for TaggedFile { /// For example: /// /// ```rust,no_run -/// use lofty::{AudioFile, Tag, TagType, TaggedFileExt}; +/// use lofty::{AudioFile, Tag, TagType, TaggedFileExt, WriteOptions}; /// # fn main() -> lofty::Result<()> { /// # let path = "tests/files/assets/minimal/full_test.mp3"; /// @@ -543,7 +547,7 @@ impl From for TaggedFile { /// // After saving, our file still "contains" the ID3v2 tag, but if we were to read /// // "foo.mp3", it would not have an ID3v2 tag. Lofty does not write empty tags, but this /// // change will not be reflected in `TaggedFile`. -/// tagged_file.save_to_path("foo.mp3")?; +/// tagged_file.save_to_path("foo.mp3", WriteOptions::new())?; /// assert!(tagged_file.contains_tag_type(TagType::Id3v2)); /// # Ok(()) } /// ``` @@ -551,7 +555,9 @@ impl From for TaggedFile { /// However, when using `BoundTaggedFile`: /// /// ```rust,no_run -/// use lofty::{AudioFile, BoundTaggedFile, ParseOptions, Tag, TagType, TaggedFileExt}; +/// use lofty::{ +/// AudioFile, BoundTaggedFile, ParseOptions, Tag, TagType, TaggedFileExt, WriteOptions, +/// }; /// use std::fs::OpenOptions; /// # fn main() -> lofty::Result<()> { /// # let path = "tests/files/assets/minimal/full_test.mp3"; @@ -570,7 +576,7 @@ impl From for TaggedFile { /// /// // Now when saving, we no longer have to specify a path, and the tags in the `BoundTaggedFile` /// // reflect those in the actual file on disk. -/// bound_tagged_file.save()?; +/// bound_tagged_file.save(WriteOptions::new())?; /// assert!(!bound_tagged_file.contains_tag_type(TagType::Id3v2)); /// # Ok(()) } /// ``` @@ -620,7 +626,9 @@ impl BoundTaggedFile { /// # Examples /// /// ```rust,no_run - /// use lofty::{AudioFile, BoundTaggedFile, ParseOptions, Tag, TagType, TaggedFileExt}; + /// use lofty::{ + /// AudioFile, BoundTaggedFile, ParseOptions, Tag, TagType, TaggedFileExt, WriteOptions, + /// }; /// use std::fs::OpenOptions; /// # fn main() -> lofty::Result<()> { /// # let path = "tests/files/assets/minimal/full_test.mp3"; @@ -634,11 +642,11 @@ impl BoundTaggedFile { /// // Do some work to the tags... /// /// // This will save the tags to the file we provided to `read_from` - /// bound_tagged_file.save()?; + /// bound_tagged_file.save(WriteOptions::new())?; /// # Ok(()) } /// ``` - pub fn save(&mut self) -> Result<()> { - self.inner.save_to(&mut self.file_handle)?; + pub fn save(&mut self, write_options: WriteOptions) -> Result<()> { + self.inner.save_to(&mut self.file_handle, write_options)?; self.inner.tags.retain(|tag| !tag.is_empty()); Ok(()) @@ -692,8 +700,8 @@ impl AudioFile for BoundTaggedFile { ) } - fn save_to(&self, file: &mut File) -> Result<()> { - self.inner.save_to(file) + fn save_to(&self, file: &mut File, write_options: WriteOptions) -> Result<()> { + self.inner.save_to(file, write_options) } fn properties(&self) -> &Self::Properties { diff --git a/src/flac/mod.rs b/src/flac/mod.rs index 500240eae..46f21b988 100644 --- a/src/flac/mod.rs +++ b/src/flac/mod.rs @@ -16,6 +16,7 @@ use crate::ogg::tag::VorbisCommentsRef; use crate::ogg::{OggPictureStorage, VorbisComments}; use crate::picture::{Picture, PictureInformation}; use crate::traits::TagExt; +use crate::write_options::WriteOptions; use std::fs::File; use std::io::Seek; @@ -55,9 +56,9 @@ pub struct FlacFile { impl FlacFile { // We need a special write fn to append our pictures into a `VorbisComments` tag - fn write_to(&self, file: &mut File) -> Result<()> { + fn write_to(&self, file: &mut File, write_options: WriteOptions) -> Result<()> { if let Some(ref id3v2) = self.id3v2_tag { - id3v2.save_to(file)?; + id3v2.save_to(file, write_options)?; file.rewind()?; } @@ -75,7 +76,7 @@ impl FlacFile { .map(|(p, i)| (p, *i)) .chain(self.pictures.iter().map(|(p, i)| (p, *i))), } - .write_to(file); + .write_to(file, write_options); } // We have pictures, but no vorbis comments tag, we'll need to create a dummy one @@ -85,7 +86,7 @@ impl FlacFile { items: std::iter::empty(), pictures: self.pictures.iter().map(|(p, i)| (p, *i)), } - .write_to(file); + .write_to(file, write_options); } Ok(()) diff --git a/src/flac/write.rs b/src/flac/write.rs index 5bb82b60a..6437b6c23 100644 --- a/src/flac/write.rs +++ b/src/flac/write.rs @@ -6,6 +6,7 @@ use crate::ogg::tag::VorbisCommentsRef; use crate::ogg::write::create_comments; use crate::picture::{Picture, PictureInformation}; use crate::tag::{Tag, TagType}; +use crate::write_options::WriteOptions; use std::fs::File; use std::io::{Cursor, Read, Seek, SeekFrom, Write}; @@ -14,7 +15,7 @@ use byteorder::{LittleEndian, WriteBytesExt}; const MAX_BLOCK_SIZE: u32 = 16_777_215; -pub(crate) fn write_to(file: &mut File, tag: &Tag) -> Result<()> { +pub(crate) fn write_to(file: &mut File, tag: &Tag, write_options: WriteOptions) -> Result<()> { match tag.tag_type() { TagType::VorbisComments => { let (vendor, items, pictures) = crate::ogg::tag::create_vorbis_comments_ref(tag); @@ -25,10 +26,10 @@ pub(crate) fn write_to(file: &mut File, tag: &Tag) -> Result<()> { pictures, }; - write_to_inner(file, &mut comments_ref) + write_to_inner(file, &mut comments_ref, write_options) }, // This tag can *only* be removed in this format - TagType::Id3v2 => crate::id3::v2::tag::Id3v2TagRef::empty().write_to(file), + TagType::Id3v2 => crate::id3::v2::tag::Id3v2TagRef::empty().write_to(file, write_options), _ => err!(UnsupportedTag), } } @@ -36,6 +37,7 @@ pub(crate) fn write_to(file: &mut File, tag: &Tag) -> Result<()> { pub(crate) fn write_to_inner<'a, II, IP>( file: &mut File, tag: &mut VorbisCommentsRef<'a, II, IP>, + _write_options: WriteOptions, ) -> Result<()> where II: Iterator, @@ -81,6 +83,7 @@ where let mut file_bytes = cursor.into_inner(); + // TODO: Respect preferred padding if !padding { log::warn!("File is missing a PADDING block. Adding one"); diff --git a/src/id3/v1/tag.rs b/src/id3/v1/tag.rs index dd32f66aa..ef6fcbc30 100644 --- a/src/id3/v1/tag.rs +++ b/src/id3/v1/tag.rs @@ -3,6 +3,7 @@ use crate::id3::v1::constants::GENRES; use crate::tag::item::{ItemKey, ItemValue, TagItem}; use crate::tag::{Tag, TagType}; use crate::traits::{Accessor, MergeTag, SplitTag, TagExt}; +use crate::write_options::WriteOptions; use std::borrow::Cow; use std::fs::File; @@ -243,8 +244,12 @@ impl TagExt for Id3v1Tag { && self.genre.is_none() } - fn save_to(&self, file: &mut File) -> std::result::Result<(), Self::Err> { - Into::>::into(self).write_to(file) + fn save_to( + &self, + file: &mut File, + write_options: WriteOptions, + ) -> std::result::Result<(), Self::Err> { + Into::>::into(self).write_to(file, write_options) } /// Dumps the tag to a writer @@ -252,8 +257,12 @@ impl TagExt for Id3v1Tag { /// # Errors /// /// * [`std::io::Error`] - fn dump_to(&self, writer: &mut W) -> std::result::Result<(), Self::Err> { - Into::>::into(self).dump_to(writer) + fn dump_to( + &self, + writer: &mut W, + write_options: WriteOptions, + ) -> std::result::Result<(), Self::Err> { + Into::>::into(self).dump_to(writer, write_options) } fn remove_from_path>(&self, path: P) -> std::result::Result<(), Self::Err> { @@ -413,11 +422,15 @@ impl<'a> Id3v1TagRef<'a> { && self.genre.is_none() } - pub(crate) fn write_to(&self, file: &mut File) -> Result<()> { - super::write::write_id3v1(file, self) + pub(crate) fn write_to(&self, file: &mut File, write_options: WriteOptions) -> Result<()> { + super::write::write_id3v1(file, self, write_options) } - pub(crate) fn dump_to(&mut self, writer: &mut W) -> Result<()> { + pub(crate) fn dump_to( + &mut self, + writer: &mut W, + _write_options: WriteOptions, + ) -> Result<()> { let temp = super::write::encode(self)?; writer.write_all(&temp)?; @@ -428,7 +441,7 @@ impl<'a> Id3v1TagRef<'a> { #[cfg(test)] mod tests { use crate::id3::v1::Id3v1Tag; - use crate::{Tag, TagExt, TagType}; + use crate::{Tag, TagExt, TagType, WriteOptions}; #[test] fn parse_id3v1() { @@ -454,7 +467,9 @@ mod tests { let parsed_tag = crate::id3::v1::read::parse_id3v1(tag.try_into().unwrap()); let mut writer = Vec::new(); - parsed_tag.dump_to(&mut writer).unwrap(); + parsed_tag + .dump_to(&mut writer, WriteOptions::new()) + .unwrap(); let temp_parsed_tag = crate::id3::v1::read::parse_id3v1(writer.try_into().unwrap()); diff --git a/src/id3/v1/write.rs b/src/id3/v1/write.rs index d2d066a23..0efd21d2d 100644 --- a/src/id3/v1/write.rs +++ b/src/id3/v1/write.rs @@ -3,6 +3,7 @@ use crate::error::Result; use crate::id3::{find_id3v1, ID3FindResults}; use crate::macros::err; use crate::probe::Probe; +use crate::write_options::WriteOptions; use std::fs::File; use std::io::{Cursor, Seek, Write}; @@ -10,7 +11,11 @@ use std::io::{Cursor, Seek, Write}; use byteorder::WriteBytesExt; #[allow(clippy::shadow_unrelated)] -pub(crate) fn write_id3v1(file: &mut File, tag: &Id3v1TagRef<'_>) -> Result<()> { +pub(crate) fn write_id3v1( + file: &mut File, + tag: &Id3v1TagRef<'_>, + _write_options: WriteOptions, +) -> Result<()> { let probe = Probe::new(file).guess_file_type()?; match probe.file_type() { diff --git a/src/id3/v2/tag.rs b/src/id3/v2/tag.rs index 33143bb13..144e3c022 100644 --- a/src/id3/v2/tag.rs +++ b/src/id3/v2/tag.rs @@ -21,6 +21,7 @@ use crate::tag::item::{ItemKey, ItemValue, TagItem}; use crate::tag::{try_parse_year, Tag, TagType}; use crate::traits::{Accessor, MergeTag, SplitTag, TagExt}; use crate::util::text::{decode_text, TextDecodeOptions, TextEncoding}; +use crate::write_options::WriteOptions; use std::borrow::Cow; use std::fs::File; @@ -913,12 +914,16 @@ impl TagExt for Id3v2Tag { /// * Attempting to write the tag to a format that does not support it /// * Attempting to write an encrypted frame without a valid method symbol or data length indicator /// * Attempting to write an invalid [`FrameId`]/[`FrameValue`] pairing - fn save_to(&self, file: &mut File) -> std::result::Result<(), Self::Err> { + fn save_to( + &self, + file: &mut File, + write_options: WriteOptions, + ) -> std::result::Result<(), Self::Err> { Id3v2TagRef { flags: self.flags, frames: self.frames.iter().filter_map(Frame::as_opt_ref), } - .write_to(file) + .write_to(file, write_options) } /// Dumps the tag to a writer @@ -927,12 +932,16 @@ impl TagExt for Id3v2Tag { /// /// * [`std::io::Error`] /// * [`ErrorKind::TooMuchData`](crate::error::ErrorKind::TooMuchData) - fn dump_to(&self, writer: &mut W) -> std::result::Result<(), Self::Err> { + fn dump_to( + &self, + writer: &mut W, + write_options: WriteOptions, + ) -> std::result::Result<(), Self::Err> { Id3v2TagRef { flags: self.flags, frames: self.frames.iter().filter_map(Frame::as_opt_ref), } - .dump_to(writer) + .dump_to(writer, write_options) } fn remove_from_path>(&self, path: P) -> std::result::Result<(), Self::Err> { @@ -1485,12 +1494,16 @@ pub(crate) fn tag_frames(tag: &Tag) -> impl Iterator> + Clon } impl<'a, I: Iterator> + Clone + 'a> Id3v2TagRef<'a, I> { - pub(crate) fn write_to(&mut self, file: &mut File) -> Result<()> { - super::write::write_id3v2(file, self) + pub(crate) fn write_to(&mut self, file: &mut File, write_options: WriteOptions) -> Result<()> { + super::write::write_id3v2(file, self, write_options) } - pub(crate) fn dump_to(&mut self, writer: &mut W) -> Result<()> { - let temp = super::write::create_tag(self)?; + pub(crate) fn dump_to( + &mut self, + writer: &mut W, + write_options: WriteOptions, + ) -> Result<()> { + let temp = super::write::create_tag(self, write_options)?; writer.write_all(&temp)?; Ok(()) diff --git a/src/id3/v2/tag/tests.rs b/src/id3/v2/tag/tests.rs index 0c10ec60e..16c8f0fba 100644 --- a/src/id3/v2/tag/tests.rs +++ b/src/id3/v2/tag/tests.rs @@ -119,7 +119,9 @@ fn id3v2_re_read() { let parsed_tag = read_tag("tests/tags/assets/id3v2/test.id3v24"); let mut writer = Vec::new(); - parsed_tag.dump_to(&mut writer).unwrap(); + parsed_tag + .dump_to(&mut writer, WriteOptions::new()) + .unwrap(); let temp_reader = &mut &*writer; @@ -200,7 +202,7 @@ fn fail_write_bad_frame() { flags: FrameFlags::default(), }); - let res = tag.dump_to(&mut Vec::::new()); + let res = tag.dump_to(&mut Vec::::new(), WriteOptions::new()); assert!(res.is_err()); assert_eq!( @@ -370,12 +372,12 @@ fn id3v24_footer() { tag.flags.footer = true; let mut writer = Vec::new(); - tag.dump_to(&mut writer).unwrap(); + tag.dump_to(&mut writer, WriteOptions::new()).unwrap(); let mut reader = &mut &writer[..]; let header = Id3v2Header::parse(&mut reader).unwrap(); - assert!(crate::id3::v2::read::parse_id3v2(reader, header, ParsingMode::Strict).is_ok()); + let _ = crate::id3::v2::read::parse_id3v2(reader, header, ParsingMode::Strict).unwrap(); assert_eq!(writer[3..10], writer[writer.len() - 7..]) } @@ -395,7 +397,7 @@ fn issue_36() { tag.push_picture(picture.clone()); let mut writer = Vec::new(); - tag.dump_to(&mut writer).unwrap(); + tag.dump_to(&mut writer, WriteOptions::new()).unwrap(); let mut reader = &mut &writer[..]; @@ -710,7 +712,7 @@ fn user_defined_frames_conversion() { assert_eq!(tag.len(), 1); let mut content = Vec::new(); - tag.dump_to(&mut content).unwrap(); + tag.dump_to(&mut content, WriteOptions::new()).unwrap(); assert!(!content.is_empty()); // And verify we can reread the tag diff --git a/src/id3/v2/write/chunk_file.rs b/src/id3/v2/write/chunk_file.rs index dee22f7c8..1ee318edd 100644 --- a/src/id3/v2/write/chunk_file.rs +++ b/src/id3/v2/write/chunk_file.rs @@ -1,12 +1,20 @@ use crate::error::Result; use crate::iff::chunk::Chunks; +use crate::write_options::WriteOptions; use std::fs::File; use std::io::{Read, Seek, SeekFrom, Write}; use byteorder::{ByteOrder, WriteBytesExt}; -pub(in crate::id3::v2) fn write_to_chunk_file(data: &mut File, tag: &[u8]) -> Result<()> +const CHUNK_NAME_UPPER: [u8; 4] = [b'I', b'D', b'3', b' ']; +const CHUNK_NAME_LOWER: [u8; 4] = [b'i', b'd', b'3', b' ']; + +pub(in crate::id3::v2) fn write_to_chunk_file( + data: &mut File, + tag: &[u8], + write_options: WriteOptions, +) -> Result<()> where B: ByteOrder, { @@ -20,7 +28,7 @@ where let mut chunks = Chunks::::new(file_len); while chunks.next(data).is_ok() { - if &chunks.fourcc == b"ID3 " || &chunks.fourcc == b"id3 " { + if chunks.fourcc == CHUNK_NAME_UPPER || chunks.fourcc == CHUNK_NAME_LOWER { id3v2_chunk = (Some(data.stream_position()? - 8), Some(chunks.size)); break; } @@ -53,7 +61,13 @@ where if !tag.is_empty() { data.seek(SeekFrom::End(0))?; - data.write_all(b"ID3 ")?; + + if write_options.uppercase_id3v2_chunk { + data.write_all(&CHUNK_NAME_UPPER)?; + } else { + data.write_all(&CHUNK_NAME_LOWER)?; + } + data.write_u32::(tag.len() as u32)?; data.write_all(tag)?; diff --git a/src/id3/v2/write/mod.rs b/src/id3/v2/write/mod.rs index 412f3e510..39ff1cdf8 100644 --- a/src/id3/v2/write/mod.rs +++ b/src/id3/v2/write/mod.rs @@ -9,8 +9,9 @@ use crate::id3::v2::tag::Id3v2TagRef; use crate::id3::v2::util::synchsafe::SynchsafeInteger; use crate::id3::v2::Id3v2Tag; use crate::id3::{find_id3v2, FindId3v2Config}; -use crate::macros::err; +use crate::macros::{err, try_vec}; use crate::probe::Probe; +use crate::write_options::WriteOptions; use std::fs::File; use std::io::{Cursor, Read, Seek, SeekFrom, Write}; @@ -40,6 +41,7 @@ fn crc_32_table() -> &'static [u32; 256] { pub(crate) fn write_id3v2<'a, I: Iterator> + Clone + 'a>( data: &mut File, tag: &mut Id3v2TagRef<'a, I>, + write_options: WriteOptions, ) -> Result<()> { let probe = Probe::new(data).guess_file_type()?; let file_type = probe.file_type(); @@ -66,21 +68,21 @@ pub(crate) fn write_id3v2<'a, I: Iterator> + Clone + 'a>( } } + let id3v2 = create_tag(tag, write_options)?; + match file_type { // Formats such as WAV and AIFF store the ID3v2 tag in an 'ID3 ' chunk rather than at the beginning of the file FileType::Wav => { tag.flags.footer = false; - return chunk_file::write_to_chunk_file::(data, &create_tag(tag)?); + return chunk_file::write_to_chunk_file::(data, &id3v2, write_options); }, FileType::Aiff => { tag.flags.footer = false; - return chunk_file::write_to_chunk_file::(data, &create_tag(tag)?); + return chunk_file::write_to_chunk_file::(data, &id3v2, write_options); }, _ => {}, } - let id3v2 = create_tag(tag)?; - // find_id3v2 will seek us to the end of the tag // TODO: Search through junk find_id3v2(data, FindId3v2Config::NO_READ_TAG)?; @@ -99,6 +101,7 @@ pub(crate) fn write_id3v2<'a, I: Iterator> + Clone + 'a>( pub(super) fn create_tag<'a, I: Iterator> + 'a>( tag: &mut Id3v2TagRef<'a, I>, + write_options: WriteOptions, ) -> Result> { let frames = &mut tag.frames; let mut peek = frames.peekable(); @@ -118,7 +121,15 @@ pub(super) fn create_tag<'a, I: Iterator> + 'a>( // Write the items frame::create_items(&mut id3v2, &mut peek)?; - let len = id3v2.get_ref().len() - header_len; + let mut len = id3v2.get_ref().len() - header_len; + + // https://mutagen-specs.readthedocs.io/en/latest/id3/id3v2.4.0-structure.html#padding: + // + // "[A tag] MUST NOT have any padding when a tag footer is added to the tag" + let padding_len = write_options.preferred_padding.unwrap_or(0) as usize; + if !has_footer { + len += padding_len; + } // Go back to the start and write the final size id3v2.seek(SeekFrom::Start(6))?; @@ -148,6 +159,8 @@ pub(super) fn create_tag<'a, I: Iterator> + 'a>( } if has_footer { + log::trace!("Footer requested, not padding tag"); + id3v2.seek(SeekFrom::Start(3))?; let mut header_without_identifier = [0; 7]; @@ -157,8 +170,20 @@ pub(super) fn create_tag<'a, I: Iterator> + 'a>( // The footer is the same as the header, but with the identifier reversed id3v2.write_all(b"3DI")?; id3v2.write_all(&header_without_identifier)?; + + return Ok(id3v2.into_inner()); + } + + if padding_len == 0 { + log::trace!("No padding requested, writing tag as-is"); + return Ok(id3v2.into_inner()); } + log::trace!("Padding tag with {} bytes", padding_len); + + id3v2.seek(SeekFrom::End(0))?; + id3v2.write_all(&try_vec![0; padding_len])?; + Ok(id3v2.into_inner()) } @@ -259,7 +284,7 @@ fn calculate_crc(content: &[u8]) -> [u8; 5] { #[cfg(test)] mod tests { use crate::id3::v2::{Id3v2Tag, Id3v2TagFlags}; - use crate::{Accessor, TagExt}; + use crate::{Accessor, TagExt, WriteOptions}; #[test] fn id3v2_write_crc32() { @@ -273,7 +298,7 @@ mod tests { tag.set_flags(flags); let mut writer = Vec::new(); - tag.dump_to(&mut writer).unwrap(); + tag.dump_to(&mut writer, WriteOptions::new()).unwrap(); let crc_content = &writer[16..22]; assert_eq!(crc_content, &[5, 0x06, 0x35, 0x69, 0x7D, 0x14]); diff --git a/src/iff/aiff/tag.rs b/src/iff/aiff/tag.rs index 68efc958b..c3e8ff853 100644 --- a/src/iff/aiff/tag.rs +++ b/src/iff/aiff/tag.rs @@ -4,6 +4,7 @@ use crate::macros::err; use crate::tag::item::{ItemKey, ItemValue, TagItem}; use crate::tag::{Tag, TagType}; use crate::traits::{Accessor, MergeTag, SplitTag, TagExt}; +use crate::write_options::WriteOptions; use std::borrow::Cow; use std::fs::File; @@ -188,7 +189,11 @@ impl TagExt for AIFFTextChunks { ) } - fn save_to(&self, file: &mut File) -> std::result::Result<(), Self::Err> { + fn save_to( + &self, + file: &mut File, + write_options: WriteOptions, + ) -> std::result::Result<(), Self::Err> { AiffTextChunksRef { name: self.name.as_deref(), author: self.author.as_deref(), @@ -196,10 +201,14 @@ impl TagExt for AIFFTextChunks { annotations: self.annotations.as_deref(), comments: self.comments.as_deref(), } - .write_to(file) + .write_to(file, write_options) } - fn dump_to(&self, writer: &mut W) -> std::result::Result<(), Self::Err> { + fn dump_to( + &self, + writer: &mut W, + write_options: WriteOptions, + ) -> std::result::Result<(), Self::Err> { AiffTextChunksRef { name: self.name.as_deref(), author: self.author.as_deref(), @@ -207,7 +216,7 @@ impl TagExt for AIFFTextChunks { annotations: self.annotations.as_deref(), comments: self.comments.as_deref(), } - .dump_to(writer) + .dump_to(writer, write_options) } fn remove_from_path>(&self, path: P) -> std::result::Result<(), Self::Err> { @@ -308,11 +317,15 @@ where T: AsRef, AI: IntoIterator, { - pub(crate) fn write_to(self, file: &mut File) -> Result<()> { + pub(crate) fn write_to(self, file: &mut File, _write_options: WriteOptions) -> Result<()> { AiffTextChunksRef::write_to_inner(file, self) } - pub(crate) fn dump_to(&mut self, writer: &mut W) -> Result<()> { + pub(crate) fn dump_to( + &mut self, + writer: &mut W, + _write_options: WriteOptions, + ) -> Result<()> { let temp = Self::create_text_chunks(self)?; writer.write_all(&temp)?; @@ -471,7 +484,7 @@ where #[cfg(test)] mod tests { use crate::iff::aiff::{AIFFTextChunks, Comment}; - use crate::{ItemKey, ItemValue, Tag, TagExt, TagItem, TagType}; + use crate::{ItemKey, ItemValue, Tag, TagExt, TagItem, TagType, WriteOptions}; use crate::probe::ParseOptions; use std::io::Cursor; @@ -528,7 +541,9 @@ mod tests { let mut writer = vec![ b'F', b'O', b'R', b'M', 0, 0, 0, 0xC6, b'A', b'I', b'F', b'F', ]; - parsed_tag.dump_to(&mut writer).unwrap(); + parsed_tag + .dump_to(&mut writer, WriteOptions::new()) + .unwrap(); let temp_parsed_tag = super::super::read::read_from( &mut Cursor::new(writer), diff --git a/src/iff/wav/tag/mod.rs b/src/iff/wav/tag/mod.rs index fbde70ef7..8fdb4b577 100644 --- a/src/iff/wav/tag/mod.rs +++ b/src/iff/wav/tag/mod.rs @@ -5,6 +5,7 @@ use crate::error::{LoftyError, Result}; use crate::tag::item::{ItemKey, ItemValue, TagItem}; use crate::tag::{try_parse_year, Tag, TagType}; use crate::traits::{Accessor, MergeTag, SplitTag, TagExt}; +use crate::write_options::WriteOptions; use std::borrow::Cow; use std::fs::File; @@ -203,14 +204,22 @@ impl TagExt for RIFFInfoList { self.items.is_empty() } - fn save_to(&self, file: &mut File) -> std::result::Result<(), Self::Err> { + fn save_to( + &self, + file: &mut File, + write_options: WriteOptions, + ) -> std::result::Result<(), Self::Err> { RIFFInfoListRef::new(self.items.iter().map(|(k, v)| (k.as_str(), v.as_str()))) - .write_to(file) + .write_to(file, write_options) } - fn dump_to(&self, writer: &mut W) -> std::result::Result<(), Self::Err> { + fn dump_to( + &self, + writer: &mut W, + write_options: WriteOptions, + ) -> std::result::Result<(), Self::Err> { RIFFInfoListRef::new(self.items.iter().map(|(k, v)| (k.as_str(), v.as_str()))) - .dump_to(writer) + .dump_to(writer, write_options) } fn remove_from_path>(&self, path: P) -> std::result::Result<(), Self::Err> { @@ -302,11 +311,15 @@ where RIFFInfoListRef { items } } - pub(crate) fn write_to(&mut self, file: &mut File) -> Result<()> { - write::write_riff_info(file, self) + pub(crate) fn write_to(&mut self, file: &mut File, write_options: WriteOptions) -> Result<()> { + write::write_riff_info(file, self, write_options) } - pub(crate) fn dump_to(&mut self, writer: &mut W) -> Result<()> { + pub(crate) fn dump_to( + &mut self, + writer: &mut W, + _write_options: WriteOptions, + ) -> Result<()> { let mut temp = Vec::new(); write::create_riff_info(&mut self.items, &mut temp)?; @@ -336,7 +349,7 @@ pub(crate) fn tagitems_into_riff<'a>( #[cfg(test)] mod tests { use crate::iff::wav::RIFFInfoList; - use crate::{Tag, TagExt, TagType}; + use crate::{Tag, TagExt, TagType, WriteOptions}; use crate::iff::chunk::Chunks; use byteorder::LittleEndian; @@ -381,7 +394,9 @@ mod tests { .unwrap(); let mut writer = Vec::new(); - parsed_tag.dump_to(&mut writer).unwrap(); + parsed_tag + .dump_to(&mut writer, WriteOptions::new()) + .unwrap(); let mut temp_parsed_tag = RIFFInfoList::default(); diff --git a/src/iff/wav/tag/write.rs b/src/iff/wav/tag/write.rs index 125d73031..58f727f3a 100644 --- a/src/iff/wav/tag/write.rs +++ b/src/iff/wav/tag/write.rs @@ -3,6 +3,7 @@ use crate::error::Result; use crate::iff::chunk::Chunks; use crate::iff::wav::read::verify_wav; use crate::macros::err; +use crate::write_options::WriteOptions; use std::fs::File; use std::io::{Read, Seek, SeekFrom, Write}; @@ -12,6 +13,7 @@ use byteorder::{LittleEndian, WriteBytesExt}; pub(in crate::iff::wav) fn write_riff_info<'a, I>( data: &mut File, tag: &mut RIFFInfoListRef<'a, I>, + _write_options: WriteOptions, ) -> Result<()> where I: Iterator, diff --git a/src/lib.rs b/src/lib.rs index 39002f5d4..a9ce4e86e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -172,10 +172,12 @@ pub(crate) mod tag; mod traits; mod util; pub mod wavpack; +mod write_options; pub use crate::error::{LoftyError, Result}; pub use crate::probe::{read_from, read_from_path, ParseOptions, ParsingMode, Probe}; +pub use crate::write_options::WriteOptions; pub use crate::file::{AudioFile, BoundTaggedFile, FileType, TaggedFile, TaggedFileExt}; pub use crate::picture::{MimeType, Picture, PictureType}; diff --git a/src/mp4/ilst/mod.rs b/src/mp4/ilst/mod.rs index 60ed5ee8d..72e716234 100644 --- a/src/mp4/ilst/mod.rs +++ b/src/mp4/ilst/mod.rs @@ -11,10 +11,11 @@ use crate::picture::{Picture, PictureType, TOMBSTONE_PICTURE}; use crate::tag::item::{ItemKey, ItemValue, TagItem}; use crate::tag::{try_parse_year, Tag, TagType}; use crate::traits::{Accessor, MergeTag, SplitTag, TagExt}; +use crate::write_options::WriteOptions; use atom::{AdvisoryRating, Atom, AtomData}; use std::borrow::Cow; -use std::fs::{File, OpenOptions}; +use std::fs::File; use std::io::Write; use std::ops::Deref; use std::path::Path; @@ -527,17 +528,20 @@ impl TagExt for Ilst { self.atoms.is_empty() } - fn save_to_path>(&self, path: P) -> std::result::Result<(), Self::Err> { - let mut f = OpenOptions::new().read(true).write(true).open(path)?; - self.save_to(&mut f) + fn save_to( + &self, + file: &mut File, + write_options: WriteOptions, + ) -> std::result::Result<(), Self::Err> { + self.as_ref().write_to(file, write_options) } - fn save_to(&self, file: &mut File) -> std::result::Result<(), Self::Err> { - self.as_ref().write_to(file) - } - - fn dump_to(&self, writer: &mut W) -> std::result::Result<(), Self::Err> { - self.as_ref().dump_to(writer) + fn dump_to( + &self, + writer: &mut W, + write_options: WriteOptions, + ) -> std::result::Result<(), Self::Err> { + self.as_ref().dump_to(writer, write_options) } fn remove_from_path>(&self, path: P) -> std::result::Result<(), Self::Err> { @@ -763,7 +767,7 @@ mod tests { use crate::tag::utils::test_utils::read_path; use crate::{ Accessor as _, AudioFile, ItemKey, ItemValue, ParseOptions, ParsingMode, SplitTag as _, - Tag, TagExt as _, TagItem, TagType, + Tag, TagExt as _, TagItem, TagType, WriteOptions, }; use std::io::{Cursor, Read as _, Seek as _, Write as _}; @@ -861,7 +865,9 @@ mod tests { let parsed_tag = read_ilst_strict("tests/tags/assets/ilst/test.ilst"); let mut writer = Vec::new(); - parsed_tag.dump_to(&mut writer).unwrap(); + parsed_tag + .dump_to(&mut writer, WriteOptions::new()) + .unwrap(); let cursor = Cursor::new(&writer[8..]); let mut reader = AtomReader::new(cursor, crate::ParsingMode::Strict).unwrap(); @@ -1018,7 +1024,7 @@ mod tests { file.rewind().unwrap(); ilst.set_title(String::from("Exactly 21 Characters")); - ilst.save_to(&mut file).unwrap(); + ilst.save_to(&mut file, WriteOptions::new()).unwrap(); // Now verify the free atom file.rewind().unwrap(); @@ -1073,7 +1079,7 @@ mod tests { data: AtomDataStorage::Single(AtomData::UTF8(String::from("Foo artist"))), }); - tag.save_to(&mut file).unwrap(); + tag.save_to(&mut file, WriteOptions::new()).unwrap(); file.rewind().unwrap(); let mp4_file = Mp4File::read_from(&mut file, ParseOptions::new()).unwrap(); diff --git a/src/mp4/ilst/ref.rs b/src/mp4/ilst/ref.rs index 7228b11d1..0bd44a999 100644 --- a/src/mp4/ilst/ref.rs +++ b/src/mp4/ilst/ref.rs @@ -4,6 +4,7 @@ use crate::error::Result; use crate::mp4::{Atom, AtomData, AtomIdent, Ilst}; +use crate::write_options::WriteOptions; use std::fs::File; use std::io::Write; @@ -24,11 +25,15 @@ impl<'a, I: 'a> IlstRef<'a, I> where I: IntoIterator, { - pub(crate) fn write_to(&mut self, file: &mut File) -> Result<()> { - super::write::write_to(file, self) + pub(crate) fn write_to(&mut self, file: &mut File, write_options: WriteOptions) -> Result<()> { + super::write::write_to(file, self, write_options) } - pub(crate) fn dump_to(&mut self, writer: &mut W) -> Result<()> { + pub(crate) fn dump_to( + &mut self, + writer: &mut W, + _write_options: WriteOptions, + ) -> Result<()> { let temp = super::write::build_ilst(&mut self.atoms)?; writer.write_all(&temp)?; diff --git a/src/mp4/ilst/write.rs b/src/mp4/ilst/write.rs index 7f182e844..21f59bb10 100644 --- a/src/mp4/ilst/write.rs +++ b/src/mp4/ilst/write.rs @@ -9,6 +9,7 @@ use crate::mp4::write::{AtomWriter, AtomWriterCompanion, ContextualAtom}; use crate::mp4::AtomData; use crate::picture::{MimeType, Picture}; use crate::probe::ParseOptions; +use crate::write_options::WriteOptions; use std::fs::File; use std::io::{Cursor, Seek, SeekFrom, Write}; @@ -18,10 +19,13 @@ use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt}; // A "full" atom is a traditional length + identifier, followed by a version (1) and flags (3) const FULL_ATOM_SIZE: u64 = ATOM_HEADER_LEN + 4; const HDLR_SIZE: u64 = ATOM_HEADER_LEN + 25; -const DEFAULT_PADDING_SIZE: u32 = 1024; // TODO: We are forcing the use of ParseOptions::DEFAULT_PARSING_MODE. This is not good. It should be caller-specified. -pub(crate) fn write_to<'a, I: 'a>(data: &mut File, tag: &mut IlstRef<'a, I>) -> Result<()> +pub(crate) fn write_to<'a, I: 'a>( + data: &mut File, + tag: &mut IlstRef<'a, I>, + write_options: WriteOptions, +) -> Result<()> where I: IntoIterator, { @@ -126,6 +130,7 @@ where &mut new_udta_size, ilst, remove_tag, + write_options, )? }, // We have to create the `meta` atom @@ -207,6 +212,7 @@ fn save_to_existing( new_udta_size: &mut u64, ilst: Vec, remove_tag: bool, + write_options: WriteOptions, ) -> Result<()> { let mut replacement; let range; @@ -303,7 +309,8 @@ fn save_to_existing( log::trace!("Tag size changed, attempting to avoid offset update"); let mut ilst_writer = Cursor::new(replacement); - let (atom_size_difference, padding_size) = pad_atom(&mut ilst_writer, difference)?; + let (atom_size_difference, padding_size) = + pad_atom(&mut ilst_writer, difference, write_options)?; replacement = ilst_writer.into_inner(); new_meta_size += padding_size; @@ -339,7 +346,11 @@ fn save_to_existing( Ok(()) } -fn pad_atom(writer: &mut W, mut atom_size_difference: i64) -> Result<(i64, u64)> +fn pad_atom( + writer: &mut W, + mut atom_size_difference: i64, + write_options: WriteOptions, +) -> Result<(i64, u64)> where W: Write + Seek, { @@ -354,7 +365,7 @@ where let padding_size: u64; let diff_abs = atom_size_difference.abs(); - if diff_abs >= 8 { + if diff_abs >= ATOM_HEADER_LEN as i64 { log::trace!( "Avoiding offset update, padding atom with {} bytes", diff_abs @@ -365,19 +376,26 @@ where write_free_atom(writer, diff_abs as u32)?; atom_size_difference = 0; padding_size = diff_abs as u64; - } else { - log::trace!( - "Cannot avoid offset update, padding atom with {} bytes", - DEFAULT_PADDING_SIZE - ); - // Otherwise, we'll have to just pad the default amount, - // and update the offsets. - write_free_atom(writer, DEFAULT_PADDING_SIZE)?; - atom_size_difference += i64::from(DEFAULT_PADDING_SIZE); - padding_size = u64::from(DEFAULT_PADDING_SIZE); + return Ok((atom_size_difference, padding_size)); } + let Some(preferred_padding) = write_options.preferred_padding else { + log::trace!("Cannot avoid offset update, not padding atom"); + return Ok((atom_size_difference, 0)); + }; + + log::trace!( + "Cannot avoid offset update, padding atom with {} bytes", + preferred_padding + ); + + // Otherwise, we'll have to just pad the default amount, + // and update the offsets. + write_free_atom(writer, preferred_padding as u32)?; + atom_size_difference += i64::from(preferred_padding); + padding_size = u64::from(preferred_padding); + Ok((atom_size_difference, padding_size)) } diff --git a/src/ogg/tag.rs b/src/ogg/tag.rs index f8569ff2c..b23c1f039 100644 --- a/src/ogg/tag.rs +++ b/src/ogg/tag.rs @@ -8,6 +8,7 @@ use crate::probe::Probe; use crate::tag::item::{ItemKey, ItemValue, TagItem}; use crate::tag::{try_parse_year, Tag, TagType}; use crate::traits::{Accessor, MergeTag, SplitTag, TagExt}; +use crate::write_options::WriteOptions; use std::borrow::Cow; use std::fs::File; @@ -454,13 +455,17 @@ impl TagExt for VorbisComments { /// * The file does not contain valid packets /// * [`PictureInformation::from_picture`] /// * [`std::io::Error`] - fn save_to(&self, file: &mut File) -> std::result::Result<(), Self::Err> { + fn save_to( + &self, + file: &mut File, + write_options: WriteOptions, + ) -> std::result::Result<(), Self::Err> { VorbisCommentsRef { vendor: self.vendor.as_str(), items: self.items.iter().map(|(k, v)| (k.as_str(), v.as_str())), pictures: self.pictures.iter().map(|(p, i)| (p, *i)), } - .write_to(file) + .write_to(file, write_options) } /// Dumps the tag to a writer @@ -472,13 +477,17 @@ impl TagExt for VorbisComments { /// /// * [`PictureInformation::from_picture`] /// * [`std::io::Error`] - fn dump_to(&self, writer: &mut W) -> std::result::Result<(), Self::Err> { + fn dump_to( + &self, + writer: &mut W, + write_options: WriteOptions, + ) -> std::result::Result<(), Self::Err> { VorbisCommentsRef { vendor: self.vendor.as_str(), items: self.items.iter().map(|(k, v)| (k.as_str(), v.as_str())), pictures: self.pictures.iter().map(|(p, i)| (p, *i)), } - .dump_to(writer) + .dump_to(writer, write_options) } fn remove_from_path>(&self, path: P) -> std::result::Result<(), Self::Err> { @@ -625,7 +634,7 @@ where IP: Iterator, { #[allow(clippy::shadow_unrelated)] - pub(crate) fn write_to(&mut self, file: &mut File) -> Result<()> { + pub(crate) fn write_to(&mut self, file: &mut File, write_options: WriteOptions) -> Result<()> { let probe = Probe::new(file).guess_file_type()?; let f_ty = probe.file_type(); @@ -638,15 +647,19 @@ where // FLAC has its own special writing needs :) if file_type == FileType::Flac { - return crate::flac::write::write_to_inner(file, self); + return crate::flac::write::write_to_inner(file, self, write_options); } let (format, header_packet_count) = OGGFormat::from_filetype(file_type); - super::write::write(file, self, format, header_packet_count) + super::write::write(file, self, format, header_packet_count, write_options) } - pub(crate) fn dump_to(&mut self, writer: &mut W) -> Result<()> { + pub(crate) fn dump_to( + &mut self, + writer: &mut W, + _write_options: WriteOptions, + ) -> Result<()> { let metadata_packet = super::write::create_metadata_packet(self, &[], self.vendor.as_bytes(), false)?; writer.write_all(&metadata_packet)?; @@ -683,7 +696,7 @@ mod tests { use crate::ogg::{OggPictureStorage, VorbisComments}; use crate::{ ItemKey, ItemValue, MergeTag as _, ParsingMode, SplitTag as _, Tag, TagExt as _, TagItem, - TagType, + TagType, WriteOptions, }; fn read_tag(tag: &[u8]) -> VorbisComments { @@ -721,7 +734,9 @@ mod tests { parsed_tag.vendor = String::new(); let mut writer = Vec::new(); - parsed_tag.dump_to(&mut writer).unwrap(); + parsed_tag + .dump_to(&mut writer, WriteOptions::new()) + .unwrap(); let temp_parsed_tag = read_tag(&writer); diff --git a/src/ogg/write.rs b/src/ogg/write.rs index e44527098..f3a945e21 100644 --- a/src/ogg/write.rs +++ b/src/ogg/write.rs @@ -6,6 +6,7 @@ use crate::ogg::constants::{OPUSTAGS, VORBIS_COMMENT_HEAD}; use crate::ogg::tag::{create_vorbis_comments_ref, VorbisCommentsRef}; use crate::picture::{Picture, PictureInformation}; use crate::tag::{Tag, TagType}; +use crate::write_options::WriteOptions; use std::fs::File; use std::io::{Cursor, Read, Seek, SeekFrom, Write}; @@ -39,7 +40,12 @@ impl OGGFormat { } } -pub(crate) fn write_to(file: &mut File, tag: &Tag, file_type: FileType) -> Result<()> { +pub(crate) fn write_to( + file: &mut File, + tag: &Tag, + file_type: FileType, + write_options: WriteOptions, +) -> Result<()> { if tag.tag_type() != TagType::VorbisComments { err!(UnsupportedTag); } @@ -54,7 +60,13 @@ pub(crate) fn write_to(file: &mut File, tag: &Tag, file_type: FileType) -> Resul let (format, header_packet_count) = OGGFormat::from_filetype(file_type); - write(file, &mut comments_ref, format, header_packet_count) + write( + file, + &mut comments_ref, + format, + header_packet_count, + write_options, + ) } pub(super) fn write<'a, II, IP>( @@ -62,6 +74,7 @@ pub(super) fn write<'a, II, IP>( tag: &mut VorbisCommentsRef<'a, II, IP>, format: OGGFormat, header_packet_count: isize, + _write_options: WriteOptions, ) -> Result<()> where II: Iterator, diff --git a/src/tag/mod.rs b/src/tag/mod.rs index c6f729ef9..bd8aa0178 100644 --- a/src/tag/mod.rs +++ b/src/tag/mod.rs @@ -9,6 +9,7 @@ use crate::probe::Probe; use crate::traits::{Accessor, MergeTag, SplitTag, TagExt}; use item::{ItemKey, ItemValue, TagItem}; +use crate::WriteOptions; use std::borrow::Cow; use std::fs::{File, OpenOptions}; use std::io::Write; @@ -532,13 +533,17 @@ impl TagExt for Tag { /// /// * A [`FileType`](crate::FileType) couldn't be determined from the File /// * Attempting to write a tag to a format that does not support it. See [`FileType::supports_tag_type`](crate::FileType::supports_tag_type) - fn save_to(&self, file: &mut File) -> std::result::Result<(), Self::Err> { + fn save_to( + &self, + file: &mut File, + write_options: WriteOptions, + ) -> std::result::Result<(), Self::Err> { let probe = Probe::new(file).guess_file_type()?; match probe.file_type() { Some(file_type) => { if file_type.supports_tag_type(self.tag_type()) { - utils::write_tag(self, probe.into_inner(), file_type) + utils::write_tag(self, probe.into_inner(), file_type, write_options) } else { err!(UnsupportedTag); } @@ -547,8 +552,8 @@ impl TagExt for Tag { } } - fn dump_to(&self, writer: &mut W) -> Result<()> { - utils::dump_tag(self, writer) + fn dump_to(&self, writer: &mut W, write_options: WriteOptions) -> Result<()> { + utils::dump_tag(self, writer, write_options) } /// Remove a tag from a [`Path`] @@ -651,7 +656,7 @@ impl TagType { } let file = probe.into_inner(); - utils::write_tag(&Tag::new(*self), file, file_type) + utils::write_tag(&Tag::new(*self), file, file_type, WriteOptions::new()) // TODO } } @@ -659,7 +664,7 @@ impl TagType { mod tests { use super::try_parse_year; use crate::tag::utils::test_utils::read_path; - use crate::{Accessor, Picture, PictureType, Tag, TagExt, TagType}; + use crate::{Accessor, Picture, PictureType, Tag, TagExt, TagType, WriteOptions}; use std::io::{Seek, Write}; use std::process::Command; @@ -677,7 +682,8 @@ mod tests { picture.set_pic_type(PictureType::CoverFront); tag.push_picture(picture); - tag.save_to(temp_file.as_file_mut()).unwrap(); + tag.save_to(temp_file.as_file_mut(), WriteOptions::new()) + .unwrap(); let cmd_output = Command::new("ffprobe") .arg(temp_file.path().to_str().unwrap()) @@ -719,7 +725,8 @@ mod tests { picture.set_pic_type(PictureType::CoverFront); tag.push_picture(picture); - tag.save_to(temp_file.as_file_mut()).unwrap(); + tag.save_to(temp_file.as_file_mut(), WriteOptions::new()) + .unwrap(); let cmd_output = Command::new("opusinfo") .arg(temp_file.path().to_str().unwrap()) diff --git a/src/tag/utils.rs b/src/tag/utils.rs index 97a62b5a5..9ecc7c279 100644 --- a/src/tag/utils.rs +++ b/src/tag/utils.rs @@ -2,6 +2,7 @@ use crate::error::Result; use crate::file::FileType; use crate::macros::err; use crate::tag::{Tag, TagType}; +use crate::write_options::WriteOptions; use crate::{aac, ape, flac, iff, mpeg, musepack, wavpack}; use crate::id3::v1::tag::Id3v1TagRef; @@ -17,41 +18,54 @@ use std::fs::File; use std::io::Write; #[allow(unreachable_patterns)] -pub(crate) fn write_tag(tag: &Tag, file: &mut File, file_type: FileType) -> Result<()> { +pub(crate) fn write_tag( + tag: &Tag, + file: &mut File, + file_type: FileType, + write_options: WriteOptions, +) -> Result<()> { match file_type { - FileType::Aac => aac::write::write_to(file, tag), - FileType::Aiff => iff::aiff::write::write_to(file, tag), - FileType::Ape => ape::write::write_to(file, tag), - FileType::Flac => flac::write::write_to(file, tag), + FileType::Aac => aac::write::write_to(file, tag, write_options), + FileType::Aiff => iff::aiff::write::write_to(file, tag, write_options), + FileType::Ape => ape::write::write_to(file, tag, write_options), + FileType::Flac => flac::write::write_to(file, tag, write_options), FileType::Opus | FileType::Speex | FileType::Vorbis => { - crate::ogg::write::write_to(file, tag, file_type) + crate::ogg::write::write_to(file, tag, file_type, write_options) }, - FileType::Mpc => musepack::write::write_to(file, tag), - FileType::Mpeg => mpeg::write::write_to(file, tag), - FileType::Mp4 => { - crate::mp4::ilst::write::write_to(file, &mut Into::::into(tag.clone()).as_ref()) - }, - FileType::Wav => iff::wav::write::write_to(file, tag), - FileType::WavPack => wavpack::write::write_to(file, tag), + FileType::Mpc => musepack::write::write_to(file, tag, write_options), + FileType::Mpeg => mpeg::write::write_to(file, tag, write_options), + FileType::Mp4 => crate::mp4::ilst::write::write_to( + file, + &mut Into::::into(tag.clone()).as_ref(), + write_options, + ), + FileType::Wav => iff::wav::write::write_to(file, tag, write_options), + FileType::WavPack => wavpack::write::write_to(file, tag, write_options), _ => err!(UnsupportedTag), } } #[allow(unreachable_patterns)] -pub(crate) fn dump_tag(tag: &Tag, writer: &mut W) -> Result<()> { +pub(crate) fn dump_tag( + tag: &Tag, + writer: &mut W, + write_options: WriteOptions, +) -> Result<()> { match tag.tag_type() { TagType::Ape => ApeTagRef { read_only: false, items: ape::tag::tagitems_into_ape(tag), } - .dump_to(writer), - TagType::Id3v1 => Into::>::into(tag).dump_to(writer), + .dump_to(writer, write_options), + TagType::Id3v1 => Into::>::into(tag).dump_to(writer, write_options), TagType::Id3v2 => Id3v2TagRef { flags: Id3v2TagFlags::default(), frames: v2::tag::tag_frames(tag), } - .dump_to(writer), - TagType::Mp4Ilst => Into::::into(tag.clone()).as_ref().dump_to(writer), + .dump_to(writer, write_options), + TagType::Mp4Ilst => Into::::into(tag.clone()) + .as_ref() + .dump_to(writer, write_options), TagType::VorbisComments => { let (vendor, items, pictures) = create_vorbis_comments_ref(tag); @@ -60,12 +74,12 @@ pub(crate) fn dump_tag(tag: &Tag, writer: &mut W) -> Result<()> { items, pictures, } - .dump_to(writer) + .dump_to(writer, write_options) }, TagType::RiffInfo => RIFFInfoListRef { items: iff::wav::tag::tagitems_into_riff(tag.items()), } - .dump_to(writer), + .dump_to(writer, write_options), TagType::AiffText => { use crate::tag::item::ItemKey; @@ -77,7 +91,7 @@ pub(crate) fn dump_tag(tag: &Tag, writer: &mut W) -> Result<()> { comments: None, } } - .dump_to(writer), + .dump_to(writer, write_options), _ => Ok(()), } } diff --git a/src/traits.rs b/src/traits.rs index f46779f12..d7c62f2f8 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -119,6 +119,7 @@ accessor_trait! { } use crate::tag::Tag; +use crate::write_options::WriteOptions; use std::fs::File; use std::path::Path; @@ -194,12 +195,17 @@ pub trait TagExt: Accessor + Into + Sized { /// * Path doesn't exist /// * Path is not writable /// * See [`TagExt::save_to`] - fn save_to_path>(&self, path: P) -> std::result::Result<(), Self::Err> { + fn save_to_path>( + &self, + path: P, + write_options: WriteOptions, + ) -> std::result::Result<(), Self::Err> { self.save_to( &mut std::fs::OpenOptions::new() .read(true) .write(true) .open(path)?, + write_options, ) } @@ -209,13 +215,21 @@ pub trait TagExt: Accessor + Into + Sized { /// /// * The file format could not be determined /// * Attempting to write a tag to a format that does not support it. - fn save_to(&self, file: &mut File) -> std::result::Result<(), Self::Err>; + fn save_to( + &self, + file: &mut File, + write_options: WriteOptions, + ) -> std::result::Result<(), Self::Err>; #[allow(clippy::missing_errors_doc)] /// Dump the tag to a writer /// /// This will only write the tag, it will not produce a usable file. - fn dump_to(&self, writer: &mut W) -> std::result::Result<(), Self::Err>; + fn dump_to( + &self, + writer: &mut W, + write_options: WriteOptions, + ) -> std::result::Result<(), Self::Err>; /// Remove a tag from a [`Path`] /// @@ -246,11 +260,12 @@ pub trait TagExt: Accessor + Into + Sized { /// /// # Example /// -/// ```no_run +/// ```rust,no_run /// use lofty::mpeg::MpegFile; -/// use lofty::{AudioFile, ItemKey, MergeTag as _, SplitTag as _}; +/// use lofty::{AudioFile, ItemKey, MergeTag as _, SplitTag as _, WriteOptions}; /// /// // Read the tag from a file +/// # fn main() -> lofty::Result<()> { /// # let mut file = std::fs::OpenOptions::new().write(true).open("/path/to/file.mp3")?; /// # let parse_options = lofty::ParseOptions::default(); /// let mut mpeg_file = ::read_from(&mut file, parse_options)?; @@ -272,9 +287,9 @@ pub trait TagExt: Accessor + Into + Sized { /// /// // Write the changes back into the file /// mpeg_file.set_id3v2(id3v2); -/// mpeg_file.save_to(&mut file)?; +/// mpeg_file.save_to(&mut file, WriteOptions::new())?; /// -/// # Ok::<(), lofty::LoftyError>(()) +/// # Ok::<(), lofty::LoftyError>(()) } /// ``` pub trait SplitTag { /// The remainder of the split operation that is not represented diff --git a/src/write_options.rs b/src/write_options.rs new file mode 100644 index 000000000..ddfeca943 --- /dev/null +++ b/src/write_options.rs @@ -0,0 +1,131 @@ +/// Options to control how Lofty writes to a file +#[derive(Copy, Clone, Debug, Ord, PartialOrd, Eq, PartialEq)] +#[non_exhaustive] +pub struct WriteOptions { + pub(crate) preferred_padding: Option, + pub(crate) remove_others: bool, + pub(crate) respect_read_only: bool, + pub(crate) uppercase_id3v2_chunk: bool, +} + +impl WriteOptions { + /// Default preferred padding size in bytes + pub const DEFAULT_PREFERRED_PADDING: u16 = 1024; + + /// Creates a new `WriteOptions`, alias for `Default` implementation + /// + /// See also: [`WriteOptions::default`] + /// + /// # Examples + /// + /// ```rust + /// use lofty::WriteOptions; + /// + /// let write_options = WriteOptions::new(); + /// ``` + pub const fn new() -> Self { + Self { + preferred_padding: Some(Self::DEFAULT_PREFERRED_PADDING), + remove_others: false, + respect_read_only: true, + uppercase_id3v2_chunk: true, + } + } + + /// Set the preferred padding size in bytes + /// + /// If the tag format being written supports padding, this will be the size of the padding + /// in bytes. + /// + /// # Examples + /// + /// ```rust + /// use lofty::WriteOptions; + /// + /// // I really don't want my files rewritten, so I'll double the padding size! + /// let options = WriteOptions::new().preferred_padding(2048); + /// + /// // ...Or I don't want padding under any circumstances! + /// let options = WriteOptions::new().preferred_padding(0); + /// ``` + pub fn preferred_padding(mut self, preferred_padding: u16) -> Self { + match preferred_padding { + 0 => self.preferred_padding = None, + _ => self.preferred_padding = Some(preferred_padding), + } + self + } + + /// Whether to remove all other tags when writing + /// + /// If set to `true`, only the tag being written will be kept in the file. + /// + /// # Examples + /// + /// ```rust,no_run + /// use lofty::{Tag, TagExt, TagType, WriteOptions}; + /// + /// # fn main() -> lofty::Result<()> { + /// let mut id3v2_tag = Tag::new(TagType::Id3v2); + /// + /// // ... + /// + /// // I only want to keep the ID3v2 tag around! + /// let options = WriteOptions::new().remove_others(true); + /// id3v2_tag.save_to_path("test.mp3", options)?; + /// # Ok(()) } + /// ``` + pub fn remove_others(mut self, remove_others: bool) -> Self { + self.remove_others = remove_others; + self + } + + /// Whether to respect read-only tag items + /// + /// Some tag formats allow for items to be marked as read-only. If set to `true`, these items + /// will take priority over newly created tag items. + /// + /// # Examples + /// + /// ```rust,no_run + /// use lofty::{Tag, TagExt, TagType, WriteOptions}; + /// + /// # fn main() -> lofty::Result<()> { + /// let mut id3v2_tag = Tag::new(TagType::Id3v2); + /// + /// // ... + /// + /// // I don't care about read-only items, I want to write my new items! + /// let options = WriteOptions::new().respect_read_only(false); + /// id3v2_tag.save_to_path("test.mp3", options)?; + /// # Ok(()) } + /// ``` + pub fn respect_read_only(mut self, respect_read_only: bool) -> Self { + self.respect_read_only = respect_read_only; + self + } + + /// Whether to uppercase the ID3v2 chunk name + pub fn uppercase_id3v2_chunk(mut self, uppercase_id3v2_chunk: bool) -> Self { + self.uppercase_id3v2_chunk = uppercase_id3v2_chunk; + self + } +} + +impl Default for WriteOptions { + /// The default implementation for `WriteOptions` + /// + /// The defaults are as follows: + /// + /// ```rust,ignore + /// WriteOptions { + /// preferred_padding: 1024, + /// remove_others: false, + /// respect_read_only: true, + /// uppercase_id3v2_chunk: true, + /// } + /// ``` + fn default() -> Self { + Self::new() + } +} diff --git a/tests/files/mpeg.rs b/tests/files/mpeg.rs index ded6552ad..04e747188 100644 --- a/tests/files/mpeg.rs +++ b/tests/files/mpeg.rs @@ -5,7 +5,7 @@ use lofty::id3::v2::{Frame, FrameFlags, FrameId, FrameValue, Id3v2Tag, KeyValueF use lofty::mpeg::MpegFile; use lofty::{ Accessor, AudioFile, FileType, ItemKey, ItemValue, ParseOptions, Probe, Tag, TagExt, TagItem, - TagType, TaggedFileExt, + TagType, TaggedFileExt, WriteOptions, }; use std::io::{Seek, Write}; @@ -149,7 +149,7 @@ fn save_to_id3v2() { tag.set_title("title".to_string()); file.rewind().unwrap(); - tag.save_to(&mut file).unwrap(); + tag.save_to(&mut file, WriteOptions::new()).unwrap(); // Now reread the file file.rewind().unwrap(); @@ -190,7 +190,7 @@ fn save_number_of_track_and_disk_to_id3v2() { tag.set_disk(disk); file.rewind().unwrap(); - tag.save_to(&mut file).unwrap(); + tag.save_to(&mut file, WriteOptions::new()).unwrap(); // Now reread the file file.rewind().unwrap(); @@ -231,7 +231,7 @@ fn save_total_of_track_and_disk_to_id3v2() { tag.set_disk_total(disk_total); file.rewind().unwrap(); - tag.save_to(&mut file).unwrap(); + tag.save_to(&mut file, WriteOptions::new()).unwrap(); // Now reread the file file.rewind().unwrap(); @@ -277,7 +277,7 @@ fn save_number_pair_of_track_and_disk_to_id3v2() { tag.set_disk_total(disk_total); file.rewind().unwrap(); - tag.save_to(&mut file).unwrap(); + tag.save_to(&mut file, WriteOptions::new()).unwrap(); // Now reread the file file.rewind().unwrap(); @@ -337,7 +337,7 @@ fn read_and_write_tpil_frame() { ); file.rewind().unwrap(); - tag.save_to(&mut file).unwrap(); + tag.save_to(&mut file, WriteOptions::new()).unwrap(); // Now reread the file file.rewind().unwrap(); diff --git a/tests/files/ogg.rs b/tests/files/ogg.rs index 9f855d0e3..8fb273271 100644 --- a/tests/files/ogg.rs +++ b/tests/files/ogg.rs @@ -1,6 +1,7 @@ use crate::{set_artist, temp_file, verify_artist}; use lofty::{ FileType, ItemKey, ItemValue, ParseOptions, Probe, TagExt, TagItem, TagType, TaggedFileExt, + WriteOptions, }; use std::io::{Seek, Write}; @@ -179,6 +180,9 @@ fn flac_try_write_non_empty_id3v2() { tag.set_artist(String::from("Foo artist")); assert!(tag - .save_to_path("tests/files/assets/flac_with_id3v2.flac") + .save_to_path( + "tests/files/assets/flac_with_id3v2.flac", + WriteOptions::new() + ) .is_err()); } diff --git a/tests/files/util/mod.rs b/tests/files/util/mod.rs index 5f4eeece2..f959e3fb3 100644 --- a/tests/files/util/mod.rs +++ b/tests/files/util/mod.rs @@ -69,7 +69,8 @@ macro_rules! set_artist { $file_write.rewind().unwrap(); - $tag.save_to(&mut $file_write).unwrap(); + $tag.save_to(&mut $file_write, lofty::WriteOptions::new()) + .unwrap(); }; }