@@ -2,19 +2,21 @@ use crate::temp_file;
2
2
3
3
use std:: borrow:: Cow ;
4
4
use std:: collections:: HashMap ;
5
- use std:: io:: Seek ;
5
+ use std:: io:: { Read , Seek } ;
6
6
7
7
use lofty:: config:: { ParseOptions , ParsingMode , WriteOptions } ;
8
8
use lofty:: file:: AudioFile ;
9
9
use lofty:: id3:: v2:: {
10
10
AttachedPictureFrame , ChannelInformation , ChannelType , CommentFrame , Event ,
11
11
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 ,
15
16
} ;
16
17
use lofty:: mpeg:: MpegFile ;
17
18
use lofty:: picture:: { MimeType , Picture , PictureType } ;
19
+ use lofty:: tag:: items:: Timestamp ;
18
20
use lofty:: tag:: { Accessor , TagExt } ;
19
21
use lofty:: TextEncoding ;
20
22
@@ -30,15 +32,66 @@ fn test_unsynch_decode() {
30
32
) ;
31
33
}
32
34
33
- // TODO: Support downgrading to ID3v2.3 (#62)
34
35
#[ 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
+ }
37
66
38
- // TODO: Support downgrading to ID3v2.3 (#62)
39
67
#[ 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
+ }
42
95
43
96
#[ test]
44
97
fn test_utf16be_delimiter ( ) {
@@ -874,21 +927,82 @@ fn test_save_utf16_comment() {
874
927
}
875
928
}
876
929
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.
878
932
#[ test]
879
933
#[ 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
+ }
881
955
882
956
#[ test]
883
- #[ ignore]
884
957
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" ) ) ;
886
978
}
887
979
888
980
#[ test]
889
- #[ ignore]
890
981
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
+ ) ;
892
1006
}
893
1007
894
1008
#[ test]
@@ -933,10 +1047,196 @@ fn test_update_full_date22() {
933
1047
) ;
934
1048
}
935
1049
936
- // TODO: Support downgrading to ID3v2.3 (#62)
937
1050
#[ 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
+ }
940
1240
941
1241
#[ test]
942
1242
fn test_compressed_frame_with_broken_length ( ) {
0 commit comments