@@ -2,19 +2,21 @@ use crate::temp_file;
22
33use std:: borrow:: Cow ;
44use std:: collections:: HashMap ;
5- use std:: io:: Seek ;
5+ use std:: io:: { Read , Seek } ;
66
77use lofty:: config:: { ParseOptions , ParsingMode , WriteOptions } ;
88use lofty:: file:: AudioFile ;
99use 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} ;
1617use lofty:: mpeg:: MpegFile ;
1718use lofty:: picture:: { MimeType , Picture , PictureType } ;
19+ use lofty:: tag:: items:: Timestamp ;
1820use lofty:: tag:: { Accessor , TagExt } ;
1921use 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]
4497fn 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]
884957fn 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]
890981fn 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\0 39\0 Power 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\0 Noise\0 Power 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 \x00 2011\
1211+ TYER\x00 \x00 \x00 \x05 \x00 \x00 \x00 2012\
1212+ TDAT\x00 \x00 \x00 \x05 \x00 \x00 \x00 1704\
1213+ TIME\x00 \x00 \x00 \x05 \x00 \x00 \x00 1201\
1214+ TCON\x00 \x00 \x00 \x14 \x00 \x00 \x00 (51)(39)Power Noise\
1215+ IPLS\x00 \x00 \x00 \x44 \x00 \x00 \x00 Guitar\x00 \
1216+ Artist 1\x00 Drums\x00 Artist 2\x00 Producer\x00 \
1217+ Artist 3\x00 Mastering\x00 Artist 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]
9421242fn test_compressed_frame_with_broken_length ( ) {
0 commit comments