Skip to content

Commit

Permalink
feat: Add write_to_file, for encoding to MP3 files and buffers (#117)
Browse files Browse the repository at this point in the history
The write_to_file family of functions takes either an fs::File or
io::Cursor and will take care to not overwrite trailing data.
  • Loading branch information
polyfloyd committed Nov 16, 2023
1 parent 46ff748 commit 7864cb0
Show file tree
Hide file tree
Showing 6 changed files with 84 additions and 23 deletions.
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

pub use crate::error::{partial_tag_ok, Error, ErrorKind, Result};
pub use crate::frame::{Content, Frame, Timestamp};
pub use crate::storage::StorageFile;
pub use crate::stream::encoding::Encoding;
pub use crate::stream::tag::Encoder;
pub use crate::tag::{Tag, Version};
Expand Down
14 changes: 13 additions & 1 deletion src/storage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,10 @@ where
region: ops::Range<u64>,
}

pub trait StorageFile: io::Read + io::Write + io::Seek {
/// This trait is the combination of the [`std::io`] stream traits with an additional method to resize the
/// file.
pub trait StorageFile: io::Read + io::Write + io::Seek + private::Sealed {
/// Performs the resize. Assumes the same behaviour as [`std::fs::File::set_len`].
fn set_len(&mut self, new_len: u64) -> io::Result<()>;
}

Expand Down Expand Up @@ -276,6 +279,15 @@ where
}
}

