Skip to content

Commit

Permalink
ID3v2: Support writing ID3v2.3 tags
Browse files Browse the repository at this point in the history
closes #62
  • Loading branch information
Serial-ATA committed Jul 4, 2024
1 parent ae94af1 commit a6b56c6
Show file tree
Hide file tree
Showing 22 changed files with 817 additions and 172 deletions.
4 changes: 2 additions & 2 deletions lofty/src/ape/read.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use crate::id3::v1::tag::Id3v1Tag;
use crate::id3::v2::read::parse_id3v2;
use crate::id3::v2::tag::Id3v2Tag;
use crate::id3::{find_id3v1, find_id3v2, find_lyrics3v2, FindId3v2Config, ID3FindResults};
use crate::macros::{decode_err, err};
use crate::macros::decode_err;

use std::io::{Read, Seek, SeekFrom};

Expand Down Expand Up @@ -82,7 +82,7 @@ where
})?;

if &remaining[..4] != b"AGEX" {
err!(FakeTag)
decode_err!(@BAIL Ape, "Found incomplete APE tag");
}

let ape_header = read_ape_header(data, false)?;
Expand Down
30 changes: 30 additions & 0 deletions lofty/src/config/write_options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ pub struct WriteOptions {
pub(crate) remove_others: bool,
pub(crate) respect_read_only: bool,
pub(crate) uppercase_id3v2_chunk: bool,
pub(crate) use_id3v23: bool,
}

impl WriteOptions {
Expand All @@ -32,6 +33,7 @@ impl WriteOptions {
remove_others: false,
respect_read_only: true,
uppercase_id3v2_chunk: true,
use_id3v23: false,
}
}

Expand Down Expand Up @@ -148,6 +150,33 @@ impl WriteOptions {
self.uppercase_id3v2_chunk = uppercase_id3v2_chunk;
self
}

/// Whether or not to use ID3v2.3 when saving [`TagType::Id3v2`](crate::tag::TagType::Id3v2)
/// or [`Id3v2Tag`](crate::id3::v2::Id3v2Tag)
///
/// By default, Lofty will save ID3v2.4 tags. This option allows you to save ID3v2.3 tags instead.
///
/// # Examples
///
/// ```rust,no_run
/// use lofty::config::WriteOptions;
/// use lofty::prelude::*;
/// use lofty::tag::{Tag, TagType};
///
/// # fn main() -> lofty::error::Result<()> {
/// let mut id3v2_tag = Tag::new(TagType::Id3v2);
///
/// // ...
///
/// // I need to save ID3v2.3 tags to support older software
/// let options = WriteOptions::new().use_id3v23(true);
/// id3v2_tag.save_to_path("test.mp3", options)?;
/// # Ok(()) }
/// ```
pub fn use_id3v23(&mut self, use_id3v23: bool) -> Self {
self.use_id3v23 = use_id3v23;
*self
}
}

