Skip to content

Commit 49623cb

Browse files
maneacSerial-ATA
andauthored
ID3v2: populate popularimeter tag from frame value (#64)
Co-authored-by: Serial <[email protected]>
1 parent 02f1314 commit 49623cb

File tree

4 files changed

+93
-26
lines changed

4 files changed

+93
-26
lines changed

src/id3/v2/frame/content.rs

Lines changed: 2 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ use std::io::{Cursor, Read};
1212

1313
use byteorder::ReadBytesExt;
1414

15+
#[rustfmt::skip]
1516
pub(super) fn parse_content(
1617
content: &mut &[u8],
1718
id: &str,
@@ -32,7 +33,7 @@ pub(super) fn parse_content(
3233
// WFED (Podcast URL), GRP1 (Grouping), MVNM (Movement Name), MVIN (Movement Number)
3334
"WFED" | "GRP1" | "MVNM" | "MVIN" => parse_text(content, version)?,
3435
_ if id.starts_with('W') => parse_link(content)?,
35-
"POPM" => Some(parse_popularimeter(content)?),
36+
"POPM" => Some(FrameValue::Popularimeter(Popularimeter::from_bytes(content)?)),
3637
// SYLT, GEOB, and any unknown frames
3738
_ => Some(FrameValue::Binary(content.to_vec())),
3839
})
@@ -160,29 +161,6 @@ fn parse_link(content: &mut &[u8]) -> Result<Option<FrameValue>> {
160161
Ok(Some(FrameValue::URL(link)))
161162
}
162163

163-
fn parse_popularimeter(content: &mut &[u8]) -> Result<FrameValue> {
164-
let email = decode_text(content, TextEncoding::Latin1, true)?;
165-
let rating = content.read_u8()?;
166-
167-
let counter;
168-
let remaining_size = content.len();
169-
if remaining_size > 8 {
170-
counter = u64::MAX;
171-
} else {
172-
let mut counter_bytes = [0; 8];
173-
let counter_start_pos = 8 - remaining_size;
174-
175-
counter_bytes[counter_start_pos..].copy_from_slice(content);
176-
counter = u64::from_be_bytes(counter_bytes);
177-
}
178-
179-
Ok(FrameValue::Popularimeter(Popularimeter {
180-
email: email.unwrap_or_default(),
181-
rating,
182-
counter,
183-
}))
184-
}
185-
186164
fn verify_encoding(encoding: u8, version: ID3v2Version) -> Result<TextEncoding> {
187165
if let ID3v2Version::V2 = version {
188166
if encoding != 0 && encoding != 1 {

src/id3/v2/frame/mod.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,9 @@ impl From<TagItem> for Option<Frame> {
296296
content: text,
297297
})
298298
},
299+
(FrameID::Valid(ref s), ItemValue::Binary(text)) if s == "POPM" => {
300+
FrameValue::Popularimeter(Popularimeter::from_bytes(&text).ok()?)
301+
},
299302
(_, value) => value.into(),
300303
};
301304

@@ -394,6 +397,9 @@ impl<'a> TryFrom<&'a TagItem> for FrameRef<'a> {
394397
description: String::new(),
395398
content: text.clone(),
396399
}),
400+
("POPM", ItemValue::Binary(contents)) => {
401+
FrameValue::Popularimeter(Popularimeter::from_bytes(contents)?)
402+
},
397403
(_, value) => value.into(),
398404
}),
399405
flags: FrameFlags::default(),

src/id3/v2/items/popularimeter.rs

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1-
use crate::util::text::{encode_text, TextEncoding};
1+
use crate::error::Result;
2+
use crate::util::text::{decode_text, encode_text, TextEncoding};
23

4+
use byteorder::ReadBytesExt;
35
use std::hash::{Hash, Hasher};
46

