1# -*- coding: utf-8 -*- 2 3import os 4 5from tests import TestCase, DATA_DIR, get_temp_copy 6from mutagen._compat import cBytesIO, text_type, xrange 7from mutagen.mp3 import MP3, error as MP3Error, delete, MPEGInfo, EasyMP3, \ 8 BitrateMode, iter_sync 9from mutagen.mp3._util import XingHeader, XingHeaderError, VBRIHeader, \ 10 VBRIHeaderError, LAMEHeader, LAMEError 11from mutagen.id3 import ID3 12 13 14class TMP3Util(TestCase): 15 16 def test_find_sync(self): 17 18 def get_syncs(fileobj, max_read): 19 start = fileobj.tell() 20 pos = [] 21 for i in iter_sync(fileobj, max_read): 22 pos.append(fileobj.tell() - start) 23 return pos 24 25 self.assertEqual(get_syncs(cBytesIO(b"abc"), 100), []) 26 self.assertEqual(get_syncs(cBytesIO(b""), 100), []) 27 self.assertEqual(get_syncs(cBytesIO(b"a\xff\xe0"), 1), []) 28 29 self.assertEqual(get_syncs(cBytesIO(b"a\xff\xc0\xff\xe0"), 100), [3]) 30 self.assertEqual( 31 get_syncs(cBytesIO(b"a\xff\xe0\xff\xe0\xff\xe0"), 100), [1, 3, 5]) 32 33 for i in xrange(400): 34 fileobj = cBytesIO(b"\x00" * i + b"\xff\xe0") 35 self.assertEqual(get_syncs(fileobj, 100 + i), [i]) 36 37 38class TMP3(TestCase): 39 silence = os.path.join(DATA_DIR, 'silence-44-s.mp3') 40 silence_nov2 = os.path.join(DATA_DIR, 'silence-44-s-v1.mp3') 41 silence_mpeg2 = os.path.join(DATA_DIR, 'silence-44-s-mpeg2.mp3') 42 silence_mpeg25 = os.path.join(DATA_DIR, 'silence-44-s-mpeg25.mp3') 43 lame = os.path.join(DATA_DIR, 'lame.mp3') 44 lame_peak = os.path.join(DATA_DIR, 'lame-peak.mp3') 45 lame_broken_short = os.path.join(DATA_DIR, 'lame397v9short.mp3') 46 47 def setUp(self): 48 self.filename = get_temp_copy( 49 os.path.join(DATA_DIR, "silence-44-s.mp3")) 50 51 self.mp3 = MP3(self.filename) 52 self.mp3_2 = MP3(self.silence_nov2) 53 self.mp3_3 = MP3(self.silence_mpeg2) 54 self.mp3_4 = MP3(self.silence_mpeg25) 55 self.mp3_lame = MP3(self.lame) 56 self.mp3_lame_peak = MP3(self.lame_peak) 57 58 def test_lame_broken_short(self): 59 # lame <=3.97 wrote broken files 60 f = MP3(self.lame_broken_short) 61 assert f.info.encoder_info == "LAME 3.97.0" 62 assert f.info.encoder_settings == "-V 9" 63 assert f.info.length == 0.0 64 assert f.info.bitrate == 40000 65 assert f.info.bitrate_mode == 2 66 assert f.info.sample_rate == 24000 67 68 def test_mode(self): 69 from mutagen.mp3 import JOINTSTEREO 70 self.failUnlessEqual(self.mp3.info.mode, JOINTSTEREO) 71 self.failUnlessEqual(self.mp3_2.info.mode, JOINTSTEREO) 72 self.failUnlessEqual(self.mp3_3.info.mode, JOINTSTEREO) 73 self.failUnlessEqual(self.mp3_4.info.mode, JOINTSTEREO) 74 75 def test_replaygain(self): 76 self.assertEqual(self.mp3_3.info.track_gain, 51.0) 77 self.assertEqual(self.mp3_4.info.track_gain, 51.0) 78 self.assertEqual(self.mp3_lame.info.track_gain, 6.0) 79 self.assertAlmostEqual(self.mp3_lame_peak.info.track_gain, 6.8, 1) 80 self.assertAlmostEqual(self.mp3_lame_peak.info.track_peak, 0.21856, 4) 81 82 self.assertTrue(self.mp3.info.track_gain is None) 83 self.assertTrue(self.mp3.info.track_peak is None) 84 self.assertTrue(self.mp3.info.album_gain is None) 85 86 def test_channels(self): 87 self.assertEqual(self.mp3.info.channels, 2) 88 self.assertEqual(self.mp3_2.info.channels, 2) 89 self.assertEqual(self.mp3_3.info.channels, 2) 90 self.assertEqual(self.mp3_4.info.channels, 2) 91 92 def test_encoder_info(self): 93 self.assertEqual(self.mp3.info.encoder_info, u"") 94 self.assertTrue(isinstance(self.mp3.info.encoder_info, text_type)) 95 self.assertEqual(self.mp3_2.info.encoder_info, u"") 96 self.assertEqual(self.mp3_3.info.encoder_info, u"LAME 3.98.1+") 97 self.assertEqual(self.mp3_4.info.encoder_info, u"LAME 3.98.1+") 98 self.assertTrue(isinstance(self.mp3_4.info.encoder_info, text_type)) 99 100 def test_bitrate_mode(self): 101 self.failUnlessEqual(self.mp3.info.bitrate_mode, BitrateMode.UNKNOWN) 102 self.failUnlessEqual(self.mp3_2.info.bitrate_mode, BitrateMode.UNKNOWN) 103 self.failUnlessEqual(self.mp3_3.info.bitrate_mode, BitrateMode.VBR) 104 self.failUnlessEqual(self.mp3_4.info.bitrate_mode, BitrateMode.VBR) 105 106 def test_id3(self): 107 self.failUnlessEqual(self.mp3.tags, ID3(self.silence)) 108 self.failUnlessEqual(self.mp3_2.tags, ID3(self.silence_nov2)) 109 110 def test_length(self): 111 self.assertAlmostEqual(self.mp3.info.length, 3.77, 2) 112 self.assertAlmostEqual(self.mp3_2.info.length, 3.77, 2) 113 self.assertAlmostEqual(self.mp3_3.info.length, 3.68475, 4) 114 self.assertAlmostEqual(self.mp3_4.info.length, 3.68475, 4) 115 116 def test_version(self): 117 self.failUnlessEqual(self.mp3.info.version, 1) 118 self.failUnlessEqual(self.mp3_2.info.version, 1) 119 self.failUnlessEqual(self.mp3_3.info.version, 2) 120 self.failUnlessEqual(self.mp3_4.info.version, 2.5) 121 122 def test_layer(self): 123 self.failUnlessEqual(self.mp3.info.layer, 3) 124 self.failUnlessEqual(self.mp3_2.info.layer, 3) 125 self.failUnlessEqual(self.mp3_3.info.layer, 3) 126 self.failUnlessEqual(self.mp3_4.info.layer, 3) 127 128 def test_bitrate(self): 129 self.failUnlessEqual(self.mp3.info.bitrate, 32000) 130 self.failUnlessEqual(self.mp3_2.info.bitrate, 32000) 131 self.failUnlessEqual(self.mp3_3.info.bitrate, 17783) 132 self.failUnlessEqual(self.mp3_4.info.bitrate, 8900) 133 134 def test_notmp3(self): 135 self.failUnlessRaises( 136 MP3Error, MP3, os.path.join(DATA_DIR, 'empty.ofr')) 137 138 self.failUnlessRaises( 139 MP3Error, MP3, os.path.join(DATA_DIR, 'emptyfile.mp3')) 140 141 def test_too_short(self): 142 self.failUnlessRaises( 143 MP3Error, MP3, os.path.join(DATA_DIR, 'too-short.mp3')) 144 145 def test_sketchy(self): 146 self.failIf(self.mp3.info.sketchy) 147 self.failIf(self.mp3_2.info.sketchy) 148 self.failIf(self.mp3_3.info.sketchy) 149 self.failIf(self.mp3_4.info.sketchy) 150 151 def test_sketchy_notmp3(self): 152 notmp3 = MP3(os.path.join(DATA_DIR, "silence-44-s.flac")) 153 self.failUnless(notmp3.info.sketchy) 154 self.assertTrue(u"sketchy" in notmp3.info.pprint()) 155 156 def test_pprint(self): 157 self.failUnless(self.mp3.pprint()) 158 159 def test_info_pprint(self): 160 res = self.mp3.info.pprint() 161 self.assertTrue(res) 162 self.assertTrue(isinstance(res, text_type)) 163 self.assertTrue(res.startswith(u"MPEG 1 layer 3")) 164 165 def test_pprint_no_tags(self): 166 self.mp3.tags = None 167 self.failUnless(self.mp3.pprint()) 168 169 def test_xing(self): 170 mp3 = MP3(os.path.join(DATA_DIR, "xing.mp3")) 171 self.assertAlmostEqual(mp3.info.length, 2.052, 3) 172 self.assertEqual(mp3.info.bitrate, 32000) 173 174 def test_vbri(self): 175 mp3 = MP3(os.path.join(DATA_DIR, "vbri.mp3")) 176 self.assertAlmostEqual(mp3.info.length, 222.19755, 3) 177 self.assertEqual(mp3.info.bitrate, 233260) 178 179 def test_empty_xing(self): 180 mp3 = MP3(os.path.join(DATA_DIR, "bad-xing.mp3")) 181 self.assertEqual(mp3.info.length, 0) 182 self.assertEqual(mp3.info.bitrate, 48000) 183 184 def test_delete(self): 185 self.mp3.delete() 186 self.failIf(self.mp3.tags) 187 self.failUnless(MP3(self.filename).tags is None) 188 189 def test_module_delete(self): 190 delete(self.filename) 191 self.failUnless(MP3(self.filename).tags is None) 192 193 def test_save(self): 194 self.mp3["TIT1"].text = ["foobar"] 195 self.mp3.save() 196 self.failUnless(MP3(self.filename)["TIT1"] == "foobar") 197 198 def test_save_padding(self): 199 self.mp3.save(padding=lambda x: 42) 200 self.assertEqual(MP3(self.filename).tags._padding, 42) 201 202 def test_load_non_id3(self): 203 filename = os.path.join(DATA_DIR, "apev2-lyricsv2.mp3") 204 from mutagen.apev2 import APEv2 205 mp3 = MP3(filename, ID3=APEv2) 206 self.failUnless("replaygain_track_peak" in mp3.tags) 207 208 def test_add_tags(self): 209 mp3 = MP3(os.path.join(DATA_DIR, "xing.mp3")) 210 self.failIf(mp3.tags) 211 mp3.add_tags() 212 self.failUnless(isinstance(mp3.tags, ID3)) 213 214 def test_add_tags_already_there(self): 215 mp3 = MP3(os.path.join(DATA_DIR, "silence-44-s.mp3")) 216 self.failUnless(mp3.tags) 217 self.failUnlessRaises(Exception, mp3.add_tags) 218 219 def test_save_no_tags(self): 220 self.mp3.tags = None 221 self.mp3.save() 222 self.assertTrue(self.mp3.tags is None) 223 224 def test_mime(self): 225 self.failUnless("audio/mp3" in self.mp3.mime) 226 # XXX 227 self.mp3.info.layer = 2 228 self.failIf("audio/mp3" in self.mp3.mime) 229 self.failUnless("audio/mp2" in self.mp3.mime) 230 231 def tearDown(self): 232 os.unlink(self.filename) 233 234 235class TMPEGInfo(TestCase): 236 237 def test_not_real_file(self): 238 filename = os.path.join(DATA_DIR, "silence-44-s-v1.mp3") 239 with open(filename, "rb") as h: 240 fileobj = cBytesIO(h.read(20)) 241 self.failUnlessRaises(MP3Error, MPEGInfo, fileobj) 242 243 def test_empty(self): 244 fileobj = cBytesIO(b"") 245 self.failUnlessRaises(MP3Error, MPEGInfo, fileobj) 246 247 def test_xing_unknown_framecount(self): 248 frame = ( 249 b'\xff\xfb\xe4\x0c\x00\x0f\xf0\x00\x00\x00\x00\x00\x00\x00\x00\x00' 250 b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' 251 b'\x00\x00\x00\x00Info\x00\x00\x00\x02\x00\xb4V@\x00\xb4R\x80\x00' 252 b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' 253 ) 254 fileobj = cBytesIO(frame) 255 info = MPEGInfo(fileobj) 256 assert info.bitrate == 320000 257 assert info.length > 0 258 259 260class TEasyMP3(TestCase): 261 262 def setUp(self): 263 self.filename = get_temp_copy( 264 os.path.join(DATA_DIR, "silence-44-s.mp3")) 265 self.mp3 = EasyMP3(self.filename) 266 267 def test_artist(self): 268 self.failUnless("artist" in self.mp3) 269 270 def test_no_composer(self): 271 self.failIf("composer" in self.mp3) 272 273 def test_length(self): 274 # https://github.com/quodlibet/mutagen/issues/125 275 # easyid3, normal id3 and mpeg loading without tags should skip 276 # the tags and get the right offset of the first frame 277 easy = self.mp3.info 278 noneasy = MP3(self.filename).info 279 with open(self.filename, "rb") as h: 280 nonid3 = MPEGInfo(h) 281 282 self.failUnlessEqual(easy.length, noneasy.length) 283 self.failUnlessEqual(noneasy.length, nonid3.length) 284 285 def tearDown(self): 286 os.unlink(self.filename) 287 288 289class TXingHeader(TestCase): 290 291 def test_valid_info_header(self): 292 data = (b'Info\x00\x00\x00\x0f\x00\x00:>\x00\xed\xbd8\x00\x03\x05\x07' 293 b'\n\r\x0f\x12\x14\x17\x1a\x1c\x1e"$&)+.1359;=@CEGJLORTVZ\\^ac' 294 b'fikmqsux{}\x80\x82\x84\x87\x8a\x8c\x8e\x92\x94\x96\x99\x9c' 295 b'\x9e\xa1\xa3\xa5\xa9\xab\xad\xb0\xb3\xb5\xb8\xba\xbd\xc0\xc2' 296 b'\xc4\xc6\xca\xcc\xce\xd1\xd4\xd6\xd9\xdb\xdd\xe1\xe3\xe5\xe8' 297 b'\xeb\xed\xf0\xf2\xf5\xf8\xfa\xfc\x00\x00\x009') 298 299 fileobj = cBytesIO(data) 300 xing = XingHeader(fileobj) 301 self.assertEqual(xing.bytes, 15580472) 302 self.assertEqual(xing.frames, 14910) 303 self.assertEqual(xing.vbr_scale, 57) 304 self.assertTrue(xing.toc) 305 self.assertEqual(len(xing.toc), 100) 306 self.assertEqual(sum(xing.toc), 12625) # only for coverage.. 307 self.assertEqual(xing.is_info, True) 308 309 XingHeader(cBytesIO(data.replace(b'Info', b'Xing'))) 310 311 def test_invalid(self): 312 self.assertRaises(XingHeaderError, XingHeader, cBytesIO(b"")) 313 self.assertRaises(XingHeaderError, XingHeader, cBytesIO(b"Xing")) 314 self.assertRaises(XingHeaderError, XingHeader, cBytesIO(b"aaaa")) 315 316 def test_get_offset(self): 317 mp3 = MP3(os.path.join(DATA_DIR, "silence-44-s.mp3")) 318 self.assertEqual(XingHeader.get_offset(mp3.info), 36) 319 320 321class TVBRIHeader(TestCase): 322 323 def test_valid(self): 324 # parts of the trailing toc zeroed... 325 data = (b'VBRI\x00\x01\t1\x00d\x00\x0c\xb05\x00\x00\x049\x00\x87\x00' 326 b'\x01\x00\x02\x00\x08\n0\x19H\x18\xe0\x18x\x18\xe0\x18x\x19H' 327 b'\x18\xe0\x19H\x18\xe0\x18\xe0\x18x' + b'\x00' * 300) 328 329 fileobj = cBytesIO(data) 330 vbri = VBRIHeader(fileobj) 331 self.assertEqual(vbri.bytes, 831541) 332 self.assertEqual(vbri.frames, 1081) 333 self.assertEqual(vbri.quality, 100) 334 self.assertEqual(vbri.version, 1) 335 self.assertEqual(vbri.toc_frames, 8) 336 self.assertTrue(vbri.toc) 337 self.assertEqual(len(vbri.toc), 135) 338 self.assertEqual(sum(vbri.toc), 72656) 339 340 def test_invalid(self): 341 self.assertRaises(VBRIHeaderError, VBRIHeader, cBytesIO(b"")) 342 self.assertRaises(VBRIHeaderError, VBRIHeader, cBytesIO(b"VBRI")) 343 self.assertRaises(VBRIHeaderError, VBRIHeader, cBytesIO(b"Xing")) 344 345 def test_get_offset(self): 346 mp3 = MP3(os.path.join(DATA_DIR, "silence-44-s.mp3")) 347 self.assertEqual(VBRIHeader.get_offset(mp3.info), 36) 348 349 350class TLAMEHeader(TestCase): 351 352 def test_version(self): 353 354 def parse(data): 355 data = cBytesIO(data + b"\x00" * (20 - len(data))) 356 return tuple(LAMEHeader.parse_version(data)[1:]) 357 358 self.assertEqual(parse(b"LAME3.80"), (u"3.80", False)) 359 self.assertEqual(parse(b"LAME3.80 "), (u"3.80", False)) 360 self.assertEqual(parse(b"LAME3.88 (beta)"), (u"3.88 (beta)", False)) 361 self.assertEqual(parse(b"LAME3.90 (alpha)"), (u"3.90 (alpha)", False)) 362 self.assertEqual(parse(b"LAME3.90 "), (u"3.90.0+", True)) 363 self.assertEqual(parse(b"LAME3.96a"), (u"3.96 (alpha)", True)) 364 self.assertEqual(parse(b"LAME3.96b"), (u"3.96 (beta)", True)) 365 self.assertEqual(parse(b"LAME3.96x"), (u"3.96 (?)", True)) 366 self.assertEqual(parse(b"LAME3.98 "), (u"3.98.0", True)) 367 self.assertEqual(parse(b"LAME3.96r"), (u"3.96.1+", True)) 368 self.assertEqual(parse(b"L3.99r"), (u"3.99.1+", True)) 369 self.assertEqual(parse(b"LAME3100r"), (u"3.100.1+", True)) 370 self.assertEqual(parse(b"LAME3.90.\x03\xbe\x00"), (u"3.90.0+", True)) 371 self.assertEqual(parse(b"LAME3.100"), (u"3.100.0+", True)) 372 373 def test_invalid(self): 374 375 def parse(data): 376 data = cBytesIO(data + b"\x00" * (20 - len(data))) 377 return LAMEHeader.parse_version(data) 378 379 self.assertRaises(LAMEError, parse, b"") 380 self.assertRaises(LAMEError, parse, b"LAME") 381 self.assertRaises(LAMEError, parse, b"LAME3.9999") 382 383 def test_real(self): 384 with open(os.path.join(DATA_DIR, "lame.mp3"), "rb") as h: 385 h.seek(36, 0) 386 xing = XingHeader(h) 387 self.assertEqual(xing.lame_version_desc, u"3.99.1+") 388 self.assertTrue(xing.lame_header) 389 self.assertEqual(xing.lame_header.track_gain_adjustment, 6.0) 390 assert xing.get_encoder_settings() == u"-V 2" 391 392 def test_settings(self): 393 with open(os.path.join(DATA_DIR, "lame.mp3"), "rb") as h: 394 h.seek(36, 0) 395 xing = XingHeader(h) 396 header = xing.lame_header 397 398 def s(major, minor, **kwargs): 399 old = vars(header) 400 for key, value in kwargs.items(): 401 assert hasattr(header, key) 402 setattr(header, key, value) 403 r = header.guess_settings(major, minor) 404 header.__dict__.update(old) 405 return r 406 407 assert s(3, 99) == "-V 2" 408 assert s(3, 98) == "-V 2" 409 assert s(3, 97) == "-V 2 --vbr-new" 410 assert s(3, 96) == "-V 2 --vbr-new" 411 assert s(3, 95) == "-V 2 --vbr-new" 412 assert s(3, 94) == "-V 2 --vbr-new" 413 assert s(3, 93) == "-V 2 --vbr-new" 414 assert s(3, 92) == "-V 2 --vbr-new" 415 assert s(3, 91) == "-V 2 --vbr-new" 416 assert s(3, 90) == "-V 2 --vbr-new" 417 assert s(3, 89) == "" 418 419 assert s(3, 91, vbr_method=2) == "--alt-preset 32" 420 assert s(3, 91, vbr_method=2, bitrate=255) == "--alt-preset 255+" 421 assert s(3, 99, vbr_method=2, preset_used=128) == "--preset 128" 422 assert s(3, 99, vbr_method=2, preset_used=0, bitrate=48) == "--abr 48" 423 assert \ 424 s(3, 94, vbr_method=2, preset_used=0, bitrate=255) == "--abr 255+" 425 assert s(3, 99, vbr_method=3) == "-V 2 --vbr-old" 426 assert s(3, 94, vbr_method=3) == "-V 2" 427 assert s(3, 99, vbr_method=1, preset_used=1003) == "--preset insane" 428 assert s(3, 93, vbr_method=3, preset_used=1001) == "--preset standard" 429 assert s(3, 93, vbr_method=3, preset_used=1002) == "--preset extreme" 430 assert s(3, 93, vbr_method=3, preset_used=1004) == \ 431 "--preset fast standard" 432 assert s(3, 93, vbr_method=3, preset_used=1005) == \ 433 "--preset fast extreme" 434 assert s(3, 93, vbr_method=3, preset_used=1006) == "--preset medium" 435 assert s(3, 93, vbr_method=3, preset_used=1007) == \ 436 "--preset fast medium" 437 assert s(3, 92, vbr_method=3) == "-V 2" 438 assert s(3, 92, vbr_method=1, preset_used=0, bitrate=254) == "-b 254" 439 assert s(3, 92, vbr_method=1, preset_used=0, bitrate=255) == "-b 255+" 440 441 def skey(major, minor, args): 442 keys = ["vbr_quality", "quality", "vbr_method", "lowpass_filter", 443 "ath_type"] 444 return s(major, minor, **dict(zip(keys, args))) 445 446 assert skey(3, 91, (1, 2, 4, 19500, 3)) == "--preset r3mix" 447 assert skey(3, 91, (2, 2, 3, 19000, 4)) == "--alt-preset standard" 448 assert skey(3, 91, (2, 2, 3, 19500, 2)) == "--alt-preset extreme" 449 450 def test_length(self): 451 mp3 = MP3(os.path.join(DATA_DIR, "lame.mp3")) 452 self.assertAlmostEqual(mp3.info.length, 0.06160, 4) 453