mirror of
https://github.com/Serial-ATA/lofty-rs
synced 2024-12-14 14:42:33 +00:00
ID3v2: populate popularimeter tag from frame value (#64)
Co-authored-by: Serial <69764315+Serial-ATA@users.noreply.github.com>
This commit is contained in:
parent
02f1314005
commit
49623cb570
4 changed files with 93 additions and 26 deletions
|
@ -12,6 +12,7 @@ use std::io::{Cursor, Read};
|
||||||
|
|
||||||
use byteorder::ReadBytesExt;
|
use byteorder::ReadBytesExt;
|
||||||
|
|
||||||
|
#[rustfmt::skip]
|
||||||
pub(super) fn parse_content(
|
pub(super) fn parse_content(
|
||||||
content: &mut &[u8],
|
content: &mut &[u8],
|
||||||
id: &str,
|
id: &str,
|
||||||
|
@ -32,7 +33,7 @@ pub(super) fn parse_content(
|
||||||
// WFED (Podcast URL), GRP1 (Grouping), MVNM (Movement Name), MVIN (Movement Number)
|
// WFED (Podcast URL), GRP1 (Grouping), MVNM (Movement Name), MVIN (Movement Number)
|
||||||
"WFED" | "GRP1" | "MVNM" | "MVIN" => parse_text(content, version)?,
|
"WFED" | "GRP1" | "MVNM" | "MVIN" => parse_text(content, version)?,
|
||||||
_ if id.starts_with('W') => parse_link(content)?,
|
_ if id.starts_with('W') => parse_link(content)?,
|
||||||
"POPM" => Some(parse_popularimeter(content)?),
|
"POPM" => Some(FrameValue::Popularimeter(Popularimeter::from_bytes(content)?)),
|
||||||
// SYLT, GEOB, and any unknown frames
|
// SYLT, GEOB, and any unknown frames
|
||||||
_ => Some(FrameValue::Binary(content.to_vec())),
|
_ => Some(FrameValue::Binary(content.to_vec())),
|
||||||
})
|
})
|
||||||
|
@ -160,29 +161,6 @@ fn parse_link(content: &mut &[u8]) -> Result<Option<FrameValue>> {
|
||||||
Ok(Some(FrameValue::URL(link)))
|
Ok(Some(FrameValue::URL(link)))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_popularimeter(content: &mut &[u8]) -> Result<FrameValue> {
|
|
||||||
let email = decode_text(content, TextEncoding::Latin1, true)?;
|
|
||||||
let rating = content.read_u8()?;
|
|
||||||
|
|
||||||
let counter;
|
|
||||||
let remaining_size = content.len();
|
|
||||||
if remaining_size > 8 {
|
|
||||||
counter = u64::MAX;
|
|
||||||
} else {
|
|
||||||
let mut counter_bytes = [0; 8];
|
|
||||||
let counter_start_pos = 8 - remaining_size;
|
|
||||||
|
|
||||||
counter_bytes[counter_start_pos..].copy_from_slice(content);
|
|
||||||
counter = u64::from_be_bytes(counter_bytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(FrameValue::Popularimeter(Popularimeter {
|
|
||||||
email: email.unwrap_or_default(),
|
|
||||||
rating,
|
|
||||||
counter,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn verify_encoding(encoding: u8, version: ID3v2Version) -> Result<TextEncoding> {
|
fn verify_encoding(encoding: u8, version: ID3v2Version) -> Result<TextEncoding> {
|
||||||
if let ID3v2Version::V2 = version {
|
if let ID3v2Version::V2 = version {
|
||||||
if encoding != 0 && encoding != 1 {
|
if encoding != 0 && encoding != 1 {
|
||||||
|
|
|
@ -296,6 +296,9 @@ impl From<TagItem> for Option<Frame> {
|
||||||
content: text,
|
content: text,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
(FrameID::Valid(ref s), ItemValue::Binary(text)) if s == "POPM" => {
|
||||||
|
FrameValue::Popularimeter(Popularimeter::from_bytes(&text).ok()?)
|
||||||
|
},
|
||||||
(_, value) => value.into(),
|
(_, value) => value.into(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -394,6 +397,9 @@ impl<'a> TryFrom<&'a TagItem> for FrameRef<'a> {
|
||||||
description: String::new(),
|
description: String::new(),
|
||||||
content: text.clone(),
|
content: text.clone(),
|
||||||
}),
|
}),
|
||||||
|
("POPM", ItemValue::Binary(contents)) => {
|
||||||
|
FrameValue::Popularimeter(Popularimeter::from_bytes(contents)?)
|
||||||
|
},
|
||||||
(_, value) => value.into(),
|
(_, value) => value.into(),
|
||||||
}),
|
}),
|
||||||
flags: FrameFlags::default(),
|
flags: FrameFlags::default(),
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
use crate::util::text::{encode_text, TextEncoding};
|
use crate::error::Result;
|
||||||
|
use crate::util::text::{decode_text, encode_text, TextEncoding};
|
||||||
|
|
||||||
|
use byteorder::ReadBytesExt;
|
||||||
use std::hash::{Hash, Hasher};
|
use std::hash::{Hash, Hasher};
|
||||||
|
|
||||||
/// The contents of a popularimeter ("POPM") frame
|
/// The contents of a popularimeter ("POPM") frame
|
||||||
|
@ -45,6 +47,32 @@ impl Popularimeter {
|
||||||
|
|
||||||
content
|
content
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Convert ID3v2 POPM frame bytes into a [`Popularimeter`].
|
||||||
|
pub fn from_bytes(mut bytes: &[u8]) -> Result<Self> {
|
||||||
|
let content = &mut bytes;
|
||||||
|
|
||||||
|
let email = decode_text(content, TextEncoding::Latin1, true)?;
|
||||||
|
let rating = content.read_u8()?;
|
||||||
|
|
||||||
|
let counter;
|
||||||
|
let remaining_size = content.len();
|
||||||
|
if remaining_size > 8 {
|
||||||
|
counter = u64::MAX;
|
||||||
|
} else {
|
||||||
|
let mut counter_bytes = [0; 8];
|
||||||
|
let counter_start_pos = 8 - remaining_size;
|
||||||
|
|
||||||
|
counter_bytes[counter_start_pos..].copy_from_slice(content);
|
||||||
|
counter = u64::from_be_bytes(counter_bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
email: email.unwrap_or_default(),
|
||||||
|
rating,
|
||||||
|
counter,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PartialEq for Popularimeter {
|
impl PartialEq for Popularimeter {
|
||||||
|
|
|
@ -81,6 +81,7 @@ macro_rules! impl_accessor {
|
||||||
/// * TXXX/WXXX - These frames will be stored as an [`ItemKey`] by their description. Some variants exist for these descriptions, such as the one for `ReplayGain`,
|
/// * TXXX/WXXX - These frames will be stored as an [`ItemKey`] by their description. Some variants exist for these descriptions, such as the one for `ReplayGain`,
|
||||||
/// otherwise [`ItemKey::Unknown`] will be used.
|
/// otherwise [`ItemKey::Unknown`] will be used.
|
||||||
/// * Any [`LanguageFrame`] - With ID3v2 being the only format that allows for language-specific items, this information is not retained. These frames **will** be discarded.
|
/// * Any [`LanguageFrame`] - With ID3v2 being the only format that allows for language-specific items, this information is not retained. These frames **will** be discarded.
|
||||||
|
/// * POPM - These frames will be stored as a raw [`ItemValue::Binary`] value under the [`ItemKey::Popularimeter`] key.
|
||||||
///
|
///
|
||||||
/// ## Special Frames
|
/// ## Special Frames
|
||||||
///
|
///
|
||||||
|
@ -571,7 +572,9 @@ impl From<ID3v2Tag> for Tag {
|
||||||
tag.push_picture(picture);
|
tag.push_picture(picture);
|
||||||
continue;
|
continue;
|
||||||
},
|
},
|
||||||
FrameValue::Popularimeter(_) => continue,
|
FrameValue::Popularimeter(popularimeter) => {
|
||||||
|
ItemValue::Binary(popularimeter.as_bytes())
|
||||||
|
},
|
||||||
FrameValue::Binary(binary) => ItemValue::Binary(binary),
|
FrameValue::Binary(binary) => ItemValue::Binary(binary),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -827,6 +830,58 @@ mod tests {
|
||||||
crate::tag::utils::test_utils::verify_tag(&tag, true, true);
|
crate::tag::utils::test_utils::verify_tag(&tag, true, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn id3v2_to_tag_popm() {
|
||||||
|
let id3v2 = read_tag("tests/tags/assets/id3v2/test_popm.id3v24");
|
||||||
|
|
||||||
|
let tag: Tag = id3v2.into();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
tag.get_binary(&ItemKey::Popularimeter, false),
|
||||||
|
Some(
|
||||||
|
&[
|
||||||
|
b'f', b'o', b'o', b'@', b'b', b'a', b'r', b'.', b'c', b'o', b'm', 0, 196, 0, 0,
|
||||||
|
255, 255,
|
||||||
|
][..]
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tag_to_id3v2_popm() {
|
||||||
|
let mut tag = Tag::new(TagType::ID3v2);
|
||||||
|
tag.insert_item(TagItem::new(
|
||||||
|
ItemKey::Popularimeter,
|
||||||
|
ItemValue::Binary(vec![
|
||||||
|
b'f', b'o', b'o', b'@', b'b', b'a', b'r', b'.', b'c', b'o', b'm', 0, 196, 0, 0,
|
||||||
|
255, 255,
|
||||||
|
]),
|
||||||
|
));
|
||||||
|
|
||||||
|
let expected = Popularimeter {
|
||||||
|
email: String::from("foo@bar.com"),
|
||||||
|
rating: 196,
|
||||||
|
counter: 65535,
|
||||||
|
};
|
||||||
|
|
||||||
|
let converted_tag: ID3v2Tag = tag.into();
|
||||||
|
|
||||||
|
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()));
|
||||||
|
// Note: as POPM frames are considered equal by email alone, each field must
|
||||||
|
// be separately validated
|
||||||
|
match actual_frame.content() {
|
||||||
|
FrameValue::Popularimeter(pop) => {
|
||||||
|
assert_eq!(pop.email, expected.email);
|
||||||
|
assert_eq!(pop.rating, expected.rating);
|
||||||
|
assert_eq!(pop.counter, expected.counter);
|
||||||
|
},
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn fail_write_bad_frame() {
|
fn fail_write_bad_frame() {
|
||||||
let mut tag = ID3v2Tag::default();
|
let mut tag = ID3v2Tag::default();
|
||||||
|
|
Loading…
Reference in a new issue