mirror of
https://github.com/Serial-ATA/lofty-rs
synced 2024-11-10 06:34:18 +00:00
misc: Make all file writes generic
This commit is contained in:
parent
3402a69f26
commit
e919cd97a3
31 changed files with 721 additions and 276 deletions
|
@ -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_options)
|
||||
.write_to(file, write_options)
|
||||
});
|
||||
|
||||
insert!(map, Id3v1, {
|
||||
Into::<lofty::id3::v1::tag::Id3v1TagRef<'_>>::into(tag).write_to(data, write_options)
|
||||
Into::<lofty::id3::v1::tag::Id3v1TagRef<'_>>::into(tag).write_to(file, write_options)
|
||||
});
|
||||
|
||||
if id3v2_strippable {
|
||||
insert!(map, Id3v2, {
|
||||
lofty::id3::v2::tag::Id3v2TagRef::empty().write_to(data, write_options)
|
||||
lofty::id3::v2::tag::Id3v2TagRef::empty().write_to(file, 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_options)
|
||||
.write_to(file, 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_options)
|
||||
.write_to(file, write_options)
|
||||
});
|
||||
|
||||
insert!(map, AiffText, {
|
||||
|
@ -84,7 +84,7 @@ pub(crate) fn init_write_lookup(
|
|||
annotations: Some(tag.get_strings(&lofty::prelude::ItemKey::Comment)),
|
||||
comments: None,
|
||||
}
|
||||
.write_to(data, write_options)
|
||||
.write_to(file, write_options)
|
||||
});
|
||||
|
||||
map
|
||||
|
@ -112,7 +112,12 @@ 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::Tag, write_options: ::lofty::config::WriteOptions) -> ::lofty::error::Result<()> {
|
||||
pub(crate) fn write_to<F>(file: &mut F, tag: &::lofty::tag::Tag, write_options: ::lofty::config::WriteOptions) -> ::lofty::error::Result<()>
|
||||
where
|
||||
F: ::lofty::io::FileLike,
|
||||
::lofty::error::LoftyError: ::std::convert::From<<F as ::lofty::io::Truncate>::Error>,
|
||||
::lofty::error::LoftyError: ::std::convert::From<<F as ::lofty::io::Length>::Error>,
|
||||
{
|
||||
match tag.tag_type() {
|
||||
#( #applicable_formats )*
|
||||
_ => crate::macros::err!(UnsupportedTag),
|
||||
|
|
|
@ -445,7 +445,12 @@ fn generate_audiofile_impl(file: &LoftyFile) -> syn::Result<proc_macro2::TokenSt
|
|||
#read_fn(reader, parse_options)
|
||||
}
|
||||
|
||||
fn save_to(&self, file: &mut ::std::fs::File, write_options: ::lofty::config::WriteOptions) -> ::lofty::error::Result<()> {
|
||||
fn save_to<F>(&self, file: &mut F, write_options: ::lofty::config::WriteOptions) -> ::lofty::error::Result<()>
|
||||
where
|
||||
F: ::lofty::io::FileLike,
|
||||
::lofty::error::LoftyError: ::std::convert::From<<F as ::lofty::io::Truncate>::Error>,
|
||||
::lofty::error::LoftyError: ::std::convert::From<<F as ::lofty::io::Length>::Error>,
|
||||
{
|
||||
use ::lofty::tag::TagExt as _;
|
||||
use ::std::io::Seek as _;
|
||||
#save_to_body
|
||||
|
|
|
@ -12,11 +12,10 @@ use crate::tag::{
|
|||
};
|
||||
|
||||
use std::borrow::Cow;
|
||||
use std::fs::File;
|
||||
use std::io::Write;
|
||||
use std::ops::Deref;
|
||||
use std::path::Path;
|
||||
|
||||
use crate::util::io::{FileLike, Truncate};
|
||||
use lofty_attr::tag;
|
||||
|
||||
macro_rules! impl_accessor {
|
||||
|
@ -304,6 +303,11 @@ impl TagExt for ApeTag {
|
|||
type Err = LoftyError;
|
||||
type RefKey<'a> = &'a str;
|
||||
|
||||
#[inline]
|
||||
fn tag_type(&self) -> TagType {
|
||||
TagType::Ape
|
||||
}
|
||||
|
||||
fn len(&self) -> usize {
|
||||
self.items.len()
|
||||
}
|
||||
|
@ -322,11 +326,15 @@ 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(
|
||||
fn save_to<F>(
|
||||
&self,
|
||||
file: &mut File,
|
||||
file: &mut F,
|
||||
write_options: WriteOptions,
|
||||
) -> std::result::Result<(), Self::Err> {
|
||||
) -> std::result::Result<(), Self::Err>
|
||||
where
|
||||
F: FileLike,
|
||||
LoftyError: From<<F as Truncate>::Error>,
|
||||
{
|
||||
ApeTagRef {
|
||||
read_only: self.read_only,
|
||||
items: self.items.iter().map(Into::into),
|
||||
|
@ -351,14 +359,6 @@ impl TagExt for ApeTag {
|
|||
.dump_to(writer, write_options)
|
||||
}
|
||||
|
||||
fn remove_from_path<P: AsRef<Path>>(&self, path: P) -> std::result::Result<(), Self::Err> {
|
||||
TagType::Ape.remove_from_path(path)
|
||||
}
|
||||
|
||||
fn remove_from(&self, file: &mut File) -> std::result::Result<(), Self::Err> {
|
||||
TagType::Ape.remove_from(file)
|
||||
}
|
||||
|
||||
fn clear(&mut self) {
|
||||
self.items.clear();
|
||||
}
|
||||
|
@ -492,7 +492,11 @@ impl<'a, I> ApeTagRef<'a, I>
|
|||
where
|
||||
I: Iterator<Item = ApeItemRef<'a>>,
|
||||
{
|
||||
pub(crate) fn write_to(&mut self, file: &mut File, write_options: WriteOptions) -> Result<()> {
|
||||
pub(crate) fn write_to<F>(&mut self, file: &mut F, write_options: WriteOptions) -> Result<()>
|
||||
where
|
||||
F: FileLike,
|
||||
LoftyError: From<<F as Truncate>::Error>,
|
||||
{
|
||||
write::write_to(file, self, write_options)
|
||||
}
|
||||
|
||||
|
|
|
@ -3,40 +3,42 @@ use super::ApeTagRef;
|
|||
use crate::ape::constants::APE_PREAMBLE;
|
||||
use crate::ape::tag::read;
|
||||
use crate::config::WriteOptions;
|
||||
use crate::error::Result;
|
||||
use crate::error::{LoftyError, Result};
|
||||
use crate::id3::{find_id3v1, find_id3v2, find_lyrics3v2, FindId3v2Config};
|
||||
use crate::macros::{decode_err, err};
|
||||
use crate::probe::Probe;
|
||||
use crate::tag::item::ItemValueRef;
|
||||
use crate::util::io::{FileLike, Truncate};
|
||||
|
||||
use std::fs::File;
|
||||
use std::io::{Cursor, Read, Seek, SeekFrom, Write};
|
||||
use std::io::{Cursor, Seek, SeekFrom, Write};
|
||||
|
||||
use byteorder::{LittleEndian, WriteBytesExt};
|
||||
|
||||
#[allow(clippy::shadow_unrelated)]
|
||||
pub(crate) fn write_to<'a, I>(
|
||||
data: &mut File,
|
||||
pub(crate) fn write_to<'a, F, I>(
|
||||
file: &mut F,
|
||||
tag_ref: &mut ApeTagRef<'a, I>,
|
||||
write_options: WriteOptions,
|
||||
) -> Result<()>
|
||||
where
|
||||
I: Iterator<Item = ApeItemRef<'a>>,
|
||||
F: FileLike,
|
||||
LoftyError: From<<F as Truncate>::Error>,
|
||||
{
|
||||
let probe = Probe::new(data).guess_file_type()?;
|
||||
let probe = Probe::new(file).guess_file_type()?;
|
||||
|
||||
match probe.file_type() {
|
||||
Some(ft) if super::ApeTag::SUPPORTED_FORMATS.contains(&ft) => {},
|
||||
_ => err!(UnsupportedTag),
|
||||
}
|
||||
|
||||
let data = probe.into_inner();
|
||||
let file = probe.into_inner();
|
||||
|
||||
// We don't actually need the ID3v2 tag, but reading it will seek to the end of it if it exists
|
||||
find_id3v2(data, FindId3v2Config::NO_READ_TAG)?;
|
||||
find_id3v2(file, FindId3v2Config::NO_READ_TAG)?;
|
||||
|
||||
let mut ape_preamble = [0; 8];
|
||||
data.read_exact(&mut ape_preamble)?;
|
||||
file.read_exact(&mut ape_preamble)?;
|
||||
|
||||
// We have to check the APE tag for any read only items first
|
||||
let mut read_only = None;
|
||||
|
@ -45,8 +47,8 @@ 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));
|
||||
|
||||
let start = data.stream_position()?;
|
||||
match read::read_ape_tag(data, false)? {
|
||||
let start = file.stream_position()?;
|
||||
match read::read_ape_tag(file, false)? {
|
||||
Some((mut existing_tag, header)) => {
|
||||
if write_options.respect_read_only {
|
||||
// Only keep metadata around that's marked read only
|
||||
|
@ -60,25 +62,25 @@ where
|
|||
header_ape_tag = (true, (start, start + u64::from(header.size)))
|
||||
},
|
||||
None => {
|
||||
data.seek(SeekFrom::Current(-8))?;
|
||||
file.seek(SeekFrom::Current(-8))?;
|
||||
},
|
||||
}
|
||||
|
||||
// Skip over ID3v1 and Lyrics3v2 tags
|
||||
find_id3v1(data, false)?;
|
||||
find_lyrics3v2(data)?;
|
||||
find_id3v1(file, false)?;
|
||||
find_lyrics3v2(file)?;
|
||||
|
||||
// In case there's no ape tag already, this is the spot it belongs
|
||||
let ape_position = data.stream_position()?;
|
||||
let ape_position = file.stream_position()?;
|
||||
|
||||
// Now search for an APE tag at the end
|
||||
data.seek(SeekFrom::Current(-32))?;
|
||||
file.seek(SeekFrom::Current(-32))?;
|
||||
|
||||
let mut ape_tag_location = None;
|
||||
|
||||
// Also check this tag for any read only items
|
||||
let start = data.stream_position()? as usize + 32;
|
||||
if let Some((mut existing_tag, header)) = read::read_ape_tag(data, true)? {
|
||||
let start = file.stream_position()? as usize + 32;
|
||||
if let Some((mut existing_tag, header)) = read::read_ape_tag(file, true)? {
|
||||
if write_options.respect_read_only {
|
||||
existing_tag.items.retain(|i| i.read_only);
|
||||
|
||||
|
@ -114,10 +116,10 @@ where
|
|||
tag = create_ape_tag(tag_ref, std::iter::empty(), write_options)?;
|
||||
};
|
||||
|
||||
data.rewind()?;
|
||||
file.rewind()?;
|
||||
|
||||
let mut file_bytes = Vec::new();
|
||||
data.read_to_end(&mut file_bytes)?;
|
||||
file.read_to_end(&mut file_bytes)?;
|
||||
|
||||
// Write the tag in the appropriate place
|
||||
if let Some(range) = ape_tag_location {
|
||||
|
@ -131,9 +133,9 @@ where
|
|||
file_bytes.drain(header_ape_tag.1 .0 as usize..header_ape_tag.1 .1 as usize);
|
||||
}
|
||||
|
||||
data.rewind()?;
|
||||
data.set_len(0)?;
|
||||
data.write_all(&file_bytes)?;
|
||||
file.rewind()?;
|
||||
file.truncate(0)?;
|
||||
file.write_all(&file_bytes)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
12
src/error.rs
12
src/error.rs
|
@ -68,6 +68,8 @@ pub enum ErrorKind {
|
|||
Io(std::io::Error),
|
||||
/// Failure to allocate enough memory
|
||||
Alloc(TryReserveError),
|
||||
/// This should **never** be encountered
|
||||
Infallible(std::convert::Infallible),
|
||||
}
|
||||
|
||||
/// The types of errors that can occur while interacting with ID3v2 tags
|
||||
|
@ -499,6 +501,14 @@ impl From<std::collections::TryReserveError> for LoftyError {
|
|||
}
|
||||
}
|
||||
|
||||
impl From<std::convert::Infallible> for LoftyError {
|
||||
fn from(input: std::convert::Infallible) -> Self {
|
||||
Self {
|
||||
kind: ErrorKind::Infallible(input),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for LoftyError {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self.kind {
|
||||
|
@ -540,6 +550,8 @@ impl Display for LoftyError {
|
|||
),
|
||||
ErrorKind::FileDecoding(ref file_decode_err) => write!(f, "{file_decode_err}"),
|
||||
ErrorKind::FileEncoding(ref file_encode_err) => write!(f, "{file_encode_err}"),
|
||||
|
||||
ErrorKind::Infallible(_) => write!(f, "A expected condition was not upheld"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
use super::tagged_file::TaggedFile;
|
||||
use crate::config::{ParseOptions, WriteOptions};
|
||||
use crate::error::Result;
|
||||
use crate::error::{LoftyError, Result};
|
||||
use crate::tag::TagType;
|
||||
|
||||
use std::fs::{File, OpenOptions};
|
||||
use crate::util::io::{FileLike, Length, Truncate};
|
||||
use std::fs::OpenOptions;
|
||||
use std::io::{Read, Seek};
|
||||
use std::path::Path;
|
||||
|
||||
|
@ -77,7 +78,11 @@ pub trait AudioFile: Into<TaggedFile> {
|
|||
/// tagged_file.save_to(&mut file, WriteOptions::default())?;
|
||||
/// # Ok(()) }
|
||||
/// ```
|
||||
fn save_to(&self, file: &mut File, write_options: WriteOptions) -> Result<()>;
|
||||
fn save_to<F>(&self, file: &mut F, write_options: WriteOptions) -> Result<()>
|
||||
where
|
||||
F: FileLike,
|
||||
LoftyError: From<<F as Truncate>::Error>,
|
||||
LoftyError: From<<F as Length>::Error>;
|
||||
|
||||
/// Returns a reference to the file's properties
|
||||
fn properties(&self) -> &Self::Properties;
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
use super::audio_file::AudioFile;
|
||||
use super::file_type::FileType;
|
||||
use crate::config::{ParseOptions, WriteOptions};
|
||||
use crate::error::Result;
|
||||
use crate::error::{LoftyError, Result};
|
||||
use crate::properties::FileProperties;
|
||||
use crate::tag::{Tag, TagExt, TagType};
|
||||
|
||||
use crate::util::io::{FileLike, Length, Truncate};
|
||||
use std::fs::File;
|
||||
use std::io::{Read, Seek};
|
||||
|
||||
|
@ -423,7 +424,12 @@ impl AudioFile for TaggedFile {
|
|||
.read()
|
||||
}
|
||||
|
||||
fn save_to(&self, file: &mut File, write_options: WriteOptions) -> Result<()> {
|
||||
fn save_to<F>(&self, file: &mut F, write_options: WriteOptions) -> Result<()>
|
||||
where
|
||||
F: FileLike,
|
||||
LoftyError: From<<F as Truncate>::Error>,
|
||||
LoftyError: From<<F as Length>::Error>,
|
||||
{
|
||||
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.
|
||||
|
@ -631,7 +637,12 @@ impl AudioFile for BoundTaggedFile {
|
|||
)
|
||||
}
|
||||
|
||||
fn save_to(&self, file: &mut File, write_options: WriteOptions) -> Result<()> {
|
||||
fn save_to<F>(&self, file: &mut F, write_options: WriteOptions) -> Result<()>
|
||||
where
|
||||
F: FileLike,
|
||||
LoftyError: From<<F as Truncate>::Error>,
|
||||
LoftyError: From<<F as Length>::Error>,
|
||||
{
|
||||
self.inner.save_to(file, write_options)
|
||||
}
|
||||
|
||||
|
|
|
@ -10,7 +10,7 @@ mod read;
|
|||
pub(crate) mod write;
|
||||
|
||||
use crate::config::WriteOptions;
|
||||
use crate::error::Result;
|
||||
use crate::error::{LoftyError, Result};
|
||||
use crate::file::{FileType, TaggedFile};
|
||||
use crate::id3::v2::tag::Id3v2Tag;
|
||||
use crate::ogg::tag::VorbisCommentsRef;
|
||||
|
@ -18,13 +18,10 @@ use crate::ogg::{OggPictureStorage, VorbisComments};
|
|||
use crate::picture::{Picture, PictureInformation};
|
||||
use crate::tag::TagExt;
|
||||
|
||||
use std::fs::File;
|
||||
use std::io::Seek;
|
||||
|
||||
use lofty_attr::LoftyFile;
|
||||
|
||||
// Exports
|
||||
|
||||
use crate::util::io::{FileLike, Length, Truncate};
|
||||
pub use properties::FlacProperties;
|
||||
|
||||
/// A FLAC file
|
||||
|
@ -56,7 +53,12 @@ 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, write_options: WriteOptions) -> Result<()> {
|
||||
fn write_to<F>(&self, file: &mut F, write_options: WriteOptions) -> Result<()>
|
||||
where
|
||||
F: FileLike,
|
||||
LoftyError: From<<F as Truncate>::Error>,
|
||||
LoftyError: From<<F as Length>::Error>,
|
||||
{
|
||||
if let Some(ref id3v2) = self.id3v2_tag {
|
||||
id3v2.save_to(file, write_options)?;
|
||||
file.rewind()?;
|
||||
|
|
|
@ -1,22 +1,27 @@
|
|||
use super::block::Block;
|
||||
use super::read::verify_flac;
|
||||
use crate::config::WriteOptions;
|
||||
use crate::error::Result;
|
||||
use crate::error::{LoftyError, Result};
|
||||
use crate::macros::{err, try_vec};
|
||||
use crate::ogg::tag::VorbisCommentsRef;
|
||||
use crate::ogg::write::create_comments;
|
||||
use crate::picture::{Picture, PictureInformation};
|
||||
use crate::tag::{Tag, TagType};
|
||||
|
||||
use std::fs::File;
|
||||
use std::io::{Cursor, Read, Seek, SeekFrom, Write};
|
||||
use std::io::{Cursor, Seek, SeekFrom, Write};
|
||||
|
||||
use crate::util::io::{FileLike, Length, Truncate};
|
||||
use byteorder::{LittleEndian, WriteBytesExt};
|
||||
|
||||
const BLOCK_HEADER_SIZE: usize = 4;
|
||||
const MAX_BLOCK_SIZE: u32 = 16_777_215;
|
||||
|
||||
pub(crate) fn write_to(file: &mut File, tag: &Tag, write_options: WriteOptions) -> Result<()> {
|
||||
pub(crate) fn write_to<F>(file: &mut F, tag: &Tag, write_options: WriteOptions) -> Result<()>
|
||||
where
|
||||
F: FileLike,
|
||||
LoftyError: From<<F as Truncate>::Error>,
|
||||
LoftyError: From<<F as Length>::Error>,
|
||||
{
|
||||
match tag.tag_type() {
|
||||
TagType::VorbisComments => {
|
||||
let (vendor, items, pictures) = crate::ogg::tag::create_vorbis_comments_ref(tag);
|
||||
|
@ -35,12 +40,14 @@ pub(crate) fn write_to(file: &mut File, tag: &Tag, write_options: WriteOptions)
|
|||
}
|
||||
}
|
||||
|
||||
pub(crate) fn write_to_inner<'a, II, IP>(
|
||||
file: &mut File,
|
||||
pub(crate) fn write_to_inner<'a, F, II, IP>(
|
||||
file: &mut F,
|
||||
tag: &mut VorbisCommentsRef<'a, II, IP>,
|
||||
write_options: WriteOptions,
|
||||
) -> Result<()>
|
||||
where
|
||||
F: FileLike,
|
||||
LoftyError: From<<F as Truncate>::Error>,
|
||||
II: Iterator<Item = (&'a str, &'a str)>,
|
||||
IP: Iterator<Item = (&'a Picture, PictureInformation)>,
|
||||
{
|
||||
|
@ -131,7 +138,7 @@ where
|
|||
}
|
||||
|
||||
file.seek(SeekFrom::Start(stream_info_end as u64))?;
|
||||
file.set_len(stream_info_end as u64)?;
|
||||
file.truncate(stream_info_end as u64)?;
|
||||
file.write_all(&file_bytes)?;
|
||||
|
||||
Ok(())
|
||||
|
|
|
@ -2,12 +2,11 @@ use crate::config::WriteOptions;
|
|||
use crate::error::{LoftyError, Result};
|
||||
use crate::id3::v1::constants::GENRES;
|
||||
use crate::tag::{Accessor, ItemKey, ItemValue, MergeTag, SplitTag, Tag, TagExt, TagItem, TagType};
|
||||
|
||||
use std::borrow::Cow;
|
||||
use std::fs::File;
|
||||
use std::io::Write;
|
||||
use std::path::Path;
|
||||
|
||||
use crate::util::io::{FileLike, Length, Truncate};
|
||||
use lofty_attr::tag;
|
||||
|
||||
macro_rules! impl_accessor {
|
||||
|
@ -209,6 +208,11 @@ impl TagExt for Id3v1Tag {
|
|||
type Err = LoftyError;
|
||||
type RefKey<'a> = &'a ItemKey;
|
||||
|
||||
#[inline]
|
||||
fn tag_type(&self) -> TagType {
|
||||
TagType::Id3v1
|
||||
}
|
||||
|
||||
fn len(&self) -> usize {
|
||||
usize::from(self.title.is_some())
|
||||
+ usize::from(self.artist.is_some())
|
||||
|
@ -242,11 +246,16 @@ impl TagExt for Id3v1Tag {
|
|||
&& self.genre.is_none()
|
||||
}
|
||||
|
||||
fn save_to(
|
||||
fn save_to<F>(
|
||||
&self,
|
||||
file: &mut File,
|
||||
file: &mut F,
|
||||
write_options: WriteOptions,
|
||||
) -> std::result::Result<(), Self::Err> {
|
||||
) -> std::result::Result<(), Self::Err>
|
||||
where
|
||||
F: FileLike,
|
||||
LoftyError: From<<F as Truncate>::Error>,
|
||||
LoftyError: From<<F as Length>::Error>,
|
||||
{
|
||||
Into::<Id3v1TagRef<'_>>::into(self).write_to(file, write_options)
|
||||
}
|
||||
|
||||
|
@ -267,7 +276,12 @@ impl TagExt for Id3v1Tag {
|
|||
TagType::Id3v1.remove_from_path(path)
|
||||
}
|
||||
|
||||
fn remove_from(&self, file: &mut File) -> std::result::Result<(), Self::Err> {
|
||||
fn remove_from<F>(&self, file: &mut F) -> std::result::Result<(), Self::Err>
|
||||
where
|
||||
F: FileLike,
|
||||
LoftyError: From<<F as Truncate>::Error>,
|
||||
LoftyError: From<<F as Length>::Error>,
|
||||
{
|
||||
TagType::Id3v1.remove_from(file)
|
||||
}
|
||||
|
||||
|
@ -420,7 +434,12 @@ impl<'a> Id3v1TagRef<'a> {
|
|||
&& self.genre.is_none()
|
||||
}
|
||||
|
||||
pub(crate) fn write_to(&self, file: &mut File, write_options: WriteOptions) -> Result<()> {
|
||||
pub(crate) fn write_to<F>(&self, file: &mut F, write_options: WriteOptions) -> Result<()>
|
||||
where
|
||||
F: FileLike,
|
||||
LoftyError: From<<F as Truncate>::Error>,
|
||||
LoftyError: From<<F as Length>::Error>,
|
||||
{
|
||||
super::write::write_id3v1(file, self, write_options)
|
||||
}
|
||||
|
||||
|
|
|
@ -1,21 +1,26 @@
|
|||
use super::tag::Id3v1TagRef;
|
||||
use crate::config::WriteOptions;
|
||||
use crate::error::Result;
|
||||
use crate::error::{LoftyError, Result};
|
||||
use crate::id3::{find_id3v1, ID3FindResults};
|
||||
use crate::macros::err;
|
||||
use crate::probe::Probe;
|
||||
|
||||
use std::fs::File;
|
||||
use std::io::{Cursor, Seek, Write};
|
||||
|
||||
use crate::util::io::{FileLike, Length, Truncate};
|
||||
use byteorder::WriteBytesExt;
|
||||
|
||||
#[allow(clippy::shadow_unrelated)]
|
||||
pub(crate) fn write_id3v1(
|
||||
file: &mut File,
|
||||
pub(crate) fn write_id3v1<F>(
|
||||
file: &mut F,
|
||||
tag: &Id3v1TagRef<'_>,
|
||||
_write_options: WriteOptions,
|
||||
) -> Result<()> {
|
||||
) -> Result<()>
|
||||
where
|
||||
F: FileLike,
|
||||
LoftyError: From<<F as Truncate>::Error>,
|
||||
LoftyError: From<<F as Length>::Error>,
|
||||
{
|
||||
let probe = Probe::new(file).guess_file_type()?;
|
||||
|
||||
match probe.file_type() {
|
||||
|
@ -31,7 +36,8 @@ pub(crate) fn write_id3v1(
|
|||
if tag.is_empty() && header.is_some() {
|
||||
// An ID3v1 tag occupies the last 128 bytes of the file, so we can just
|
||||
// shrink it down.
|
||||
file.set_len(file.metadata()?.len().saturating_sub(128))?;
|
||||
let new_length = file.len()?.saturating_sub(128);
|
||||
file.truncate(new_length)?;
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
|
|
@ -24,11 +24,10 @@ use crate::tag::{
|
|||
use crate::util::text::{decode_text, TextDecodeOptions, TextEncoding};
|
||||
|
||||
use std::borrow::Cow;
|
||||
use std::fs::File;
|
||||
use std::io::{Cursor, Write};
|
||||
use std::ops::Deref;
|
||||
use std::path::Path;
|
||||
|
||||
use crate::util::io::{FileLike, Length, Truncate};
|
||||
use lofty_attr::tag;
|
||||
|
||||
const USER_DEFINED_TEXT_FRAME_ID: &str = "TXXX";
|
||||
|
@ -896,6 +895,11 @@ impl TagExt for Id3v2Tag {
|
|||
type Err = LoftyError;
|
||||
type RefKey<'a> = &'a FrameId<'a>;
|
||||
|
||||
#[inline]
|
||||
fn tag_type(&self) -> TagType {
|
||||
TagType::Id3v2
|
||||
}
|
||||
|
||||
fn len(&self) -> usize {
|
||||
self.frames.len()
|
||||
}
|
||||
|
@ -915,11 +919,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(
|
||||
fn save_to<F>(
|
||||
&self,
|
||||
file: &mut File,
|
||||
file: &mut F,
|
||||
write_options: WriteOptions,
|
||||
) -> std::result::Result<(), Self::Err> {
|
||||
) -> std::result::Result<(), Self::Err>
|
||||
where
|
||||
F: FileLike,
|
||||
LoftyError: From<<F as Truncate>::Error>,
|
||||
LoftyError: From<<F as Length>::Error>,
|
||||
{
|
||||
Id3v2TagRef {
|
||||
flags: self.flags,
|
||||
frames: self.frames.iter().filter_map(Frame::as_opt_ref),
|
||||
|
@ -945,14 +954,6 @@ impl TagExt for Id3v2Tag {
|
|||
.dump_to(writer, write_options)
|
||||
}
|
||||
|
||||
fn remove_from_path<P: AsRef<Path>>(&self, path: P) -> std::result::Result<(), Self::Err> {
|
||||
TagType::Id3v2.remove_from_path(path)
|
||||
}
|
||||
|
||||
fn remove_from(&self, file: &mut File) -> std::result::Result<(), Self::Err> {
|
||||
TagType::Id3v2.remove_from(file)
|
||||
}
|
||||
|
||||
fn clear(&mut self) {
|
||||
self.frames.clear();
|
||||
}
|
||||
|
@ -1495,7 +1496,12 @@ pub(crate) fn tag_frames(tag: &Tag) -> impl Iterator<Item = FrameRef<'_>> + Clon
|
|||
}
|
||||
|
||||
impl<'a, I: Iterator<Item = FrameRef<'a>> + Clone + 'a> Id3v2TagRef<'a, I> {
|
||||
pub(crate) fn write_to(&mut self, file: &mut File, write_options: WriteOptions) -> Result<()> {
|
||||
pub(crate) fn write_to<F>(&mut self, file: &mut F, write_options: WriteOptions) -> Result<()>
|
||||
where
|
||||
F: FileLike,
|
||||
LoftyError: From<<F as Truncate>::Error>,
|
||||
LoftyError: From<<F as Length>::Error>,
|
||||
{
|
||||
super::write::write_id3v2(file, self, write_options)
|
||||
}
|
||||
|
||||
|
|
|
@ -1,45 +1,47 @@
|
|||
use crate::config::WriteOptions;
|
||||
use crate::error::Result;
|
||||
use crate::error::{LoftyError, Result};
|
||||
use crate::iff::chunk::Chunks;
|
||||
use std::io::SeekFrom;
|
||||
|
||||
use std::fs::File;
|
||||
use std::io::{Read, Seek, SeekFrom, Write};
|
||||
|
||||
use crate::util::io::{FileLike, Length, Truncate};
|
||||
use byteorder::{ByteOrder, WriteBytesExt};
|
||||
|
||||
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<B>(
|
||||
data: &mut File,
|
||||
pub(in crate::id3::v2) fn write_to_chunk_file<F, B>(
|
||||
file: &mut F,
|
||||
tag: &[u8],
|
||||
write_options: WriteOptions,
|
||||
) -> Result<()>
|
||||
where
|
||||
F: FileLike,
|
||||
LoftyError: From<<F as Truncate>::Error>,
|
||||
LoftyError: From<<F as Length>::Error>,
|
||||
B: ByteOrder,
|
||||
{
|
||||
// RIFF....WAVE
|
||||
data.seek(SeekFrom::Current(12))?;
|
||||
file.seek(SeekFrom::Current(12))?;
|
||||
|
||||
let file_len = data.metadata()?.len().saturating_sub(12);
|
||||
let file_len = file.len()?.saturating_sub(12);
|
||||
|
||||
let mut id3v2_chunk = (None, None);
|
||||
|
||||
let mut chunks = Chunks::<B>::new(file_len);
|
||||
|
||||
while chunks.next(data).is_ok() {
|
||||
while chunks.next(file).is_ok() {
|
||||
if chunks.fourcc == CHUNK_NAME_UPPER || chunks.fourcc == CHUNK_NAME_LOWER {
|
||||
id3v2_chunk = (Some(data.stream_position()? - 8), Some(chunks.size));
|
||||
id3v2_chunk = (Some(file.stream_position()? - 8), Some(chunks.size));
|
||||
break;
|
||||
}
|
||||
|
||||
data.seek(SeekFrom::Current(i64::from(chunks.size)))?;
|
||||
file.seek(SeekFrom::Current(i64::from(chunks.size)))?;
|
||||
|
||||
chunks.correct_position(data)?;
|
||||
chunks.correct_position(file)?;
|
||||
}
|
||||
|
||||
if let (Some(chunk_start), Some(mut chunk_size)) = id3v2_chunk {
|
||||
data.rewind()?;
|
||||
file.rewind()?;
|
||||
|
||||
// We need to remove the padding byte if it exists
|
||||
if chunk_size % 2 != 0 {
|
||||
|
@ -47,41 +49,41 @@ where
|
|||
}
|
||||
|
||||
let mut file_bytes = Vec::new();
|
||||
data.read_to_end(&mut file_bytes)?;
|
||||
file.read_to_end(&mut file_bytes)?;
|
||||
|
||||
file_bytes.splice(
|
||||
chunk_start as usize..(chunk_start + u64::from(chunk_size) + 8) as usize,
|
||||
[],
|
||||
);
|
||||
|
||||
data.rewind()?;
|
||||
data.set_len(0)?;
|
||||
data.write_all(&file_bytes)?;
|
||||
file.rewind()?;
|
||||
file.truncate(0)?;
|
||||
file.write_all(&file_bytes)?;
|
||||
}
|
||||
|
||||
if !tag.is_empty() {
|
||||
data.seek(SeekFrom::End(0))?;
|
||||
file.seek(SeekFrom::End(0))?;
|
||||
|
||||
if write_options.uppercase_id3v2_chunk {
|
||||
data.write_all(&CHUNK_NAME_UPPER)?;
|
||||
file.write_all(&CHUNK_NAME_UPPER)?;
|
||||
} else {
|
||||
data.write_all(&CHUNK_NAME_LOWER)?;
|
||||
file.write_all(&CHUNK_NAME_LOWER)?;
|
||||
}
|
||||
|
||||
data.write_u32::<B>(tag.len() as u32)?;
|
||||
data.write_all(tag)?;
|
||||
file.write_u32::<B>(tag.len() as u32)?;
|
||||
file.write_all(tag)?;
|
||||
|
||||
// It is required an odd length chunk be padded with a 0
|
||||
// The 0 isn't included in the chunk size, however
|
||||
if tag.len() % 2 != 0 {
|
||||
data.write_u8(0)?;
|
||||
file.write_u8(0)?;
|
||||
}
|
||||
|
||||
let total_size = data.stream_position()? - 8;
|
||||
let total_size = file.stream_position()? - 8;
|
||||
|
||||
data.seek(SeekFrom::Start(4))?;
|
||||
file.seek(SeekFrom::Start(4))?;
|
||||
|
||||
data.write_u32::<B>(total_size as u32)?;
|
||||
file.write_u32::<B>(total_size as u32)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
|
|
@ -3,7 +3,7 @@ mod frame;
|
|||
|
||||
use super::Id3v2TagFlags;
|
||||
use crate::config::WriteOptions;
|
||||
use crate::error::Result;
|
||||
use crate::error::{LoftyError, Result};
|
||||
use crate::file::FileType;
|
||||
use crate::id3::v2::frame::FrameRef;
|
||||
use crate::id3::v2::tag::Id3v2TagRef;
|
||||
|
@ -13,11 +13,11 @@ use crate::id3::{find_id3v2, FindId3v2Config};
|
|||
use crate::macros::{err, try_vec};
|
||||
use crate::probe::Probe;
|
||||
|
||||
use std::fs::File;
|
||||
use std::io::{Cursor, Read, Seek, SeekFrom, Write};
|
||||
use std::ops::Not;
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use crate::util::io::{FileLike, Length, Truncate};
|
||||
use byteorder::{BigEndian, LittleEndian, WriteBytesExt};
|
||||
|
||||
// In the very rare chance someone wants to write a CRC in their extended header
|
||||
|
@ -38,15 +38,21 @@ fn crc_32_table() -> &'static [u32; 256] {
|
|||
}
|
||||
|
||||
#[allow(clippy::shadow_unrelated)]
|
||||
pub(crate) fn write_id3v2<'a, I: Iterator<Item = FrameRef<'a>> + Clone + 'a>(
|
||||
data: &mut File,
|
||||
pub(crate) fn write_id3v2<'a, F, I>(
|
||||
file: &mut F,
|
||||
tag: &mut Id3v2TagRef<'a, I>,
|
||||
write_options: WriteOptions,
|
||||
) -> Result<()> {
|
||||
let probe = Probe::new(data).guess_file_type()?;
|
||||
) -> Result<()>
|
||||
where
|
||||
F: FileLike,
|
||||
LoftyError: From<<F as Truncate>::Error>,
|
||||
LoftyError: From<<F as Length>::Error>,
|
||||
I: Iterator<Item = FrameRef<'a>> + Clone + 'a,
|
||||
{
|
||||
let probe = Probe::new(file).guess_file_type()?;
|
||||
let file_type = probe.file_type();
|
||||
|
||||
let data = probe.into_inner();
|
||||
let file = probe.into_inner();
|
||||
|
||||
// Unable to determine a format
|
||||
if file_type.is_none() {
|
||||
|
@ -74,27 +80,27 @@ pub(crate) fn write_id3v2<'a, I: Iterator<Item = FrameRef<'a>> + Clone + 'a>(
|
|||
// 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::<LittleEndian>(data, &id3v2, write_options);
|
||||
return chunk_file::write_to_chunk_file::<F, LittleEndian>(file, &id3v2, write_options);
|
||||
},
|
||||
FileType::Aiff => {
|
||||
tag.flags.footer = false;
|
||||
return chunk_file::write_to_chunk_file::<BigEndian>(data, &id3v2, write_options);
|
||||
return chunk_file::write_to_chunk_file::<F, BigEndian>(file, &id3v2, write_options);
|
||||
},
|
||||
_ => {},
|
||||
}
|
||||
|
||||
// find_id3v2 will seek us to the end of the tag
|
||||
// TODO: Search through junk
|
||||
find_id3v2(data, FindId3v2Config::NO_READ_TAG)?;
|
||||
find_id3v2(file, FindId3v2Config::NO_READ_TAG)?;
|
||||
|
||||
let mut file_bytes = Vec::new();
|
||||
data.read_to_end(&mut file_bytes)?;
|
||||
file.read_to_end(&mut file_bytes)?;
|
||||
|
||||
file_bytes.splice(0..0, id3v2);
|
||||
|
||||
data.rewind()?;
|
||||
data.set_len(0)?;
|
||||
data.write_all(&file_bytes)?;
|
||||
file.rewind()?;
|
||||
file.truncate(0)?;
|
||||
file.write_all(&file_bytes)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -5,10 +5,10 @@ use crate::macros::err;
|
|||
use crate::tag::{Accessor, ItemKey, ItemValue, MergeTag, SplitTag, Tag, TagExt, TagItem, TagType};
|
||||
|
||||
use std::borrow::Cow;
|
||||
use std::fs::File;
|
||||
use std::io::{Read, Seek, SeekFrom, Write};
|
||||
use std::path::Path;
|
||||
use std::convert::TryFrom;
|
||||
use std::io::{SeekFrom, Write};
|
||||
|
||||
use crate::util::io::{FileLike, Length, Truncate};
|
||||
use byteorder::BigEndian;
|
||||
use lofty_attr::tag;
|
||||
|
||||
|
@ -156,6 +156,11 @@ impl TagExt for AIFFTextChunks {
|
|||
type Err = LoftyError;
|
||||
type RefKey<'a> = &'a ItemKey;
|
||||
|
||||
#[inline]
|
||||
fn tag_type(&self) -> TagType {
|
||||
TagType::AiffText
|
||||
}
|
||||
|
||||
fn len(&self) -> usize {
|
||||
usize::from(self.name.is_some())
|
||||
+ usize::from(self.author.is_some())
|
||||
|
@ -187,11 +192,16 @@ impl TagExt for AIFFTextChunks {
|
|||
)
|
||||
}
|
||||
|
||||
fn save_to(
|
||||
fn save_to<F>(
|
||||
&self,
|
||||
file: &mut File,
|
||||
file: &mut F,
|
||||
write_options: WriteOptions,
|
||||
) -> std::result::Result<(), Self::Err> {
|
||||
) -> std::result::Result<(), Self::Err>
|
||||
where
|
||||
F: FileLike,
|
||||
LoftyError: From<<F as Truncate>::Error>,
|
||||
LoftyError: From<<F as Length>::Error>,
|
||||
{
|
||||
AiffTextChunksRef {
|
||||
name: self.name.as_deref(),
|
||||
author: self.author.as_deref(),
|
||||
|
@ -217,14 +227,6 @@ impl TagExt for AIFFTextChunks {
|
|||
.dump_to(writer, write_options)
|
||||
}
|
||||
|
||||
fn remove_from_path<P: AsRef<Path>>(&self, path: P) -> std::result::Result<(), Self::Err> {
|
||||
TagType::AiffText.remove_from_path(path)
|
||||
}
|
||||
|
||||
fn remove_from(&self, file: &mut File) -> std::result::Result<(), Self::Err> {
|
||||
TagType::AiffText.remove_from(file)
|
||||
}
|
||||
|
||||
fn clear(&mut self) {
|
||||
*self = Self::default();
|
||||
}
|
||||
|
@ -315,7 +317,12 @@ where
|
|||
T: AsRef<str>,
|
||||
AI: IntoIterator<Item = T>,
|
||||
{
|
||||
pub(crate) fn write_to(self, file: &mut File, _write_options: WriteOptions) -> Result<()> {
|
||||
pub(crate) fn write_to<F>(self, file: &mut F, _write_options: WriteOptions) -> Result<()>
|
||||
where
|
||||
F: FileLike,
|
||||
LoftyError: From<<F as Truncate>::Error>,
|
||||
LoftyError: From<<F as Length>::Error>,
|
||||
{
|
||||
AiffTextChunksRef::write_to_inner(file, self)
|
||||
}
|
||||
|
||||
|
@ -414,9 +421,14 @@ where
|
|||
Ok(text_chunks)
|
||||
}
|
||||
|
||||
fn write_to_inner(data: &mut File, mut tag: AiffTextChunksRef<'_, T, AI>) -> Result<()> {
|
||||
super::read::verify_aiff(data)?;
|
||||
let file_len = data.metadata()?.len().saturating_sub(12);
|
||||
fn write_to_inner<F>(file: &mut F, mut tag: AiffTextChunksRef<'_, T, AI>) -> Result<()>
|
||||
where
|
||||
F: FileLike,
|
||||
LoftyError: From<<F as Truncate>::Error>,
|
||||
LoftyError: From<<F as Length>::Error>,
|
||||
{
|
||||
super::read::verify_aiff(file)?;
|
||||
let file_len = file.len()?.saturating_sub(12);
|
||||
|
||||
let text_chunks = Self::create_text_chunks(&mut tag)?;
|
||||
|
||||
|
@ -424,10 +436,10 @@ where
|
|||
|
||||
let mut chunks = Chunks::<BigEndian>::new(file_len);
|
||||
|
||||
while chunks.next(data).is_ok() {
|
||||
while chunks.next(file).is_ok() {
|
||||
match &chunks.fourcc {
|
||||
b"NAME" | b"AUTH" | b"(c) " | b"ANNO" | b"COMT" => {
|
||||
let start = (data.stream_position()? - 8) as usize;
|
||||
let start = (file.stream_position()? - 8) as usize;
|
||||
let mut end = start + 8 + chunks.size as usize;
|
||||
|
||||
if chunks.size % 2 != 0 {
|
||||
|
@ -439,19 +451,19 @@ where
|
|||
_ => {},
|
||||
}
|
||||
|
||||
chunks.skip(data)?;
|
||||
chunks.skip(file)?;
|
||||
}
|
||||
|
||||
data.rewind()?;
|
||||
file.rewind()?;
|
||||
|
||||
let mut file_bytes = Vec::new();
|
||||
data.read_to_end(&mut file_bytes)?;
|
||||
file.read_to_end(&mut file_bytes)?;
|
||||
|
||||
if chunks_remove.is_empty() {
|
||||
data.seek(SeekFrom::Start(16))?;
|
||||
file.seek(SeekFrom::Start(16))?;
|
||||
|
||||
let mut size = [0; 4];
|
||||
data.read_exact(&mut size)?;
|
||||
file.read_exact(&mut size)?;
|
||||
|
||||
let comm_end = (20 + u32::from_le_bytes(size)) as usize;
|
||||
file_bytes.splice(comm_end..comm_end, text_chunks);
|
||||
|
@ -471,9 +483,9 @@ where
|
|||
let total_size = ((file_bytes.len() - 8) as u32).to_be_bytes();
|
||||
file_bytes.splice(4..8, total_size.to_vec());
|
||||
|
||||
data.rewind()?;
|
||||
data.set_len(0)?;
|
||||
data.write_all(&file_bytes)?;
|
||||
file.rewind()?;
|
||||
file.truncate(0)?;
|
||||
file.write_all(&file_bytes)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -8,10 +8,9 @@ use crate::tag::{
|
|||
};
|
||||
|
||||
use std::borrow::Cow;
|
||||
use std::fs::File;
|
||||
use std::io::Write;
|
||||
use std::path::Path;
|
||||
|
||||
use crate::util::io::{FileLike, Length, Truncate};
|
||||
use lofty_attr::tag;
|
||||
|
||||
macro_rules! impl_accessor {
|
||||
|
@ -190,6 +189,11 @@ impl TagExt for RIFFInfoList {
|
|||
type Err = LoftyError;
|
||||
type RefKey<'a> = &'a str;
|
||||
|
||||
#[inline]
|
||||
fn tag_type(&self) -> TagType {
|
||||
TagType::RiffInfo
|
||||
}
|
||||
|
||||
fn len(&self) -> usize {
|
||||
self.items.len()
|
||||
}
|
||||
|
@ -204,11 +208,16 @@ impl TagExt for RIFFInfoList {
|
|||
self.items.is_empty()
|
||||
}
|
||||
|
||||
fn save_to(
|
||||
fn save_to<F>(
|
||||
&self,
|
||||
file: &mut File,
|
||||
file: &mut F,
|
||||
write_options: WriteOptions,
|
||||
) -> std::result::Result<(), Self::Err> {
|
||||
) -> std::result::Result<(), Self::Err>
|
||||
where
|
||||
F: FileLike,
|
||||
LoftyError: From<<F as Truncate>::Error>,
|
||||
LoftyError: From<<F as Length>::Error>,
|
||||
{
|
||||
RIFFInfoListRef::new(self.items.iter().map(|(k, v)| (k.as_str(), v.as_str())))
|
||||
.write_to(file, write_options)
|
||||
}
|
||||
|
@ -222,14 +231,6 @@ impl TagExt for RIFFInfoList {
|
|||
.dump_to(writer, write_options)
|
||||
}
|
||||
|
||||
fn remove_from_path<P: AsRef<Path>>(&self, path: P) -> std::result::Result<(), Self::Err> {
|
||||
TagType::RiffInfo.remove_from_path(path)
|
||||
}
|
||||
|
||||
fn remove_from(&self, file: &mut File) -> std::result::Result<(), Self::Err> {
|
||||
TagType::RiffInfo.remove_from(file)
|
||||
}
|
||||
|
||||
fn clear(&mut self) {
|
||||
self.items.clear();
|
||||
}
|
||||
|
@ -311,7 +312,12 @@ where
|
|||
RIFFInfoListRef { items }
|
||||
}
|
||||
|
||||
pub(crate) fn write_to(&mut self, file: &mut File, write_options: WriteOptions) -> Result<()> {
|
||||
pub(crate) fn write_to<F>(&mut self, file: &mut F, write_options: WriteOptions) -> Result<()>
|
||||
where
|
||||
F: FileLike,
|
||||
LoftyError: From<<F as Truncate>::Error>,
|
||||
LoftyError: From<<F as Length>::Error>,
|
||||
{
|
||||
write::write_riff_info(file, self, write_options)
|
||||
}
|
||||
|
||||
|
|
|
@ -1,56 +1,64 @@
|
|||
use super::RIFFInfoListRef;
|
||||
use crate::config::WriteOptions;
|
||||
use crate::error::Result;
|
||||
use crate::error::{LoftyError, Result};
|
||||
use crate::iff::chunk::Chunks;
|
||||
use crate::iff::wav::read::verify_wav;
|
||||
use crate::macros::err;
|
||||
|
||||
use std::fs::File;
|
||||
use std::io::{Read, Seek, SeekFrom, Write};
|
||||
use std::io::{Read, Seek, SeekFrom};
|
||||
|
||||
use crate::util::io::{FileLike, Length, Truncate};
|
||||
use byteorder::{LittleEndian, WriteBytesExt};
|
||||
|
||||
pub(in crate::iff::wav) fn write_riff_info<'a, I>(
|
||||
data: &mut File,
|
||||
pub(in crate::iff::wav) fn write_riff_info<'a, F, I>(
|
||||
file: &mut F,
|
||||
tag: &mut RIFFInfoListRef<'a, I>,
|
||||
_write_options: WriteOptions,
|
||||
) -> Result<()>
|
||||
where
|
||||
F: FileLike,
|
||||
LoftyError: From<<F as Truncate>::Error>,
|
||||
LoftyError: From<<F as Length>::Error>,
|
||||
I: Iterator<Item = (&'a str, &'a str)>,
|
||||
{
|
||||
verify_wav(data)?;
|
||||
let file_len = data.metadata()?.len().saturating_sub(12);
|
||||
verify_wav(file)?;
|
||||
let file_len = file.len()?.saturating_sub(12);
|
||||
|
||||
let mut riff_info_bytes = Vec::new();
|
||||
create_riff_info(&mut tag.items, &mut riff_info_bytes)?;
|
||||
|
||||
if let Some(info_list_size) = find_info_list(data, file_len)? {
|
||||
let info_list_start = data.seek(SeekFrom::Current(-12))? as usize;
|
||||
let info_list_end = info_list_start + 8 + info_list_size as usize;
|
||||
let Some(info_list_size) = find_info_list(file, file_len)? else {
|
||||
// Simply append the info list to the end of the file and update the file size
|
||||
file.seek(SeekFrom::End(0))?;
|
||||
|
||||
data.rewind()?;
|
||||
file.write_all(&riff_info_bytes)?;
|
||||
|
||||
let mut file_bytes = Vec::new();
|
||||
data.read_to_end(&mut file_bytes)?;
|
||||
let len = (file.stream_position()? - 8) as u32;
|
||||
|
||||
let _ = file_bytes.splice(info_list_start..info_list_end, riff_info_bytes);
|
||||
file.seek(SeekFrom::Start(4))?;
|
||||
file.write_u32::<LittleEndian>(len)?;
|
||||
|
||||
let total_size = (file_bytes.len() - 8) as u32;
|
||||
let _ = file_bytes.splice(4..8, total_size.to_le_bytes());
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
data.rewind()?;
|
||||
data.set_len(0)?;
|
||||
data.write_all(&file_bytes)?;
|
||||
} else {
|
||||
data.seek(SeekFrom::End(0))?;
|
||||
// Replace the existing tag
|
||||
|
||||
data.write_all(&riff_info_bytes)?;
|
||||
let info_list_start = file.seek(SeekFrom::Current(-12))? as usize;
|
||||
let info_list_end = info_list_start + 8 + info_list_size as usize;
|
||||
|
||||
let len = (data.stream_position()? - 8) as u32;
|
||||
file.rewind()?;
|
||||
|
||||
data.seek(SeekFrom::Start(4))?;
|
||||
data.write_u32::<LittleEndian>(len)?;
|
||||
}
|
||||
let mut file_bytes = Vec::new();
|
||||
file.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());
|
||||
|
||||
file.rewind()?;
|
||||
file.truncate(0)?;
|
||||
file.write_all(&file_bytes)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -179,6 +179,8 @@ pub use util::text::TextEncoding;
|
|||
|
||||
pub use lofty_attr::LoftyFile;
|
||||
|
||||
pub use util::io;
|
||||
|
||||
pub mod prelude {
|
||||
//! A prelude for commonly used items in the library.
|
||||
//!
|
||||
|
|
|
@ -15,11 +15,10 @@ use crate::tag::{
|
|||
use atom::{AdvisoryRating, Atom, AtomData};
|
||||
|
||||
use std::borrow::Cow;
|
||||
use std::fs::File;
|
||||
use std::io::Write;
|
||||
use std::ops::Deref;
|
||||
use std::path::Path;
|
||||
|
||||
use crate::util::io::{FileLike, Length, Truncate};
|
||||
use lofty_attr::tag;
|
||||
|
||||
const ARTIST: AtomIdent<'_> = AtomIdent::Fourcc(*b"\xa9ART");
|
||||
|
@ -518,6 +517,11 @@ impl TagExt for Ilst {
|
|||
type Err = LoftyError;
|
||||
type RefKey<'a> = &'a AtomIdent<'a>;
|
||||
|
||||
#[inline]
|
||||
fn tag_type(&self) -> TagType {
|
||||
TagType::Mp4Ilst
|
||||
}
|
||||
|
||||
fn len(&self) -> usize {
|
||||
self.atoms.len()
|
||||
}
|
||||
|
@ -530,11 +534,16 @@ impl TagExt for Ilst {
|
|||
self.atoms.is_empty()
|
||||
}
|
||||
|
||||
fn save_to(
|
||||
fn save_to<F>(
|
||||
&self,
|
||||
file: &mut File,
|
||||
file: &mut F,
|
||||
write_options: WriteOptions,
|
||||
) -> std::result::Result<(), Self::Err> {
|
||||
) -> std::result::Result<(), Self::Err>
|
||||
where
|
||||
F: FileLike,
|
||||
LoftyError: From<<F as Truncate>::Error>,
|
||||
LoftyError: From<<F as Length>::Error>,
|
||||
{
|
||||
self.as_ref().write_to(file, write_options)
|
||||
}
|
||||
|
||||
|
@ -546,14 +555,6 @@ impl TagExt for Ilst {
|
|||
self.as_ref().dump_to(writer, write_options)
|
||||
}
|
||||
|
||||
fn remove_from_path<P: AsRef<Path>>(&self, path: P) -> std::result::Result<(), Self::Err> {
|
||||
TagType::Mp4Ilst.remove_from_path(path)
|
||||
}
|
||||
|
||||
fn remove_from(&self, file: &mut File) -> std::result::Result<(), Self::Err> {
|
||||
TagType::Mp4Ilst.remove_from(file)
|
||||
}
|
||||
|
||||
fn clear(&mut self) {
|
||||
self.atoms.clear();
|
||||
}
|
||||
|
|
|
@ -3,10 +3,10 @@
|
|||
// *********************
|
||||
|
||||
use crate::config::WriteOptions;
|
||||
use crate::error::Result;
|
||||
use crate::error::{LoftyError, Result};
|
||||
use crate::mp4::{Atom, AtomData, AtomIdent, Ilst};
|
||||
|
||||
use std::fs::File;
|
||||
use crate::util::io::{FileLike, Length, Truncate};
|
||||
use std::io::Write;
|
||||
|
||||
impl Ilst {
|
||||
|
@ -25,7 +25,12 @@ impl<'a, I: 'a> IlstRef<'a, I>
|
|||
where
|
||||
I: IntoIterator<Item = &'a AtomData>,
|
||||
{
|
||||
pub(crate) fn write_to(&mut self, file: &mut File, write_options: WriteOptions) -> Result<()> {
|
||||
pub(crate) fn write_to<F>(&mut self, file: &mut F, write_options: WriteOptions) -> Result<()>
|
||||
where
|
||||
F: FileLike,
|
||||
LoftyError: From<<F as Truncate>::Error>,
|
||||
LoftyError: From<<F as Length>::Error>,
|
||||
{
|
||||
super::write::write_to(file, self, write_options)
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use super::r#ref::IlstRef;
|
||||
use crate::config::{ParseOptions, WriteOptions};
|
||||
use crate::error::{FileEncodingError, Result};
|
||||
use crate::error::{FileEncodingError, LoftyError, Result};
|
||||
use crate::file::FileType;
|
||||
use crate::macros::{decode_err, err, try_vec};
|
||||
use crate::mp4::atom_info::{AtomIdent, AtomInfo, ATOM_HEADER_LEN, FOURCC_LEN};
|
||||
|
@ -9,10 +9,9 @@ use crate::mp4::read::{atom_tree, meta_is_full, nested_atom, verify_mp4, AtomRea
|
|||
use crate::mp4::write::{AtomWriter, AtomWriterCompanion, ContextualAtom};
|
||||
use crate::mp4::AtomData;
|
||||
use crate::picture::{MimeType, Picture};
|
||||
|
||||
use std::fs::File;
|
||||
use std::io::{Cursor, Seek, SeekFrom, Write};
|
||||
|
||||
use crate::util::io::{FileLike, Length, Truncate};
|
||||
use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt};
|
||||
|
||||
// A "full" atom is a traditional length + identifier, followed by a version (1) and flags (3)
|
||||
|
@ -20,25 +19,28 @@ const FULL_ATOM_SIZE: u64 = ATOM_HEADER_LEN + 4;
|
|||
const HDLR_SIZE: u64 = ATOM_HEADER_LEN + 25;
|
||||
|
||||
// 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,
|
||||
pub(crate) fn write_to<'a, F, I: 'a>(
|
||||
file: &mut F,
|
||||
tag: &mut IlstRef<'a, I>,
|
||||
write_options: WriteOptions,
|
||||
) -> Result<()>
|
||||
where
|
||||
F: FileLike,
|
||||
LoftyError: From<<F as Truncate>::Error>,
|
||||
LoftyError: From<<F as Length>::Error>,
|
||||
I: IntoIterator<Item = &'a AtomData>,
|
||||
{
|
||||
log::debug!("Attempting to write `ilst` tag to file");
|
||||
|
||||
// Create a temporary `AtomReader`, just to verify that this is a valid MP4 file
|
||||
let mut reader = AtomReader::new(data, ParseOptions::DEFAULT_PARSING_MODE)?;
|
||||
let mut reader = AtomReader::new(file, ParseOptions::DEFAULT_PARSING_MODE)?;
|
||||
verify_mp4(&mut reader)?;
|
||||
|
||||
// Now we can just read the entire file into memory
|
||||
let data = reader.into_inner();
|
||||
data.rewind()?;
|
||||
let file = reader.into_inner();
|
||||
file.rewind()?;
|
||||
|
||||
let mut atom_writer = AtomWriter::new_from_file(data, ParseOptions::DEFAULT_PARSING_MODE)?;
|
||||
let mut atom_writer = AtomWriter::new_from_file(file, ParseOptions::DEFAULT_PARSING_MODE)?;
|
||||
|
||||
let Some(moov) = atom_writer.find_contextual_atom(*b"moov") else {
|
||||
return Err(FileEncodingError::new(
|
||||
|
@ -198,7 +200,7 @@ where
|
|||
|
||||
drop(write_handle);
|
||||
|
||||
atom_writer.save_to(data)?;
|
||||
atom_writer.save_to(file)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
use crate::config::ParsingMode;
|
||||
use crate::error::Result;
|
||||
use crate::error::{LoftyError, Result};
|
||||
use crate::mp4::atom_info::{AtomIdent, AtomInfo, IDENTIFIER_LEN};
|
||||
use crate::mp4::read::skip_unneeded;
|
||||
|
||||
use std::cell::{RefCell, RefMut};
|
||||
use std::fs::File;
|
||||
use std::io::{Cursor, Read, Seek, SeekFrom, Write};
|
||||
use std::ops::RangeBounds;
|
||||
|
||||
use crate::io::{FileLike, Length, Truncate};
|
||||
use byteorder::{BigEndian, WriteBytesExt};
|
||||
|
||||
/// A wrapper around [`AtomInfo`] that allows us to track all of the children of containers we deem important
|
||||
|
@ -111,7 +111,12 @@ impl AtomWriter {
|
|||
/// Create a new [`AtomWriter`]
|
||||
///
|
||||
/// This will read the entire file into memory, and parse its atoms.
|
||||
pub(super) fn new_from_file(file: &mut File, parse_mode: ParsingMode) -> Result<Self> {
|
||||
pub(super) fn new_from_file<F>(file: &mut F, parse_mode: ParsingMode) -> Result<Self>
|
||||
where
|
||||
F: FileLike,
|
||||
LoftyError: From<<F as Truncate>::Error>,
|
||||
LoftyError: From<<F as Length>::Error>,
|
||||
{
|
||||
let mut contents = Cursor::new(Vec::new());
|
||||
file.read_to_end(contents.get_mut())?;
|
||||
|
||||
|
@ -145,9 +150,14 @@ impl AtomWriter {
|
|||
}
|
||||
}
|
||||
|
||||
pub(super) fn save_to(&mut self, file: &mut File) -> Result<()> {
|
||||
pub(super) fn save_to<F>(&mut self, file: &mut F) -> Result<()>
|
||||
where
|
||||
F: FileLike,
|
||||
LoftyError: From<<F as Truncate>::Error>,
|
||||
LoftyError: From<<F as Length>::Error>,
|
||||
{
|
||||
file.rewind()?;
|
||||
file.set_len(0)?;
|
||||
file.truncate(0)?;
|
||||
file.write_all(self.contents.borrow().get_ref())?;
|
||||
|
||||
Ok(())
|
||||
|
|
|
@ -11,11 +11,10 @@ use crate::tag::{
|
|||
};
|
||||
|
||||
use std::borrow::Cow;
|
||||
use std::fs::File;
|
||||
use std::io::Write;
|
||||
use std::ops::Deref;
|
||||
use std::path::Path;
|
||||
|
||||
use crate::util::io::{FileLike, Length, Truncate};
|
||||
use lofty_attr::tag;
|
||||
|
||||
macro_rules! impl_accessor {
|
||||
|
@ -433,6 +432,11 @@ impl TagExt for VorbisComments {
|
|||
type Err = LoftyError;
|
||||
type RefKey<'a> = &'a str;
|
||||
|
||||
#[inline]
|
||||
fn tag_type(&self) -> TagType {
|
||||
TagType::VorbisComments
|
||||
}
|
||||
|
||||
fn len(&self) -> usize {
|
||||
self.items.len() + self.pictures.len()
|
||||
}
|
||||
|
@ -455,11 +459,16 @@ impl TagExt for VorbisComments {
|
|||
/// * The file does not contain valid packets
|
||||
/// * [`PictureInformation::from_picture`]
|
||||
/// * [`std::io::Error`]
|
||||
fn save_to(
|
||||
fn save_to<F>(
|
||||
&self,
|
||||
file: &mut File,
|
||||
file: &mut F,
|
||||
write_options: WriteOptions,
|
||||
) -> std::result::Result<(), Self::Err> {
|
||||
) -> std::result::Result<(), Self::Err>
|
||||
where
|
||||
F: FileLike,
|
||||
LoftyError: From<<F as Truncate>::Error>,
|
||||
LoftyError: From<<F as Length>::Error>,
|
||||
{
|
||||
VorbisCommentsRef {
|
||||
vendor: self.vendor.as_str(),
|
||||
items: self.items.iter().map(|(k, v)| (k.as_str(), v.as_str())),
|
||||
|
@ -490,14 +499,6 @@ impl TagExt for VorbisComments {
|
|||
.dump_to(writer, write_options)
|
||||
}
|
||||
|
||||
fn remove_from_path<P: AsRef<Path>>(&self, path: P) -> std::result::Result<(), Self::Err> {
|
||||
TagType::VorbisComments.remove_from_path(path)
|
||||
}
|
||||
|
||||
fn remove_from(&self, file: &mut File) -> std::result::Result<(), Self::Err> {
|
||||
TagType::VorbisComments.remove_from(file)
|
||||
}
|
||||
|
||||
fn clear(&mut self) {
|
||||
self.items.clear();
|
||||
self.pictures.clear();
|
||||
|
@ -634,7 +635,12 @@ where
|
|||
IP: Iterator<Item = (&'a Picture, PictureInformation)>,
|
||||
{
|
||||
#[allow(clippy::shadow_unrelated)]
|
||||
pub(crate) fn write_to(&mut self, file: &mut File, write_options: WriteOptions) -> Result<()> {
|
||||
pub(crate) fn write_to<F>(&mut self, file: &mut F, write_options: WriteOptions) -> Result<()>
|
||||
where
|
||||
F: FileLike,
|
||||
LoftyError: From<<F as Truncate>::Error>,
|
||||
LoftyError: From<<F as Length>::Error>,
|
||||
{
|
||||
let probe = Probe::new(file).guess_file_type()?;
|
||||
let f_ty = probe.file_type();
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use super::verify_signature;
|
||||
use crate::config::WriteOptions;
|
||||
use crate::error::Result;
|
||||
use crate::error::{LoftyError, Result};
|
||||
use crate::file::FileType;
|
||||
use crate::macros::{decode_err, err, try_vec};
|
||||
use crate::ogg::constants::{OPUSTAGS, VORBIS_COMMENT_HEAD};
|
||||
|
@ -8,9 +8,9 @@ use crate::ogg::tag::{create_vorbis_comments_ref, VorbisCommentsRef};
|
|||
use crate::picture::{Picture, PictureInformation};
|
||||
use crate::tag::{Tag, TagType};
|
||||
|
||||
use std::fs::File;
|
||||
use std::io::{Cursor, Read, Seek, SeekFrom, Write};
|
||||
|
||||
use crate::util::io::{FileLike, Length, Truncate};
|
||||
use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
|
||||
use ogg_pager::{Packets, Page, PageHeader, CONTAINS_FIRST_PAGE_OF_BITSTREAM};
|
||||
|
||||
|
@ -40,12 +40,17 @@ impl OGGFormat {
|
|||
}
|
||||
}
|
||||
|
||||
pub(crate) fn write_to(
|
||||
file: &mut File,
|
||||
pub(crate) fn write_to<F>(
|
||||
file: &mut F,
|
||||
tag: &Tag,
|
||||
file_type: FileType,
|
||||
write_options: WriteOptions,
|
||||
) -> Result<()> {
|
||||
) -> Result<()>
|
||||
where
|
||||
F: FileLike,
|
||||
LoftyError: From<<F as Truncate>::Error>,
|
||||
LoftyError: From<<F as Length>::Error>,
|
||||
{
|
||||
if tag.tag_type() != TagType::VorbisComments {
|
||||
err!(UnsupportedTag);
|
||||
}
|
||||
|
@ -69,14 +74,17 @@ pub(crate) fn write_to(
|
|||
)
|
||||
}
|
||||
|
||||
pub(super) fn write<'a, II, IP>(
|
||||
file: &mut File,
|
||||
pub(super) fn write<'a, F, II, IP>(
|
||||
file: &mut F,
|
||||
tag: &mut VorbisCommentsRef<'a, II, IP>,
|
||||
format: OGGFormat,
|
||||
header_packet_count: isize,
|
||||
_write_options: WriteOptions,
|
||||
) -> Result<()>
|
||||
where
|
||||
F: FileLike,
|
||||
LoftyError: From<<F as Truncate>::Error>,
|
||||
LoftyError: From<<F as Length>::Error>,
|
||||
II: Iterator<Item = (&'a str, &'a str)>,
|
||||
IP: Iterator<Item = (&'a Picture, PictureInformation)>,
|
||||
{
|
||||
|
@ -120,7 +128,7 @@ where
|
|||
packets.set(1, new_metadata_packet);
|
||||
|
||||
file.rewind()?;
|
||||
file.set_len(0)?;
|
||||
file.truncate(0)?;
|
||||
|
||||
let pages_written =
|
||||
packets.write_to(file, stream_serial, 0, CONTAINS_FIRST_PAGE_OF_BITSTREAM)? as u32;
|
||||
|
|
|
@ -14,11 +14,11 @@ use crate::picture::{Picture, PictureType};
|
|||
use crate::probe::Probe;
|
||||
|
||||
use std::borrow::Cow;
|
||||
use std::fs::File;
|
||||
use std::io::Write;
|
||||
use std::path::Path;
|
||||
|
||||
// Exports
|
||||
use crate::util::io::{FileLike, Length, Truncate};
|
||||
pub use accessor::Accessor;
|
||||
pub use item::{ItemKey, ItemValue, TagItem};
|
||||
pub use split_merge_tag::{MergeTag, SplitTag};
|
||||
|
@ -543,6 +543,11 @@ impl TagExt for Tag {
|
|||
type Err = LoftyError;
|
||||
type RefKey<'a> = &'a ItemKey;
|
||||
|
||||
#[inline]
|
||||
fn tag_type(&self) -> TagType {
|
||||
self.tag_type
|
||||
}
|
||||
|
||||
fn len(&self) -> usize {
|
||||
self.items.len() + self.pictures.len()
|
||||
}
|
||||
|
@ -555,17 +560,22 @@ impl TagExt for Tag {
|
|||
self.items.is_empty() && self.pictures.is_empty()
|
||||
}
|
||||
|
||||
/// Save the `Tag` to a [`File`](std::fs::File)
|
||||
/// Save the `Tag` to a [`FileLike`]
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// * A [`FileType`](crate::file::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::file::FileType::supports_tag_type)
|
||||
fn save_to(
|
||||
/// * 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<F>(
|
||||
&self,
|
||||
file: &mut File,
|
||||
file: &mut F,
|
||||
write_options: WriteOptions,
|
||||
) -> std::result::Result<(), Self::Err> {
|
||||
) -> std::result::Result<(), Self::Err>
|
||||
where
|
||||
F: FileLike,
|
||||
LoftyError: From<<F as Truncate>::Error>,
|
||||
LoftyError: From<<F as Length>::Error>,
|
||||
{
|
||||
let probe = Probe::new(file).guess_file_type()?;
|
||||
|
||||
match probe.file_type() {
|
||||
|
@ -593,12 +603,17 @@ impl TagExt for Tag {
|
|||
self.tag_type.remove_from_path(path)
|
||||
}
|
||||
|
||||
/// Remove a tag from a [`File`]
|
||||
/// Remove a tag from a [`FileLike`](crate::FileLike)
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// See [`TagType::remove_from`]
|
||||
fn remove_from(&self, file: &mut File) -> std::result::Result<(), Self::Err> {
|
||||
fn remove_from<F>(&self, file: &mut F) -> std::result::Result<(), Self::Err>
|
||||
where
|
||||
F: FileLike,
|
||||
LoftyError: From<<F as Truncate>::Error>,
|
||||
LoftyError: From<<F as Length>::Error>,
|
||||
{
|
||||
self.tag_type.remove_from(file)
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
use crate::config::WriteOptions;
|
||||
use crate::tag::{Accessor, Tag};
|
||||
use crate::error::LoftyError;
|
||||
use crate::io::{FileLike, Length, Truncate};
|
||||
use crate::tag::{Accessor, Tag, TagType};
|
||||
|
||||
use std::fs::File;
|
||||
use std::path::Path;
|
||||
|
||||
/// A set of common methods between tags
|
||||
|
@ -12,12 +13,15 @@ use std::path::Path;
|
|||
/// This can be implemented downstream to provide a familiar interface for custom tags.
|
||||
pub trait TagExt: Accessor + Into<Tag> + Sized {
|
||||
/// The associated error which can be returned from IO operations
|
||||
type Err: From<std::io::Error>;
|
||||
type Err: From<std::io::Error> + From<LoftyError>;
|
||||
/// The type of key used in the tag for non-mutating functions
|
||||
type RefKey<'a>
|
||||
where
|
||||
Self: 'a;
|
||||
|
||||
#[doc(hidden)]
|
||||
fn tag_type(&self) -> TagType;
|
||||
|
||||
/// Returns the number of items in the tag
|
||||
///
|
||||
/// This will also include any extras, such as pictures.
|
||||
|
@ -95,11 +99,15 @@ pub trait TagExt: Accessor + Into<Tag> + Sized {
|
|||
///
|
||||
/// * The file format could not be determined
|
||||
/// * Attempting to write a tag to a format that does not support it.
|
||||
fn save_to(
|
||||
fn save_to<F>(
|
||||
&self,
|
||||
file: &mut File,
|
||||
file: &mut F,
|
||||
write_options: WriteOptions,
|
||||
) -> std::result::Result<(), Self::Err>;
|
||||
) -> std::result::Result<(), Self::Err>
|
||||
where
|
||||
F: FileLike,
|
||||
LoftyError: From<<F as Truncate>::Error>,
|
||||
LoftyError: From<<F as Length>::Error>;
|
||||
|
||||
#[allow(clippy::missing_errors_doc)]
|
||||
/// Dump the tag to a writer
|
||||
|
@ -116,7 +124,9 @@ pub trait TagExt: Accessor + Into<Tag> + Sized {
|
|||
/// # Errors
|
||||
///
|
||||
/// See [`TagExt::remove_from`]
|
||||
fn remove_from_path<P: AsRef<Path>>(&self, path: P) -> std::result::Result<(), Self::Err>;
|
||||
fn remove_from_path<P: AsRef<Path>>(&self, path: P) -> std::result::Result<(), Self::Err> {
|
||||
self.tag_type().remove_from_path(path).map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Remove a tag from a [`File`]
|
||||
///
|
||||
|
@ -125,7 +135,14 @@ pub trait TagExt: Accessor + Into<Tag> + Sized {
|
|||
/// * It is unable to guess the file format
|
||||
/// * The format doesn't support the tag
|
||||
/// * It is unable to write to the file
|
||||
fn remove_from(&self, file: &mut File) -> std::result::Result<(), Self::Err>;
|
||||
fn remove_from<F>(&self, file: &mut F) -> std::result::Result<(), Self::Err>
|
||||
where
|
||||
F: FileLike,
|
||||
LoftyError: From<<F as Truncate>::Error>,
|
||||
LoftyError: From<<F as Length>::Error>,
|
||||
{
|
||||
self.tag_type().remove_from(file).map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Clear the tag, removing all items
|
||||
///
|
||||
|
|
|
@ -4,7 +4,9 @@ use crate::file::FileType;
|
|||
use crate::macros::err;
|
||||
use crate::probe::Probe;
|
||||
|
||||
use std::fs::{File, OpenOptions};
|
||||
use crate::error::LoftyError;
|
||||
use crate::io::{FileLike, Length, Truncate};
|
||||
use std::fs::OpenOptions;
|
||||
use std::path::Path;
|
||||
|
||||
/// The tag's format
|
||||
|
@ -46,7 +48,12 @@ impl TagType {
|
|||
/// * It is unable to guess the file format
|
||||
/// * The format doesn't support the tag
|
||||
/// * It is unable to write to the file
|
||||
pub fn remove_from(&self, file: &mut File) -> crate::error::Result<()> {
|
||||
pub fn remove_from<F>(&self, file: &mut F) -> crate::error::Result<()>
|
||||
where
|
||||
F: FileLike,
|
||||
LoftyError: From<<F as Truncate>::Error>,
|
||||
LoftyError: From<<F as Length>::Error>,
|
||||
{
|
||||
let probe = Probe::new(file).guess_file_type()?;
|
||||
let Some(file_type) = probe.file_type() else {
|
||||
err!(UnknownFormat);
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
use crate::config::WriteOptions;
|
||||
use crate::error::Result;
|
||||
use crate::error::{LoftyError, Result};
|
||||
use crate::file::FileType;
|
||||
use crate::macros::err;
|
||||
use crate::tag::{Tag, TagType};
|
||||
|
@ -14,16 +14,21 @@ use ape::tag::ApeTagRef;
|
|||
use iff::aiff::tag::AiffTextChunksRef;
|
||||
use iff::wav::tag::RIFFInfoListRef;
|
||||
|
||||
use std::fs::File;
|
||||
use crate::util::io::{FileLike, Length, Truncate};
|
||||
use std::io::Write;
|
||||
|
||||
#[allow(unreachable_patterns)]
|
||||
pub(crate) fn write_tag(
|
||||
pub(crate) fn write_tag<F>(
|
||||
tag: &Tag,
|
||||
file: &mut File,
|
||||
file: &mut F,
|
||||
file_type: FileType,
|
||||
write_options: WriteOptions,
|
||||
) -> Result<()> {
|
||||
) -> Result<()>
|
||||
where
|
||||
F: FileLike,
|
||||
LoftyError: From<<F as Truncate>::Error>,
|
||||
LoftyError: From<<F as Length>::Error>,
|
||||
{
|
||||
match file_type {
|
||||
FileType::Aac => aac::write::write_to(file, tag, write_options),
|
||||
FileType::Aiff => iff::aiff::write::write_to(file, tag, write_options),
|
||||
|
|
0
src/traits.rs
Normal file
0
src/traits.rs
Normal file
249
src/util/io.rs
249
src/util/io.rs
|
@ -1,3 +1,10 @@
|
|||
//! Various traits for reading and writing to file-like objects
|
||||
|
||||
use crate::error::LoftyError;
|
||||
use std::collections::VecDeque;
|
||||
use std::fs::File;
|
||||
use std::io::{Cursor, Read, Seek, Write};
|
||||
|
||||
// TODO: https://github.com/rust-lang/rust/issues/59359
|
||||
pub(crate) trait SeekStreamLen: std::io::Seek {
|
||||
fn stream_len_hack(&mut self) -> crate::error::Result<u64> {
|
||||
|
@ -13,3 +20,245 @@ pub(crate) trait SeekStreamLen: std::io::Seek {
|
|||
}
|
||||
|
||||
impl<T> SeekStreamLen for T where T: std::io::Seek {}
|
||||
|
||||
/// Provides a method to truncate an object to the specified length
|
||||
///
|
||||
/// This is one component of the [`FileLike`] trait, which is used to provide implementors access to any
|
||||
/// file saving methods such as [`crate::AudioFile::save_to`].
|
||||
///
|
||||
/// Take great care in implementing this for downstream types, as Lofty will assume that the
|
||||
/// container has the new length specified. If this assumption were to be broken, files **will** become corrupted.
|
||||
pub trait Truncate {
|
||||
/// The error type of the truncation operation
|
||||
type Error: Into<LoftyError>;
|
||||
|
||||
/// Truncate a storage object to the specified length
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Errors depend on the object being truncated, which may not always be fallible.
|
||||
fn truncate(&mut self, new_len: u64) -> std::result::Result<(), Self::Error>;
|
||||
}
|
||||
|
||||
impl Truncate for File {
|
||||
type Error = std::io::Error;
|
||||
|
||||
fn truncate(&mut self, new_len: u64) -> std::result::Result<(), Self::Error> {
|
||||
self.set_len(new_len)
|
||||
}
|
||||
}
|
||||
|
||||
impl Truncate for Vec<u8> {
|
||||
type Error = std::convert::Infallible;
|
||||
|
||||
fn truncate(&mut self, new_len: u64) -> std::result::Result<(), Self::Error> {
|
||||
self.truncate(new_len as usize);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Truncate for VecDeque<u8> {
|
||||
type Error = std::convert::Infallible;
|
||||
|
||||
fn truncate(&mut self, new_len: u64) -> std::result::Result<(), Self::Error> {
|
||||
self.truncate(new_len as usize);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Truncate for Cursor<T>
|
||||
where
|
||||
T: Truncate,
|
||||
{
|
||||
type Error = <T as Truncate>::Error;
|
||||
|
||||
fn truncate(&mut self, new_len: u64) -> std::result::Result<(), Self::Error> {
|
||||
self.get_mut().truncate(new_len)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Truncate for Box<T>
|
||||
where
|
||||
T: Truncate,
|
||||
{
|
||||
type Error = <T as Truncate>::Error;
|
||||
|
||||
fn truncate(&mut self, new_len: u64) -> std::result::Result<(), Self::Error> {
|
||||
self.as_mut().truncate(new_len)
|
||||
}
|
||||
}
|
||||
|
||||
/// Provides a method to get the length of a storage object
|
||||
///
|
||||
/// This is one component of the [`FileLike`] trait, which is used to provide implementors access to any
|
||||
/// file saving methods such as [`crate::AudioFile::save_to`].
|
||||
///
|
||||
/// Take great care in implementing this for downstream types, as Lofty will assume that the
|
||||
/// container has the exact length specified. If this assumption were to be broken, files **may** become corrupted.
|
||||
pub trait Length {
|
||||
/// The error type of the length operation
|
||||
type Error: Into<LoftyError>;
|
||||
|
||||
/// Get the length of a storage object
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Errors depend on the object being read, which may not always be fallible.
|
||||
fn len(&self) -> std::result::Result<u64, Self::Error>;
|
||||
}
|
||||
|
||||
impl Length for File {
|
||||
type Error = std::io::Error;
|
||||
|
||||
fn len(&self) -> std::result::Result<u64, Self::Error> {
|
||||
self.metadata().map(|m| m.len())
|
||||
}
|
||||
}
|
||||
|
||||
impl Length for Vec<u8> {
|
||||
type Error = std::convert::Infallible;
|
||||
|
||||
fn len(&self) -> std::result::Result<u64, Self::Error> {
|
||||
Ok(self.len() as u64)
|
||||
}
|
||||
}
|
||||
|
||||
impl Length for VecDeque<u8> {
|
||||
type Error = std::convert::Infallible;
|
||||
|
||||
fn len(&self) -> std::result::Result<u64, Self::Error> {
|
||||
Ok(self.len() as u64)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Length for Cursor<T>
|
||||
where
|
||||
T: Length,
|
||||
{
|
||||
type Error = <T as Length>::Error;
|
||||
|
||||
fn len(&self) -> std::result::Result<u64, Self::Error> {
|
||||
Length::len(self.get_ref())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Length for Box<T>
|
||||
where
|
||||
T: Length,
|
||||
{
|
||||
type Error = <T as Length>::Error;
|
||||
|
||||
fn len(&self) -> std::result::Result<u64, Self::Error> {
|
||||
Length::len(self.as_ref())
|
||||
}
|
||||
}
|
||||
|
||||
/// Provides a set of methods to read and write to a file-like object
|
||||
///
|
||||
/// This is a combination of the [`Read`], [`Write`], [`Seek`], [`Truncate`], and [`Length`] traits.
|
||||
/// It is used to provide implementors access to any file saving methods such as [`crate::AudioFile::save_to`].
|
||||
///
|
||||
/// Take great care in implementing this for downstream types, as Lofty will assume that the
|
||||
/// trait implementations are correct. If this assumption were to be broken, files **may** become corrupted.
|
||||
pub trait FileLike: Read + Write + Seek + Truncate + Length
|
||||
where
|
||||
<Self as Truncate>::Error: Into<LoftyError>,
|
||||
<Self as Length>::Error: Into<LoftyError>,
|
||||
{
|
||||
}
|
||||
|
||||
impl<T> FileLike for T
|
||||
where
|
||||
T: Read + Write + Seek + Truncate + Length,
|
||||
<T as Truncate>::Error: Into<LoftyError>,
|
||||
<T as Length>::Error: Into<LoftyError>,
|
||||
{
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::config::{ParseOptions, WriteOptions};
|
||||
use crate::file::AudioFile;
|
||||
use crate::mpeg::MpegFile;
|
||||
use crate::tag::Accessor;
|
||||
|
||||
use std::io::{Cursor, Read, Seek, Write};
|
||||
|
||||
const TEST_ASSET: &str = "tests/files/assets/minimal/full_test.mp3";
|
||||
|
||||
fn test_asset_contents() -> Vec<u8> {
|
||||
std::fs::read(TEST_ASSET).unwrap()
|
||||
}
|
||||
|
||||
fn file() -> MpegFile {
|
||||
let file_contents = test_asset_contents();
|
||||
let mut reader = Cursor::new(file_contents);
|
||||
MpegFile::read_from(&mut reader, ParseOptions::new()).unwrap()
|
||||
}
|
||||
|
||||
fn alter_tag(file: &mut MpegFile) {
|
||||
let tag = file.id3v2_mut().unwrap();
|
||||
tag.set_artist(String::from("Bar artist"));
|
||||
}
|
||||
|
||||
fn revert_tag(file: &mut MpegFile) {
|
||||
let tag = file.id3v2_mut().unwrap();
|
||||
tag.set_artist(String::from("Foo artist"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn io_save_to_file() {
|
||||
// Read the file and change the artist
|
||||
let mut file = file();
|
||||
alter_tag(&mut file);
|
||||
|
||||
let mut temp_file = tempfile::tempfile().unwrap();
|
||||
let file_content = std::fs::read(TEST_ASSET).unwrap();
|
||||
temp_file.write_all(&file_content).unwrap();
|
||||
temp_file.rewind().unwrap();
|
||||
|
||||
// Save the new artist
|
||||
file.save_to(&mut temp_file, WriteOptions::new().preferred_padding(0))
|
||||
.expect("Failed to save to file");
|
||||
|
||||
// Read the file again and change the artist back
|
||||
temp_file.rewind().unwrap();
|
||||
let mut file = MpegFile::read_from(&mut temp_file, ParseOptions::new()).unwrap();
|
||||
revert_tag(&mut file);
|
||||
|
||||
temp_file.rewind().unwrap();
|
||||
file.save_to(&mut temp_file, WriteOptions::new().preferred_padding(0))
|
||||
.expect("Failed to save to file");
|
||||
|
||||
// The contents should be the same as the original file
|
||||
temp_file.rewind().unwrap();
|
||||
let mut current_file_contents = Vec::new();
|
||||
temp_file.read_to_end(&mut current_file_contents).unwrap();
|
||||
|
||||
assert_eq!(current_file_contents, test_asset_contents());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn io_save_to_vec() {
|
||||
// Same test as above, but using a Cursor<Vec<u8>> instead of a file
|
||||
let mut file = file();
|
||||
alter_tag(&mut file);
|
||||
|
||||
let file_content = std::fs::read(TEST_ASSET).unwrap();
|
||||
|
||||
let mut reader = Cursor::new(file_content);
|
||||
file.save_to(&mut reader, WriteOptions::new().preferred_padding(0))
|
||||
.expect("Failed to save to vec");
|
||||
|
||||
reader.rewind().unwrap();
|
||||
let mut file = MpegFile::read_from(&mut reader, ParseOptions::new()).unwrap();
|
||||
revert_tag(&mut file);
|
||||
|
||||
reader.rewind().unwrap();
|
||||
file.save_to(&mut reader, WriteOptions::new().preferred_padding(0))
|
||||
.expect("Failed to save to vec");
|
||||
|
||||
let current_file_contents = reader.into_inner();
|
||||
assert_eq!(current_file_contents, test_asset_contents());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
pub(crate) mod alloc;
|
||||
pub(crate) mod io;
|
||||
pub mod io;
|
||||
pub(crate) mod math;
|
||||
pub(crate) mod text;
|
||||
|
|
Loading…
Reference in a new issue