57
/// The contents of a popularimeter ("POPM") frame
@@ -45,6 +47,32 @@ impl Popularimeter {
4547

4648
content
4749
}
50+
51+
/// Convert ID3v2 POPM frame bytes into a [`Popularimeter`].
52+
pub fn from_bytes(mut bytes: &[u8]) -> Result<Self> {
53+
let content = &mut bytes;
54+
55+
let email = decode_text(content, TextEncoding::Latin1, true)?;
56+
let rating = content.read_u8()?;
57+
58+
let counter;
59+
let remaining_size = content.len();
60+
if remaining_size > 8 {
61+
counter = u64::MAX;
62+
} else {
63+
let mut counter_bytes = [0; 8];
64+
let counter_start_pos = 8 - remaining_size;
65+
66+
counter_bytes[counter_start_pos..].copy_from_slice(content);
67+
counter = u64::from_be_bytes(counter_bytes);
68+
}
69+
70+
Ok(Self {
71+
email: email.unwrap_or_default(),
72+
rating,
73+
counter,
74+
})
75+
}
4876
}
4977

5078
impl PartialEq for Popularimeter {

src/id3/v2/tag.rs

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ macro_rules! impl_accessor {
8181
/// * 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`,
8282
/// otherwise [`ItemKey::Unknown`] will be used.
8383
/// * Any [`LanguageFrame`] - With ID3v2 being the only format that allows for language-specific items, this information is not retained. These frames **will** be discarded.
84+
/// * POPM - These frames will be stored as a raw [`ItemValue::Binary`] value under the [`ItemKey::Popularimeter`] key.
8485
///
8586
/// ## Special Frames
8687
///
@@ -571,7 +572,9 @@ impl From<ID3v2Tag> for Tag {
571572
tag.push_picture(picture);
572573
continue;
573574
},
574-
FrameValue::Popularimeter(_) => continue,
575+
FrameValue::Popularimeter(popularimeter) => {
576+
ItemValue::Binary(popularimeter.as_bytes())
577+
},
575578
FrameValue::Binary(binary) => ItemValue::Binary(binary),
576579
};
577580

@@ -827,6 +830,58 @@ mod tests {
827830
crate::tag::utils::test_utils::verify_tag(&tag, true, true);
828831
}
829832

833+
#[test]
834+
fn id3v2_to_tag_popm() {
835+
let id3v2 = read_tag("tests/tags/assets/id3v2/test_popm.id3v24");
836+
837+
let tag: Tag = id3v2.into();
838+
839+
assert_eq!(
840+
tag.get_binary(&ItemKey::Popularimeter, false),
841+
Some(
842+
&[
843+
b'f', b'o', b'o', b'@', b'b', b'a', b'r', b'.', b'c', b'o', b'm', 0, 196, 0, 0,
844+
255, 255,
845+
][..]
846+
),
847+
);
848+
}
849+
850+
#[test]
851+
fn tag_to_id3v2_popm() {
852+
let mut tag = Tag::new(TagType::ID3v2);
853+
tag.insert_item(TagItem::new(
854+
ItemKey::Popularimeter,
855+
ItemValue::Binary(vec![
856+
b'f', b'o', b'o', b'@', b'b', b'a', b'r', b'.', b'c', b'o', b'm', 0, 196, 0, 0,
857+
255, 255,
858+
]),
859+
));
860+
861+
let expected = Popularimeter {
862+
email: String::from("[email protected]"),
863+
rating: 196,
864+
counter: 65535,
865+
};
866+
867+
let converted_tag: ID3v2Tag = tag.into();
868+
869+
assert_eq!(converted_tag.frames.len(), 1);
870+
let actual_frame = converted_tag.frames.first().unwrap();
871+
872+
assert_eq!(actual_frame.id, FrameID::Valid("POPM".to_string()));
873+
// Note: as POPM frames are considered equal by email alone, each field must
874+
// be separately validated
875+
match actual_frame.content() {
876+
FrameValue::Popularimeter(pop) => {
877+
assert_eq!(pop.email, expected.email);
878+
assert_eq!(pop.rating, expected.rating);
879+
assert_eq!(pop.counter, expected.counter);
880+
},
881+
_ => unreachable!(),
882+
}
883+
}
884+
830885
#[test]
831886
fn fail_write_bad_frame() {
832887
let mut tag = ID3v2Tag::default();

0 commit comments

Comments
 (0)