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