import os
from io import BytesIO
from tests import TestCase, DATA_DIR, get_temp_copy
from mutagen.mp3 import MP3, error as MP3Error, delete, MPEGInfo, EasyMP3, \
BitrateMode, iter_sync
from mutagen.mp3._util import XingHeader, XingHeaderError, VBRIHeader, \
VBRIHeaderError, LAMEHeader, LAMEError
from mutagen.id3 import ID3
class TMP3Util(TestCase):
def test_find_sync(self):
def get_syncs(fileobj, max_read):
start = fileobj.tell()
pos = []
for i in iter_sync(fileobj, max_read):
pos.append(fileobj.tell() - start)
return pos
self.assertEqual(get_syncs(BytesIO(b"abc"), 100), [])
self.assertEqual(get_syncs(BytesIO(b""), 100), [])
self.assertEqual(get_syncs(BytesIO(b"a\xff\xe0"), 1), [])
self.assertEqual(get_syncs(BytesIO(b"a\xff\xc0\xff\xe0"), 100), [3])
self.assertEqual(
get_syncs(BytesIO(b"a\xff\xe0\xff\xe0\xff\xe0"), 100), [1, 3, 5])
for i in range(400):
fileobj = BytesIO(b"\x00" * i + b"\xff\xe0")
self.assertEqual(get_syncs(fileobj, 100 + i), [i])
class TMP3(TestCase):
silence = os.path.join(DATA_DIR, 'silence-44-s.mp3')
silence_nov2 = os.path.join(DATA_DIR, 'silence-44-s-v1.mp3')
silence_mpeg2 = os.path.join(DATA_DIR, 'silence-44-s-mpeg2.mp3')
silence_mpeg25 = os.path.join(DATA_DIR, 'silence-44-s-mpeg25.mp3')
lame = os.path.join(DATA_DIR, 'lame.mp3')
lame_peak = os.path.join(DATA_DIR, 'lame-peak.mp3')
lame_broken_short = os.path.join(DATA_DIR, 'lame397v9short.mp3')
def setUp(self):
self.filename = get_temp_copy(
os.path.join(DATA_DIR, "silence-44-s.mp3"))
self.mp3 = MP3(self.filename)
self.mp3_2 = MP3(self.silence_nov2)
self.mp3_3 = MP3(self.silence_mpeg2)
self.mp3_4 = MP3(self.silence_mpeg25)
self.mp3_lame = MP3(self.lame)
self.mp3_lame_peak = MP3(self.lame_peak)
def test_lame_broken_short(self):
f = MP3(self.lame_broken_short)
assert f.info.encoder_info == "LAME 3.97.0"
assert f.info.encoder_settings == "-V 9"
assert f.info.length == 0.0
assert f.info.bitrate == 40000
assert f.info.bitrate_mode == 2
assert f.info.sample_rate == 24000
def test_mode(self):
from mutagen.mp3 import JOINTSTEREO
self.failUnlessEqual(self.mp3.info.mode, JOINTSTEREO)
self.failUnlessEqual(self.mp3_2.info.mode, JOINTSTEREO)
self.failUnlessEqual(self.mp3_3.info.mode, JOINTSTEREO)
self.failUnlessEqual(self.mp3_4.info.mode, JOINTSTEREO)
def test_replaygain(self):
self.assertEqual(self.mp3_3.info.track_gain, 51.0)
self.assertEqual(self.mp3_4.info.track_gain, 51.0)
self.assertEqual(self.mp3_lame.info.track_gain, 6.0)
self.assertAlmostEqual(self.mp3_lame_peak.info.track_gain, 6.8, 1)
self.assertAlmostEqual(self.mp3_lame_peak.info.track_peak, 0.21856, 4)
self.assertTrue(self.mp3.info.track_gain is None)
self.assertTrue(self.mp3.info.track_peak is None)
self.assertTrue(self.mp3.info.album_gain is None)
def test_channels(self):
self.assertEqual(self.mp3.info.channels, 2)
self.assertEqual(self.mp3_2.info.channels, 2)
self.assertEqual(self.mp3_3.info.channels, 2)
self.assertEqual(self.mp3_4.info.channels, 2)
def test_encoder_info(self):
self.assertEqual(self.mp3.info.encoder_info, u"")
self.assertTrue(isinstance(self.mp3.info.encoder_info, str))
self.assertEqual(self.mp3_2.info.encoder_info, u"")
self.assertEqual(self.mp3_3.info.encoder_info, u"LAME 3.98.1+")
self.assertEqual(self.mp3_4.info.encoder_info, u"LAME 3.98.1+")
self.assertTrue(isinstance(self.mp3_4.info.encoder_info, str))
def test_bitrate_mode(self):
self.failUnlessEqual(self.mp3.info.bitrate_mode, BitrateMode.UNKNOWN)
self.failUnlessEqual(self.mp3_2.info.bitrate_mode, BitrateMode.UNKNOWN)
self.failUnlessEqual(self.mp3_3.info.bitrate_mode, BitrateMode.VBR)
self.failUnlessEqual(self.mp3_4.info.bitrate_mode, BitrateMode.VBR)
def test_id3(self):
self.failUnlessEqual(self.mp3.tags, ID3(self.silence))
self.failUnlessEqual(self.mp3_2.tags, ID3(self.silence_nov2))
def test_length(self):
self.assertAlmostEqual(self.mp3.info.length, 3.77, 2)
self.assertAlmostEqual(self.mp3_2.info.length, 3.77, 2)
self.assertAlmostEqual(self.mp3_3.info.length, 3.68475, 4)
self.assertAlmostEqual(self.mp3_4.info.length, 3.68475, 4)
def test_version(self):
self.failUnlessEqual(self.mp3.info.version, 1)
self.failUnlessEqual(self.mp3_2.info.version, 1)
self.failUnlessEqual(self.mp3_3.info.version, 2)
self.failUnlessEqual(self.mp3_4.info.version, 2.5)
def test_layer(self):
self.failUnlessEqual(self.mp3.info.layer, 3)
self.failUnlessEqual(self.mp3_2.info.layer, 3)
self.failUnlessEqual(self.mp3_3.info.layer, 3)
self.failUnlessEqual(self.mp3_4.info.layer, 3)
def test_bitrate(self):
self.failUnlessEqual(self.mp3.info.bitrate, 32000)
self.failUnlessEqual(self.mp3_2.info.bitrate, 32000)
self.failUnlessEqual(self.mp3_3.info.bitrate, 17783)
self.failUnlessEqual(self.mp3_4.info.bitrate, 8900)
def test_notmp3(self):
self.failUnlessRaises(
MP3Error, MP3, os.path.join(DATA_DIR, 'empty.ofr'))
self.failUnlessRaises(
MP3Error, MP3, os.path.join(DATA_DIR, 'emptyfile.mp3'))
def test_too_short(self):
self.failUnlessRaises(
MP3Error, MP3, os.path.join(DATA_DIR, 'too-short.mp3'))
def test_sketchy(self):
self.failIf(self.mp3.info.sketchy)
self.failIf(self.mp3_2.info.sketchy)
self.failIf(self.mp3_3.info.sketchy)
self.failIf(self.mp3_4.info.sketchy)
def test_sketchy_notmp3(self):
notmp3 = MP3(os.path.join(DATA_DIR, "silence-44-s.flac"))
self.failUnless(notmp3.info.sketchy)
self.assertTrue(u"sketchy" in notmp3.info.pprint())
def test_pprint(self):
self.failUnless(self.mp3.pprint())
def test_info_pprint(self):
res = self.mp3.info.pprint()
self.assertTrue(res)
self.assertTrue(isinstance(res, str))
self.assertTrue(res.startswith(u"MPEG 1 layer 3"))
def test_pprint_no_tags(self):
self.mp3.tags = None
self.failUnless(self.mp3.pprint())
def test_xing(self):
mp3 = MP3(os.path.join(DATA_DIR, "xing.mp3"))
self.assertAlmostEqual(mp3.info.length, 2.052, 3)
self.assertEqual(mp3.info.bitrate, 32000)
def test_vbri(self):
mp3 = MP3(os.path.join(DATA_DIR, "vbri.mp3"))
self.assertAlmostEqual(mp3.info.length, 222.19755, 3)
self.assertEqual(mp3.info.bitrate, 233260)
def test_empty_xing(self):
mp3 = MP3(os.path.join(DATA_DIR, "bad-xing.mp3"))
self.assertEqual(mp3.info.length, 0)
self.assertEqual(mp3.info.bitrate, 48000)
def test_delete(self):
self.mp3.delete()
self.failIf(self.mp3.tags)
self.failUnless(MP3(self.filename).tags is None)
def test_module_delete(self):
delete(self.filename)
self.failUnless(MP3(self.filename).tags is None)
def test_save(self):
self.mp3["TIT1"].text = ["foobar"]
self.mp3.save()
self.failUnless(MP3(self.filename)["TIT1"] == "foobar")
def test_save_padding(self):
self.mp3.save(padding=lambda x: 42)
self.assertEqual(MP3(self.filename).tags._padding, 42)
def test_load_non_id3(self):
filename = os.path.join(DATA_DIR, "apev2-lyricsv2.mp3")
from mutagen.apev2 import APEv2
mp3 = MP3(filename, ID3=APEv2)
self.failUnless("replaygain_track_peak" in mp3.tags)
def test_add_tags(self):
mp3 = MP3(os.path.join(DATA_DIR, "xing.mp3"))
self.failIf(mp3.tags)
mp3.add_tags()
self.failUnless(isinstance(mp3.tags, ID3))
def test_add_tags_already_there(self):
mp3 = MP3(os.path.join(DATA_DIR, "silence-44-s.mp3"))
self.failUnless(mp3.tags)
self.failUnlessRaises(Exception, mp3.add_tags)
def test_save_no_tags(self):
self.mp3.tags = None
self.mp3.save()
self.assertTrue(self.mp3.tags is None)
def test_mime(self):
self.failUnless("audio/mp3" in self.mp3.mime)
self.mp3.info.layer = 2
self.failIf("audio/mp3" in self.mp3.mime)
self.failUnless("audio/mp2" in self.mp3.mime)
def tearDown(self):
os.unlink(self.filename)
class TMPEGInfo(TestCase):
def test_not_real_file(self):
filename = os.path.join(DATA_DIR, "silence-44-s-v1.mp3")
with open(filename, "rb") as h:
fileobj = BytesIO(h.read(20))
self.failUnlessRaises(MP3Error, MPEGInfo, fileobj)
def test_empty(self):
fileobj = BytesIO(b"")
self.failUnlessRaises(MP3Error, MPEGInfo, fileobj)
def test_xing_unknown_framecount(self):
frame = (
b'\xff\xfb\xe4\x0c\x00\x0f\xf0\x00\x00\x00\x00\x00\x00\x00\x00\x00'
b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
b'\x00\x00\x00\x00Info\x00\x00\x00\x02\x00\xb4V@\x00\xb4R\x80\x00'
b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
)
fileobj = BytesIO(frame)
info = MPEGInfo(fileobj)
assert info.bitrate == 320000
assert info.length > 0
class TEasyMP3(TestCase):
def setUp(self):
self.filename = get_temp_copy(
os.path.join(DATA_DIR, "silence-44-s.mp3"))
self.mp3 = EasyMP3(self.filename)
def test_artist(self):
self.failUnless("artist" in self.mp3)
def test_no_composer(self):
self.failIf("composer" in self.mp3)
def test_length(self):
easy = self.mp3.info
noneasy = MP3(self.filename).info
with open(self.filename, "rb") as h:
nonid3 = MPEGInfo(h)
self.failUnlessEqual(easy.length, noneasy.length)
self.failUnlessEqual(noneasy.length, nonid3.length)
def tearDown(self):
os.unlink(self.filename)
class TXingHeader(TestCase):
def test_valid_info_header(self):
data = (b'Info\x00\x00\x00\x0f\x00\x00:>\x00\xed\xbd8\x00\x03\x05\x07'
b'\n\r\x0f\x12\x14\x17\x1a\x1c\x1e"$&)+.1359;=@CEGJLORTVZ\\^ac'
b'fikmqsux{}\x80\x82\x84\x87\x8a\x8c\x8e\x92\x94\x96\x99\x9c'
b'\x9e\xa1\xa3\xa5\xa9\xab\xad\xb0\xb3\xb5\xb8\xba\xbd\xc0\xc2'
b'\xc4\xc6\xca\xcc\xce\xd1\xd4\xd6\xd9\xdb\xdd\xe1\xe3\xe5\xe8'
b'\xeb\xed\xf0\xf2\xf5\xf8\xfa\xfc\x00\x00\x009')
fileobj = BytesIO(data)
xing = XingHeader(fileobj)
self.assertEqual(xing.bytes, 15580472)
self.assertEqual(xing.frames, 14910)
self.assertEqual(xing.vbr_scale, 57)
self.assertTrue(xing.toc)
self.assertEqual(len(xing.toc), 100)
self.assertEqual(sum(xing.toc), 12625)
self.assertEqual(xing.is_info, True)
XingHeader(BytesIO(data.replace(b'Info', b'Xing')))
def test_invalid(self):
self.assertRaises(XingHeaderError, XingHeader, BytesIO(b""))
self.assertRaises(XingHeaderError, XingHeader, BytesIO(b"Xing"))
self.assertRaises(XingHeaderError, XingHeader, BytesIO(b"aaaa"))
def test_get_offset(self):
mp3 = MP3(os.path.join(DATA_DIR, "silence-44-s.mp3"))
self.assertEqual(XingHeader.get_offset(mp3.info), 36)
class TVBRIHeader(TestCase):
def test_valid(self):
data = (b'VBRI\x00\x01\t1\x00d\x00\x0c\xb05\x00\x00\x049\x00\x87\x00'
b'\x01\x00\x02\x00\x08\n0\x19H\x18\xe0\x18x\x18\xe0\x18x\x19H'
b'\x18\xe0\x19H\x18\xe0\x18\xe0\x18x' + b'\x00' * 300)
fileobj = BytesIO(data)
vbri = VBRIHeader(fileobj)
self.assertEqual(vbri.bytes, 831541)
self.assertEqual(vbri.frames, 1081)
self.assertEqual(vbri.quality, 100)
self.assertEqual(vbri.version, 1)
self.assertEqual(vbri.toc_frames, 8)
self.assertTrue(vbri.toc)
self.assertEqual(len(vbri.toc), 135)
self.assertEqual(sum(vbri.toc), 72656)
def test_invalid(self):
self.assertRaises(VBRIHeaderError, VBRIHeader, BytesIO(b""))
self.assertRaises(VBRIHeaderError, VBRIHeader, BytesIO(b"VBRI"))
self.assertRaises(VBRIHeaderError, VBRIHeader, BytesIO(b"Xing"))
def test_get_offset(self):
mp3 = MP3(os.path.join(DATA_DIR, "silence-44-s.mp3"))
self.assertEqual(VBRIHeader.get_offset(mp3.info), 36)
class TLAMEHeader(TestCase):
def test_version(self):
def parse(data):
data = BytesIO(data + b"\x00" * (20 - len(data)))
return tuple(LAMEHeader.parse_version(data)[1:])
self.assertEqual(parse(b"LAME3.80"), (u"3.80", False))
self.assertEqual(parse(b"LAME3.80 "), (u"3.80", False))
self.assertEqual(parse(b"LAME3.88 (beta)"), (u"3.88 (beta)", False))
self.assertEqual(parse(b"LAME3.90 (alpha)"), (u"3.90 (alpha)", False))
self.assertEqual(parse(b"LAME3.90 "), (u"3.90.0+", True))
self.assertEqual(parse(b"LAME3.96a"), (u"3.96 (alpha)", True))
self.assertEqual(parse(b"LAME3.96b"), (u"3.96 (beta)", True))
self.assertEqual(parse(b"LAME3.96x"), (u"3.96 (?)", True))
self.assertEqual(parse(b"LAME3.98 "), (u"3.98.0", True))
self.assertEqual(parse(b"LAME3.96r"), (u"3.96.1+", True))
self.assertEqual(parse(b"L3.99r"), (u"3.99.1+", True))
self.assertEqual(parse(b"LAME3100r"), (u"3.100.1+", True))
self.assertEqual(parse(b"LAME3.90.\x03\xbe\x00"), (u"3.90.0+", True))
self.assertEqual(parse(b"LAME3.100"), (u"3.100.0+", True))
def test_invalid(self):
def parse(data):
data = BytesIO(data + b"\x00" * (20 - len(data)))
return LAMEHeader.parse_version(data)
self.assertRaises(LAMEError, parse, b"")
self.assertRaises(LAMEError, parse, b"LAME")
self.assertRaises(LAMEError, parse, b"LAME3.9999")
def test_real(self):
with open(os.path.join(DATA_DIR, "lame.mp3"), "rb") as h:
h.seek(36, 0)
xing = XingHeader(h)
self.assertEqual(xing.lame_version_desc, u"3.99.1+")
self.assertTrue(xing.lame_header)
self.assertEqual(xing.lame_header.track_gain_adjustment, 6.0)
assert xing.get_encoder_settings() == u"-V 2"
def test_settings(self):
with open(os.path.join(DATA_DIR, "lame.mp3"), "rb") as h:
h.seek(36, 0)
xing = XingHeader(h)
header = xing.lame_header
def s(major, minor, **kwargs):
old = vars(header)
for key, value in kwargs.items():
assert hasattr(header, key)
setattr(header, key, value)
r = header.guess_settings(major, minor)
header.__dict__.update(old)
return r
assert s(3, 99) == "-V 2"
assert s(3, 98) == "-V 2"
assert s(3, 97) == "-V 2 --vbr-new"
assert s(3, 96) == "-V 2 --vbr-new"
assert s(3, 95) == "-V 2 --vbr-new"
assert s(3, 94) == "-V 2 --vbr-new"
assert s(3, 93) == "-V 2 --vbr-new"
assert s(3, 92) == "-V 2 --vbr-new"
assert s(3, 91) == "-V 2 --vbr-new"
assert s(3, 90) == "-V 2 --vbr-new"
assert s(3, 89) == ""
assert s(3, 91, vbr_method=2) == "--alt-preset 32"
assert s(3, 91, vbr_method=2, bitrate=255) == "--alt-preset 255+"
assert s(3, 99, vbr_method=2, preset_used=128) == "--preset 128"
assert s(3, 99, vbr_method=2, preset_used=0, bitrate=48) == "--abr 48"
assert \
s(3, 94, vbr_method=2, preset_used=0, bitrate=255) == "--abr 255+"
assert s(3, 99, vbr_method=3) == "-V 2 --vbr-old"
assert s(3, 94, vbr_method=3) == "-V 2"
assert s(3, 99, vbr_method=1, preset_used=1003) == "--preset insane"
assert s(3, 93, vbr_method=3, preset_used=1001) == "--preset standard"
assert s(3, 93, vbr_method=3, preset_used=1002) == "--preset extreme"
assert s(3, 93, vbr_method=3, preset_used=1004) == \
"--preset fast standard"
assert s(3, 93, vbr_method=3, preset_used=1005) == \
"--preset fast extreme"
assert s(3, 93, vbr_method=3, preset_used=1006) == "--preset medium"
assert s(3, 93, vbr_method=3, preset_used=1007) == \
"--preset fast medium"
assert s(3, 92, vbr_method=3) == "-V 2"
assert s(3, 92, vbr_method=1, preset_used=0, bitrate=254) == "-b 254"
assert s(3, 92, vbr_method=1, preset_used=0, bitrate=255) == "-b 255+"
def skey(major, minor, args):
keys = ["vbr_quality", "quality", "vbr_method", "lowpass_filter",
"ath_type"]
return s(major, minor, **dict(zip(keys, args)))
assert skey(3, 91, (1, 2, 4, 19500, 3)) == "--preset r3mix"
assert skey(3, 91, (2, 2, 3, 19000, 4)) == "--alt-preset standard"
assert skey(3, 91, (2, 2, 3, 19500, 2)) == "--alt-preset extreme"
def test_length(self):
mp3 = MP3(os.path.join(DATA_DIR, "lame.mp3"))
self.assertAlmostEqual(mp3.info.length, 0.06160, 4)