1# -*- coding: utf-8 -*- 2 3import os 4 5from mutagen import id3 6from mutagen import MutagenError 7from mutagen.apev2 import APEv2 8from mutagen.id3 import ID3, Frames, ID3UnsupportedVersionError, TIT2, \ 9 CHAP, CTOC, TT1, TCON, COMM, TORY, PIC, MakeID3v1, TRCK, TYER, TDRC, \ 10 TDAT, TIME, LNK, IPLS, TPE1, BinaryFrame, TIT3, POPM, APIC, CRM, \ 11 TALB, TPE2, TSOT, TDEN, TIPL, ParseID3v1, Encoding, ID3Tags, RVAD, \ 12 ID3NoHeaderError, Frames_2_2 13from mutagen.id3._util import BitPaddedInt, error as ID3Error 14from mutagen.id3._tags import determine_bpi, ID3Header, \ 15 save_frame, ID3SaveConfig 16from mutagen.id3._id3v1 import find_id3v1 17from mutagen._compat import cBytesIO, xrange 18 19from tests import TestCase, DATA_DIR, get_temp_copy, get_temp_empty 20 21 22def test_id3_module_exports_all_frames(): 23 for key in Frames: 24 assert getattr(id3, key) is Frames[key] 25 for key in Frames_2_2: 26 assert getattr(id3, key) is Frames_2_2[key] 27 28 29class TID3Read(TestCase): 30 31 empty = os.path.join(DATA_DIR, 'emptyfile.mp3') 32 silence = os.path.join(DATA_DIR, 'silence-44-s.mp3') 33 silence_v1 = os.path.join(DATA_DIR, 'silence-44-s-v1.mp3') 34 unsynch = os.path.join(DATA_DIR, 'id3v23_unsynch.id3') 35 v22 = os.path.join(DATA_DIR, "id3v22-test.mp3") 36 bad_tyer = os.path.join(DATA_DIR, 'bad-TYER-frame.mp3') 37 v1v2_combined = os.path.join(DATA_DIR, "id3v1v2-combined.mp3") 38 39 def test_PIC_in_23(self): 40 filename = get_temp_empty(".mp3") 41 42 try: 43 with open(filename, "wb") as h: 44 # contains a bad upgraded frame, 2.3 structure with 2.2 name. 45 # PIC was upgraded to APIC, but mime was not 46 h.write(b"ID3\x03\x00\x00\x00\x00\x08\x00PIC\x00\x00\x00" 47 b"\x00\x0b\x00\x00\x00JPG\x00\x03foo\x00\x42" 48 b"\x00" * 100) 49 id3 = ID3(filename) 50 self.assertEqual(id3.version, (2, 3, 0)) 51 self.assertTrue(id3.getall("APIC")) 52 frame = id3.getall("APIC")[0] 53 self.assertEqual(frame.mime, "image/jpeg") 54 self.assertEqual(frame.data, b"\x42") 55 self.assertEqual(frame.type, 3) 56 self.assertEqual(frame.desc, "foo") 57 finally: 58 os.remove(filename) 59 60 def test_bad_tyer(self): 61 audio = ID3(self.bad_tyer) 62 self.failIf("TYER" in audio) 63 self.failUnless("TIT2" in audio) 64 65 def test_tdrc(self): 66 tags = ID3() 67 tags.add(id3.TDRC(encoding=1, text="2003-04-05 12:03")) 68 tags.update_to_v23() 69 self.failUnlessEqual(tags["TYER"].text, ["2003"]) 70 self.failUnlessEqual(tags["TDAT"].text, ["0504"]) 71 self.failUnlessEqual(tags["TIME"].text, ["1203"]) 72 73 def test_tdor(self): 74 tags = ID3() 75 tags.add(id3.TDOR(encoding=1, text="2003-04-05 12:03")) 76 tags.update_to_v23() 77 self.failUnlessEqual(tags["TORY"].text, ["2003"]) 78 79 def test_genre_from_v24_1(self): 80 tags = ID3() 81 tags.add(id3.TCON(encoding=1, text=["4", "Rock"])) 82 tags.update_to_v23() 83 self.failUnlessEqual(tags["TCON"].text, ["Disco", "Rock"]) 84 85 def test_genre_from_v24_2(self): 86 tags = ID3() 87 tags.add(id3.TCON(encoding=1, text=["RX", "3", "CR"])) 88 tags.update_to_v23() 89 self.failUnlessEqual(tags["TCON"].text, ["Remix", "Dance", "Cover"]) 90 91 def test_genre_from_v23_1(self): 92 tags = ID3() 93 tags.add(id3.TCON(encoding=1, text=["(4)Rock"])) 94 tags.update_to_v23() 95 self.failUnlessEqual(tags["TCON"].text, ["Disco", "Rock"]) 96 97 def test_genre_from_v23_2(self): 98 tags = ID3() 99 tags.add(id3.TCON(encoding=1, text=["(RX)(3)(CR)"])) 100 tags.update_to_v23() 101 self.failUnlessEqual(tags["TCON"].text, ["Remix", "Dance", "Cover"]) 102 103 def test_ipls_to_v23(self): 104 tags = ID3() 105 tags.version = (2, 3) 106 tags.add(id3.TIPL(encoding=0, people=[["a", "b"], ["c", "d"]])) 107 tags.add(id3.TMCL(encoding=0, people=[["e", "f"], ["g", "h"]])) 108 tags.update_to_v23() 109 self.failUnlessEqual(tags["IPLS"], [["a", "b"], ["c", "d"], 110 ["e", "f"], ["g", "h"]]) 111 112 def test_tags(self): 113 tags = ID3(self.v22) 114 self.failUnless(tags["TRCK"].text == ["3/11"]) 115 self.failUnless(tags["TPE1"].text == ["Anais Mitchell"]) 116 117 def test_load_v1(self): 118 tags = ID3(self.silence_v1) 119 self.assertEquals(tags["TALB"], "Quod Libet Test Data") 120 121 with self.assertRaises(ID3NoHeaderError): 122 tags = ID3(self.silence_v1, load_v1=False) 123 124 def test_load_v1_v2(self): 125 tags = ID3(self.v1v2_combined) 126 # From ID3v2 127 self.assertEquals(tags["TPE1"].text, ["Anais Mitchell"]) 128 # From ID3v1 129 self.assertEquals(tags["TALB"].text, ["Hymns for the Exiled"]) 130 131 tags = ID3(self.v1v2_combined, load_v1=False) 132 self.assertEquals(tags["TPE1"].text, ["Anais Mitchell"]) 133 with self.assertRaises(KeyError): 134 tags["TALB"] 135 136 def test_load_v1_v2_no_translate(self): 137 tags = ID3(self.v1v2_combined, v2_version=4, translate=False) 138 assert tags.version == (2, 4, 0) 139 assert str(tags["TDRC"].text[0]) == "1337" 140 tags = ID3(self.v1v2_combined, v2_version=3, translate=False) 141 assert tags.version == (2, 4, 0) 142 assert str(tags["TDRC"].text[0]) == "1337" 143 144 def test_load_v1_v2_tcon_translate(self): 145 tags = ID3() 146 tags.add(TCON(text=["12"])) 147 v1_data = MakeID3v1(tags) 148 149 filename = get_temp_copy(self.empty) 150 try: 151 tags = ID3() 152 tags.save(filename=filename, v1=0) 153 with open(filename, "ab") as h: 154 h.write(v1_data) 155 tags = ID3(filename, load_v1=True) 156 assert tags["TCON"][0] == "Other" 157 tags = ID3(filename, load_v1=False) 158 assert "TCON" not in tags 159 finally: 160 os.unlink(filename) 161 162 def test_load_v1_v2_precedence(self): 163 tags = ID3(self.v1v2_combined) 164 self.assertEquals(tags["TRCK"].text, ["3/11"]) # i.e. not 123 165 166 # ID3v2 has TYER=2004 (which isn't a valid v2.4 frame), 167 # ID3v1 has TDRC=1337. 168 self.assertEquals(str(tags["TDRC"].text[0]), "1337") 169 with self.assertRaises(KeyError): 170 tags["TYER"] 171 172 tags = ID3(self.v1v2_combined, v2_version=3) 173 174 # With v2_version=3, the ID3v2 tag should still have precedence 175 self.assertEquals(str(tags["TYER"].text[0]), "2004") 176 with self.assertRaises(KeyError): 177 tags["TDRC"] 178 179 def test_load_v1_comment(self): 180 # Tags with different HashKeys but equal FrameIDs (like COMM) 181 # should be kept separate 182 tags = ID3(self.v1v2_combined) 183 comments = tags.getall("COMM") 184 # From ID3v2 185 self.failUnless("Waterbug Records, www.anaismitchell.com" in comments) 186 # From ID3v1 187 self.failUnless("v1 comment" in comments) 188 189 def test_load_v1_known_frames_override(self): 190 class MyCOMM(COMM): 191 @property 192 def FrameID(self): 193 # We want to replace the existing COMM, so override 194 # the FrameID 195 return COMM.__name__ 196 197 frames = dict(id3.Frames) 198 frames["COMM"] = MyCOMM 199 tags = ID3(self.v1v2_combined, known_frames=frames) 200 201 comments = tags.getall("COMM") 202 self.failUnless(len(comments) > 0) 203 for comm in comments: 204 self.assertIsInstance(comm, MyCOMM) 205 206 def test_empty_file(self): 207 self.assertRaises(ID3Error, ID3, filename=self.empty) 208 209 def test_nonexistent_file(self): 210 name = os.path.join(DATA_DIR, 'does', 'not', 'exist') 211 self.assertRaises(MutagenError, ID3, name) 212 213 def test_read_padding(self): 214 self.assertEqual(ID3(self.silence)._padding, 1142) 215 self.assertEqual(ID3(self.unsynch)._padding, 0) 216 217 def test_load_v23_unsynch(self): 218 id3 = ID3(self.unsynch) 219 self.assertEquals(id3["TPE1"], ["Nina Simone"]) 220 221 def test_bad_extended_header_flags(self): 222 # Files with bad extended header flags failed to read tags. 223 # Ensure the extended header is turned off, and the frames are 224 # read. 225 id3 = ID3(os.path.join(DATA_DIR, 'issue_21.id3')) 226 self.failIf(id3.f_extended) 227 self.failUnless("TIT2" in id3) 228 self.failUnless("TALB" in id3) 229 self.failUnlessEqual(id3["TIT2"].text, [u"Punk To Funk"]) 230 231 def test_no_known_frames(self): 232 id3 = ID3(self.silence, known_frames={}) 233 self.assertEquals(0, len(id3.keys())) 234 self.assertEquals(9, len(id3.unknown_frames)) 235 236 def test_unknown_reset(self): 237 id3 = ID3(self.silence, known_frames={}) 238 self.assertEquals(9, len(id3.unknown_frames)) 239 id3.load(self.silence, known_frames={}) 240 self.assertEquals(9, len(id3.unknown_frames)) 241 242 def test_23_multiframe_hack(self): 243 244 # loaded_frame is no longer used in mutagen, but this makes 245 # sure that old code keeps working (used in quod libet <=3.6) 246 class ID3hack(ID3): 247 "Override 'correct' behavior with desired behavior" 248 def loaded_frame(self, tag): 249 if tag.HashKey in self: 250 self[tag.HashKey].extend(tag[:]) 251 else: 252 self[tag.HashKey] = tag 253 254 id3 = ID3hack(self.silence) 255 self.assertEquals(8, len(id3.keys())) 256 self.assertEquals(0, len(id3.unknown_frames)) 257 self.assertEquals('Quod Libet Test Data', id3['TALB']) 258 self.assertEquals('Silence', str(id3['TCON'])) 259 self.assertEquals('Silence', str(id3['TIT1'])) 260 self.assertEquals('Silence', str(id3['TIT2'])) 261 self.assertEquals(3000, +id3['TLEN']) 262 self.assertEquals(['piman', 'jzig'], id3['TPE1']) 263 self.assertEquals('02/10', id3['TRCK']) 264 self.assertEquals(2, +id3['TRCK']) 265 self.assertEquals('2004', id3['TDRC']) 266 267 def test_chap_subframes(self): 268 id3 = ID3() 269 id3.version = (2, 3) 270 id3.add(CHAP(element_id="foo", start_time=0, end_time=0, 271 start_offset=0, end_offset=0, 272 sub_frames=[TYER(encoding=0, text="2006")])) 273 id3.update_to_v24() 274 chap = id3.getall("CHAP:foo")[0] 275 self.assertEqual(chap.sub_frames.getall("TDRC")[0], u"2006") 276 self.assertFalse(chap.sub_frames.getall("TYER")) 277 id3.update_to_v23() 278 self.assertEqual(chap.sub_frames.getall("TYER")[0], u"2006") 279 280 def test_ctoc_subframes(self): 281 id3 = ID3() 282 id3.version = (2, 3) 283 id3.add(CTOC(sub_frames=[TYER(encoding=0, text="2006")])) 284 id3.update_to_v24() 285 ctoc = id3.getall("CTOC")[0] 286 self.assertEqual(ctoc.sub_frames.getall("TDRC")[0], u"2006") 287 self.assertFalse(ctoc.sub_frames.getall("TYER")) 288 id3.update_to_v23() 289 self.assertEqual(ctoc.sub_frames.getall("TYER")[0], u"2006") 290 291 def test_pic(self): 292 id3 = ID3() 293 id3.version = (2, 2) 294 id3.add(PIC(encoding=0, mime="PNG", desc="cover", type=3, data=b"")) 295 id3.update_to_v24() 296 self.failUnlessEqual(id3["APIC:cover"].mime, "image/png") 297 298 def test_lnk(self): 299 id3 = ID3() 300 id3.version = (2, 2) 301 id3.add(LNK(frameid="PIC", url="http://foo.bar")) 302 id3.update_to_v24() 303 self.assertTrue(id3.getall("LINK")) 304 305 def test_tyer(self): 306 id3 = ID3() 307 id3.version = (2, 3) 308 id3.add(TYER(encoding=0, text="2006")) 309 id3.update_to_v24() 310 self.failUnlessEqual(id3["TDRC"], "2006") 311 312 def test_tyer_tdat(self): 313 id3 = ID3() 314 id3.version = (2, 3) 315 id3.add(TYER(encoding=0, text="2006")) 316 id3.add(TDAT(encoding=0, text="0603")) 317 id3.update_to_v24() 318 self.failUnlessEqual(id3["TDRC"], "2006-03-06") 319 320 def test_tyer_tdat_time(self): 321 id3 = ID3() 322 id3.version = (2, 3) 323 id3.add(TYER(encoding=0, text="2006")) 324 id3.add(TDAT(encoding=0, text="0603")) 325 id3.add(TIME(encoding=0, text="1127")) 326 id3.update_to_v24() 327 self.failUnlessEqual(id3["TDRC"], "2006-03-06 11:27:00") 328 329 def test_tory(self): 330 id3 = ID3() 331 id3.version = (2, 3) 332 id3.add(TORY(encoding=0, text="2006")) 333 id3.update_to_v24() 334 self.failUnlessEqual(id3["TDOR"], "2006") 335 336 def test_ipls(self): 337 id3 = ID3() 338 id3.version = (2, 3) 339 id3.add(IPLS(encoding=0, people=[["a", "b"], ["c", "d"]])) 340 id3.update_to_v24() 341 self.failUnlessEqual(id3["TIPL"], [["a", "b"], ["c", "d"]]) 342 343 def test_time_dropped(self): 344 id3 = ID3() 345 id3.version = (2, 3) 346 id3.add(TIME(encoding=0, text=["1155"])) 347 id3.update_to_v24() 348 self.assertFalse(id3.getall("TIME")) 349 350 def test_rvad_dropped(self): 351 id3 = ID3() 352 id3.version = (2, 3) 353 id3.add(RVAD()) 354 id3.update_to_v24() 355 self.assertFalse(id3.getall("RVAD")) 356 357 358class TID3Header(TestCase): 359 360 silence = os.path.join(DATA_DIR, 'silence-44-s.mp3') 361 empty = os.path.join(DATA_DIR, 'emptyfile.mp3') 362 363 def test_header_empty(self): 364 with open(self.empty, 'rb') as fileobj: 365 self.assertRaises(ID3Error, ID3Header, fileobj) 366 367 def test_header_silence(self): 368 with open(self.silence, 'rb') as fileobj: 369 header = ID3Header(fileobj) 370 self.assertEquals(header.version, (2, 3, 0)) 371 self.assertEquals(header.size, 1314) 372 373 def test_header_2_4_invalid_flags(self): 374 fileobj = cBytesIO(b'ID3\x04\x00\x1f\x00\x00\x00\x00') 375 self.assertRaises(ID3Error, ID3Header, fileobj) 376 377 def test_header_2_4_unsynch_size(self): 378 fileobj = cBytesIO(b'ID3\x04\x00\x10\x00\x00\x00\xFF') 379 self.assertRaises(ID3Error, ID3Header, fileobj) 380 381 def test_header_2_4_allow_footer(self): 382 fileobj = cBytesIO(b'ID3\x04\x00\x10\x00\x00\x00\x00') 383 self.assertTrue(ID3Header(fileobj).f_footer) 384 385 def test_header_2_3_invalid_flags(self): 386 fileobj = cBytesIO(b'ID3\x03\x00\x1f\x00\x00\x00\x00') 387 self.assertRaises(ID3Error, ID3Header, fileobj) 388 389 fileobj = cBytesIO(b'ID3\x03\x00\x0f\x00\x00\x00\x00') 390 self.assertRaises(ID3Error, ID3Header, fileobj) 391 392 def test_header_2_2(self): 393 fileobj = cBytesIO(b'ID3\x02\x00\x00\x00\x00\x00\x00') 394 header = ID3Header(fileobj) 395 self.assertEquals(header.version, (2, 2, 0)) 396 397 def test_header_2_1(self): 398 fileobj = cBytesIO(b'ID3\x01\x00\x00\x00\x00\x00\x00') 399 self.assertRaises(ID3UnsupportedVersionError, ID3Header, fileobj) 400 401 def test_header_too_small(self): 402 fileobj = cBytesIO(b'ID3\x01\x00\x00\x00\x00\x00') 403 self.assertRaises(ID3Error, ID3Header, fileobj) 404 405 def test_header_2_4_extended(self): 406 fileobj = cBytesIO( 407 b'ID3\x04\x00\x40\x00\x00\x00\x00\x00\x00\x00\x05\x5a') 408 header = ID3Header(fileobj) 409 self.assertEquals(header._extdata, b'\x5a') 410 411 def test_header_2_4_extended_unsynch_size(self): 412 fileobj = cBytesIO( 413 b'ID3\x04\x00\x40\x00\x00\x00\x00\x00\x00\x00\xFF\x5a') 414 self.assertRaises(ID3Error, ID3Header, fileobj) 415 416 def test_header_2_4_extended_but_not(self): 417 fileobj = cBytesIO( 418 b'ID3\x04\x00\x40\x00\x00\x00\x00TIT1\x00\x00\x00\x01a') 419 header = ID3Header(fileobj) 420 self.assertEquals(header._extdata, b'') 421 422 def test_header_2_4_extended_but_not_but_not_tag(self): 423 fileobj = cBytesIO(b'ID3\x04\x00\x40\x00\x00\x00\x00TIT9') 424 self.failUnlessRaises(ID3Error, ID3Header, fileobj) 425 426 def test_header_2_3_extended(self): 427 fileobj = cBytesIO( 428 b'ID3\x03\x00\x40\x00\x00\x00\x00\x00\x00\x00\x06' 429 b'\x00\x00\x56\x78\x9a\xbc') 430 header = ID3Header(fileobj) 431 self.assertEquals(header._extdata, b'\x00\x00\x56\x78\x9a\xbc') 432 433 def test_23(self): 434 id3 = ID3(self.silence) 435 self.assertEqual(id3.version, (2, 3, 0)) 436 self.assertEquals(8, len(id3.keys())) 437 self.assertEquals(0, len(id3.unknown_frames)) 438 self.assertEquals('Quod Libet Test Data', id3['TALB']) 439 self.assertEquals('Silence', str(id3['TCON'])) 440 self.assertEquals('Silence', str(id3['TIT1'])) 441 self.assertEquals('Silence', str(id3['TIT2'])) 442 self.assertEquals(3000, +id3['TLEN']) 443 self.assertEquals(['piman', 'jzig'], id3['TPE1']) 444 self.assertEquals('02/10', id3['TRCK']) 445 self.assertEquals(2, +id3['TRCK']) 446 self.assertEquals('2004', id3['TDRC']) 447 448 449class TID3Tags(TestCase): 450 451 silence = os.path.join(DATA_DIR, 'silence-44-s.mp3') 452 453 def setUp(self): 454 self.frames = [ 455 TIT2(text=["1"]), TIT2(text=["2"]), 456 TIT2(text=["3"]), TIT2(text=["4"])] 457 self.i = ID3Tags() 458 self.i["BLAH"] = self.frames[0] 459 self.i["QUUX"] = self.frames[1] 460 self.i["FOOB:ar"] = self.frames[2] 461 self.i["FOOB:az"] = self.frames[3] 462 463 def test_apic_duplicate_hash(self): 464 id3 = ID3Tags() 465 for i in xrange(10): 466 apic = APIC(encoding=0, mime=u"b", type=3, desc=u"", data=b"a") 467 id3._add(apic, False) 468 469 self.assertEqual(len(id3), 10) 470 for key, value in id3.items(): 471 self.assertEqual(key, value.HashKey) 472 473 def test_text_duplicate_frame_different_encoding(self): 474 id3 = ID3Tags() 475 frame = TPE2(encoding=Encoding.LATIN1, text=[u"foo"]) 476 id3._add(frame, False) 477 assert id3.getall("TPE2")[0].encoding == Encoding.LATIN1 478 frame = TPE2(encoding=Encoding.LATIN1, text=[u"bar"]) 479 id3._add(frame, False) 480 assert id3.getall("TPE2")[0].encoding == Encoding.LATIN1 481 frame = TPE2(encoding=Encoding.UTF8, text=[u"baz\u0400"]) 482 id3._add(frame, False) 483 assert id3.getall("TPE2")[0].encoding == Encoding.UTF8 484 485 frames = id3.getall("TPE2") 486 assert len(frames) == 1 487 assert len(frames[0].text) == 3 488 489 def test_add_CRM(self): 490 id3 = ID3Tags() 491 self.assertRaises(TypeError, id3.add, CRM()) 492 493 def test_read__ignore_CRM(self): 494 tags = ID3Tags() 495 header = ID3Header() 496 header.version = ID3Header._V22 497 498 framedata = CRM(owner="foo", desc="bar", data=b"bla")._writeData() 499 datasize = BitPaddedInt.to_str(len(framedata), width=3, bits=8) 500 tags._read(header, b"CRM" + datasize + framedata) 501 self.assertEqual(len(tags), 0) 502 503 def test_update_v22_add(self): 504 id3 = ID3Tags() 505 tt1 = TT1(encoding=0, text=u'whatcha staring at?') 506 id3.loaded_frame(tt1) 507 tit1 = id3['TIT1'] 508 509 self.assertEquals(tt1.encoding, tit1.encoding) 510 self.assertEquals(tt1.text, tit1.text) 511 self.assert_('TT1' not in id3) 512 513 def test_getnormal(self): 514 self.assertEquals(self.i.getall("BLAH"), [self.frames[0]]) 515 self.assertEquals(self.i.getall("QUUX"), [self.frames[1]]) 516 self.assertEquals(self.i.getall("FOOB:ar"), [self.frames[2]]) 517 self.assertEquals(self.i.getall("FOOB:az"), [self.frames[3]]) 518 519 def test_getlist(self): 520 self.assertTrue( 521 self.i.getall("FOOB") in [[self.frames[2], self.frames[3]], 522 [self.frames[3], self.frames[2]]]) 523 524 def test_delnormal(self): 525 self.assert_("BLAH" in self.i) 526 self.i.delall("BLAH") 527 self.assert_("BLAH" not in self.i) 528 529 def test_delone(self): 530 self.i.delall("FOOB:ar") 531 self.assertEquals(self.i.getall("FOOB"), [self.frames[3]]) 532 533 def test_delall(self): 534 self.assert_("FOOB:ar" in self.i) 535 self.assert_("FOOB:az" in self.i) 536 self.i.delall("FOOB") 537 self.assert_("FOOB:ar" not in self.i) 538 self.assert_("FOOB:az" not in self.i) 539 540 def test_setone(self): 541 class TEST(TIT2): 542 HashKey = "" 543 544 t = TEST() 545 t.HashKey = "FOOB:ar" 546 self.i.setall("FOOB", [t]) 547 self.assertEquals(self.i["FOOB:ar"], t) 548 self.assertEquals(self.i.getall("FOOB"), [t]) 549 550 def test_settwo(self): 551 class TEST(TIT2): 552 HashKey = "" 553 554 t = TEST() 555 t.HashKey = "FOOB:ar" 556 t2 = TEST() 557 t2.HashKey = "FOOB:az" 558 self.i.setall("FOOB", [t, t2]) 559 self.assertEquals(self.i["FOOB:ar"], t) 560 self.assertEquals(self.i["FOOB:az"], t2) 561 self.assert_(self.i.getall("FOOB") in [[t, t2], [t2, t]]) 562 563 def test_set_wrong_type(self): 564 id3 = ID3Tags() 565 self.assertRaises(TypeError, id3.__setitem__, "FOO", object()) 566 567 568class ID3v1Tags(TestCase): 569 570 def setUp(self): 571 self.filename = os.path.join(DATA_DIR, 'silence-44-s-v1.mp3') 572 self.id3 = ID3(self.filename) 573 574 def test_album(self): 575 self.assertEquals('Quod Libet Test Data', self.id3['TALB']) 576 577 def test_genre(self): 578 self.assertEquals('Darkwave', self.id3['TCON'].genres[0]) 579 580 def test_title(self): 581 self.assertEquals('Silence', str(self.id3['TIT2'])) 582 583 def test_artist(self): 584 self.assertEquals(['piman'], self.id3['TPE1']) 585 586 def test_track(self): 587 self.assertEquals('2', self.id3['TRCK']) 588 self.assertEquals(2, +self.id3['TRCK']) 589 590 def test_year(self): 591 self.assertEquals('2004', self.id3['TDRC']) 592 593 def test_v1_not_v11(self): 594 self.id3["TRCK"] = TRCK(encoding=0, text="32") 595 tag = MakeID3v1(self.id3) 596 self.failUnless(32, ParseID3v1(tag)["TRCK"]) 597 del(self.id3["TRCK"]) 598 tag = MakeID3v1(self.id3) 599 tag = tag[:125] + b' ' + tag[-1:] 600 self.failIf("TRCK" in ParseID3v1(tag)) 601 602 def test_nulls(self): 603 s = u'TAG%(title)30s%(artist)30s%(album)30s%(year)4s%(cmt)29s\x03\x01' 604 s = s % dict(artist=u'abcd\00fg', title=u'hijklmn\x00p', 605 album=u'qrst\x00v', cmt=u'wxyz', year=u'1224') 606 tags = ParseID3v1(s.encode("ascii")) 607 self.assertEquals(b'abcd'.decode('latin1'), tags['TPE1']) 608 self.assertEquals(b'hijklmn'.decode('latin1'), tags['TIT2']) 609 self.assertEquals(b'qrst'.decode('latin1'), tags['TALB']) 610 611 def test_nonascii(self): 612 s = u'TAG%(title)30s%(artist)30s%(album)30s%(year)4s%(cmt)29s\x03\x01' 613 s = s % dict(artist=u'abcd\xe9fg', title=u'hijklmn\xf3p', 614 album=u'qrst\xfcv', cmt=u'wxyz', year=u'1234') 615 tags = ParseID3v1(s.encode("latin-1")) 616 self.assertEquals(b'abcd\xe9fg'.decode('latin1'), tags['TPE1']) 617 self.assertEquals(b'hijklmn\xf3p'.decode('latin1'), tags['TIT2']) 618 self.assertEquals(b'qrst\xfcv'.decode('latin1'), tags['TALB']) 619 self.assertEquals('wxyz', tags['COMM']) 620 self.assertEquals("3", tags['TRCK']) 621 self.assertEquals("1234", tags['TDRC']) 622 623 def test_roundtrip(self): 624 frames = {} 625 for key in ["TIT2", "TALB", "TPE1", "TDRC"]: 626 frames[key] = self.id3[key] 627 self.assertEquals(ParseID3v1(MakeID3v1(frames)), frames) 628 629 def test_make_from_empty(self): 630 empty = b'TAG' + b'\x00' * 124 + b'\xff' 631 self.assertEquals(MakeID3v1({}), empty) 632 self.assertEquals(MakeID3v1({'TCON': TCON()}), empty) 633 self.assertEquals( 634 MakeID3v1({'COMM': COMM(encoding=0, text="")}), empty) 635 636 def test_make_v1_from_tyer(self): 637 self.assertEquals( 638 MakeID3v1({"TDRC": TDRC(text="2010-10-10")}), 639 MakeID3v1({"TYER": TYER(text="2010")})) 640 self.assertEquals( 641 ParseID3v1(MakeID3v1({"TDRC": TDRC(text="2010-10-10")})), 642 ParseID3v1(MakeID3v1({"TYER": TYER(text="2010")}))) 643 644 def test_invalid(self): 645 self.failUnless(ParseID3v1(b"") is None) 646 647 def test_invalid_track(self): 648 tag = {} 649 tag["TRCK"] = TRCK(encoding=0, text="not a number") 650 v1tag = MakeID3v1(tag) 651 self.failIf("TRCK" in ParseID3v1(v1tag)) 652 653 def test_v1_genre(self): 654 tag = {} 655 tag["TCON"] = TCON(encoding=0, text="Pop") 656 v1tag = MakeID3v1(tag) 657 self.failUnlessEqual(ParseID3v1(v1tag)["TCON"].genres, ["Pop"]) 658 659 660class TestWriteID3v1(TestCase): 661 662 def setUp(self): 663 self.filename = get_temp_copy( 664 os.path.join(DATA_DIR, "silence-44-s.mp3")) 665 self.audio = ID3(self.filename) 666 667 def failIfV1(self): 668 with open(self.filename, "rb") as fileobj: 669 fileobj.seek(-128, 2) 670 self.failIf(fileobj.read(3) == b"TAG") 671 672 def failUnlessV1(self): 673 with open(self.filename, "rb") as fileobj: 674 fileobj.seek(-128, 2) 675 self.failUnless(fileobj.read(3) == b"TAG") 676 677 def test_save_delete(self): 678 self.audio.save(v1=0) 679 self.failIfV1() 680 681 def test_save_add(self): 682 self.audio.save(v1=2) 683 self.failUnlessV1() 684 685 def test_save_defaults(self): 686 self.audio.save(v1=0) 687 self.failIfV1() 688 self.audio.save(v1=1) 689 self.failIfV1() 690 self.audio.save(v1=2) 691 self.failUnlessV1() 692 self.audio.save(v1=1) 693 self.failUnlessV1() 694 695 def tearDown(self): 696 os.unlink(self.filename) 697 698 699class Issue97_UpgradeUnknown23(TestCase): 700 701 def setUp(self): 702 self.filename = get_temp_copy( 703 os.path.join(DATA_DIR, "97-unknown-23-update.mp3")) 704 705 def tearDown(self): 706 os.unlink(self.filename) 707 708 def test_unknown(self): 709 orig = ID3(self.filename) 710 self.failUnlessEqual(orig.version, (2, 3, 0)) 711 712 # load a 2.3 file and pretend we don't support TIT2 713 unknown = ID3(self.filename, known_frames={"TPE1": TPE1}, 714 translate=False) 715 # TIT2 ends up in unknown_frames 716 self.failUnlessEqual(unknown.unknown_frames[0][:4], b"TIT2") 717 # save as 2.3 718 unknown.save(v2_version=3) 719 # load again with support for TIT2, all should be there again 720 new = ID3(self.filename) 721 self.failUnlessEqual(new["TIT2"].text, orig["TIT2"].text) 722 self.failUnlessEqual(new["TPE1"].text, orig["TPE1"].text) 723 724 def test_unknown_invalid(self): 725 frame = BinaryFrame(data=b"\xff" * 50) 726 f = ID3(self.filename) 727 self.assertEqual(f.version, ID3Header._V23) 728 config = ID3SaveConfig(3, None) 729 f.unknown_frames = [save_frame(frame, b"NOPE", config)] 730 f.save() 731 f = ID3(self.filename) 732 self.assertFalse(f.unknown_frames) 733 734 735class TID3Write(TestCase): 736 737 def setUp(self): 738 self.filename = get_temp_copy( 739 os.path.join(DATA_DIR, 'silence-44-s.mp3')) 740 741 def tearDown(self): 742 try: 743 os.unlink(self.filename) 744 except OSError: 745 pass 746 747 def test_corrupt_header_too_small(self): 748 with open(self.filename, "r+b") as h: 749 h.truncate(5) 750 self.assertRaises(id3.error, ID3, self.filename) 751 752 def test_corrupt_tag_too_small(self): 753 with open(self.filename, "r+b") as h: 754 h.truncate(50) 755 self.assertRaises(id3.error, ID3, self.filename) 756 757 def test_corrupt_save(self): 758 with open(self.filename, "r+b") as h: 759 h.seek(5, 0) 760 h.write(b"nope") 761 self.assertRaises(id3.error, ID3().save, self.filename) 762 763 def test_padding_fill_all(self): 764 tag = ID3(self.filename) 765 self.assertEqual(tag._padding, 1142) 766 tag.delall("TPE1") 767 # saving should increase the padding not decrease the tag size 768 tag.save() 769 tag = ID3(self.filename) 770 self.assertEqual(tag._padding, 1166) 771 772 def test_padding_remove_add_padding(self): 773 ID3(self.filename).save() 774 775 tag = ID3(self.filename) 776 old_padding = tag._padding 777 old_size = os.path.getsize(self.filename) 778 tag.save(padding=lambda x: 0) 779 self.assertEqual(os.path.getsize(self.filename), 780 old_size - old_padding) 781 old_size = old_size - old_padding 782 tag.save(padding=lambda x: 137) 783 self.assertEqual(os.path.getsize(self.filename), 784 old_size + 137) 785 786 def test_save_id3_over_ape(self): 787 id3.delete(self.filename, delete_v2=False) 788 789 ape_tag = APEv2() 790 ape_tag["oh"] = ["no"] 791 ape_tag.save(self.filename) 792 793 ID3(self.filename).save() 794 self.assertEqual(APEv2(self.filename)["oh"], "no") 795 796 def test_delete_id3_with_ape(self): 797 ID3(self.filename).save(v1=2) 798 799 ape_tag = APEv2() 800 ape_tag["oh"] = ["no"] 801 ape_tag.save(self.filename) 802 803 id3.delete(self.filename, delete_v2=False) 804 self.assertEqual(APEv2(self.filename)["oh"], "no") 805 806 def test_ape_id3_lookalike(self): 807 # mp3 with apev2 tag that parses as id3v1 (at least with ParseID3v1) 808 809 id3.delete(self.filename, delete_v2=False) 810 811 ape_tag = APEv2() 812 ape_tag["oh"] = [ 813 "noooooooooo0000000000000000000000000000000000ooooooooooo"] 814 ape_tag.save(self.filename) 815 816 ID3(self.filename).save() 817 self.assertTrue(APEv2(self.filename)) 818 819 def test_update_to_v23_on_load(self): 820 audio = ID3(self.filename) 821 audio.add(TSOT(text=["Ha"], encoding=3)) 822 audio.save() 823 824 # update_to_v23 called 825 id3 = ID3(self.filename, v2_version=3) 826 self.assertFalse(id3.getall("TSOT")) 827 828 # update_to_v23 not called 829 id3 = ID3(self.filename, v2_version=3, translate=False) 830 self.assertTrue(id3.getall("TSOT")) 831 832 def test_load_save_inval_version(self): 833 audio = ID3(self.filename) 834 self.assertRaises(ValueError, audio.save, v2_version=5) 835 self.assertRaises(ValueError, ID3, self.filename, v2_version=5) 836 837 def test_save(self): 838 audio = ID3(self.filename) 839 strings = ["one", "two", "three"] 840 audio.add(TPE1(text=strings, encoding=3)) 841 audio.save(v2_version=3) 842 843 frame = audio["TPE1"] 844 self.assertEqual(frame.encoding, 3) 845 self.assertEqual(frame.text, strings) 846 847 id3 = ID3(self.filename, translate=False) 848 self.assertEqual(id3.version, (2, 3, 0)) 849 frame = id3["TPE1"] 850 self.assertEqual(frame.encoding, 1) 851 self.assertEqual(frame.text, ["/".join(strings)]) 852 853 # null separator, mutagen can still read it 854 audio.save(v2_version=3, v23_sep=None) 855 856 id3 = ID3(self.filename, translate=False) 857 self.assertEqual(id3.version, (2, 3, 0)) 858 frame = id3["TPE1"] 859 self.assertEqual(frame.encoding, 1) 860 self.assertEqual(frame.text, strings) 861 862 def test_save_off_spec_frames(self): 863 # These are not defined in v2.3 and shouldn't be written. 864 # Still make sure reading them again works and the encoding 865 # is at least changed 866 867 audio = ID3(self.filename) 868 dates = ["2013", "2014"] 869 frame = TDEN(text=dates, encoding=3) 870 audio.add(frame) 871 tipl_frame = TIPL(people=[("a", "b"), ("c", "d")], encoding=2) 872 audio.add(tipl_frame) 873 audio.save(v2_version=3) 874 875 id3 = ID3(self.filename, translate=False) 876 self.assertEqual(id3.version, (2, 3, 0)) 877 878 self.assertEqual([stamp.text for stamp in id3["TDEN"].text], dates) 879 self.assertEqual(id3["TDEN"].encoding, 1) 880 881 self.assertEqual(id3["TIPL"].people, tipl_frame.people) 882 self.assertEqual(id3["TIPL"].encoding, 1) 883 884 def test_wrong_encoding(self): 885 t = ID3(self.filename) 886 t.add(TIT2(encoding=Encoding.LATIN1, text=[u"\u0243"])) 887 self.assertRaises(MutagenError, t.save) 888 889 def test_toemptyfile(self): 890 t = ID3(self.filename) 891 os.unlink(self.filename) 892 open(self.filename, "wb").close() 893 t.save(self.filename) 894 895 def test_tononfile(self): 896 t = ID3(self.filename) 897 os.unlink(self.filename) 898 t.save(self.filename) 899 900 def test_1bfile(self): 901 t = ID3(self.filename) 902 os.unlink(self.filename) 903 with open(self.filename, "wb") as f: 904 f.write(b"!") 905 t.save(self.filename) 906 self.assert_(os.path.getsize(self.filename) > 1) 907 with open(self.filename, "rb") as h: 908 self.assertEquals(h.read()[-1], b"!"[0]) 909 910 def test_unknown_chap(self): 911 # add ctoc 912 id3 = ID3(self.filename) 913 id3.add(CTOC(element_id="foo", flags=3, child_element_ids=["ch0"], 914 sub_frames=[TIT2(encoding=3, text=["bla"])])) 915 id3.save() 916 917 # pretend we don't know ctoc and save 918 id3 = ID3(self.filename, known_frames={"CTOC": CTOC}) 919 ctoc = id3.getall("CTOC")[0] 920 self.assertFalse(ctoc.sub_frames) 921 self.assertTrue(ctoc.sub_frames.unknown_frames) 922 id3.save() 923 924 # make sure we wrote all sub frames back 925 id3 = ID3(self.filename) 926 self.assertEqual( 927 id3.getall("CTOC")[0].sub_frames.getall("TIT2")[0].text, ["bla"]) 928 929 def test_same(self): 930 ID3(self.filename).save() 931 id3 = ID3(self.filename) 932 self.assertEquals(id3["TALB"], "Quod Libet Test Data") 933 self.assertEquals(id3["TCON"], "Silence") 934 self.assertEquals(id3["TIT2"], "Silence") 935 self.assertEquals(id3["TPE1"], ["piman", "jzig"]) 936 937 def test_same_v23(self): 938 id3 = ID3(self.filename, v2_version=3) 939 id3.save(v2_version=3) 940 id3 = ID3(self.filename) 941 self.assertEqual(id3.version, (2, 3, 0)) 942 self.assertEquals(id3["TALB"], "Quod Libet Test Data") 943 self.assertEquals(id3["TCON"], "Silence") 944 self.assertEquals(id3["TIT2"], "Silence") 945 self.assertEquals(id3["TPE1"], "piman/jzig") 946 947 def test_addframe(self): 948 f = ID3(self.filename) 949 self.assert_("TIT3" not in f) 950 f["TIT3"] = TIT3(encoding=0, text="A subtitle!") 951 f.save() 952 id3 = ID3(self.filename) 953 self.assertEquals(id3["TIT3"], "A subtitle!") 954 955 def test_changeframe(self): 956 f = ID3(self.filename) 957 self.assertEquals(f["TIT2"], "Silence") 958 f["TIT2"].text = [u"The sound of silence."] 959 f.save() 960 id3 = ID3(self.filename) 961 self.assertEquals(id3["TIT2"], "The sound of silence.") 962 963 def test_replaceframe(self): 964 f = ID3(self.filename) 965 self.assertEquals(f["TPE1"], [u'piman', u'jzig']) 966 f["TPE1"] = TPE1(encoding=0, text=u"jzig\x00piman") 967 f.save() 968 id3 = ID3(self.filename) 969 self.assertEquals(id3["TPE1"], ["jzig", "piman"]) 970 971 def test_compressibly_large(self): 972 f = ID3(self.filename) 973 self.assert_("TPE2" not in f) 974 f["TPE2"] = TPE2(encoding=0, text="Ab" * 1025) 975 f.save() 976 id3 = ID3(self.filename) 977 self.assertEquals(id3["TPE2"], "Ab" * 1025) 978 979 def test_nofile_silencetag(self): 980 id3 = ID3(self.filename) 981 os.unlink(self.filename) 982 id3.save(self.filename) 983 with open(self.filename, 'rb') as h: 984 self.assertEquals(b'ID3', h.read(3)) 985 self.test_same() 986 987 def test_emptyfile_silencetag(self): 988 id3 = ID3(self.filename) 989 with open(self.filename, 'wb') as h: 990 h.truncate() 991 id3.save(self.filename) 992 with open(self.filename, 'rb') as h: 993 self.assertEquals(b'ID3', h.read(3)) 994 self.test_same() 995 996 def test_empty_plustag_minustag_empty(self): 997 id3 = ID3(self.filename) 998 with open(self.filename, 'wb') as h: 999 h.truncate() 1000 id3.save() 1001 id3.delete() 1002 self.failIf(id3) 1003 with open(self.filename, 'rb') as h: 1004 self.assertEquals(h.read(10), b'') 1005 1006 def test_delete_invalid_zero(self): 1007 with open(self.filename, 'wb') as f: 1008 f.write(b'ID3\x04\x00\x00\x00\x00\x00\x00abc') 1009 ID3(self.filename).delete() 1010 with open(self.filename, 'rb') as f: 1011 self.assertEquals(f.read(10), b'abc') 1012 1013 def test_frame_order(self): 1014 f = ID3(self.filename) 1015 f["TIT2"] = TIT2(encoding=0, text="A title!") 1016 f["APIC"] = APIC(encoding=0, mime="b", type=3, desc='', data=b"a") 1017 f["TALB"] = TALB(encoding=0, text="c") 1018 f["COMM"] = COMM(encoding=0, desc="x", text="y") 1019 f.save() 1020 with open(self.filename, 'rb') as h: 1021 data = h.read() 1022 self.assert_(data.find(b"TIT2") < data.find(b"APIC")) 1023 self.assert_(data.find(b"TIT2") < data.find(b"COMM")) 1024 self.assert_(data.find(b"TALB") < data.find(b"APIC")) 1025 self.assert_(data.find(b"TALB") < data.find(b"COMM")) 1026 self.assert_(data.find(b"TIT2") < data.find(b"TALB")) 1027 1028 def test_apic_last(self): 1029 # https://github.com/quodlibet/mutagen/issues/278 1030 f = ID3(self.filename) 1031 f.add(TYER(text=[u"2016"])) 1032 f.add(APIC(data=b"x" * 500)) 1033 f.save() 1034 with open(self.filename, 'rb') as h: 1035 data = h.read() 1036 assert data.find(b"TYER") < data.find(b"APIC") 1037 1038 1039class WriteForEyeD3(TestCase): 1040 1041 def setUp(self): 1042 self.silence = os.path.join(DATA_DIR, 'silence-44-s.mp3') 1043 self.newsilence = get_temp_copy(self.silence) 1044 1045 # remove ID3v1 tag 1046 with open(self.newsilence, "rb+") as f: 1047 f.seek(-128, 2) 1048 f.truncate() 1049 1050 def tearDown(self): 1051 os.unlink(self.newsilence) 1052 1053 def test_same(self): 1054 ID3(self.newsilence).save() 1055 id3 = eyeD3.tag.Tag(eyeD3.ID3_V2_4) 1056 id3.link(self.newsilence) 1057 1058 self.assertEquals(id3.frames["TALB"][0].text, "Quod Libet Test Data") 1059 self.assertEquals(id3.frames["TCON"][0].text, "Silence") 1060 self.assertEquals(id3.frames["TIT2"][0].text, "Silence") 1061 self.assertEquals(len(id3.frames["TPE1"]), 1) 1062 self.assertEquals(id3.frames["TPE1"][0].text, "piman/jzig") 1063 1064 def test_addframe(self): 1065 f = ID3(self.newsilence) 1066 self.assert_("TIT3" not in f) 1067 f["TIT3"] = TIT3(encoding=0, text="A subtitle!") 1068 f.save() 1069 id3 = eyeD3.tag.Tag(eyeD3.ID3_V2_4) 1070 id3.link(self.newsilence) 1071 self.assertEquals(id3.frames["TIT3"][0].text, "A subtitle!") 1072 1073 def test_changeframe(self): 1074 f = ID3(self.newsilence) 1075 self.assertEquals(f["TIT2"], "Silence") 1076 f["TIT2"].text = [u"The sound of silence."] 1077 f.save() 1078 id3 = eyeD3.tag.Tag(eyeD3.ID3_V2_4) 1079 id3.link(self.newsilence) 1080 self.assertEquals(id3.frames["TIT2"][0].text, "The sound of silence.") 1081 1082 1083class BadPOPM(TestCase): 1084 1085 def setUp(self): 1086 self.filename = get_temp_copy( 1087 os.path.join(DATA_DIR, 'bad-POPM-frame.mp3')) 1088 1089 def tearDown(self): 1090 os.unlink(self.filename) 1091 1092 def test_read_popm_long_counter(self): 1093 f = ID3(self.filename) 1094 self.failUnless("POPM:Windows Media Player 9 Series" in f) 1095 popm = f["POPM:Windows Media Player 9 Series"] 1096 self.assertEquals(popm.rating, 255) 1097 self.assertEquals(popm.count, 2709193061) 1098 1099 def test_write_popm_long_counter(self): 1100 f = ID3(self.filename) 1101 f.add(POPM(email="foo@example.com", rating=125, count=2 ** 32 + 1)) 1102 f.save() 1103 f = ID3(self.filename) 1104 self.failUnless("POPM:foo@example.com" in f) 1105 self.failUnless("POPM:Windows Media Player 9 Series" in f) 1106 popm = f["POPM:foo@example.com"] 1107 self.assertEquals(popm.rating, 125) 1108 self.assertEquals(popm.count, 2 ** 32 + 1) 1109 1110 1111class Issue69_BadV1Year(TestCase): 1112 1113 def test_missing_year(self): 1114 tag = ParseID3v1( 1115 b'ABCTAGhello world\x00\x00\x00\x00\x00\x00\x00\x00' 1116 b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' 1117 b'x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' 1118 b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' 1119 b'x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' 1120 b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' 1121 b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' 1122 b'\x00\x00\x00\x00\x00\xff' 1123 ) 1124 self.failUnlessEqual(tag["TIT2"], "hello world") 1125 1126 def test_short_year(self): 1127 data = ( 1128 b'XTAGhello world\x00\x00\x00\x00\x00\x00\x00\x00' 1129 b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' 1130 b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' 1131 b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' 1132 b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' 1133 b'\x00\x00\x00\x00\x00\x00\x001\x00\x00\x00\x00\x00\x00\x00\x00' 1134 b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' 1135 b'\x00\x00\x00\x00\x00\x00\xff' 1136 ) 1137 tag = ParseID3v1(data) 1138 self.failUnlessEqual(tag["TIT2"], "hello world") 1139 self.failUnlessEqual(tag["TDRC"], "0001") 1140 1141 frames, offset = find_id3v1(cBytesIO(data)) 1142 self.assertEqual(offset, -125) 1143 self.assertEqual(frames, tag) 1144 1145 def test_none(self): 1146 s = MakeID3v1(dict()) 1147 self.failUnlessEqual(len(s), 128) 1148 tag = ParseID3v1(s) 1149 self.failIf("TDRC" in tag) 1150 1151 def test_empty(self): 1152 s = MakeID3v1(dict(TDRC="")) 1153 self.failUnlessEqual(len(s), 128) 1154 tag = ParseID3v1(s) 1155 self.failIf("TDRC" in tag) 1156 1157 def test_short(self): 1158 s = MakeID3v1(dict(TDRC="1")) 1159 self.failUnlessEqual(len(s), 128) 1160 tag = ParseID3v1(s) 1161 self.failUnlessEqual(tag["TDRC"], "0001") 1162 1163 def test_long(self): 1164 s = MakeID3v1(dict(TDRC="123456789")) 1165 self.failUnlessEqual(len(s), 128) 1166 tag = ParseID3v1(s) 1167 self.failUnlessEqual(tag["TDRC"], "1234") 1168 1169 1170class TID3Trailing(TestCase): 1171 1172 def test_audacious_trailing_id3(self): 1173 # https://github.com/quodlibet/mutagen/issues/78 1174 # tagged with audacious 3.2.4, both are id3v2 at the end despite the 1175 # spec saying it should be before other tags. 1176 # Audacious changed it to write in the beginning with 3.4 or 3.5 1177 # Now with Audacious 3.7, re-saving the files results in the id3v3 1178 # tag moved to the front and the id3v1/apev2 tags left as is at the 1179 # end. 1180 path = os.path.join(DATA_DIR, 'audacious-trailing-id32-id31.mp3') 1181 self.assertRaises(ID3NoHeaderError, ID3, path) 1182 path = os.path.join(DATA_DIR, 'audacious-trailing-id32-apev2.mp3') 1183 self.assertRaises(ID3NoHeaderError, ID3, path) 1184 1185 1186class TID3Misc(TestCase): 1187 1188 def test_main(self): 1189 self.assertEqual(id3.Encoding.UTF8, 3) 1190 self.assertEqual(id3.ID3v1SaveOptions.UPDATE, 1) 1191 self.assertEqual(id3.PictureType.COVER_FRONT, 3) 1192 1193 def test_determine_bpi(self): 1194 # default to BitPaddedInt 1195 self.assertTrue(determine_bpi("", {}) is BitPaddedInt) 1196 1197 def get_frame_data(name, size, bpi=True): 1198 data = name 1199 if bpi: 1200 data += BitPaddedInt.to_str(size) 1201 else: 1202 data += BitPaddedInt.to_str(size, bits=8) 1203 data += b"\x00\x00" + b"\x01" * size 1204 return data 1205 1206 data = get_frame_data(b"TPE2", 1000, True) 1207 self.assertTrue(determine_bpi(data, Frames) is BitPaddedInt) 1208 self.assertTrue( 1209 determine_bpi(data + b"\x00" * 1000, Frames) is BitPaddedInt) 1210 1211 data = get_frame_data(b"TPE2", 1000, False) 1212 self.assertTrue(determine_bpi(data, Frames) is int) 1213 self.assertTrue(determine_bpi(data + b"\x00" * 1000, Frames) is int) 1214 1215 # in this case it helps that we know the frame name 1216 d = get_frame_data(b"TPE2", 1000) + get_frame_data(b"TPE2", 10) + \ 1217 b"\x01" * 875 1218 self.assertTrue(determine_bpi(d, Frames) is BitPaddedInt) 1219 1220 1221try: 1222 import eyeD3 1223except ImportError: 1224 print("WARNING: Skipping eyeD3 tests.") 1225 del WriteForEyeD3 1226