Skip to content

Commit 223eca2

Browse files
committed
Tests: Add ID3v2.3 tests from TagLib
1 parent ede3223 commit 223eca2

File tree

1 file changed

+319
-19
lines changed

1 file changed

+319
-19
lines changed

lofty/tests/taglib/test_id3v2.rs

Lines changed: 319 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,21 @@ use crate::temp_file;
22

33
use std::borrow::Cow;
44
use std::collections::HashMap;
5-
use std::io::Seek;
5+
use std::io::{Read, Seek};
66

77
use lofty::config::{ParseOptions, ParsingMode, WriteOptions};
88
use lofty::file::AudioFile;
99
use lofty::id3::v2::{
1010
AttachedPictureFrame, ChannelInformation, ChannelType, CommentFrame, Event,
1111
EventTimingCodesFrame, EventType, ExtendedTextFrame, ExtendedUrlFrame, Frame, FrameFlags,
12-
FrameId, GeneralEncapsulatedObject, Id3v2Tag, Id3v2Version, OwnershipFrame, PopularimeterFrame,
13-
PrivateFrame, RelativeVolumeAdjustmentFrame, SyncTextContentType, SynchronizedTextFrame,
14-
TextInformationFrame, TimestampFormat, UniqueFileIdentifierFrame, UrlLinkFrame,
12+
FrameId, GeneralEncapsulatedObject, Id3v2Tag, Id3v2Version, KeyValueFrame, OwnershipFrame,
13+
PopularimeterFrame, PrivateFrame, RelativeVolumeAdjustmentFrame, SyncTextContentType,
14+
SynchronizedTextFrame, TextInformationFrame, TimestampFormat, TimestampFrame,
15+
UniqueFileIdentifierFrame, UnsynchronizedTextFrame, UrlLinkFrame,
1516
};
1617
use lofty::mpeg::MpegFile;
1718
use lofty::picture::{MimeType, Picture, PictureType};
19+
use lofty::tag::items::Timestamp;
1820
use lofty::tag::{Accessor, TagExt};
1921
use lofty::TextEncoding;
2022

@@ -30,15 +32,66 @@ fn test_unsynch_decode() {
3032
);
3133
}
3234

33-
// TODO: Support downgrading to ID3v2.3 (#62)
3435
#[test]
35-
#[ignore]
36-
fn test_downgrade_utf8_for_id3v23_1() {}
36+
fn test_downgrade_utf8_for_id3v23_1() {
37+
let mut file = temp_file!("tests/taglib/data/xing.mp3");
38+
39+
let f = TextInformationFrame::new(
40+
FrameId::Valid(Cow::Borrowed("TPE1")),
41+
TextEncoding::UTF8,
42+
String::from("Foo"),
43+
);
44+
45+
let mut id3v2 = Id3v2Tag::new();
46+
id3v2.insert(Frame::Text(f.clone()));
47+
id3v2
48+
.save_to(&mut file, WriteOptions::new().use_id3v23(true))
49+
.unwrap();
50+
51+
let data = f.as_bytes(true);
52+
assert_eq!(data.len(), 1 + 6 + 2); // NOTE: This does not include frame headers like TagLib does
53+
54+
let f2 = TextInformationFrame::parse(
55+
&mut &data[..],
56+
FrameId::Valid(Cow::Borrowed("TPE1")),
57+
FrameFlags::default(),
58+
Id3v2Version::V3,
59+
)
60+
.unwrap()
61+
.unwrap();
62+
63+
assert_eq!(f.value, f2.value);
64+
assert_eq!(f2.encoding, TextEncoding::UTF16);
65+
}
3766

38-
// TODO: Support downgrading to ID3v2.3 (#62)
3967
#[test]
40-
#[ignore]
41-
fn test_downgrade_utf8_for_id3v23_2() {}
68+
fn test_downgrade_utf8_for_id3v23_2() {
69+
let mut file = temp_file!("tests/taglib/data/xing.mp3");
70+
71+
let f = UnsynchronizedTextFrame::new(
72+
TextEncoding::UTF8,
73+
*b"XXX",
74+
String::new(),
75+
String::from("Foo"),
76+
);
77+
78+
let mut id3v2 = Id3v2Tag::new();
79+
id3v2.insert(Frame::UnsynchronizedText(f.clone()));
80+
id3v2
81+
.save_to(&mut file, WriteOptions::new().use_id3v23(true))
82+
.unwrap();
83+
84+
let data = f.as_bytes(true).unwrap();
85+
assert_eq!(data.len(), 1 + 3 + 2 + 2 + 6 + 2); // NOTE: This does not include frame headers like TagLib does
86+
87+
let f2 =
88+
UnsynchronizedTextFrame::parse(&mut &data[..], FrameFlags::default(), Id3v2Version::V3)
89+
.unwrap()
90+
.unwrap();
91+
92+
assert_eq!(f2.content, String::from("Foo"));
93+
assert_eq!(f2.encoding, TextEncoding::UTF16);
94+
}
4295

