Skip to content

Commit 10be465

Browse files
committed
ID3v2: Unify generic conversions
`Tag::save_to()` and `Into::<Id3v2Tag>::into(tag)` would take different paths, causing frames to not get merged (and possibly other weird behavior). Now, both take the same path. All frames have been updated to use `Cow`, to avoid allocations in the `Tag::save_to()` path (unless absolutely necessary, like for track numbers). This should be easier to maintain now that everything's unified. And possibly more efficient, since it's no longer cloning everything.
1 parent bf44f1c commit 10be465

36 files changed

+1084
-877
lines changed

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2424
- Support can be enabled with the new `serde` feature (not enabled by default)
2525
- **Probe**: `Probe::read_bound()` ([PR](https://github.com/Serial-ATA/lofty-rs/pull/557))
2626
- Same as `Probe::read()`, but returns a [`BoundTaggedFile`](https://docs.rs/lofty/latest/lofty/file/struct.BoundTaggedFile.html)
27+
- **ID3v2** ([PR](https://github.com/Serial-ATA/lofty-rs/pull/576)):
28+
- Unified the two generic conversion paths
29+
- The background conversion used in `Tag::save_to()`, and the direct conversion done via `Into::<Id3v2Tag>::into(tag)` used to
30+
take different paths, causing certain conversions and frame merging to not occur in the former case ([issue](https://github.com/Serial-ATA/lofty-rs/issues/349)).
31+
They now use the same logic, which has also been rewritten to reuse data whenever possible, instead of cloning like before.
32+
- The following frames now use `Cow` internally: `CommentFrame`, `UnsynchronizedTextFrame`, `TextInformationFrame`, `ExtendedTextFrame`, `UrlLinkFrame`, `ExtendedUrlFrame`,
33+
`AttachedPictureFrame`, `PopularimeterFrame`, `KeyValueFrame`, `RelativeVolumeAdjustmentFrame`, `UniqueFileIdentifierFrame`, `OwnershipFrame`, `EventTimingCodesFrame`,
34+
`PrivateFrame`, `BinaryFrame`
35+
- `FrameId::is_valid()` and `FrameId::is_outdated()`
2736
- **Other**: `EXTENSIONS` list containing common file extensions for all supported audio file types ([issue](https://github.com/Serial-ATA/lofty-rs/issues/509)) ([PR](https://github.com/Serial-ATA/lofty-rs/pull/558))
2837
- This is useful for filtering files when scanning directories. If your app uses extension filtering, **please consider switching to this**, as to not
2938
miss any supported files.

lofty/src/flac/write.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,9 @@ where
3636
write_to_inner(file, &mut comments_ref, write_options)
3737
},
3838
// This tag can *only* be removed in this format
39-
TagType::Id3v2 => crate::id3::v2::tag::Id3v2TagRef::empty().write_to(file, write_options),
39+
TagType::Id3v2 => {
40+
crate::id3::v2::tag::conversion::Id3v2TagRef::empty().write_to(file, write_options)
41+
},
4042
_ => err!(UnsupportedTag),
4143
}
4244
}

lofty/src/id3/v2/frame/conversion.rs

Lines changed: 0 additions & 141 deletions
This file was deleted.

lofty/src/id3/v2/frame/header/mod.rs

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,15 @@ impl<'a> FrameHeader<'a> {
3434
}
3535
}
3636

37+
impl FrameHeader<'static> {
38+
pub(crate) fn downgrade(&self) -> FrameHeader<'_> {
39+
FrameHeader {
40+
id: self.id.downgrade(),
41+
flags: self.flags,
42+
}
43+
}
44+
}
45+
3746
/// An `ID3v2` frame ID
3847
///
3948
/// ⚠ WARNING ⚠: Be very careful when constructing this by hand. It is recommended to use [`FrameId::new`].
@@ -78,6 +87,47 @@ impl<'a> FrameId<'a> {
7887
}
7988
}
8089

90+
/// Whether this frame ID represents an outdated (ID3v2.2) ID
91+
///
92+
/// Note that frames with ID3v2.2 IDs *must* be upgraded to a 4-character ID3v2.3/4 ID in order to be
93+
/// written, otherwise they will be discarded.
94+
///
95+
/// # Examples
96+
///
97+
/// ```rust
98+
/// use lofty::id3::v2::FrameId;
99+
///
100+
/// # fn main() -> lofty::error::Result<()> {
101+
/// let id_valid = FrameId::new("TPE1")?;
102+
/// assert!(!id_valid.is_outdated());
103+
///
104+
/// let id_outdated = FrameId::new("TP1")?;
105+
/// assert!(id_outdated.is_outdated());
106+
/// # Ok(()) }
107+
/// ```
108+
pub fn is_outdated(&self) -> bool {
109+
matches!(self, FrameId::Outdated(_))
110+
}
111+
112+
/// Whether this frame ID represents a valid (ID3v2.3 or ID3v2.4) ID
113+
///
114+
/// # Examples
115+
///
116+
/// ```rust
117+
/// use lofty::id3::v2::FrameId;
118+
///
119+
/// # fn main() -> lofty::error::Result<()> {
120+
/// let id_valid = FrameId::new("TPE1")?;
121+
/// assert!(id_valid.is_valid());
122+
///
123+
/// let id_outdated = FrameId::new("TP1")?;
124+
/// assert!(!id_outdated.is_valid());
125+
/// # Ok(()) }
126+
/// ```
127+
pub fn is_valid(&self) -> bool {
128+
matches!(self, FrameId::Valid(_))
129+
}
130+
81131
/// Extracts the string from the ID
82132
pub fn as_str(&self) -> &str {
83133
match self {
@@ -122,6 +172,15 @@ impl<'a> FrameId<'a> {
122172
}
123173
}
124174

175+
impl FrameId<'static> {
176+
pub(crate) fn downgrade(&self) -> FrameId<'_> {
177+
match self {
178+
FrameId::Valid(id) => FrameId::Valid(Cow::Borrowed(&**id)),
179+
FrameId::Outdated(id) => FrameId::Outdated(Cow::Borrowed(&**id)),
180+
}
181+
}
182+
}
183+
125184
impl Display for FrameId<'_> {
126185
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
127186
f.write_str(self.as_str())

lofty/src/id3/v2/frame/header/parse.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
use super::FrameFlags;
2+
use crate::config::ParseOptions;
23
use crate::error::{Id3v2Error, Id3v2ErrorKind, Result};
34
use crate::id3::v2::FrameId;
45
use crate::id3::v2::util::synchsafe::SynchsafeInteger;
56
use crate::id3::v2::util::upgrade::{upgrade_v2, upgrade_v3};
67
use crate::util::text::utf8_decode_str;
78

8-
use crate::config::ParseOptions;
99
use std::borrow::Cow;
1010
use std::io::Read;
1111

lofty/src/id3/v2/frame/mod.rs

Lines changed: 25 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
pub(super) mod content;
2-
mod conversion;
32
pub(super) mod header;
43
pub(super) mod read;
54

@@ -17,7 +16,6 @@ use header::FrameId;
1716

1817
use std::borrow::Cow;
1918
use std::hash::Hash;
20-
use std::ops::Deref;
2119

2220
pub(super) const MUSICBRAINZ_UFID_OWNER: &str = "http://musicbrainz.org";
2321

@@ -27,7 +25,7 @@ pub(super) const MUSICBRAINZ_UFID_OWNER: &str = "http://musicbrainz.org";
2725
/// are supposed to have an empty content descriptor. Only those
2826
/// are currently supported as [`TagItem`]s to avoid ambiguities
2927
/// and to prevent inconsistencies when writing them.
30-
pub(super) const EMPTY_CONTENT_DESCRIPTOR: String = String::new();
28+
pub(super) const EMPTY_CONTENT_DESCRIPTOR: Cow<'static, str> = Cow::Borrowed("");
3129

3230
// TODO: Messy module, rough conversions
3331

@@ -159,11 +157,34 @@ impl<'a> Frame<'a> {
159157
Frame::Text(TextInformationFrame {
160158
header: FrameHeader::new(FrameId::Valid(id), FrameFlags::default()),
161159
encoding: TextEncoding::UTF8,
162-
value: content,
160+
value: Cow::Owned(content),
163161
})
164162
}
165163
}
166164

