Reduce memory allocations for id3v2::FrameID

This commit is contained in:
Uwe Klotz 2023-01-06 13:13:02 +01:00 committed by Alex
parent a5b1ccaff0
commit 738d47fd4f
6 changed files with 111 additions and 86 deletions

View file

@ -3,9 +3,12 @@ use crate::error::{ID3v2Error, ID3v2ErrorKind, Result};
use crate::id3::v2::util::upgrade::{upgrade_v2, upgrade_v3};
use crate::id3::v2::FrameID;
use std::borrow::Cow;
use std::io::Read;
pub(crate) fn parse_v2_header<R>(reader: &mut R) -> Result<Option<(FrameID, u32, FrameFlags)>>
pub(crate) fn parse_v2_header<R>(
reader: &mut R,
) -> Result<Option<(FrameID<'static>, u32, FrameFlags)>>
where
R: Read,
{
@ -22,7 +25,7 @@ where
let id_str = std::str::from_utf8(&frame_header[..3])
.map_err(|_| ID3v2Error::new(ID3v2ErrorKind::BadFrameID))?;
let id = upgrade_v2(id_str).unwrap_or(id_str);
let id = upgrade_v2(id_str).map_or_else(|| Cow::Owned(id_str.to_owned()), Cow::Borrowed);
let frame_id = FrameID::new(id)?;
@ -35,7 +38,7 @@ where
pub(crate) fn parse_header<R>(
reader: &mut R,
synchsafe: bool,
) -> Result<Option<(FrameID, u32, FrameFlags)>>
) -> Result<Option<(FrameID<'static>, u32, FrameFlags)>>
where
R: Read,
{
@ -59,7 +62,7 @@ where
frame_id_end = 3;
}
let mut id_str = std::str::from_utf8(&frame_header[..frame_id_end])
let id_str = std::str::from_utf8(&frame_header[..frame_id_end])
.map_err(|_| ID3v2Error::new(ID3v2ErrorKind::BadFrameID))?;
let mut size = u32::from_be_bytes([
@ -70,21 +73,24 @@ where
]);
// Now upgrade the FrameID
if invalid_v2_frame {
let id = if invalid_v2_frame {
if let Some(id) = upgrade_v2(id_str) {
id_str = id;
Cow::Borrowed(id)
} else {
Cow::Owned(id_str.to_owned())
}
} else if !synchsafe {
id_str = upgrade_v3(id_str).unwrap_or(id_str);
}
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(id)?;
// unsynch the frame size if necessary
if synchsafe {
size = crate::id3::v2::util::unsynch_u32(size);
}
let frame_id = FrameID::new(id_str)?;
let flags = u16::from_be_bytes([frame_header[8], frame_header[9]]);
let flags = parse_flags(flags, synchsafe);

View file

@ -1,33 +1,35 @@
use std::borrow::Cow;
use crate::error::{ID3v2Error, ID3v2ErrorKind, LoftyError, Result};
use crate::tag::item::ItemKey;
use crate::tag::TagType;
/// An `ID3v2` frame ID
#[derive(PartialEq, Clone, Debug, Eq, Hash)]
pub enum FrameID {
pub enum FrameID<'a> {
/// A valid `ID3v2.3/4` frame
Valid(String),
Valid(Cow<'a, str>),
/// When an `ID3v2.2` key couldn't be upgraded
///
/// This **will not** be written. It is up to the user to upgrade and store the key as [`Id3v2Frame::Valid`](Self::Valid).
///
/// The entire frame is stored as [`ItemValue::Binary`](crate::ItemValue::Binary).
Outdated(String),
Outdated(Cow<'a, str>),
}
impl FrameID {
impl<'a> FrameID<'a> {
/// Attempts to create a `FrameID` from an ID string
///
/// # Errors
///
/// * `id` contains invalid characters (must be 'A'..='Z' and '0'..='9')
/// * `id` is an invalid length (must be 3 or 4)
pub fn new(id: &str) -> Result<Self> {
Self::verify_id(id)?;
pub fn new(id: Cow<'a, str>) -> Result<Self> {
Self::verify_id(&id)?;
match id.len() {
3 => Ok(FrameID::Outdated(id.to_string())),
4 => Ok(FrameID::Valid(id.to_string())),
3 => Ok(FrameID::Outdated(id)),
4 => Ok(FrameID::Valid(id)),
_ => Err(ID3v2Error::new(ID3v2ErrorKind::BadFrameID).into()),
}
}
@ -35,11 +37,11 @@ impl FrameID {
/// Extracts the string from the ID
pub fn as_str(&self) -> &str {
match self {
FrameID::Valid(v) | FrameID::Outdated(v) => v.as_str(),
FrameID::Valid(v) | FrameID::Outdated(v) => &v,
}
}
pub(crate) fn verify_id(id_str: &str) -> Result<()> {
pub(super) fn verify_id(id_str: &str) -> Result<()> {
for c in id_str.chars() {
if !c.is_ascii_uppercase() && !c.is_ascii_digit() {
return Err(ID3v2Error::new(ID3v2ErrorKind::BadFrameID).into());
@ -48,12 +50,19 @@ impl FrameID {
Ok(())
}
pub(super) fn into_owned(self) -> FrameID<'static> {
match self {
Self::Valid(inner) => FrameID::Valid(Cow::Owned(inner.into_owned())),
Self::Outdated(inner) => FrameID::Outdated(Cow::Owned(inner.into_owned())),
}
}
}
impl TryFrom<&ItemKey> for FrameID {
impl<'a> TryFrom<&'a ItemKey> for FrameID<'a> {
type Error = LoftyError;
fn try_from(value: &ItemKey) -> std::prelude::rust_2015::Result<Self, Self::Error> {
fn try_from(value: &'a ItemKey) -> std::prelude::rust_2015::Result<Self, Self::Error> {
match value {
ItemKey::Unknown(unknown)
if unknown.len() == 4
@ -61,11 +70,11 @@ impl TryFrom<&ItemKey> for FrameID {
.chars()
.all(|c| c.is_ascii_uppercase() || c.is_ascii_digit()) =>
{
Ok(Self::Valid(unknown.clone()))
Ok(Self::Valid(Cow::Borrowed(unknown)))
},
k => k.map_key(TagType::ID3v2, false).map_or(
Err(ID3v2Error::new(ID3v2ErrorKind::BadFrameID).into()),
|id| Ok(Self::Valid(id.to_string())),
|id| Ok(Self::Valid(Cow::Borrowed(id))),
),
}
}

View file

@ -37,13 +37,13 @@ use std::hash::{Hash, Hasher};
/// `ID3v2.3`, unlike `ID3v2.2`, stores frame IDs in 4 characters like `ID3v2.4`. There are some IDs that need upgrading (See [`upgrade_v3`]),
/// but anything that fails to be upgraded **will not** be stored as [`FrameID::Outdated`], as it is likely not an issue to write.
#[derive(Clone, Debug, Eq)]
pub struct Frame {
pub(super) id: FrameID,
pub struct Frame<'a> {
pub(super) id: FrameID<'a>,
pub(super) value: FrameValue,
pub(super) flags: FrameFlags,
}
impl PartialEq for Frame {
impl<'a> PartialEq for Frame<'a> {
fn eq(&self, other: &Self) -> bool {
match self.value {
FrameValue::Text { .. } => self.id == other.id,
@ -52,7 +52,7 @@ impl PartialEq for Frame {
}
}
impl Hash for Frame {
impl<'a> Hash for Frame<'a> {
fn hash<H: Hasher>(&self, state: &mut H) {
match self.value {
FrameValue::Text { .. } => self.id.hash(state),
@ -64,7 +64,7 @@ impl Hash for Frame {
}
}
impl Frame {
impl<'a> Frame<'a> {
/// Create a new frame
///
/// NOTE: This will accept both `ID3v2.2` and `ID3v2.3/4` frame IDs
@ -73,16 +73,16 @@ impl Frame {
///
/// * `id` is less than 3 or greater than 4 bytes
/// * `id` contains non-ascii characters
pub fn new(id: &str, value: FrameValue, flags: FrameFlags) -> Result<Self> {
pub fn new(id: Cow<'a, str>, value: FrameValue, flags: FrameFlags) -> Result<Self> {
let id_updated = match id.len() {
// An ID with a length of 4 could be either V3 or V4.
4 => match upgrade_v3(id) {
4 => match upgrade_v3(&id) {
None => id,
Some(upgraded) => upgraded,
Some(upgraded) => Cow::Borrowed(upgraded),
},
3 => match upgrade_v2(id) {
3 => match upgrade_v2(&id) {
None => id,
Some(upgraded) => upgraded,
Some(upgraded) => Cow::Borrowed(upgraded),
},
_ => return Err(ID3v2Error::new(ID3v2ErrorKind::BadFrameID).into()),
};
@ -113,9 +113,9 @@ impl Frame {
}
// Used internally, has no correctness checks
pub(crate) fn text(id: &str, content: String) -> Self {
pub(crate) fn text(id: Cow<'a, str>, content: String) -> Self {
Self {
id: FrameID::Valid(String::from(id)),
id: FrameID::Valid(id),
value: FrameValue::Text {
encoding: TextEncoding::UTF8,
value: content,
@ -256,11 +256,11 @@ pub struct FrameFlags {
pub data_length_indicator: Option<u32>,
}
impl From<TagItem> for Option<Frame> {
impl From<TagItem> for Option<Frame<'static>> {
fn from(input: TagItem) -> Self {
let frame_id;
let value;
match input.key().try_into() {
match input.key().try_into().map(FrameID::into_owned) {
Ok(id) => {
// We make the VERY bold assumption the language is English
value = match (&id, input.item_value) {
@ -307,7 +307,7 @@ impl From<TagItem> for Option<Frame> {
Err(_) => match input.item_key.map_key(TagType::ID3v2, true) {
Some(desc) => match input.item_value {
ItemValue::Text(text) => {
frame_id = FrameID::Valid(String::from("TXXX"));
frame_id = FrameID::Valid(Cow::Borrowed("TXXX"));
value = FrameValue::UserText(EncodedTextFrame {
encoding: TextEncoding::UTF8,
description: String::from(desc),
@ -315,7 +315,7 @@ impl From<TagItem> for Option<Frame> {
})
},
ItemValue::Locator(locator) => {
frame_id = FrameID::Valid(String::from("WXXX"));
frame_id = FrameID::Valid(Cow::Borrowed("WXXX"));
value = FrameValue::UserURL(EncodedTextFrame {
encoding: TextEncoding::UTF8,
description: String::from(desc),
@ -342,7 +342,7 @@ pub(crate) struct FrameRef<'a> {
pub flags: FrameFlags,
}
impl<'a> Frame {
impl<'a> Frame<'a> {
pub(crate) fn as_opt_ref(&'a self) -> Option<FrameRef<'a>> {
if let FrameID::Valid(id) = &self.id {
Some(FrameRef {

View file

@ -9,7 +9,7 @@ use std::io::Read;
use byteorder::{BigEndian, ReadBytesExt};
impl Frame {
impl<'a> Frame<'a> {
pub(crate) fn read<R>(reader: &mut R, version: ID3v2Version) -> Result<(Option<Self>, bool)>
where
R: Read,

View file

@ -35,7 +35,7 @@ macro_rules! impl_accessor {
}
self.insert(Frame {
id: FrameID::Valid(String::from($id)),
id: FrameID::Valid(String::from($id).into()),
value: FrameValue::Text {
encoding: TextEncoding::UTF8,
value,
@ -89,12 +89,12 @@ macro_rules! impl_accessor {
pub struct ID3v2Tag {
flags: ID3v2TagFlags,
pub(super) original_version: ID3v2Version,
pub(crate) frames: Vec<Frame>,
pub(crate) frames: Vec<Frame<'static>>,
}
impl IntoIterator for ID3v2Tag {
type Item = Frame;
type IntoIter = std::vec::IntoIter<Frame>;
type Item = Frame<'static>;
type IntoIter = std::vec::IntoIter<Frame<'static>>;
fn into_iter(self) -> Self::IntoIter {
self.frames.into_iter()
@ -133,7 +133,7 @@ impl ID3v2Tag {
impl ID3v2Tag {
/// Returns an iterator over the tag's frames
pub fn iter(&self) -> impl Iterator<Item = &Frame> {
pub fn iter(&self) -> impl Iterator<Item = &Frame<'static>> {
self.frames.iter()
}
@ -145,7 +145,7 @@ impl ID3v2Tag {
/// Gets a [`Frame`] from an id
///
/// NOTE: This is *not* case-sensitive
pub fn get(&self, id: &str) -> Option<&Frame> {
pub fn get(&self, id: &str) -> Option<&Frame<'static>> {
self.frames
.iter()
.find(|f| f.id_str().eq_ignore_ascii_case(id))
@ -175,7 +175,7 @@ impl ID3v2Tag {
/// Inserts a [`Frame`]
///
/// This will replace any frame of the same id (**or description!** See [`EncodedTextFrame`])
pub fn insert(&mut self, frame: Frame) -> Option<Frame> {
pub fn insert(&mut self, frame: Frame<'static>) -> Option<Frame<'static>> {
let replaced = self
.frames
.iter()
@ -195,7 +195,7 @@ impl ID3v2Tag {
///
/// According to spec, there can only be one picture of type [`PictureType::Icon`] and [`PictureType::OtherIcon`].
/// When attempting to insert these types, if another is found it will be removed and returned.
pub fn insert_picture(&mut self, picture: Picture) -> Option<Frame> {
pub fn insert_picture(&mut self, picture: Picture) -> Option<Frame<'static>> {
let ret = if picture.pic_type == PictureType::Icon
|| picture.pic_type == PictureType::OtherIcon
{
@ -225,7 +225,7 @@ impl ID3v2Tag {
};
let picture_frame = Frame {
id: FrameID::Valid(String::from("APIC")),
id: FrameID::Valid(Cow::Borrowed("APIC")),
value: FrameValue::Picture {
encoding: TextEncoding::UTF8,
picture,
@ -305,7 +305,7 @@ impl Accessor for ID3v2Tag {
}
fn set_track(&mut self, value: u32) {
self.insert(Frame::text("TRCK", value.to_string()));
self.insert(Frame::text(Cow::Borrowed("TRCK"), value.to_string()));
}
fn remove_track(&mut self) {
@ -319,7 +319,10 @@ impl Accessor for ID3v2Tag {
fn set_track_total(&mut self, value: u32) {
let current_track = self.split_num_pair("TRCK").0.unwrap_or(1);
self.insert(Frame::text("TRCK", format!("{current_track}/{value}")));
self.insert(Frame::text(
Cow::Borrowed("TRCK"),
format!("{current_track}/{value}"),
));
}
fn remove_track_total(&mut self) {
@ -327,7 +330,7 @@ impl Accessor for ID3v2Tag {
self.remove("TRCK");
if let Some(track) = existing_track_number {
self.insert(Frame::text("TRCK", track.to_string()));
self.insert(Frame::text(Cow::Borrowed("TRCK"), track.to_string()));
}
}
@ -336,7 +339,7 @@ impl Accessor for ID3v2Tag {
}
fn set_disk(&mut self, value: u32) {
self.insert(Frame::text("TPOS", value.to_string()));
self.insert(Frame::text(Cow::Borrowed("TPOS"), value.to_string()));
}
fn remove_disk(&mut self) {
@ -350,7 +353,10 @@ impl Accessor for ID3v2Tag {
fn set_disk_total(&mut self, value: u32) {
let current_disk = self.split_num_pair("TPOS").0.unwrap_or(1);
self.insert(Frame::text("TPOS", format!("{current_disk}/{value}")));
self.insert(Frame::text(
Cow::Borrowed("TPOS"),
format!("{current_disk}/{value}"),
));
}
fn remove_disk_total(&mut self) {
@ -358,7 +364,7 @@ impl Accessor for ID3v2Tag {
self.remove("TPOS");
if let Some(track) = existing_track_number {
self.insert(Frame::text("TPOS", track.to_string()));
self.insert(Frame::text(Cow::Borrowed("TPOS"), track.to_string()));
}
}
@ -380,7 +386,7 @@ impl Accessor for ID3v2Tag {
}
fn set_year(&mut self, value: u32) {
self.insert(Frame::text("TDRC", value.to_string()));
self.insert(Frame::text(Cow::Borrowed("TDRC"), value.to_string()));
}
fn remove_year(&mut self) {
@ -413,7 +419,7 @@ impl Accessor for ID3v2Tag {
if !value.is_empty() {
self.insert(Frame {
id: FrameID::Valid(String::from("COMM")),
id: FrameID::Valid(Cow::Borrowed("COMM")),
value: FrameValue::Comment(LanguageFrame {
encoding: TextEncoding::UTF8,
language: *b"eng",
@ -432,7 +438,7 @@ impl Accessor for ID3v2Tag {
impl TagExt for ID3v2Tag {
type Err = LoftyError;
type RefKey<'a> = &'a FrameID;
type RefKey<'a> = &'a FrameID<'a>;
fn contains<'a>(&'a self, key: Self::RefKey<'a>) -> bool {
self.frames.iter().any(|frame| &frame.id == key)
@ -633,7 +639,7 @@ impl From<Tag> for ID3v2Tag {
id3v2_tag.set_artist(artists);
for item in input.items {
let frame: Frame = match item.into() {
let frame: Frame<'_> = match item.into() {
Some(frame) => frame,
None => continue,
};
@ -643,7 +649,7 @@ impl From<Tag> for ID3v2Tag {
for picture in input.pictures {
id3v2_tag.frames.push(Frame {
id: FrameID::Valid(String::from("APIC")),
id: FrameID::Valid(Cow::Borrowed("APIC")),
value: FrameValue::Picture {
encoding: TextEncoding::UTF8,
picture,
@ -705,6 +711,8 @@ impl<'a, I: Iterator<Item = FrameRef<'a>> + Clone + 'a> Id3v2TagRef<'a, I> {
#[cfg(test)]
mod tests {
use std::borrow::Cow;
use crate::id3::v2::items::popularimeter::Popularimeter;
use crate::id3::v2::{
read_id3v2_header, EncodedTextFrame, Frame, FrameFlags, FrameID, FrameValue, ID3v2Tag,
@ -734,7 +742,7 @@ mod tests {
expected_tag.insert(
Frame::new(
"TPE1",
Cow::Borrowed("TPE1"),
FrameValue::Text {
encoding,
value: String::from("Bar artist"),
@ -746,7 +754,7 @@ mod tests {
expected_tag.insert(
Frame::new(
"TIT2",
Cow::Borrowed("TIT2"),
FrameValue::Text {
encoding,
value: String::from("Foo title"),
@ -758,7 +766,7 @@ mod tests {
expected_tag.insert(
Frame::new(
"TALB",
Cow::Borrowed("TALB"),
FrameValue::Text {
encoding,
value: String::from("Baz album"),
@ -770,7 +778,7 @@ mod tests {
expected_tag.insert(
Frame::new(
"COMM",
Cow::Borrowed("COMM"),
FrameValue::Comment(LanguageFrame {
encoding,
language: *b"eng",
@ -784,7 +792,7 @@ mod tests {
expected_tag.insert(
Frame::new(
"TDRC",
Cow::Borrowed("TDRC"),
FrameValue::Text {
encoding,
value: String::from("1984"),
@ -796,7 +804,7 @@ mod tests {
expected_tag.insert(
Frame::new(
"TRCK",
Cow::Borrowed("TRCK"),
FrameValue::Text {
encoding,
value: String::from("1"),
@ -808,7 +816,7 @@ mod tests {
expected_tag.insert(
Frame::new(
"TCON",
Cow::Borrowed("TCON"),
FrameValue::Text {
encoding,
value: String::from("Classical"),
@ -886,7 +894,7 @@ mod tests {
assert_eq!(converted_tag.frames.len(), 1);
let actual_frame = converted_tag.frames.first().unwrap();
assert_eq!(actual_frame.id, FrameID::Valid("POPM".to_string()));
assert_eq!(actual_frame.id, FrameID::Valid(Cow::Borrowed("POPM")));
// Note: as POPM frames are considered equal by email alone, each field must
// be separately validated
match actual_frame.content() {
@ -903,7 +911,7 @@ mod tests {
fn fail_write_bad_frame() {
let mut tag = ID3v2Tag::default();
tag.insert(Frame {
id: FrameID::Valid(String::from("ABCD")),
id: FrameID::Valid(Cow::Borrowed("ABCD")),
value: FrameValue::URL(String::from("FOO URL")),
flags: FrameFlags::default(),
});
@ -969,7 +977,7 @@ mod tests {
let flags = FrameFlags::default();
tag.insert(Frame {
id: FrameID::Valid(String::from("TIT2")),
id: FrameID::Valid(Cow::Borrowed("TIT2")),
value: FrameValue::Text {
encoding,
value: String::from("TempleOS Hymn Risen (Remix)"),
@ -978,7 +986,7 @@ mod tests {
});
tag.insert(Frame {
id: FrameID::Valid(String::from("TPE1")),
id: FrameID::Valid(Cow::Borrowed("TPE1")),
value: FrameValue::Text {
encoding,
value: String::from("Dave Eddy"),
@ -987,7 +995,7 @@ mod tests {
});
tag.insert(Frame {
id: FrameID::Valid(String::from("TRCK")),
id: FrameID::Valid(Cow::Borrowed("TRCK")),
value: FrameValue::Text {
encoding: TextEncoding::Latin1,
value: String::from("1"),
@ -996,7 +1004,7 @@ mod tests {
});
tag.insert(Frame {
id: FrameID::Valid(String::from("TALB")),
id: FrameID::Valid(Cow::Borrowed("TALB")),
value: FrameValue::Text {
encoding,
value: String::from("Summer"),
@ -1005,7 +1013,7 @@ mod tests {
});
tag.insert(Frame {
id: FrameID::Valid(String::from("TDRC")),
id: FrameID::Valid(Cow::Borrowed("TDRC")),
value: FrameValue::Text {
encoding,
value: String::from("2017"),
@ -1014,7 +1022,7 @@ mod tests {
});
tag.insert(Frame {
id: FrameID::Valid(String::from("TCON")),
id: FrameID::Valid(Cow::Borrowed("TCON")),
value: FrameValue::Text {
encoding,
value: String::from("Electronic"),
@ -1023,7 +1031,7 @@ mod tests {
});
tag.insert(Frame {
id: FrameID::Valid(String::from("TLEN")),
id: FrameID::Valid(Cow::Borrowed("TLEN")),
value: FrameValue::Text {
encoding: TextEncoding::UTF16,
value: String::from("213017"),
@ -1032,7 +1040,7 @@ mod tests {
});
tag.insert(Frame {
id: FrameID::Valid(String::from("APIC")),
id: FrameID::Valid(Cow::Borrowed("APIC")),
value: FrameValue::Picture {
encoding: TextEncoding::Latin1,
picture: Picture {
@ -1114,7 +1122,7 @@ mod tests {
assert_eq!(
tag.frames.first(),
Some(&Frame {
id: FrameID::Valid(String::from("APIC")),
id: FrameID::Valid(Cow::Borrowed("APIC")),
value: FrameValue::Picture {
encoding: TextEncoding::UTF8,
picture
@ -1131,7 +1139,7 @@ mod tests {
assert_eq!(parsed_tag.frames.len(), 1);
let popm_frame = parsed_tag.frames.first().unwrap();
assert_eq!(popm_frame.id, FrameID::Valid(String::from("POPM")));
assert_eq!(popm_frame.id, FrameID::Valid(Cow::Borrowed("POPM")));
assert_eq!(
popm_frame.value,
FrameValue::Popularimeter(Popularimeter {
@ -1186,7 +1194,7 @@ mod tests {
let mut tag = ID3v2Tag::default();
tag.insert(
Frame::new(
"TXXX",
Cow::Borrowed("TXXX"),
FrameValue::UserText(EncodedTextFrame {
encoding: TextEncoding::UTF8,
description: String::from("REPLAYGAIN_ALBUM_GAIN"),
@ -1212,7 +1220,7 @@ mod tests {
#[test]
fn txxx_wxxx_tag_conversion() {
let txxx_frame = Frame::new(
"TXXX",
Cow::Borrowed("TXXX"),
FrameValue::UserText(EncodedTextFrame {
encoding: TextEncoding::UTF8,
description: String::from("FOO_TEXT_FRAME"),
@ -1223,7 +1231,7 @@ mod tests {
.unwrap();
let wxxx_frame = Frame::new(
"WXXX",
Cow::Borrowed("WXXX"),
FrameValue::UserURL(EncodedTextFrame {
encoding: TextEncoding::UTF8,
description: String::from("BAR_URL_FRAME"),

View file

@ -1,5 +1,7 @@
// Tests for special case conversions
use std::borrow::Cow;
use lofty::id3::v2::{Frame, FrameFlags, FrameValue, ID3v2Tag, LanguageFrame};
use lofty::{ItemKey, Tag, TagType, TextEncoding};
@ -14,7 +16,7 @@ fn tag_to_id3v2_lang_frame() {
assert_eq!(
id3.get("USLT"),
Frame::new(
"USLT",
Cow::Borrowed("USLT"),
FrameValue::UnSyncText(LanguageFrame {
encoding: TextEncoding::UTF8,
language: *b"eng",
@ -30,7 +32,7 @@ fn tag_to_id3v2_lang_frame() {
assert_eq!(
id3.get("COMM"),
Frame::new(
"COMM",
Cow::Borrowed("COMM"),
FrameValue::Comment(LanguageFrame {
encoding: TextEncoding::UTF8,
language: *b"eng",