4396
#[test]
4497
fn test_utf16be_delimiter() {
@@ -874,21 +927,82 @@ fn test_save_utf16_comment() {
874927
}
875928
}
876929

877-
// TODO: Support downgrading to ID3v2.3 (#62)
930+
// TODO: Probably won't ever support this, it's a weird edge case with
931+
// duplicate genres. That can be up to the caller to figure out.
878932
#[test]
879933
#[ignore]
880-
fn test_update_genre_23_1() {}
934+
fn test_update_genre_23_1() {
935+
// "Refinement" is the same as the ID3v1 genre - duplicate
936+
let frame_value = TextInformationFrame::parse(
937+
&mut &b"\x00\
938+
(22)Death Metal"[..],
939+
FrameId::Valid(Cow::Borrowed("TCON")),
940+
FrameFlags::default(),
941+
Id3v2Version::V4,
942+
)
943+
.unwrap()
944+
.unwrap();
945+
946+
let mut tag = Id3v2Tag::new();
947+
tag.insert(Frame::Text(frame_value));
948+
949+
let mut genres = tag.genres().unwrap();
950+
assert_eq!(genres.next(), Some("Death Metal"));
951+
assert!(genres.next().is_none());
952+
953+
assert_eq!(tag.genre().as_deref(), Some("Death Metal"));
954+
}
881955

882956
#[test]
883-
#[ignore]
884957
fn test_update_genre23_2() {
885-
// Marker test, Lofty doesn't do additional work with the genre string
958+
// "Refinement" is different from the ID3v1 genre
959+
let frame_value = TextInformationFrame::parse(
960+
&mut &b"\x00\
961+
(4)Eurodisco"[..],
962+
FrameId::Valid(Cow::Borrowed("TCON")),
963+
FrameFlags::default(),
964+
Id3v2Version::V4,
965+
)
966+
.unwrap()
967+
.unwrap();
968+
969+
let mut tag = Id3v2Tag::new();
970+
tag.insert(Frame::Text(frame_value));
971+
972+
let mut genres = tag.genres().unwrap();
973+
assert_eq!(genres.next(), Some("Disco"));
974+
assert_eq!(genres.next(), Some("Eurodisco"));
975+
assert!(genres.next().is_none());
976+
977+
assert_eq!(tag.genre().as_deref(), Some("Disco / Eurodisco"));
886978
}
887979

888980
#[test]
889-
#[ignore]
890981
fn test_update_genre23_3() {
891-
// Marker test, Lofty doesn't do additional work with the genre string
982+
// Multiple references and a refinement
983+
let frame_value = TextInformationFrame::parse(
984+
&mut &b"\x00\
985+
(9)(138)Viking Metal"[..],
986+
FrameId::Valid(Cow::Borrowed("TCON")),
987+
FrameFlags::default(),
988+
Id3v2Version::V4,
989+
)
990+
.unwrap()
991+
.unwrap();
992+
993+
let mut tag = Id3v2Tag::new();
994+
tag.insert(Frame::Text(frame_value));
995+
996+
let mut genres = tag.genres().unwrap();
997+
assert_eq!(genres.next(), Some("Metal"));
998+
assert_eq!(genres.next(), Some("Black Metal"));
999+
assert_eq!(genres.next(), Some("Viking Metal"));
1000+
assert!(genres.next().is_none());
1001+
1002+
assert_eq!(
1003+
tag.genre().as_deref(),
1004+
Some("Metal / Black Metal / Viking Metal")
1005+
);
8921006
}
8931007

8941008
#[test]
@@ -933,10 +1047,196 @@ fn test_update_full_date22() {
9331047
);
9341048
}
9351049

