mirror of
https://github.com/Serial-ATA/lofty-rs
synced 2024-11-10 06:34:18 +00:00
parent
ae94af1a88
commit
a6b56c620d
22 changed files with 817 additions and 172 deletions
|
@ -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};
|
||||
|
||||
|
@ -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)?;
|
||||
|
|
|
@ -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 {
|
||||
|
@ -32,6 +33,7 @@ impl WriteOptions {
|
|||
remove_others: false,
|
||||
respect_read_only: true,
|
||||
uppercase_id3v2_chunk: true,
|
||||
use_id3v23: false,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
|
|
@ -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)?))
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
@ -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,
|
||||
|
@ -87,7 +89,7 @@ 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())
|
||||
|
@ -95,37 +97,11 @@ where
|
|||
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),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(),
|
||||
})
|
||||
}
|
||||
|
@ -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>>);
|
||||
|
||||
|
|
|
@ -29,13 +29,12 @@ impl<'a> ParsedFrame<'a> {
|
|||
R: Read,
|
||||
{
|
||||
let mut size = 0u32;
|
||||
let parse_mode = parse_options.parsing_mode;
|
||||
|
||||
// The header will be upgraded to ID3v2.4 past this point, so they can all be treated the same
|
||||
let parse_header_result = match version {
|
||||
Id3v2Version::V2 => parse_v2_header(reader, &mut size),
|
||||
Id3v2Version::V3 => parse_header(reader, &mut size, false),
|
||||
Id3v2Version::V4 => parse_header(reader, &mut size, true),
|
||||
Id3v2Version::V3 => parse_header(reader, &mut size, false, parse_options),
|
||||
Id3v2Version::V4 => parse_header(reader, &mut size, true, parse_options),
|
||||
};
|
||||
let (id, mut flags) = match parse_header_result {
|
||||
Ok(None) => {
|
||||
|
@ -44,7 +43,7 @@ impl<'a> ParsedFrame<'a> {
|
|||
},
|
||||
Ok(Some(some)) => some,
|
||||
Err(err) => {
|
||||
match parse_mode {
|
||||
match parse_options.parsing_mode {
|
||||
ParsingMode::Strict => return Err(err),
|
||||
ParsingMode::BestAttempt | ParsingMode::Relaxed => {
|
||||
// Skip this frame and continue reading
|
||||
|
@ -60,7 +59,7 @@ impl<'a> ParsedFrame<'a> {
|
|||
}
|
||||
|
||||
if size == 0 {
|
||||
if parse_mode == ParsingMode::Strict {
|
||||
if parse_options.parsing_mode == ParsingMode::Strict {
|
||||
return Err(Id3v2Error::new(Id3v2ErrorKind::EmptyFrame(id)).into());
|
||||
}
|
||||
|
||||
|
@ -146,7 +145,7 @@ impl<'a> ParsedFrame<'a> {
|
|||
id,
|
||||
flags,
|
||||
version,
|
||||
parse_mode,
|
||||
parse_options.parsing_mode,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -160,7 +159,7 @@ impl<'a> ParsedFrame<'a> {
|
|||
id,
|
||||
flags,
|
||||
version,
|
||||
parse_mode,
|
||||
parse_options.parsing_mode,
|
||||
);
|
||||
},
|
||||
// Possible combinations:
|
||||
|
@ -182,7 +181,7 @@ impl<'a> ParsedFrame<'a> {
|
|||
id,
|
||||
flags,
|
||||
version,
|
||||
parse_mode,
|
||||
parse_options.parsing_mode,
|
||||
);
|
||||
},
|
||||
// Possible combinations:
|
||||
|
@ -196,7 +195,14 @@ impl<'a> ParsedFrame<'a> {
|
|||
},
|
||||
// Everything else that doesn't have special flags
|
||||
_ => {
|
||||
return parse_frame(&mut reader, size, id, flags, version, parse_mode);
|
||||
return parse_frame(
|
||||
&mut reader,
|
||||
size,
|
||||
id,
|
||||
flags,
|
||||
version,
|
||||
parse_options.parsing_mode,
|
||||
);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
@ -40,6 +40,46 @@ pub struct Id3v2TagFlags {
|
|||
pub restrictions: Option<TagRestrictions>,
|
||||
}
|
||||
|
||||
impl Id3v2TagFlags {
|
||||
/// Get the **ID3v2.4** byte representation of the flags
|
||||
///
|
||||
/// NOTE: This does not include the extended header flags
|
||||
pub fn as_id3v24_byte(&self) -> u8 {
|
||||
let mut byte = 0;
|
||||
|
||||
if self.unsynchronisation {
|
||||
byte |= 0x80;
|
||||
}
|
||||
|
||||
if self.experimental {
|
||||
byte |= 0x20;
|
||||
}
|
||||
|
||||
if self.footer {
|
||||
byte |= 0x10;
|
||||
}
|
||||
|
||||
byte
|
||||
}
|
||||
|
||||
/// Get the **ID3v2.3** byte representation of the flags
|
||||
///
|
||||
/// NOTE: This does not include the extended header flags
|
||||
pub fn as_id3v23_byte(&self) -> u8 {
|
||||
let mut byte = 0;
|
||||
|
||||
if self.experimental {
|
||||
byte |= 0x40;
|
||||
}
|
||||
|
||||
if self.footer {
|
||||
byte |= 0x10;
|
||||
}
|
||||
|
||||
byte
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub(crate) struct Id3v2Header {
|
||||
pub version: Id3v2Version,
|
||||
|
|
|
@ -135,7 +135,12 @@ impl<'a> AttachedPictureFrame<'a> {
|
|||
///
|
||||
/// * The mimetype is not [`MimeType::Png`] or [`MimeType::Jpeg`]
|
||||
pub fn as_bytes(&self, version: Id3v2Version) -> Result<Vec<u8>> {
|
||||
let mut data = vec![self.encoding as u8];
|
||||
let mut encoding = self.encoding;
|
||||
if version != Id3v2Version::V4 {
|
||||
encoding = encoding.to_id3v23();
|
||||
}
|
||||
|
||||
let mut data = vec![encoding as u8];
|
||||
|
||||
let max_size = match version {
|
||||
// ID3v2.2 uses a 24-bit number for sizes
|
||||
|
@ -168,7 +173,7 @@ impl<'a> AttachedPictureFrame<'a> {
|
|||
data.write_u8(self.picture.pic_type.as_u8())?;
|
||||
|
||||
match &self.picture.description {
|
||||
Some(description) => data.write_all(&encode_text(description, self.encoding, true))?,
|
||||
Some(description) => data.write_all(&encode_text(description, encoding, true))?,
|
||||
None => data.write_u8(0)?,
|
||||
}
|
||||
|
||||
|
|
|
@ -165,11 +165,16 @@ impl<'a> ExtendedTextFrame<'a> {
|
|||
}
|
||||
|
||||
/// Convert an [`ExtendedTextFrame`] to a byte vec
|
||||
pub fn as_bytes(&self) -> Vec<u8> {
|
||||
let mut bytes = vec![self.encoding as u8];
|
||||
pub fn as_bytes(&self, is_id3v23: bool) -> Vec<u8> {
|
||||
let mut encoding = self.encoding;
|
||||
if is_id3v23 {
|
||||
encoding = encoding.to_id3v23();
|
||||
}
|
||||
|
||||
bytes.extend(encode_text(&self.description, self.encoding, true).iter());
|
||||
bytes.extend(encode_text(&self.content, self.encoding, false));
|
||||
let mut bytes = vec![encoding as u8];
|
||||
|
||||
bytes.extend(encode_text(&self.description, encoding, true).iter());
|
||||
bytes.extend(encode_text(&self.content, encoding, false));
|
||||
|
||||
bytes
|
||||
}
|
||||
|
|
|
@ -113,11 +113,16 @@ impl<'a> ExtendedUrlFrame<'a> {
|
|||
}
|
||||
|
||||
/// Convert an [`ExtendedUrlFrame`] to a byte vec
|
||||
pub fn as_bytes(&self) -> Vec<u8> {
|
||||
let mut bytes = vec![self.encoding as u8];
|
||||
pub fn as_bytes(&self, is_id3v23: bool) -> Vec<u8> {
|
||||
let mut encoding = self.encoding;
|
||||
if is_id3v23 {
|
||||
encoding = encoding.to_id3v23();
|
||||
}
|
||||
|
||||
bytes.extend(encode_text(&self.description, self.encoding, true).iter());
|
||||
bytes.extend(encode_text(&self.content, self.encoding, false));
|
||||
let mut bytes = vec![encoding as u8];
|
||||
|
||||
bytes.extend(encode_text(&self.description, encoding, true).iter());
|
||||
bytes.extend(encode_text(&self.content, encoding, false));
|
||||
|
||||
bytes
|
||||
}
|
||||
|
|
|
@ -114,12 +114,17 @@ impl<'a> KeyValueFrame<'a> {
|
|||
}
|
||||
|
||||
/// Convert a [`KeyValueFrame`] to a byte vec
|
||||
pub fn as_bytes(&self) -> Vec<u8> {
|
||||
let mut content = vec![self.encoding as u8];
|
||||
pub fn as_bytes(&self, is_id3v23: bool) -> Vec<u8> {
|
||||
let mut encoding = self.encoding;
|
||||
if is_id3v23 {
|
||||
encoding = encoding.to_id3v23();
|
||||
}
|
||||
|
||||
let mut content = vec![encoding as u8];
|
||||
|
||||
for (key, value) in &self.key_value_pairs {
|
||||
content.append(&mut encode_text(key, self.encoding, true));
|
||||
content.append(&mut encode_text(value, self.encoding, true));
|
||||
content.append(&mut encode_text(key, encoding, true));
|
||||
content.append(&mut encode_text(value, encoding, true));
|
||||
}
|
||||
content
|
||||
}
|
||||
|
|
|
@ -51,11 +51,16 @@ impl LanguageFrame {
|
|||
}
|
||||
|
||||
fn create_bytes(
|
||||
encoding: TextEncoding,
|
||||
mut encoding: TextEncoding,
|
||||
language: [u8; 3],
|
||||
description: &str,
|
||||
content: &str,
|
||||
is_id3v23: bool,
|
||||
) -> Result<Vec<u8>> {
|
||||
if is_id3v23 {
|
||||
encoding = encoding.to_id3v23();
|
||||
}
|
||||
|
||||
let mut bytes = vec![encoding as u8];
|
||||
|
||||
if language.len() != 3 || language.iter().any(|c| !c.is_ascii_alphabetic()) {
|
||||
|
@ -175,12 +180,13 @@ impl<'a> CommentFrame<'a> {
|
|||
///
|
||||
/// * `language` is not exactly 3 bytes
|
||||
/// * `language` contains invalid characters (Only `'a'..='z'` and `'A'..='Z'` allowed)
|
||||
pub fn as_bytes(&self) -> Result<Vec<u8>> {
|
||||
pub fn as_bytes(&self, is_id3v23: bool) -> Result<Vec<u8>> {
|
||||
LanguageFrame::create_bytes(
|
||||
self.encoding,
|
||||
self.language,
|
||||
&self.description,
|
||||
&self.content,
|
||||
is_id3v23,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -290,12 +296,13 @@ impl<'a> UnsynchronizedTextFrame<'a> {
|
|||
///
|
||||
/// * `language` is not exactly 3 bytes
|
||||
/// * `language` contains invalid characters (Only `'a'..='z'` and `'A'..='Z'` allowed)
|
||||
pub fn as_bytes(&self) -> Result<Vec<u8>> {
|
||||
pub fn as_bytes(&self, is_id3v23: bool) -> Result<Vec<u8>> {
|
||||
LanguageFrame::create_bytes(
|
||||
self.encoding,
|
||||
self.language,
|
||||
&self.description,
|
||||
&self.content,
|
||||
is_id3v23,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -116,8 +116,13 @@ impl<'a> OwnershipFrame<'a> {
|
|||
/// # Errors
|
||||
///
|
||||
/// * `date_of_purchase` is not at least 8 characters (it will be truncated if greater)
|
||||
pub fn as_bytes(&self) -> Result<Vec<u8>> {
|
||||
let mut bytes = vec![self.encoding as u8];
|
||||
pub fn as_bytes(&self, is_id3v23: bool) -> Result<Vec<u8>> {
|
||||
let mut encoding = self.encoding;
|
||||
if is_id3v23 {
|
||||
encoding = encoding.to_id3v23();
|
||||
}
|
||||
|
||||
let mut bytes = vec![encoding as u8];
|
||||
|
||||
bytes.extend(encode_text(&self.price_paid, TextEncoding::Latin1, true));
|
||||
if self.date_of_purchase.len() < 8 {
|
||||
|
@ -125,7 +130,7 @@ impl<'a> OwnershipFrame<'a> {
|
|||
}
|
||||
|
||||
bytes.extend(self.date_of_purchase.as_bytes().iter().take(8));
|
||||
bytes.extend(encode_text(&self.seller, self.encoding, false));
|
||||
bytes.extend(encode_text(&self.seller, encoding, false));
|
||||
|
||||
Ok(bytes)
|
||||
}
|
||||
|
@ -161,7 +166,7 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn owne_encode() {
|
||||
let encoded = expected().as_bytes().unwrap();
|
||||
let encoded = expected().as_bytes(false).unwrap();
|
||||
|
||||
let expected_bytes =
|
||||
crate::tag::utils::test_utils::read_path("tests/tags/assets/id3v2/test.owne");
|
||||
|
|
|
@ -93,10 +93,15 @@ impl<'a> TextInformationFrame<'a> {
|
|||
}
|
||||
|
||||
/// Convert an [`TextInformationFrame`] to a byte vec
|
||||
pub fn as_bytes(&self) -> Vec<u8> {
|
||||
let mut content = encode_text(&self.value, self.encoding, false);
|
||||
pub fn as_bytes(&self, is_id3v23: bool) -> Vec<u8> {
|
||||
let mut encoding = self.encoding;
|
||||
if is_id3v23 {
|
||||
encoding = encoding.to_id3v23();
|
||||
}
|
||||
|
||||
content.insert(0, self.encoding as u8);
|
||||
let mut content = encode_text(&self.value, encoding, false);
|
||||
|
||||
content.insert(0, encoding as u8);
|
||||
content
|
||||
}
|
||||
}
|
||||
|
|
|
@ -106,11 +106,16 @@ impl<'a> TimestampFrame<'a> {
|
|||
///
|
||||
/// * The timestamp is invalid
|
||||
/// * Failure to write to the buffer
|
||||
pub fn as_bytes(&self) -> Result<Vec<u8>> {
|
||||
pub fn as_bytes(&self, is_id3v23: bool) -> Result<Vec<u8>> {
|
||||
let mut encoding = self.encoding;
|
||||
if is_id3v23 {
|
||||
encoding = encoding.to_id3v23();
|
||||
}
|
||||
|
||||
self.timestamp.verify()?;
|
||||
|
||||
let mut encoded_text = encode_text(&self.timestamp.to_string(), self.encoding, false);
|
||||
encoded_text.insert(0, self.encoding as u8);
|
||||
let mut encoded_text = encode_text(&self.timestamp.to_string(), encoding, false);
|
||||
encoded_text.insert(0, encoding as u8);
|
||||
|
||||
Ok(encoded_text)
|
||||
}
|
||||
|
|
|
@ -4,7 +4,10 @@ use super::tag::Id3v2Tag;
|
|||
use crate::config::ParseOptions;
|
||||
use crate::error::{Id3v2Error, Id3v2ErrorKind, Result};
|
||||
use crate::id3::v2::util::synchsafe::UnsynchronizedStream;
|
||||
use crate::id3::v2::{Frame, FrameId, Id3v2Version, TimestampFrame};
|
||||
use crate::tag::items::Timestamp;
|
||||
|
||||
use std::borrow::Cow;
|
||||
use std::io::Read;
|
||||
|
||||
pub(crate) fn parse_id3v2<R>(
|
||||
|
@ -23,7 +26,7 @@ where
|
|||
|
||||
let mut tag_bytes = bytes.take(u64::from(header.size - header.extended_size));
|
||||
|
||||
let ret;
|
||||
let mut ret;
|
||||
if header.flags.unsynchronisation {
|
||||
// Unsynchronize the entire tag
|
||||
let mut unsynchronized_reader = UnsynchronizedStream::new(tag_bytes);
|
||||
|
@ -37,9 +40,96 @@ where
|
|||
|
||||
// Throw away the rest of the tag (padding, bad frames)
|
||||
std::io::copy(&mut tag_bytes, &mut std::io::sink())?;
|
||||
|
||||
// Construct TDRC frame from TYER, TDAT, and TIME frames
|
||||
if parse_options.implicit_conversions && header.version == Id3v2Version::V3 {
|
||||
construct_tdrc_from_v3(&mut ret);
|
||||
}
|
||||
|
||||
Ok(ret)
|
||||
}
|
||||
|
||||
fn construct_tdrc_from_v3(tag: &mut Id3v2Tag) {
|
||||
const TDRC: FrameId<'_> = FrameId::Valid(Cow::Borrowed("TDRC"));
|
||||
const TDAT: FrameId<'_> = FrameId::Valid(Cow::Borrowed("TDAT"));
|
||||
const TIME: FrameId<'_> = FrameId::Valid(Cow::Borrowed("TIME"));
|
||||
|
||||
// Our TYER frame gets converted to TDRC earlier
|
||||
let Some(year_frame) = tag.remove(&TDRC).next() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Frame::Timestamp(year_frame) = year_frame else {
|
||||
log::warn!("TYER frame is not a timestamp frame, retaining.");
|
||||
tag.insert(year_frame);
|
||||
return;
|
||||
};
|
||||
|
||||
// This is not a TYER frame
|
||||
if year_frame.timestamp.month.is_some() {
|
||||
return;
|
||||
}
|
||||
|
||||
let date = tag.get_text(&TDAT);
|
||||
let mut date_used = false;
|
||||
|
||||
let time = tag.get_text(&TIME);
|
||||
let mut time_used = false;
|
||||
|
||||
let mut tdrc = Timestamp {
|
||||
year: year_frame.timestamp.year,
|
||||
..Timestamp::default()
|
||||
};
|
||||
'build: {
|
||||
if let Some(date) = date {
|
||||
if date.len() != 4 {
|
||||
log::warn!("Invalid TDAT frame, retaining.");
|
||||
break 'build;
|
||||
}
|
||||
|
||||
let (Ok(month), Ok(day)) = (date[..2].parse::<u8>(), date[2..].parse::<u8>()) else {
|
||||
log::warn!("Invalid TDAT frame, retaining.");
|
||||
break 'build;
|
||||
};
|
||||
|
||||
tdrc.month = Some(month);
|
||||
tdrc.day = Some(day);
|
||||
date_used = true;
|
||||
|
||||
if let Some(time) = time {
|
||||
if time.len() != 4 {
|
||||
log::warn!("Invalid TIME frame, retaining.");
|
||||
break 'build;
|
||||
}
|
||||
|
||||
let (Ok(hour), Ok(minute)) = (time[..2].parse::<u8>(), time[2..].parse::<u8>())
|
||||
else {
|
||||
log::warn!("Invalid TIME frame, retaining.");
|
||||
break 'build;
|
||||
};
|
||||
|
||||
tdrc.hour = Some(hour);
|
||||
tdrc.minute = Some(minute);
|
||||
time_used = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tag.insert(Frame::Timestamp(TimestampFrame::new(
|
||||
FrameId::Valid(Cow::Borrowed("TDRC")),
|
||||
year_frame.encoding,
|
||||
tdrc,
|
||||
)));
|
||||
|
||||
if date_used {
|
||||
let _ = tag.remove(&TDAT);
|
||||
}
|
||||
|
||||
if time_used {
|
||||
let _ = tag.remove(&TIME);
|
||||
}
|
||||
}
|
||||
|
||||
fn skip_frame(reader: &mut impl Read, size: u32) -> Result<()> {
|
||||
log::trace!("Skipping frame of size {}", size);
|
||||
|
||||
|
|
|
@ -20,9 +20,7 @@ use crate::mp4::AdvisoryRating;
|
|||
use crate::picture::{Picture, PictureType, TOMBSTONE_PICTURE};
|
||||
use crate::tag::companion_tag::CompanionTag;
|
||||
use crate::tag::items::{Lang, Timestamp, UNKNOWN_LANGUAGE};
|
||||
use crate::tag::{
|
||||
try_parse_year, Accessor, ItemKey, ItemValue, MergeTag, SplitTag, Tag, TagExt, TagItem, TagType,
|
||||
};
|
||||
use crate::tag::{Accessor, ItemKey, ItemValue, MergeTag, SplitTag, Tag, TagExt, TagItem, TagType};
|
||||
use crate::util::flag_item;
|
||||
use crate::util::io::{FileLike, Length, Truncate};
|
||||
use crate::util::text::{decode_text, TextDecodeOptions, TextEncoding};
|
||||
|
@ -569,7 +567,7 @@ impl Id3v2Tag {
|
|||
}
|
||||
}
|
||||
|
||||
struct GenresIter<'a> {
|
||||
pub(crate) struct GenresIter<'a> {
|
||||
value: &'a str,
|
||||
pos: usize,
|
||||
}
|
||||
|
@ -817,9 +815,10 @@ impl Accessor for Id3v2Tag {
|
|||
}
|
||||
|
||||
fn year(&self) -> Option<u32> {
|
||||
if let Some(Frame::Text(TextInformationFrame { value, .. })) = self.get(&RECORDING_TIME_ID)
|
||||
if let Some(Frame::Timestamp(TimestampFrame { timestamp, .. })) =
|
||||
self.get(&RECORDING_TIME_ID)
|
||||
{
|
||||
return try_parse_year(value);
|
||||
return Some(u32::from(timestamp.year));
|
||||
}
|
||||
|
||||
None
|
||||
|
|
|
@ -17,12 +17,10 @@ const COMMENT_FRAME_ID: &str = "COMM";
|
|||
|
||||
fn read_tag(path: &str) -> Id3v2Tag {
|
||||
let tag_bytes = read_path(path);
|
||||
read_tag_raw(&tag_bytes)
|
||||
}
|
||||
|
||||
fn read_tag_raw(bytes: &[u8]) -> Id3v2Tag {
|
||||
let options = ParseOptions::new().parsing_mode(ParsingMode::Strict);
|
||||
read_tag_with_options(bytes, options)
|
||||
read_tag_with_options(
|
||||
&tag_bytes,
|
||||
ParseOptions::new().parsing_mode(ParsingMode::Strict),
|
||||
)
|
||||
}
|
||||
|
||||
fn read_tag_with_options(bytes: &[u8], parse_options: ParseOptions) -> Id3v2Tag {
|
||||
|
@ -32,6 +30,17 @@ fn read_tag_with_options(bytes: &[u8], parse_options: ParseOptions) -> Id3v2Tag
|
|||
crate::id3::v2::read::parse_id3v2(&mut reader, header, parse_options).unwrap()
|
||||
}
|
||||
|
||||
fn dump_and_re_read(tag: &Id3v2Tag, write_options: WriteOptions) -> Id3v2Tag {
|
||||
let mut tag_bytes = Vec::new();
|
||||
let mut writer = Cursor::new(&mut tag_bytes);
|
||||
tag.dump_to(&mut writer, write_options).unwrap();
|
||||
|
||||
read_tag_with_options(
|
||||
&tag_bytes[..],
|
||||
ParseOptions::new().parsing_mode(ParsingMode::Strict),
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_id3v2() {
|
||||
let mut expected_tag = Id3v2Tag::default();
|
||||
|
@ -63,10 +72,13 @@ fn parse_id3v2() {
|
|||
String::from("Qux comment"),
|
||||
)));
|
||||
|
||||
expected_tag.insert(Frame::Text(TextInformationFrame::new(
|
||||
expected_tag.insert(Frame::Timestamp(TimestampFrame::new(
|
||||
FrameId::Valid(Cow::Borrowed("TDRC")),
|
||||
encoding,
|
||||
String::from("1984"),
|
||||
Timestamp {
|
||||
year: 1984,
|
||||
..Timestamp::default()
|
||||
},
|
||||
)));
|
||||
|
||||
expected_tag.insert(Frame::Text(TextInformationFrame::new(
|
||||
|
@ -210,10 +222,13 @@ fn create_full_test_tag(version: Id3v2Version) -> Id3v2Tag {
|
|||
String::from("Summer"),
|
||||
)));
|
||||
|
||||
tag.insert(Frame::Text(TextInformationFrame::new(
|
||||
tag.insert(Frame::Timestamp(TimestampFrame::new(
|
||||
FrameId::Valid(Cow::Borrowed("TDRC")),
|
||||
encoding,
|
||||
String::from("2017"),
|
||||
Timestamp {
|
||||
year: 2017,
|
||||
..Timestamp::default()
|
||||
},
|
||||
)));
|
||||
|
||||
tag.insert(Frame::Text(TextInformationFrame::new(
|
||||
|
@ -251,9 +266,14 @@ fn id3v24_full() {
|
|||
|
||||
#[test]
|
||||
fn id3v23_full() {
|
||||
let tag = create_full_test_tag(Id3v2Version::V3);
|
||||
let parsed_tag = read_tag("tests/tags/assets/id3v2/test_full.id3v23");
|
||||
let mut tag = create_full_test_tag(Id3v2Version::V3);
|
||||
let mut parsed_tag = read_tag("tests/tags/assets/id3v2/test_full.id3v23");
|
||||
|
||||
// Tags may change order after being read, due to the TDRC conversion
|
||||
tag.frames.sort_by_key(|frame| frame.id_str().to_string());
|
||||
parsed_tag
|
||||
.frames
|
||||
.sort_by_key(|frame| frame.id_str().to_string());
|
||||
assert_eq!(tag, parsed_tag);
|
||||
}
|
||||
|
||||
|
@ -1261,7 +1281,10 @@ fn special_items_roundtrip() {
|
|||
tag.dump_to(&mut tag_bytes, WriteOptions::default())
|
||||
.unwrap();
|
||||
|
||||
let mut tag_re_read = read_tag_raw(&tag_bytes[..]);
|
||||
let mut tag_re_read = read_tag_with_options(
|
||||
&tag_bytes[..],
|
||||
ParseOptions::new().parsing_mode(ParsingMode::Strict),
|
||||
);
|
||||
|
||||
// Ensure ordered comparison
|
||||
tag.frames.sort_by_key(|frame| frame.id().to_string());
|
||||
|
@ -1277,7 +1300,10 @@ fn special_items_roundtrip() {
|
|||
tag.dump_to(&mut tag_bytes, WriteOptions::default())
|
||||
.unwrap();
|
||||
|
||||
let mut generic_tag_re_read = read_tag_raw(&tag_bytes[..]);
|
||||
let mut generic_tag_re_read = read_tag_with_options(
|
||||
&tag_bytes[..],
|
||||
ParseOptions::new().parsing_mode(ParsingMode::Strict),
|
||||
);
|
||||
|
||||
generic_tag_re_read
|
||||
.frames
|
||||
|
@ -1350,3 +1376,161 @@ fn skip_reading_cover_art() {
|
|||
assert_eq!(id3v2.len(), 1); // Artist, no picture
|
||||
assert!(id3v2.artist().is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remove_id3v24_frames_on_id3v23_save() {
|
||||
let mut tag = Id3v2Tag::new();
|
||||
|
||||
tag.insert(Frame::RelativeVolumeAdjustment(
|
||||
RelativeVolumeAdjustmentFrame::new(
|
||||
String::from("Foo RVA"),
|
||||
HashMap::from([(
|
||||
ChannelType::MasterVolume,
|
||||
ChannelInformation {
|
||||
channel_type: ChannelType::MasterVolume,
|
||||
volume_adjustment: 30,
|
||||
bits_representing_peak: 0,
|
||||
peak_volume: None,
|
||||
},
|
||||
)]),
|
||||
),
|
||||
));
|
||||
|
||||
let tag_re_read = dump_and_re_read(&tag, WriteOptions::default().use_id3v23(true));
|
||||
|
||||
assert_eq!(tag_re_read.frames.len(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn change_text_encoding_on_id3v23_save() {
|
||||
let mut tag = Id3v2Tag::new();
|
||||
|
||||
// UTF-16 BE is not supported in ID3v2.3
|
||||
tag.insert(Frame::Text(TextInformationFrame::new(
|
||||
FrameId::Valid(Cow::from("TFOO")),
|
||||
TextEncoding::UTF16BE,
|
||||
String::from("Foo"),
|
||||
)));
|
||||
|
||||
let tag_re_read = dump_and_re_read(&tag, WriteOptions::default().use_id3v23(true));
|
||||
|
||||
let frame = tag_re_read
|
||||
.get(&FrameId::Valid(Cow::Borrowed("TFOO")))
|
||||
.unwrap();
|
||||
match frame {
|
||||
Frame::Text(frame) => {
|
||||
assert_eq!(frame.encoding, TextEncoding::UTF16);
|
||||
assert_eq!(frame.value, "Foo");
|
||||
},
|
||||
_ => panic!("Expected a TextInformationFrame"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn split_tdor_on_id3v23_save() {
|
||||
let mut tag = Id3v2Tag::new();
|
||||
|
||||
// ID3v2.3 ONLY supports the original release year.
|
||||
// This will be written as a TORY frame. Lofty just automatically upgrades it to a TDOR
|
||||
// when reading it back.
|
||||
tag.insert(Frame::Timestamp(TimestampFrame::new(
|
||||
FrameId::Valid(Cow::Borrowed("TDOR")),
|
||||
TextEncoding::UTF8,
|
||||
Timestamp {
|
||||
year: 2024,
|
||||
month: Some(6),
|
||||
day: Some(3),
|
||||
hour: Some(14),
|
||||
minute: Some(8),
|
||||
second: Some(49),
|
||||
},
|
||||
)));
|
||||
|
||||
let tag_re_read = dump_and_re_read(&tag, WriteOptions::default().use_id3v23(true));
|
||||
|
||||
let frame = tag_re_read
|
||||
.get(&FrameId::Valid(Cow::Borrowed("TDOR")))
|
||||
.unwrap();
|
||||
match frame {
|
||||
Frame::Timestamp(frame) => {
|
||||
assert_eq!(frame.encoding, TextEncoding::UTF16);
|
||||
assert_eq!(frame.timestamp.year, 2024);
|
||||
assert_eq!(frame.timestamp.month, None);
|
||||
assert_eq!(frame.timestamp.day, None);
|
||||
assert_eq!(frame.timestamp.hour, None);
|
||||
assert_eq!(frame.timestamp.minute, None);
|
||||
assert_eq!(frame.timestamp.second, None);
|
||||
},
|
||||
_ => panic!("Expected a TimestampFrame"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn split_tdrc_on_id3v23_save() {
|
||||
let mut tag = Id3v2Tag::new();
|
||||
|
||||
// TDRC gets split into 3 frames in ID3v2.3:
|
||||
//
|
||||
// TYER: YYYY
|
||||
// TDAT: DDMM
|
||||
// TIME: HHMM
|
||||
tag.insert(Frame::Timestamp(TimestampFrame::new(
|
||||
FrameId::Valid(Cow::Borrowed("TDRC")),
|
||||
TextEncoding::UTF8,
|
||||
Timestamp {
|
||||
year: 2024,
|
||||
month: Some(6),
|
||||
day: Some(3),
|
||||
hour: Some(14),
|
||||
minute: Some(8),
|
||||
second: None, // Seconds are not supported in ID3v2.3 TIME
|
||||
},
|
||||
)));
|
||||
|
||||
let tag_re_read = dump_and_re_read(&tag, WriteOptions::default().use_id3v23(true));
|
||||
|
||||
// First, check the default behavior which should return the same TDRC frame
|
||||
let frame = tag_re_read
|
||||
.get(&FrameId::Valid(Cow::Borrowed("TDRC")))
|
||||
.unwrap();
|
||||
|
||||
match frame {
|
||||
Frame::Timestamp(frame) => {
|
||||
assert_eq!(frame.encoding, TextEncoding::UTF16);
|
||||
assert_eq!(frame.timestamp.year, 2024);
|
||||
assert_eq!(frame.timestamp.month, Some(6));
|
||||
assert_eq!(frame.timestamp.day, Some(3));
|
||||
assert_eq!(frame.timestamp.hour, Some(14));
|
||||
assert_eq!(frame.timestamp.minute, Some(8));
|
||||
},
|
||||
_ => panic!("Expected a TimestampFrame"),
|
||||
}
|
||||
|
||||
// Now, re-read with implicit_conversions off, which retains the split frames
|
||||
let mut bytes = Cursor::new(Vec::new());
|
||||
tag_re_read
|
||||
.dump_to(&mut bytes, WriteOptions::default().use_id3v23(true))
|
||||
.unwrap();
|
||||
|
||||
let tag_re_read = read_tag_with_options(
|
||||
&bytes.into_inner(),
|
||||
ParseOptions::new()
|
||||
.parsing_mode(ParsingMode::Strict)
|
||||
.implicit_conversions(false),
|
||||
);
|
||||
|
||||
let year = tag_re_read
|
||||
.get_text(&FrameId::Valid(Cow::Borrowed("TYER")))
|
||||
.expect("Expected TYER frame");
|
||||
assert_eq!(year, "2024");
|
||||
|
||||
let date = tag_re_read
|
||||
.get_text(&FrameId::Valid(Cow::Borrowed("TDAT")))
|
||||
.expect("Expected TDAT frame");
|
||||
assert_eq!(date, "0603");
|
||||
|
||||
let time = tag_re_read
|
||||
.get_text(&FrameId::Valid(Cow::Borrowed("TIME")))
|
||||
.expect("Expected TIME frame");
|
||||
assert_eq!(time, "1408");
|
||||
}
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
use crate::error::{Id3v2Error, Id3v2ErrorKind, Result};
|
||||
use crate::id3::v2::frame::{FrameFlags, FrameRef};
|
||||
use crate::id3::v2::tag::GenresIter;
|
||||
use crate::id3::v2::util::synchsafe::SynchsafeInteger;
|
||||
use crate::id3::v2::{Frame, FrameId, TextInformationFrame};
|
||||
use crate::tag::items::Timestamp;
|
||||
|
||||
use std::io::Write;
|
||||
|
||||
use crate::id3::v2::Frame;
|
||||
use byteorder::{BigEndian, WriteBytesExt};
|
||||
|
||||
pub(in crate::id3::v2) fn create_items<W>(
|
||||
|
@ -14,11 +16,157 @@ pub(in crate::id3::v2) fn create_items<W>(
|
|||
where
|
||||
W: Write,
|
||||
{
|
||||
let is_id3v23 = false;
|
||||
|
||||
for frame in frames {
|
||||
verify_frame(&frame)?;
|
||||
let value = frame.as_bytes()?;
|
||||
let value = frame.as_bytes(is_id3v23)?;
|
||||
|
||||
write_frame(writer, frame.id().as_str(), frame.flags(), &value)?;
|
||||
write_frame(
|
||||
writer,
|
||||
frame.id().as_str(),
|
||||
frame.flags(),
|
||||
&value,
|
||||
is_id3v23,
|
||||
)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(in crate::id3::v2) fn create_items_v3<W>(
|
||||
writer: &mut W,
|
||||
frames: &mut dyn Iterator<Item = FrameRef<'_>>,
|
||||
) -> Result<()>
|
||||
where
|
||||
W: Write,
|
||||
{
|
||||
// These are all frames from ID3v2.4
|
||||
const FRAMES_TO_DISCARD: &[&str] = &[
|
||||
"ASPI", "EQU2", "RVA2", "SEEK", "SIGN", "TDEN", "TDRL", "TDTG", "TIPL", "TMCL", "TMOO",
|
||||
"TPRO", "TSOA", "TSOP", "TSOT", "TSST",
|
||||
];
|
||||
|
||||
let is_id3v23 = true;
|
||||
|
||||
for mut frame in frames {
|
||||
let id = frame.id_str();
|
||||
|
||||
if FRAMES_TO_DISCARD.contains(&id) {
|
||||
log::warn!("Discarding frame: {}, not supported in ID3v2.3", id);
|
||||
continue;
|
||||
}
|
||||
|
||||
verify_frame(&frame)?;
|
||||
|
||||
match id {
|
||||
// TORY (Original release year) is the only component of TDOR
|
||||
// that is supported in ID3v2.3
|
||||
//
|
||||
// TDRC (Recording time) gets split into three frames: TYER, TDAT, and TIME
|
||||
"TDOR" | "TDRC" => {
|
||||
let mut value = frame.0.clone();
|
||||
let Frame::Timestamp(ref mut f) = value.to_mut() else {
|
||||
log::warn!("Discarding frame: {}, not supported in ID3v2.3", id);
|
||||
continue;
|
||||
};
|
||||
|
||||
if f.timestamp.verify().is_err() {
|
||||
log::warn!("Discarding frame: {}, invalid timestamp", id);
|
||||
continue;
|
||||
}
|
||||
|
||||
if id == "TDOR" {
|
||||
let year = f.timestamp.year;
|
||||
f.timestamp = Timestamp {
|
||||
year,
|
||||
..Timestamp::default()
|
||||
};
|
||||
|
||||
f.header.id = FrameId::Valid("TORY".into());
|
||||
|
||||
frame.0 = value;
|
||||
} else {
|
||||
let mut new_frames = Vec::with_capacity(3);
|
||||
|
||||
let timestamp = f.timestamp;
|
||||
|
||||
let year = timestamp.year;
|
||||
new_frames.push(Frame::Text(TextInformationFrame::new(
|
||||
FrameId::Valid("TYER".into()),
|
||||
f.encoding.to_id3v23(),
|
||||
year.to_string(),
|
||||
)));
|
||||
|
||||
if let (Some(month), Some(day)) = (timestamp.month, timestamp.day) {
|
||||
let date = format!("{:02}{:02}", month, day);
|
||||
new_frames.push(Frame::Text(TextInformationFrame::new(
|
||||
FrameId::Valid("TDAT".into()),
|
||||
f.encoding.to_id3v23(),
|
||||
date,
|
||||
)));
|
||||
}
|
||||
|
||||
if let (Some(hour), Some(minute)) = (timestamp.hour, timestamp.minute) {
|
||||
let time = format!("{:02}{:02}", hour, minute);
|
||||
new_frames.push(Frame::Text(TextInformationFrame::new(
|
||||
FrameId::Valid("TIME".into()),
|
||||
f.encoding.to_id3v23(),
|
||||
time,
|
||||
)));
|
||||
}
|
||||
|
||||
for mut frame in new_frames {
|
||||
frame.set_flags(f.header.flags);
|
||||
let value = frame.as_bytes(is_id3v23)?;
|
||||
|
||||
write_frame(
|
||||
writer,
|
||||
frame.id().as_str(),
|
||||
frame.flags(),
|
||||
&value,
|
||||
is_id3v23,
|
||||
)?;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
},
|
||||
// TCON (Content type) cannot be separated by nulls, so we have to wrap its
|
||||
// components in parentheses
|
||||
"TCON" => {
|
||||
let mut value = frame.0.clone();
|
||||
let Frame::Text(ref mut f) = value.to_mut() else {
|
||||
log::warn!("Discarding frame: {}, not supported in ID3v2.3", id);
|
||||
continue;
|
||||
};
|
||||
|
||||
let mut new_genre_string = String::new();
|
||||
for genre in GenresIter::new(&f.value) {
|
||||
match genre {
|
||||
"Remix" => new_genre_string.push_str("(RX)"),
|
||||
"Cover" => new_genre_string.push_str("(CR)"),
|
||||
_ => {
|
||||
new_genre_string.push_str(genre);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
f.value = new_genre_string;
|
||||
frame.0 = value;
|
||||
},
|
||||
_ => {},
|
||||
}
|
||||
|
||||
let value = frame.as_bytes(is_id3v23)?;
|
||||
|
||||
write_frame(
|
||||
writer,
|
||||
frame.id().as_str(),
|
||||
frame.flags(),
|
||||
&value,
|
||||
is_id3v23,
|
||||
)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
@ -49,12 +197,18 @@ fn verify_frame(frame: &FrameRef<'_>) -> Result<()> {
|
|||
}
|
||||
}
|
||||
|
||||
fn write_frame<W>(writer: &mut W, name: &str, flags: FrameFlags, value: &[u8]) -> Result<()>
|
||||
fn write_frame<W>(
|
||||
writer: &mut W,
|
||||
name: &str,
|
||||
flags: FrameFlags,
|
||||
value: &[u8],
|
||||
is_id3v23: bool,
|
||||
) -> Result<()>
|
||||
where
|
||||
W: Write,
|
||||
{
|
||||
if flags.encryption.is_some() {
|
||||
write_encrypted(writer, name, value, flags)?;
|
||||
write_encrypted(writer, name, value, flags, is_id3v23)?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
|
@ -66,6 +220,7 @@ where
|
|||
name,
|
||||
if is_grouping_identity { len + 1 } else { len },
|
||||
flags,
|
||||
is_id3v23,
|
||||
)?;
|
||||
|
||||
if is_grouping_identity {
|
||||
|
@ -78,7 +233,13 @@ where
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn write_encrypted<W>(writer: &mut W, name: &str, value: &[u8], flags: FrameFlags) -> Result<()>
|
||||
fn write_encrypted<W>(
|
||||
writer: &mut W,
|
||||
name: &str,
|
||||
value: &[u8],
|
||||
flags: FrameFlags,
|
||||
is_id3v23: bool,
|
||||
) -> Result<()>
|
||||
where
|
||||
W: Write,
|
||||
{
|
||||
|
@ -93,7 +254,7 @@ where
|
|||
|
||||
if let Some(len) = flags.data_length_indicator {
|
||||
if len > 0 {
|
||||
write_frame_header(writer, name, (value.len() + 1) as u32, flags)?;
|
||||
write_frame_header(writer, name, (value.len() + 1) as u32, flags, is_id3v23)?;
|
||||
writer.write_u32::<BigEndian>(len.synch()?)?;
|
||||
writer.write_u8(method_symbol)?;
|
||||
writer.write_all(value)?;
|
||||
|
@ -105,55 +266,25 @@ where
|
|||
Err(Id3v2Error::new(Id3v2ErrorKind::MissingDataLengthIndicator).into())
|
||||
}
|
||||
|
||||
fn write_frame_header<W>(writer: &mut W, name: &str, len: u32, flags: FrameFlags) -> Result<()>
|
||||
fn write_frame_header<W>(
|
||||
writer: &mut W,
|
||||
name: &str,
|
||||
len: u32,
|
||||
flags: FrameFlags,
|
||||
is_id3v23: bool,
|
||||
) -> Result<()>
|
||||
where
|
||||
W: Write,
|
||||
{
|
||||
let flags = if is_id3v23 {
|
||||
flags.as_id3v23_bytes()
|
||||
} else {
|
||||
flags.as_id3v24_bytes()
|
||||
};
|
||||
|
||||
writer.write_all(name.as_bytes())?;
|
||||
writer.write_u32::<BigEndian>(len.synch()?)?;
|
||||
writer.write_u16::<BigEndian>(get_flags(flags))?;
|
||||
writer.write_u16::<BigEndian>(flags)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_flags(tag_flags: FrameFlags) -> u16 {
|
||||
let mut flags = 0;
|
||||
|
||||
if tag_flags == FrameFlags::default() {
|
||||
return flags;
|
||||
}
|
||||
|
||||
if tag_flags.tag_alter_preservation {
|
||||
flags |= 0x4000
|
||||
}
|
||||
|
||||
if tag_flags.file_alter_preservation {
|
||||
flags |= 0x2000
|
||||
}
|
||||
|
||||
if tag_flags.read_only {
|
||||
flags |= 0x1000
|
||||
}
|
||||
|
||||
if tag_flags.grouping_identity.is_some() {
|
||||
flags |= 0x0040
|
||||
}
|
||||
|
||||
if tag_flags.compression {
|
||||
flags |= 0x0008
|
||||
}
|
||||
|
||||
if tag_flags.encryption.is_some() {
|
||||
flags |= 0x0004
|
||||
}
|
||||
|
||||
if tag_flags.unsynchronisation {
|
||||
flags |= 0x0002
|
||||
}
|
||||
|
||||
if tag_flags.data_length_indicator.is_some() {
|
||||
flags |= 0x0001
|
||||
}
|
||||
|
||||
flags
|
||||
}
|
||||
|
|
|
@ -114,15 +114,24 @@ pub(super) fn create_tag<'a, I: Iterator<Item = FrameRef<'a>> + 'a>(
|
|||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
let is_id3v23 = write_options.use_id3v23;
|
||||
if is_id3v23 {
|
||||
log::debug!("Using ID3v2.3");
|
||||
}
|
||||
|
||||
let has_footer = tag.flags.footer;
|
||||
let needs_crc = tag.flags.crc;
|
||||
let has_restrictions = tag.flags.restrictions.is_some();
|
||||
|
||||
let (mut id3v2, extended_header_len) = create_tag_header(tag.flags)?;
|
||||
let (mut id3v2, extended_header_len) = create_tag_header(tag.flags, is_id3v23)?;
|
||||
let header_len = id3v2.get_ref().len();
|
||||
|
||||
// Write the items
|
||||
frame::create_items(&mut id3v2, &mut peek)?;
|
||||
if is_id3v23 {
|
||||
frame::create_items_v3(&mut id3v2, &mut peek)?;
|
||||
} else {
|
||||
frame::create_items(&mut id3v2, &mut peek)?;
|
||||
}
|
||||
|
||||
let mut len = id3v2.get_ref().len() - header_len;
|
||||
|
||||
|
@ -190,29 +199,26 @@ pub(super) fn create_tag<'a, I: Iterator<Item = FrameRef<'a>> + 'a>(
|
|||
Ok(id3v2.into_inner())
|
||||
}
|
||||
|
||||
fn create_tag_header(flags: Id3v2TagFlags) -> Result<(Cursor<Vec<u8>>, u32)> {
|
||||
fn create_tag_header(flags: Id3v2TagFlags, is_id3v23: bool) -> Result<(Cursor<Vec<u8>>, u32)> {
|
||||
let mut header = Cursor::new(Vec::new());
|
||||
|
||||
header.write_all(&[b'I', b'D', b'3'])?;
|
||||
|
||||
let mut tag_flags = 0;
|
||||
|
||||
// Version 4, rev 0
|
||||
header.write_all(&[4, 0])?;
|
||||
if is_id3v23 {
|
||||
// Version 3, rev 0
|
||||
header.write_all(&[3, 0])?;
|
||||
} else {
|
||||
// Version 4, rev 0
|
||||
header.write_all(&[4, 0])?;
|
||||
}
|
||||
|
||||
let extended_header = flags.crc || flags.restrictions.is_some();
|
||||
|
||||
if flags.footer {
|
||||
tag_flags |= 0x10
|
||||
}
|
||||
|
||||
if flags.experimental {
|
||||
tag_flags |= 0x20
|
||||
}
|
||||
|
||||
if extended_header {
|
||||
tag_flags |= 0x40
|
||||
}
|
||||
let tag_flags = if is_id3v23 {
|
||||
flags.as_id3v23_byte()
|
||||
} else {
|
||||
flags.as_id3v24_byte()
|
||||
};
|
||||
|
||||
header.write_u8(tag_flags)?;
|
||||
header.write_u32::<BigEndian>(0)?;
|
||||
|
|
|
@ -34,6 +34,22 @@ impl TextEncoding {
|
|||
pub(crate) fn verify_latin1(text: &str) -> bool {
|
||||
text.chars().all(|c| c as u32 <= 255)
|
||||
}
|
||||
|
||||
/// ID3v2.4 introduced two new text encodings.
|
||||
///
|
||||
/// When writing ID3v2.3, we just substitute with UTF-16.
|
||||
pub(crate) fn to_id3v23(self) -> Self {
|
||||
match self {
|
||||
Self::UTF8 | Self::UTF16BE => {
|
||||
log::warn!(
|
||||
"Text encoding {:?} is not supported in ID3v2.3, substituting with UTF-16",
|
||||
self
|
||||
);
|
||||
Self::UTF16
|
||||
},
|
||||
_ => self,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Eq, PartialEq, Debug)]
|
||||
|
|
Loading…
Reference in a new issue