// https://rust-lang.github.io/api-guidelines/future-proofing.html#c-sealed
mod private {
pub trait Sealed {}

impl<'a, T: Sealed> Sealed for &'a mut T {}
impl Sealed for std::fs::File {}
impl Sealed for std::io::Cursor<Vec<u8>> {}
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
20 changes: 16 additions & 4 deletions src/stream/tag.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::storage::{PlainStorage, Storage};
use crate::storage::{PlainStorage, Storage, StorageFile};
use crate::stream::{frame, unsynch};
use crate::tag::{Tag, Version};
use crate::taglike::TagLike;
Expand Down Expand Up @@ -420,7 +420,7 @@ impl Encoder {
}

/// Encodes a [`Tag`] and replaces any existing tag in the file.
pub fn encode_to_file(&self, tag: &Tag, mut file: &mut fs::File) -> crate::Result<()> {
pub fn write_to_file(&self, tag: &Tag, mut file: impl StorageFile) -> crate::Result<()> {
#[allow(clippy::reversed_empty_ranges)]
let location = locate_id3v2(&mut file)?.unwrap_or(0..0); // Create a new tag if none could be located.

Expand All @@ -431,13 +431,25 @@ impl Encoder {
Ok(())
}

/// Encodes a [`Tag`] and replaces any existing tag in the file.
#[deprecated(note = "Use write_to_file")]
pub fn encode_to_file(&self, tag: &Tag, file: &mut fs::File) -> crate::Result<()> {
self.write_to_file(tag, file)
}

/// Encodes a [`Tag`] and replaces any existing tag in the file pointed to by the specified path.
pub fn encode_to_path(&self, tag: &Tag, path: impl AsRef<Path>) -> crate::Result<()> {
pub fn write_to_path(&self, tag: &Tag, path: impl AsRef<Path>) -> crate::Result<()> {
let mut file = fs::OpenOptions::new().read(true).write(true).open(path)?;
self.encode_to_file(tag, &mut file)?;
self.write_to_file(tag, &mut file)?;
file.flush()?;
Ok(())
}

/// Encodes a [`Tag`] and replaces any existing tag in the file pointed to by the specified path.
#[deprecated(note = "Use write_to_path")]
pub fn encode_to_path(&self, tag: &Tag, path: impl AsRef<Path>) -> crate::Result<()> {
self.write_to_path(tag, path)
}
}

impl Default for Encoder {
Expand Down
12 changes: 9 additions & 3 deletions src/tag.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use crate::storage::{PlainStorage, Storage};
use crate::stream;
use crate::taglike::TagLike;
use crate::v1;
use crate::StorageFile;
use std::fmt;
use std::fs::{self, File};
use std::io::{self, BufReader, Write};
Expand Down Expand Up @@ -189,7 +190,7 @@ impl<'a> Tag {
/// Attempts to write the ID3 tag to the writer using the specified version.
///
/// Note that the plain tag is written, regardless of the original contents. To safely encode a
/// tag to an MP3 file, use `Tag::write_to_path`.
/// tag to an MP3 file, use `Tag::write_to_file`.
pub fn write_to(&self, writer: impl io::Write, version: Version) -> crate::Result<()> {
stream::tag::Encoder::new()
.version(version)
Expand All @@ -199,8 +200,7 @@ impl<'a> Tag {
/// Attempts to write the ID3 tag from the file at the indicated path. If the specified path is
/// the same path which the tag was read from, then the tag will be written to the padding if
/// possible.
pub fn write_to_path(&self, path: impl AsRef<Path>, version: Version) -> crate::Result<()> {
let mut file = fs::OpenOptions::new().read(true).write(true).open(path)?;
pub fn write_to_file(&self, mut file: impl StorageFile, version: Version) -> crate::Result<()> {
#[allow(clippy::reversed_empty_ranges)]
let location = stream::tag::locate_id3v2(&mut file)?.unwrap_or(0..0); // Create a new tag if none could be located.

Expand All @@ -213,6 +213,12 @@ impl<'a> Tag {
Ok(())
}

/// Conventience function for [`write_to_file`].
pub fn write_to_path(&self, path: impl AsRef<Path>, version: Version) -> crate::Result<()> {
let file = fs::OpenOptions::new().read(true).write(true).open(path)?;
self.write_to_file(file, version)
}

/// Overwrite WAV file ID3 chunk in a file
pub fn write_to_aiff_path(
&self,
Expand Down
23 changes: 17 additions & 6 deletions src/v1.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use crate::{Error, ErrorKind};
use crate::{Error, ErrorKind, StorageFile};
use std::cmp;
use std::fs;
use std::io::{self, Read, Seek};
use std::io;
use std::ops;
use std::path::Path;

Expand Down Expand Up @@ -306,9 +306,19 @@ impl Tag {
/// The file cursor position will be reset back to the previous position before returning.
///
/// Returns true if the file initially contained a tag.
#[deprecated(note = "Use remove_from_file")]
pub fn remove(file: &mut fs::File) -> crate::Result<bool> {
Self::remove_from_file(file)
}

/// Removes an ID3v1 tag plus possible extended data if any.
///
/// The file cursor position will be reset back to the previous position before returning.
///
/// Returns true if the file initially contained a tag.
pub fn remove_from_file(mut file: impl StorageFile) -> crate::Result<bool> {
let cur_pos = file.stream_position()?;
let file_len = file.metadata()?.len();
let file_len = file.seek(io::SeekFrom::End(0))?;
let has_ext_tag = if file_len >= XTAG_CHUNK.start.unsigned_abs() {
file.seek(io::SeekFrom::End(XTAG_CHUNK.start))?;
let mut b = [0; 4];
Expand Down Expand Up @@ -348,7 +358,7 @@ impl Tag {
/// Returns true if the file initially contained a tag.
pub fn remove_from_path(path: impl AsRef<Path>) -> crate::Result<bool> {
let mut file = fs::OpenOptions::new().read(true).write(true).open(path)?;
Tag::remove(&mut file)
Tag::remove_from_file(&mut file)
}

/// Returns `genre_str`, falling back to translating `genre_id` to a string.
Expand All @@ -366,6 +376,7 @@ impl Tag {
mod tests {
use super::*;
use std::fs;
use std::io::Seek;
use tempfile::tempdir;

#[test]
Expand Down Expand Up @@ -401,8 +412,8 @@ mod tests {
.open(&tmp_name)
.unwrap();
tag_file.seek(io::SeekFrom::Start(0)).unwrap();
assert!(Tag::remove(&mut tag_file).unwrap());
assert!(Tag::remove_from_file(&mut tag_file).unwrap());
tag_file.seek(io::SeekFrom::Start(0)).unwrap();
assert!(!Tag::remove(&mut tag_file).unwrap());
assert!(!Tag::remove_from_file(&mut tag_file).unwrap());
}
}
37 changes: 28 additions & 9 deletions src/v1v2.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
use crate::{v1, Error, ErrorKind, Tag, Version};
use crate::{v1, Error, ErrorKind, StorageFile, Tag, Version};
use std::fs;
use std::fs::File;
use std::io;
use std::path::Path;

/// Returns which tags are present in the specified file.
pub fn is_candidate_path(path: impl AsRef<Path>) -> crate::Result<FormatVersion> {
let mut file = File::open(path)?;
pub fn is_candidate(mut file: impl io::Read + io::Seek) -> crate::Result<FormatVersion> {
let v2 = Tag::is_candidate(&mut file)?;
let v1 = v1::Tag::is_candidate(&mut file)?;
Ok(match (v1, v2) {
Expand All @@ -15,11 +16,16 @@ pub fn is_candidate_path(path: impl AsRef<Path>) -> crate::Result<FormatVersion>
})
}

/// Returns which tags are present in the specified file.
pub fn is_candidate_path(path: impl AsRef<Path>) -> crate::Result<FormatVersion> {
is_candidate(File::open(path)?)
}

/// Attempts to read an ID3v2 or ID3v1 tag, in that order.
///
/// If neither version tag is found, an error with [`ErrorKind::NoTag`] is returned.
pub fn read_from_path(path: impl AsRef<Path>) -> crate::Result<Tag> {
match Tag::read_from_path(&path) {
pub fn read_from(mut file: impl io::Read + io::Seek) -> crate::Result<Tag> {
match Tag::read_from(&mut file) {
Err(Error {
kind: ErrorKind::NoTag,
..
Expand All @@ -28,7 +34,7 @@ pub fn read_from_path(path: impl AsRef<Path>) -> crate::Result<Tag> {
Ok(tag) => return Ok(tag),
}

match v1::Tag::read_from_path(path) {
match v1::Tag::read_from(file) {
Err(Error {
kind: ErrorKind::NoTag,
..
Expand All @@ -43,17 +49,30 @@ pub fn read_from_path(path: impl AsRef<Path>) -> crate::Result<Tag> {
))
}

/// Attempts to read an ID3v2 or ID3v1 tag, in that order.
///
/// If neither version tag is found, an error with [`ErrorKind::NoTag`] is returned.
pub fn read_from_path(path: impl AsRef<Path>) -> crate::Result<Tag> {
read_from(File::open(path)?)
}

/// Writes the specified tag to a file. Any existing ID3v2 tag is replaced or added if it is not
/// present.
///
/// If any ID3v1 tag is present it will be REMOVED as it is not able to fully represent a ID3v2
/// tag.
pub fn write_to_path(path: impl AsRef<Path>, tag: &Tag, version: Version) -> crate::Result<()> {
tag.write_to_path(&path, version)?;
v1::Tag::remove_from_path(path)?;
pub fn write_to_file(mut file: impl StorageFile, tag: &Tag, version: Version) -> crate::Result<()> {
tag.write_to_file(&mut file, version)?;
v1::Tag::remove_from_file(&mut file)?;
Ok(())
}

/// Conventience function for [`write_to_file`].
pub fn write_to_path(path: impl AsRef<Path>, tag: &Tag, version: Version) -> crate::Result<()> {
let file = fs::OpenOptions::new().read(true).write(true).open(path)?;
write_to_file(file, tag, version)
}

/// Ensures that both ID3v1 and ID3v2 are not present in the specified file.
///
/// Returns [`FormatVersion`] representing the previous state.
Expand Down

0 comments on commit 7864cb0

Please sign in to comment.