impl Default for WriteOptions {
Expand All @@ -161,6 +190,7 @@ impl Default for WriteOptions {
/// remove_others: false,
/// respect_read_only: true,
/// uppercase_id3v2_chunk: true,
/// use_id3v23: false,
/// }
/// ```
fn default() -> Self {
Expand Down
2 changes: 1 addition & 1 deletion lofty/src/id3/v2/frame/content.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,13 @@ pub(super) fn parse_content<R: Read>(
"OWNE" => OwnershipFrame::parse(reader, flags)?.map(Frame::Ownership),
"ETCO" => EventTimingCodesFrame::parse(reader, flags)?.map(Frame::EventTimingCodes),
"PRIV" => PrivateFrame::parse(reader, flags)?.map(Frame::Private),
"TDEN" | "TDOR" | "TDRC" | "TDRL" | "TDTG" => TimestampFrame::parse(reader, id, flags, parse_mode)?.map(Frame::Timestamp),
i if i.starts_with('T') => TextInformationFrame::parse(reader, id, flags, version)?.map(Frame::Text),
// Apple proprietary frames
// WFED (Podcast URL), GRP1 (Grouping), MVNM (Movement Name), MVIN (Movement Number)
"WFED" | "GRP1" | "MVNM" | "MVIN" => TextInformationFrame::parse(reader, id, flags, version)?.map(Frame::Text),
i if i.starts_with('W') => UrlLinkFrame::parse(reader, id, flags)?.map(Frame::Url),
"POPM" => Some(Frame::Popularimeter(PopularimeterFrame::parse(reader, flags)?)),
"TDEN" | "TDOR" | "TDRC" | "TDRL" | "TDTG" => TimestampFrame::parse(reader, id, flags, parse_mode)?.map(Frame::Timestamp),
// SYLT, GEOB, and any unknown frames
_ => {
Some(Frame::Binary(BinaryFrame::parse(reader, id, flags)?))
Expand Down
2 changes: 1 addition & 1 deletion lofty/src/id3/v2/frame/header/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ impl<'a> FrameHeader<'a> {
}

/// Get the ID of the frame
pub const fn id(&self) -> &FrameId<'a> {
pub const fn id(&'a self) -> &'a FrameId<'a> {
&self.id
}
}
Expand Down
40 changes: 8 additions & 32 deletions lofty/src/id3/v2/frame/header/parse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use crate::id3::v2::util::upgrade::{upgrade_v2, upgrade_v3};
use crate::id3::v2::FrameId;
use crate::util::text::utf8_decode_str;

use crate::config::ParseOptions;
use std::borrow::Cow;
use std::io::Read;

Expand Down Expand Up @@ -44,6 +45,7 @@ pub(crate) fn parse_header<R>(
reader: &mut R,
size: &mut u32,
synchsafe: bool,
parse_options: ParseOptions,
) -> Result<Option<(FrameId<'static>, FrameFlags)>>
where
R: Read,
Expand Down Expand Up @@ -87,45 +89,19 @@ where
} else {
Cow::Owned(id_str.to_owned())
}
} else if !synchsafe {
} else if !synchsafe && parse_options.implicit_conversions {
upgrade_v3(id_str).map_or_else(|| Cow::Owned(id_str.to_owned()), Cow::Borrowed)
} else {
Cow::Owned(id_str.to_owned())
};
let frame_id = FrameId::new_cow(id)?;

let flags = u16::from_be_bytes([header[8], header[9]]);
let flags = parse_flags(flags, synchsafe);
let flags = if synchsafe {
FrameFlags::parse_id3v24(flags)
} else {
FrameFlags::parse_id3v23(flags)
};

Ok(Some((frame_id, flags)))
}

pub(crate) fn parse_flags(flags: u16, v4: bool) -> FrameFlags {
FrameFlags {
tag_alter_preservation: if v4 {
flags & 0x4000 == 0x4000
} else {
flags & 0x8000 == 0x8000
},
file_alter_preservation: if v4 {
flags & 0x2000 == 0x2000
} else {
flags & 0x4000 == 0x4000
},
read_only: if v4 {
flags & 0x1000 == 0x1000
} else {
flags & 0x2000 == 0x2000
},
grouping_identity: ((v4 && flags & 0x0040 == 0x0040) || (flags & 0x0020 == 0x0020))
.then_some(0),
compression: if v4 {
flags & 0x0008 == 0x0008
} else {
flags & 0x0080 == 0x0080
},
encryption: ((v4 && flags & 0x0004 == 0x0004) || flags & 0x0040 == 0x0040).then_some(0),
unsynchronisation: if v4 { flags & 0x0002 == 0x0002 } else { false },
data_length_indicator: (v4 && flags & 0x0001 == 0x0001).then_some(0),
}
}
145 changes: 135 additions & 10 deletions lofty/src/id3/v2/frame/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -195,23 +195,31 @@ impl<'a> Frame<'a> {
}

impl<'a> Frame<'a> {
pub(super) fn as_bytes(&self) -> Result<Vec<u8>> {
pub(super) fn as_bytes(&self, is_id3v23: bool) -> Result<Vec<u8>> {
Ok(match self {
Frame::Comment(comment) => comment.as_bytes()?,
Frame::UnsynchronizedText(lf) => lf.as_bytes()?,
Frame::Text(tif) => tif.as_bytes(),
Frame::UserText(content) => content.as_bytes(),
Frame::UserUrl(content) => content.as_bytes(),
Frame::Comment(comment) => comment.as_bytes(is_id3v23)?,
Frame::UnsynchronizedText(lf) => lf.as_bytes(is_id3v23)?,
Frame::Text(tif) => tif.as_bytes(is_id3v23),
Frame::UserText(content) => content.as_bytes(is_id3v23),
Frame::UserUrl(content) => content.as_bytes(is_id3v23),
Frame::Url(link) => link.as_bytes(),
Frame::Picture(attached_picture) => attached_picture.as_bytes(Id3v2Version::V4)?,
Frame::Picture(attached_picture) => {
let version = if is_id3v23 {
Id3v2Version::V3
} else {
Id3v2Version::V4
};

attached_picture.as_bytes(version)?
},
Frame::Popularimeter(popularimeter) => popularimeter.as_bytes(),
Frame::KeyValue(content) => content.as_bytes(),
Frame::KeyValue(content) => content.as_bytes(is_id3v23),
Frame::RelativeVolumeAdjustment(frame) => frame.as_bytes(),
Frame::UniqueFileIdentifier(frame) => frame.as_bytes(),
Frame::Ownership(frame) => frame.as_bytes()?,
Frame::Ownership(frame) => frame.as_bytes(is_id3v23)?,
Frame::EventTimingCodes(frame) => frame.as_bytes(),
Frame::Private(frame) => frame.as_bytes(),
Frame::Timestamp(frame) => frame.as_bytes()?,
Frame::Timestamp(frame) => frame.as_bytes(is_id3v23)?,
Frame::Binary(frame) => frame.as_bytes(),
})
}
Expand Down Expand Up @@ -281,6 +289,123 @@ pub struct FrameFlags {
pub data_length_indicator: Option<u32>,
}

impl FrameFlags {
/// Parse the flags from an ID3v2.4 frame
///
/// NOTE: If any of the following flags are set, they will be set to `Some(0)`:
/// * `grouping_identity`
/// * `encryption`
/// * `data_length_indicator`
pub fn parse_id3v24(flags: u16) -> Self {
FrameFlags {
tag_alter_preservation: flags & 0x4000 == 0x4000,
file_alter_preservation: flags & 0x2000 == 0x2000,
read_only: flags & 0x1000 == 0x1000,
grouping_identity: (flags & 0x0040 == 0x0040).then_some(0),
compression: flags & 0x0008 == 0x0008,
encryption: (flags & 0x0004 == 0x0004).then_some(0),
unsynchronisation: flags & 0x0002 == 0x0002,
data_length_indicator: (flags & 0x0001 == 0x0001).then_some(0),
}
}

/// Parse the flags from an ID3v2.3 frame
///
/// NOTE: If any of the following flags are set, they will be set to `Some(0)`:
/// * `grouping_identity`
/// * `encryption`
pub fn parse_id3v23(flags: u16) -> Self {
FrameFlags {
tag_alter_preservation: flags & 0x8000 == 0x8000,
file_alter_preservation: flags & 0x4000 == 0x4000,
read_only: flags & 0x2000 == 0x2000,
grouping_identity: (flags & 0x0020 == 0x0020).then_some(0),
compression: flags & 0x0080 == 0x0080,
encryption: (flags & 0x0040 == 0x0040).then_some(0),
unsynchronisation: false,
data_length_indicator: None,
}
}

/// Get the ID3v2.4 byte representation of the flags
pub fn as_id3v24_bytes(&self) -> u16 {
let mut flags = 0;

if *self == FrameFlags::default() {
return flags;
}

if self.tag_alter_preservation {
flags |= 0x4000
}

if self.file_alter_preservation {
flags |= 0x2000
}

if self.read_only {
flags |= 0x1000
}

if self.grouping_identity.is_some() {
flags |= 0x0040
}

if self.compression {
flags |= 0x0008
}

if self.encryption.is_some() {
flags |= 0x0004
}

if self.unsynchronisation {
flags |= 0x0002
}

if self.data_length_indicator.is_some() {
flags |= 0x0001
}

flags
}

/// Get the ID3v2.3 byte representation of the flags
pub fn as_id3v23_bytes(&self) -> u16 {
let mut flags = 0;

if *self == FrameFlags::default() {
return flags;
}

if self.tag_alter_preservation {
flags |= 0x8000
}

if self.file_alter_preservation {
flags |= 0x4000
}

if self.read_only {
flags |= 0x2000
}

if self.grouping_identity.is_some() {
flags |= 0x0020
}

if self.compression {
flags |= 0x0080
}

if self.encryption.is_some() {
flags |= 0x0040
}

flags
}
}

#[derive(Clone)]
pub(crate) struct FrameRef<'a>(pub(crate) Cow<'a, Frame<'a>>);

Expand Down
Loading

0 comments on commit a6b56c6

Please sign in to comment.