936-
// TODO: Support downgrading to ID3v2.3 (#62)
9371050
#[test]
938-
#[ignore]
939-
fn test_downgrade_to_23() {}
1051+
fn test_downgrade_to_23() {
1052+
let mut file = temp_file!("tests/taglib/data/xing.mp3");
1053+
1054+
{
1055+
let mut id3v2 = Id3v2Tag::new();
1056+
1057+
id3v2.insert(Frame::Timestamp(TimestampFrame::new(
1058+
FrameId::Valid(Cow::Borrowed("TDOR")),
1059+
TextEncoding::Latin1,
1060+
Timestamp::parse(&mut &b"2011-03-16"[..], ParsingMode::Strict)
1061+
.unwrap()
1062+
.unwrap(),
1063+
)));
1064+
1065+
id3v2.insert(Frame::Timestamp(TimestampFrame::new(
1066+
FrameId::Valid(Cow::Borrowed("TDRC")),
1067+
TextEncoding::Latin1,
1068+
Timestamp::parse(&mut &b"2012-04-17T12:01"[..], ParsingMode::Strict)
1069+
.unwrap()
1070+
.unwrap(),
1071+
)));
1072+
1073+
id3v2.insert(Frame::KeyValue(KeyValueFrame::new(
1074+
FrameId::Valid(Cow::Borrowed("TMCL")),
1075+
TextEncoding::Latin1,
1076+
vec![
1077+
(String::from("Guitar"), String::from("Artist 1")),
1078+
(String::from("Drums"), String::from("Artist 2")),
1079+
],
1080+
)));
1081+
1082+
id3v2.insert(Frame::KeyValue(KeyValueFrame::new(
1083+
FrameId::Valid(Cow::Borrowed("TIPL")),
1084+
TextEncoding::Latin1,
1085+
vec![
1086+
(String::from("Producer"), String::from("Artist 3")),
1087+
(String::from("Mastering"), String::from("Artist 4")),
1088+
],
1089+
)));
1090+
1091+
id3v2.insert(Frame::Text(TextInformationFrame::new(
1092+
FrameId::Valid(Cow::Borrowed("TCON")),
1093+
TextEncoding::Latin1,
1094+
String::from("51\039\0Power Noise"),
1095+
)));
1096+
1097+
id3v2.insert(Frame::Text(TextInformationFrame::new(
1098+
FrameId::Valid(Cow::Borrowed("TDRL")),
1099+
TextEncoding::Latin1,
1100+
String::new(),
1101+
)));
1102+
1103+
id3v2.insert(Frame::Text(TextInformationFrame::new(
1104+
FrameId::Valid(Cow::Borrowed("TDTG")),
1105+
TextEncoding::Latin1,
1106+
String::new(),
1107+
)));
1108+
1109+
id3v2.insert(Frame::Text(TextInformationFrame::new(
1110+
FrameId::Valid(Cow::Borrowed("TMOO")),
1111+
TextEncoding::Latin1,
1112+
String::new(),
1113+
)));
1114+
1115+
id3v2.insert(Frame::Text(TextInformationFrame::new(
1116+
FrameId::Valid(Cow::Borrowed("TPRO")),
1117+
TextEncoding::Latin1,
1118+
String::new(),
1119+
)));
1120+
1121+
id3v2.insert(Frame::Text(TextInformationFrame::new(
1122+
FrameId::Valid(Cow::Borrowed("TSOA")),
1123+
TextEncoding::Latin1,
1124+
String::new(),
1125+
)));
1126+
1127+
id3v2.insert(Frame::Text(TextInformationFrame::new(
1128+
FrameId::Valid(Cow::Borrowed("TSOT")),
1129+
TextEncoding::Latin1,
1130+
String::new(),
1131+
)));
1132+
1133+
id3v2.insert(Frame::Text(TextInformationFrame::new(
1134+
FrameId::Valid(Cow::Borrowed("TSST")),
1135+
TextEncoding::Latin1,
1136+
String::new(),
1137+
)));
1138+
1139+
id3v2.insert(Frame::Text(TextInformationFrame::new(
1140+
FrameId::Valid(Cow::Borrowed("TSOP")),
1141+
TextEncoding::Latin1,
1142+
String::new(),
1143+
)));
1144+
1145+
id3v2
1146+
.save_to(&mut file, WriteOptions::new().use_id3v23(true))
1147+
.unwrap();
1148+
}
1149+
file.rewind().unwrap();
1150+
{
1151+
let f = MpegFile::read_from(&mut file, ParseOptions::new()).unwrap();
1152+
assert!(f.id3v2().is_some());
1153+
1154+
let id3v2 = f.id3v2().unwrap();
1155+
let tf = id3v2.get(&FrameId::Valid(Cow::Borrowed("TDOR"))).unwrap();
1156+
let Frame::Timestamp(TimestampFrame { timestamp, .. }) = tf else {
1157+
unreachable!()
1158+
};
1159+
assert_eq!(timestamp.to_string(), "2011");
1160+
1161+
let tf = id3v2.get(&FrameId::Valid(Cow::Borrowed("TDRC"))).unwrap();
1162+
let Frame::Timestamp(TimestampFrame { timestamp, .. }) = tf else {
1163+
unreachable!()
1164+
};
1165+
assert_eq!(timestamp.to_string(), "2012-04-17T12:01");
1166+
1167+
let tf = id3v2.get(&FrameId::Valid(Cow::Borrowed("TIPL"))).unwrap();
1168+
let Frame::KeyValue(KeyValueFrame {
1169+
key_value_pairs, ..
1170+
}) = tf
1171+
else {
1172+
unreachable!()
1173+
};
1174+
assert_eq!(key_value_pairs.len(), 4);
1175+
assert_eq!(
1176+
key_value_pairs[0],
1177+
(String::from("Guitar"), String::from("Artist 1"))
1178+
);
1179+
assert_eq!(
1180+
key_value_pairs[1],
1181+
(String::from("Drums"), String::from("Artist 2"))
1182+
);
1183+
assert_eq!(
1184+
key_value_pairs[2],
1185+
(String::from("Producer"), String::from("Artist 3"))
1186+
);
1187+
assert_eq!(
1188+
key_value_pairs[3],
1189+
(String::from("Mastering"), String::from("Artist 4"))
1190+
);
1191+
1192+
// NOTE: Lofty upgrades the first genre (originally 51) to "Techno-Industrial"
1193+
// TagLib retains the original genre index.
1194+
let tf = id3v2.genres().unwrap().collect::<Vec<_>>();
1195+
assert_eq!(tf.join("\0"), "Techno-Industrial\0Noise\0Power Noise");
1196+
1197+
assert!(!id3v2.contains(&FrameId::Valid(Cow::Borrowed("TDRL"))));
1198+
assert!(!id3v2.contains(&FrameId::Valid(Cow::Borrowed("TDTG"))));
1199+
assert!(!id3v2.contains(&FrameId::Valid(Cow::Borrowed("TMOO"))));
1200+
assert!(!id3v2.contains(&FrameId::Valid(Cow::Borrowed("TPRO"))));
1201+
assert!(!id3v2.contains(&FrameId::Valid(Cow::Borrowed("TSOA"))));
1202+
assert!(!id3v2.contains(&FrameId::Valid(Cow::Borrowed("TSOT"))));
1203+
assert!(!id3v2.contains(&FrameId::Valid(Cow::Borrowed("TSST"))));
1204+
assert!(!id3v2.contains(&FrameId::Valid(Cow::Borrowed("TSOP"))));
1205+
}
1206+
file.rewind().unwrap();
1207+
{
1208+
#[allow(clippy::items_after_statements)]
1209+
const EXPECTED_ID3V23_DATA: &[u8] = b"ID3\x03\x00\x00\x00\x00\x09\x28\
1210+
TORY\x00\x00\x00\x05\x00\x00\x002011\
1211+
TYER\x00\x00\x00\x05\x00\x00\x002012\
1212+
TDAT\x00\x00\x00\x05\x00\x00\x001704\
1213+
TIME\x00\x00\x00\x05\x00\x00\x001201\
1214+
TCON\x00\x00\x00\x14\x00\x00\x00(51)(39)Power Noise\
1215+
IPLS\x00\x00\x00\x44\x00\x00\x00Guitar\x00\
1216+
Artist 1\x00Drums\x00Artist 2\x00Producer\x00\
1217+
Artist 3\x00Mastering\x00Artist 4";
1218+
1219+
let mut file_id3v2 = vec![0; EXPECTED_ID3V23_DATA.len()];
1220+
file.read_exact(&mut file_id3v2).unwrap();
1221+
assert_eq!(file_id3v2.as_slice(), EXPECTED_ID3V23_DATA);
1222+
}
1223+
{
1224+
let mut file = temp_file!("tests/taglib/data/rare_frames.mp3");
1225+
let f = MpegFile::read_from(&mut file, ParseOptions::new()).unwrap();
1226+
assert!(f.id3v2().is_some());
1227+
file.rewind().unwrap();
1228+
f.save_to(&mut file, WriteOptions::new().use_id3v23(true))
1229+
.unwrap();
1230+
1231+
file.rewind().unwrap();
1232+
let mut file_content = Vec::new();
1233+
file.read_to_end(&mut file_content).unwrap();
1234+
1235+
let tcon_pos = file_content.windows(4).position(|w| w == b"TCON").unwrap();
1236+
let tcon = &file_content[tcon_pos + 11..];
1237+
assert_eq!(&tcon[..4], &b"(13)"[..]);
1238+
}
1239+
}
9401240

9411241
#[test]
9421242
fn test_compressed_frame_with_broken_length() {

0 commit comments

Comments
 (0)