165+
impl Frame<'static> {
166+
pub(super) fn downgrade(&self) -> Frame<'_> {
167+
match self {
168+
Frame::Comment(f) => Frame::Comment(f.downgrade()),
169+
Frame::UnsynchronizedText(f) => Frame::UnsynchronizedText(f.downgrade()),
170+
Frame::Text(f) => Frame::Text(f.downgrade()),
171+
Frame::UserText(f) => Frame::UserText(f.downgrade()),
172+
Frame::Url(f) => Frame::Url(f.downgrade()),
173+
Frame::UserUrl(f) => Frame::UserUrl(f.downgrade()),
174+
Frame::Picture(f) => Frame::Picture(f.downgrade()),
175+
Frame::Popularimeter(f) => Frame::Popularimeter(f.downgrade()),
176+
Frame::KeyValue(f) => Frame::KeyValue(f.downgrade()),
177+
Frame::RelativeVolumeAdjustment(f) => Frame::RelativeVolumeAdjustment(f.downgrade()),
178+
Frame::UniqueFileIdentifier(f) => Frame::UniqueFileIdentifier(f.downgrade()),
179+
Frame::Ownership(f) => Frame::Ownership(f.downgrade()),
180+
Frame::EventTimingCodes(f) => Frame::EventTimingCodes(f.downgrade()),
181+
Frame::Private(f) => Frame::Private(f.downgrade()),
182+
Frame::Timestamp(f) => Frame::Timestamp(f.downgrade()),
183+
Frame::Binary(f) => Frame::Binary(f.downgrade()),
184+
}
185+
}
186+
}
187+
167188
impl Frame<'_> {
168189
/// Check for empty content
169190
///
@@ -405,24 +426,3 @@ impl FrameFlags {
405426
flags
406427
}
407428
}
408-
409-
#[derive(Clone)]
410-
pub(crate) struct FrameRef<'a>(pub(crate) Cow<'a, Frame<'a>>);
411-
412-
impl<'a> Deref for FrameRef<'a> {
413-
type Target = Frame<'a>;
414-
415-
fn deref(&self) -> &Self::Target {
416-
self.0.as_ref()
417-
}
418-
}
419-
420-
impl<'a> Frame<'a> {
421-
pub(crate) fn as_opt_ref(&'a self) -> Option<FrameRef<'a>> {
422-
if let FrameId::Valid(_) = self.id() {
423-
Some(FrameRef(Cow::Borrowed(self)))
424-
} else {
425-
None
426-
}
427-
}
428-
}

lofty/src/id3/v2/frame/read.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ use crate::id3::v2::util::synchsafe::{SynchsafeInteger, UnsynchronizedStream};
99
use crate::id3::v2::{BinaryFrame, FrameFlags, FrameHeader, FrameId};
1010
use crate::macros::try_vec;
1111

12+
use std::borrow::Cow;
1213
use std::io::Read;
1314

1415
use byteorder::{BigEndian, ReadBytesExt};
@@ -240,7 +241,7 @@ fn handle_encryption<R: Read>(
240241

241242
let encrypted_frame = Frame::Binary(BinaryFrame {
242243
header: FrameHeader::new(id, flags),
243-
data: content,
244+
data: Cow::Owned(content),
244245
});
245246

246247
// Nothing further we can do with encrypted frames

0 commit comments

Comments
 (0)