1# -*- coding: utf-8 -*-
2
3import operator
4
5from tests import TestCase
6
7from mutagen._compat import text_type, xrange, PY2, PY3, iteritems, izip, \
8    integer_types
9from mutagen._constants import GENRES
10from mutagen.id3._tags import read_frames, save_frame, ID3Header
11from mutagen.id3._util import ID3SaveConfig, is_valid_frame_id, \
12    ID3JunkFrameError
13from mutagen.id3 import APIC, CTOC, CHAP, TPE2, Frames, Frames_2_2, CRA, \
14    AENC, PIC, LNK, LINK, SIGN, PRIV, GRID, ENCR, COMR, USER, UFID, GEOB, \
15    POPM, EQU2, RVA2, COMM, SYLT, USLT, WXXX, TXXX, WCOM, TextFrame, \
16    UrlFrame, NumericTextFrame, NumericPartTextFrame, TPE1, TIT2, \
17    TimeStampTextFrame, TCON, ID3TimeStamp, Frame, RVRB, RBUF, CTOCFlags, \
18    PairedTextFrame, BinaryFrame, ETCO, MLLT, SYTC, PCNT, PCST, POSS, OWNE, \
19    SEEK, ASPI, PictureType, CRM, RVAD, RVA, ID3Tags
20
21_22 = ID3Header()
22_22.version = (2, 2, 0)
23
24_23 = ID3Header()
25_23.version = (2, 3, 0)
26
27_24 = ID3Header()
28_24.version = (2, 4, 0)
29
30
31class TVariousFrames(TestCase):
32
33    DATA = [
34        ['TALB', b'\x00a/b', 'a/b', '', dict(encoding=0)],
35        ['TBPM', b'\x00120', '120', 120, dict(encoding=0)],
36        ['TCMP', b'\x001', '1', 1, dict(encoding=0)],
37        ['TCMP', b'\x000', '0', 0, dict(encoding=0)],
38        ['TCOM', b'\x00a/b', 'a/b', '', dict(encoding=0)],
39        ['TCON', b'\x00(21)Disco', '(21)Disco', '', dict(encoding=0)],
40        ['TCOP', b'\x001900 c', '1900 c', '', dict(encoding=0)],
41        ['TDAT', b'\x00a/b', 'a/b', '', dict(encoding=0)],
42        ['TDEN', b'\x001987', '1987', '', dict(encoding=0, year=[1987])],
43        [
44            'TDOR', b'\x001987-12', '1987-12', '',
45            dict(encoding=0, year=[1987], month=[12])
46        ],
47        ['TDRC', b'\x001987\x00', '1987', '', dict(encoding=0, year=[1987])],
48        [
49            'TDRL', b'\x001987\x001988', '1987,1988', '',
50            dict(encoding=0, year=[1987, 1988])
51        ],
52        ['TDTG', b'\x001987', '1987', '', dict(encoding=0, year=[1987])],
53        ['TDLY', b'\x001205', '1205', 1205, dict(encoding=0)],
54        ['TENC', b'\x00a b/c d', 'a b/c d', '', dict(encoding=0)],
55        ['TEXT', b'\x00a b\x00c d', ['a b', 'c d'], '', dict(encoding=0)],
56        ['TFLT', b'\x00MPG/3', 'MPG/3', '', dict(encoding=0)],
57        ['TIME', b'\x001205', '1205', '', dict(encoding=0)],
58        [
59            'TIPL', b'\x02\x00a\x00\x00\x00b', [["a", "b"]], '',
60            dict(encoding=2)
61        ],
62        ['TIT1', b'\x00a/b', 'a/b', '', dict(encoding=0)],
63        # TIT2 checks misaligned terminator '\x00\x00' across crosses utf16
64        # chars
65        [
66            'TIT2', b'\x01\xff\xfe\x38\x00\x00\x38', u'8\u3800', '',
67            dict(encoding=1)
68        ],
69        ['TIT3', b'\x00a/b', 'a/b', '', dict(encoding=0)],
70        ['TKEY', b'\x00A#m', 'A#m', '', dict(encoding=0)],
71        ['TLAN', b'\x006241', '6241', '', dict(encoding=0)],
72        ['TLEN', b'\x006241', '6241', 6241, dict(encoding=0)],
73        [
74            'TMCL', b'\x02\x00a\x00\x00\x00b', [["a", "b"]], '',
75            dict(encoding=2)
76        ],
77        ['TMED', b'\x00med', 'med', '', dict(encoding=0)],
78        ['TMOO', b'\x00moo', 'moo', '', dict(encoding=0)],
79        ['TOAL', b'\x00alb', 'alb', '', dict(encoding=0)],
80        ['TOFN', b'\x0012 : bar', '12 : bar', '', dict(encoding=0)],
81        ['TOLY', b'\x00lyr', 'lyr', '', dict(encoding=0)],
82        ['TOPE', b'\x00own/lic', 'own/lic', '', dict(encoding=0)],
83        ['TORY', b'\x001923', '1923', 1923, dict(encoding=0)],
84        ['TOWN', b'\x00own/lic', 'own/lic', '', dict(encoding=0)],
85        ['TPE1', b'\x00ab', ['ab'], '', dict(encoding=0)],
86        [
87            'TPE2', b'\x00ab\x00cd\x00ef', ['ab', 'cd', 'ef'], '',
88            dict(encoding=0)
89        ],
90        ['TPE3', b'\x00ab\x00cd', ['ab', 'cd'], '', dict(encoding=0)],
91        ['TPE4', b'\x00ab\x00', ['ab'], '', dict(encoding=0)],
92        ['TPOS', b'\x0008/32', '08/32', 8, dict(encoding=0)],
93        ['TPRO', b'\x00pro', 'pro', '', dict(encoding=0)],
94        ['TPUB', b'\x00pub', 'pub', '', dict(encoding=0)],
95        ['TRCK', b'\x004/9', '4/9', 4, dict(encoding=0)],
96        ['TRDA', b'\x00Sun Jun 12', 'Sun Jun 12', '', dict(encoding=0)],
97        ['TRSN', b'\x00ab/cd', 'ab/cd', '', dict(encoding=0)],
98        ['TRSO', b'\x00ab', 'ab', '', dict(encoding=0)],
99        ['TSIZ', b'\x0012345', '12345', 12345, dict(encoding=0)],
100        ['TSOA', b'\x00ab', 'ab', '', dict(encoding=0)],
101        ['TSOP', b'\x00ab', 'ab', '', dict(encoding=0)],
102        ['TSOT', b'\x00ab', 'ab', '', dict(encoding=0)],
103        ['TSO2', b'\x00ab', 'ab', '', dict(encoding=0)],
104        ['TSOC', b'\x00ab', 'ab', '', dict(encoding=0)],
105        ['TSRC', b'\x0012345', '12345', '', dict(encoding=0)],
106        ['TSSE', b'\x0012345', '12345', '', dict(encoding=0)],
107        ['TSST', b'\x0012345', '12345', '', dict(encoding=0)],
108        ['TYER', b'\x002004', '2004', 2004, dict(encoding=0)],
109        ['MVNM', b'\x00ab\x00', 'ab', '', dict(encoding=0)],
110        ['MVIN', b'\x001/3\x00', '1/3', 1, dict(encoding=0)],
111        ['GRP1', b'\x00ab\x00', 'ab', '', dict(encoding=0)],
112        [
113            'TXXX', b'\x00usr\x00a/b\x00c', ['a/b', 'c'], '',
114            dict(encoding=0, desc='usr')
115        ],
116        ['WCOM', b'http://foo', 'http://foo', '', {}],
117        ['WCOP', b'http://bar', 'http://bar', '', {}],
118        ['WOAF', b'http://baz', 'http://baz', '', {}],
119        ['WOAR', b'http://bar', 'http://bar', '', {}],
120        ['WOAS', b'http://bar', 'http://bar', '', {}],
121        ['WORS', b'http://bar', 'http://bar', '', {}],
122        ['WPAY', b'http://bar', 'http://bar', '', {}],
123        ['WPUB', b'http://bar', 'http://bar', '', {}],
124        ['WXXX', b'\x00usr\x00http', 'http', '', dict(encoding=0, desc='usr')],
125        [
126            'IPLS', b'\x00a\x00A\x00b\x00B\x00', [['a', 'A'], ['b', 'B']], '',
127            dict(encoding=0)
128        ],
129        ['MCDI', b'\x01\x02\x03\x04', b'\x01\x02\x03\x04', '', {}],
130        [
131            'ETCO', b'\x01\x12\x00\x00\x7f\xff', [(18, 32767)], '',
132            dict(format=1)
133        ],
134        [
135            'COMM', b'\x00ENUT\x00Com', 'Com', '',
136            dict(desc='T', lang='ENU', encoding=0)
137        ],
138        [
139            'APIC', b'\x00-->\x00\x03cover\x00cover.jpg', b'cover.jpg', '',
140            dict(mime='-->', type=3, desc='cover', encoding=0)
141        ],
142        ['USER', b'\x00ENUCom', 'Com', '', dict(lang='ENU', encoding=0)],
143        [
144            'RVA2', b'testdata\x00\x01\xfb\x8c\x10\x12\x23',
145            'Master volume: -2.2266 dB/0.1417', '',
146            dict(desc='testdata', channel=1, gain=-2.22656, peak=0.14169)
147        ],
148        [
149            'RVA2', b'testdata\x00\x01\xfb\x8c\x24\x01\x22\x30\x00\x00',
150            'Master volume: -2.2266 dB/0.1417', '',
151            dict(desc='testdata', channel=1, gain=-2.22656, peak=0.14169)
152        ],
153        [
154            'RVA2', b'testdata2\x00\x01\x04\x01\x00',
155            'Master volume: +2.0020 dB/0.0000', '',
156            dict(desc='testdata2', channel=1, gain=2.001953125, peak=0)
157        ],
158        ['PCNT', b'\x00\x00\x00\x11', 17, 17, dict(count=17)],
159        [
160            'POPM', b'foo@bar.org\x00\xde\x00\x00\x00\x11', 222, 222,
161            dict(email="foo@bar.org", rating=222, count=17)
162        ],
163        [
164            'POPM', b'foo@bar.org\x00\xde\x00', 222, 222,
165            dict(email="foo@bar.org", rating=222, count=0)
166        ],
167        # Issue #33 - POPM may have no playcount at all.
168        [
169            'POPM', b'foo@bar.org\x00\xde', 222, 222,
170            dict(email="foo@bar.org", rating=222)
171        ],
172        ['UFID', b'own\x00data', b'data', '', dict(data=b'data', owner='own')],
173        ['UFID', b'own\x00\xdd', b'\xdd', '', dict(data=b'\xdd', owner='own')],
174        [
175            'GEOB', b'\x00mime\x00name\x00desc\x00data', b'data', '',
176            dict(encoding=0, mime='mime', filename='name', desc='desc')
177        ],
178        [
179            'USLT', b'\x00engsome lyrics\x00woo\nfun', 'woo\nfun', '',
180            dict(encoding=0, lang='eng', desc='some lyrics', text='woo\nfun')
181        ],
182        [
183            'SYLT', (b'\x00eng\x02\x01some lyrics\x00foo\x00\x00\x00\x00\x01'
184                     b'bar\x00\x00\x00\x00\x10'),
185            "[1ms]: foo\n[16ms]: bar", '',
186            dict(encoding=0, lang='eng', type=1, format=2, desc='some lyrics')
187        ],
188        ['POSS', b'\x01\x0f', 15, 15, dict(format=1, position=15)],
189        [
190            'OWNE', b'\x00USD10.01\x0020041010CDBaby', 'CDBaby', 'CDBaby',
191            dict(encoding=0, price="USD10.01", date='20041010',
192                 seller='CDBaby')
193        ],
194        [
195            'PRIV', b'a@b.org\x00random data', b'random data', 'random data',
196            dict(owner='a@b.org', data=b'random data')
197        ],
198        [
199            'PRIV', b'a@b.org\x00\xdd', b'\xdd', '\xdd',
200            dict(owner='a@b.org', data=b'\xdd')
201        ],
202        ['SIGN', b'\x92huh?', b'huh?', 'huh?', dict(group=0x92, sig=b'huh?')],
203        [
204            'ENCR', b'a@b.org\x00\x92Data!', b'Data!', 'Data!',
205            dict(owner='a@b.org', method=0x92, data=b'Data!')
206        ],
207        [
208            'SEEK', b'\x00\x12\x00\x56',
209            0x12 * 256 * 256 + 0x56, 0x12 * 256 * 256 + 0x56,
210            dict(offset=0x12 * 256 * 256 + 0x56)
211        ],
212        [
213            'SYTC', b"\x01\x10obar", b'\x10obar', '',
214            dict(format=1, data=b'\x10obar')
215        ],
216        [
217            'RBUF', b'\x00\x12\x00', 0x12 * 256, 0x12 * 256,
218            dict(size=0x12 * 256)
219        ],
220        [
221            'RBUF', b'\x00\x12\x00\x01', 0x12 * 256, 0x12 * 256,
222            dict(size=0x12 * 256, info=1)
223        ],
224        [
225            'RBUF', b'\x00\x12\x00\x01\x00\x00\x00\x23',
226            0x12 * 256, 0x12 * 256,
227            dict(size=0x12 * 256, info=1, offset=0x23)
228        ],
229        [
230            'RVRB', b'\x12\x12\x23\x23\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11',
231            (0x12 * 256 + 0x12, 0x23 * 256 + 0x23), '',
232            dict(left=0x12 * 256 + 0x12, right=0x23 * 256 + 0x23)
233        ],
234        [
235            'AENC', b'a@b.org\x00\x00\x12\x00\x23', 'a@b.org', 'a@b.org',
236            dict(owner='a@b.org', preview_start=0x12, preview_length=0x23)
237        ],
238        [
239            'AENC', b'a@b.org\x00\x00\x12\x00\x23!', 'a@b.org', 'a@b.org',
240            dict(owner='a@b.org', preview_start=0x12,
241                 preview_length=0x23, data=b'!')
242        ],
243        [
244            'GRID', b'a@b.org\x00\x99', 'a@b.org', 0x99,
245            dict(owner='a@b.org', group=0x99)
246        ],
247        [
248            'GRID', b'a@b.org\x00\x99data', 'a@b.org', 0x99,
249            dict(owner='a@b.org', group=0x99, data=b'data')
250        ],
251        [
252            'COMR',
253            (b'\x00USD10.00\x0020051010ql@sc.net\x00\x09Joe\x00A song\x00'
254             b'x-image/fake\x00some data'),
255            COMR(encoding=0, price="USD10.00", valid_until="20051010",
256                 contact="ql@sc.net", format=9, seller="Joe", desc="A song",
257                 mime='x-image/fake', logo=b'some data'), '',
258            dict(encoding=0, price="USD10.00", valid_until="20051010",
259                 contact="ql@sc.net", format=9, seller="Joe", desc="A song",
260                 mime='x-image/fake', logo=b'some data')
261        ],
262        [
263            'COMR',
264            b'\x00USD10.00\x0020051010ql@sc.net\x00\x09Joe\x00A song\x00',
265            COMR(encoding=0, price="USD10.00", valid_until="20051010",
266                 contact="ql@sc.net", format=9, seller="Joe", desc="A song"),
267            '',
268            dict(encoding=0, price="USD10.00", valid_until="20051010",
269                 contact="ql@sc.net", format=9, seller="Joe", desc="A song")
270        ],
271        [
272            'MLLT', b'\x00\x01\x00\x00\x02\x00\x00\x03\x04\x08foobar',
273            b'foobar', '',
274            dict(frames=1, bytes=2, milliseconds=3, bits_for_bytes=4,
275                 bits_for_milliseconds=8, data=b'foobar')
276        ],
277        [
278            'EQU2', b'\x00Foobar\x00\x01\x01\x04\x00', [(128.5, 2.0)], '',
279            dict(method=0, desc="Foobar")
280        ],
281        [
282            'ASPI',
283            b'\x00\x00\x00\x00\x00\x00\x00\x10\x00\x03\x08\x01\x02\x03',
284            [1, 2, 3], '', dict(S=0, L=16, N=3, b=8)
285        ],
286        [
287            'ASPI', b'\x00\x00\x00\x00\x00\x00\x00\x10\x00\x03\x10'
288            b'\x00\x01\x00\x02\x00\x03', [1, 2, 3], '',
289            dict(S=0, L=16, N=3, b=16)
290        ],
291        [
292            'LINK', b'TIT1http://www.example.org/TIT1.txt\x00',
293            ("TIT1", 'http://www.example.org/TIT1.txt', b''), '',
294            dict(frameid='TIT1', url='http://www.example.org/TIT1.txt',
295                 data=b'')
296        ],
297        [
298            'LINK', b'COMMhttp://www.example.org/COMM.txt\x00engfoo',
299            ("COMM", 'http://www.example.org/COMM.txt', b'engfoo'), '',
300            dict(frameid='COMM', url='http://www.example.org/COMM.txt',
301                 data=b'engfoo')
302        ],
303        # iTunes podcast frames
304        ['TGID', b'\x00i', u'i', '', dict(encoding=0)],
305        ['TDES', b'\x00ii', u'ii', '', dict(encoding=0)],
306        ['TKWD', b'\x00ii', u'ii', '', dict(encoding=0)],
307        ['TCAT', b'\x00ii', u'ii', '', dict(encoding=0)],
308        ['WFED', b'http://zzz', 'http://zzz', '', {}],
309        ['PCST', b'\x00\x00\x00\x00', 0, 0, dict(value=0)],
310
311        # Chapter extension
312        ['CHAP', (b'foo\x00\x11\x11\x11\x11\x22\x22\x22\x22'
313                  b'\x33\x33\x33\x33\x44\x44\x44\x44'),
314         CHAP(element_id=u'foo', start_time=286331153, end_time=572662306,
315              start_offset=858993459, end_offset=1145324612), '', dict()],
316        ['CTOC', b'foo\x00\x03\x01bla\x00',
317         CTOC(element_id=u'foo',
318              flags=CTOCFlags.ORDERED | CTOCFlags.TOP_LEVEL,
319              child_element_ids=[u'bla']),
320         '', dict()],
321
322        ['RVAD', b'\x03\x10\x00\x00\x00\x00',
323         RVAD(adjustments=[0, 0]), '', dict()],
324        ['RVAD', b'\x03\x08\x00\x01\x02\x03\x04\x05\x06\x07\x00\x00\x00\x00',
325         RVAD(adjustments=[0, 1, 2, 3, -4, -5, 6, 7, 0, 0, 0, 0]), '', dict()],
326
327        # 2.2 tags
328        ['RVA', b'\x03\x10\x00\x00\x00\x00',
329         RVA(adjustments=[0, 0]), '', dict()],
330        ['UFI', b'own\x00data', b'data', '', dict(data=b'data', owner='own')],
331        [
332            'SLT', (b'\x00eng\x02\x01some lyrics\x00foo\x00\x00\x00\x00\x01bar'
333                    b'\x00\x00\x00\x00\x10'),
334            "[1ms]: foo\n[16ms]: bar", '',
335            dict(encoding=0, lang='eng', type=1, format=2, desc='some lyrics')
336        ],
337        ['TT1', b'\x00ab\x00', 'ab', '', dict(encoding=0)],
338        ['TT2', b'\x00ab', 'ab', '', dict(encoding=0)],
339        ['TT3', b'\x00ab', 'ab', '', dict(encoding=0)],
340        ['TP1', b'\x00ab\x00', 'ab', '', dict(encoding=0)],
341        ['TP2', b'\x00ab', 'ab', '', dict(encoding=0)],
342        ['TP3', b'\x00ab', 'ab', '', dict(encoding=0)],
343        ['TP4', b'\x00ab', 'ab', '', dict(encoding=0)],
344        ['TCM', b'\x00ab/cd', 'ab/cd', '', dict(encoding=0)],
345        ['TXT', b'\x00lyr', 'lyr', '', dict(encoding=0)],
346        ['TLA', b'\x00ENU', 'ENU', '', dict(encoding=0)],
347        ['TCO', b'\x00gen', 'gen', '', dict(encoding=0)],
348        ['TAL', b'\x00alb', 'alb', '', dict(encoding=0)],
349        ['TPA', b'\x001/9', '1/9', 1, dict(encoding=0)],
350        ['TRK', b'\x002/8', '2/8', 2, dict(encoding=0)],
351        ['TRC', b'\x00isrc', 'isrc', '', dict(encoding=0)],
352        ['TYE', b'\x001900', '1900', 1900, dict(encoding=0)],
353        ['TDA', b'\x002512', '2512', '', dict(encoding=0)],
354        ['TIM', b'\x001225', '1225', '', dict(encoding=0)],
355        ['TRD', b'\x00Jul 17', 'Jul 17', '', dict(encoding=0)],
356        ['TMT', b'\x00DIG/A', 'DIG/A', '', dict(encoding=0)],
357        ['TFT', b'\x00MPG/3', 'MPG/3', '', dict(encoding=0)],
358        ['TBP', b'\x00133', '133', 133, dict(encoding=0)],
359        ['TCP', b'\x001', '1', 1, dict(encoding=0)],
360        ['TCP', b'\x000', '0', 0, dict(encoding=0)],
361        ['TCR', b'\x00Me', 'Me', '', dict(encoding=0)],
362        ['TPB', b'\x00Him', 'Him', '', dict(encoding=0)],
363        ['TEN', b'\x00Lamer', 'Lamer', '', dict(encoding=0)],
364        ['TSS', b'\x00ab', 'ab', '', dict(encoding=0)],
365        ['TOF', b'\x00ab:cd', 'ab:cd', '', dict(encoding=0)],
366        ['TLE', b'\x0012', '12', 12, dict(encoding=0)],
367        ['TSI', b'\x0012', '12', 12, dict(encoding=0)],
368        ['TDY', b'\x0012', '12', 12, dict(encoding=0)],
369        ['TKE', b'\x00A#m', 'A#m', '', dict(encoding=0)],
370        ['TOT', b'\x00org', 'org', '', dict(encoding=0)],
371        ['TOA', b'\x00org', 'org', '', dict(encoding=0)],
372        ['TOL', b'\x00org', 'org', '', dict(encoding=0)],
373        ['TOR', b'\x001877', '1877', 1877, dict(encoding=0)],
374        ['TXX', b'\x00desc\x00val', 'val', '', dict(encoding=0, desc='desc')],
375        ['TSC', b'\x00ab', 'ab', '', dict(encoding=0)],
376        ['TSA', b'\x00ab', 'ab', '', dict(encoding=0)],
377        ['TS2', b'\x00ab', 'ab', '', dict(encoding=0)],
378        ['TST', b'\x00ab', 'ab', '', dict(encoding=0)],
379        ['TSP', b'\x00ab', 'ab', '', dict(encoding=0)],
380        ['MVN', b'\x00ab\x00', 'ab', '', dict(encoding=0)],
381        ['MVI', b'\x001/3\x00', '1/3', 1, dict(encoding=0)],
382        ['GP1', b'\x00ab\x00', 'ab', '', dict(encoding=0)],
383
384        ['WAF', b'http://zzz', 'http://zzz', '', {}],
385        ['WAR', b'http://zzz', 'http://zzz', '', {}],
386        ['WAS', b'http://zzz', 'http://zzz', '', {}],
387        ['WCM', b'http://zzz', 'http://zzz', '', {}],
388        ['WCP', b'http://zzz', 'http://zzz', '', {}],
389        ['WPB', b'http://zzz', 'http://zzz', '', {}],
390        [
391            'WXX', b'\x00desc\x00http', 'http', '',
392            dict(encoding=0, desc='desc')
393        ],
394        [
395            'IPL', b'\x00a\x00A\x00b\x00B\x00', [['a', 'A'], ['b', 'B']], '',
396            dict(encoding=0)
397        ],
398        ['MCI', b'\x01\x02\x03\x04', b'\x01\x02\x03\x04', '', {}],
399        [
400            'ETC', b'\x01\x12\x00\x00\x7f\xff', [(18, 32767)], '',
401            dict(format=1)
402        ],
403        [
404            'COM', b'\x00ENUT\x00Com', 'Com', '',
405            dict(desc='T', lang='ENU', encoding=0)
406        ],
407        [
408            'PIC', b'\x00-->\x03cover\x00cover.jpg', b'cover.jpg', '',
409            dict(mime='-->', type=3, desc='cover', encoding=0)
410        ],
411        [
412            'POP', b'foo@bar.org\x00\xde\x00\x00\x00\x11', 222, 222,
413            dict(email="foo@bar.org", rating=222, count=17)
414        ],
415        ['CNT', b'\x00\x00\x00\x11', 17, 17, dict(count=17)],
416        [
417            'GEO', b'\x00mime\x00name\x00desc\x00data', b'data', '',
418            dict(encoding=0, mime='mime', filename='name', desc='desc')
419        ],
420        [
421            'ULT', b'\x00engsome lyrics\x00woo\nfun', 'woo\nfun', '',
422            dict(encoding=0, lang='eng', desc='some lyrics', text='woo\nfun')],
423        [
424            'BUF', b'\x00\x12\x00', 0x12 * 256, 0x12 * 256,
425            dict(size=0x12 * 256)
426        ],
427        [
428            'CRA', b'a@b.org\x00\x00\x12\x00\x23', 'a@b.org', 'a@b.org',
429            dict(owner='a@b.org', preview_start=0x12, preview_length=0x23)
430        ],
431        [
432            'CRA', b'a@b.org\x00\x00\x12\x00\x23!', 'a@b.org', 'a@b.org',
433            dict(owner='a@b.org', preview_start=0x12,
434                 preview_length=0x23, data=b'!')
435        ],
436        [
437            'REV', b'\x12\x12\x23\x23\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11',
438            (0x12 * 256 + 0x12, 0x23 * 256 + 0x23), '',
439            dict(left=0x12 * 256 + 0x12, right=0x23 * 256 + 0x23)
440        ],
441        [
442            'STC', b"\x01\x10obar", b'\x10obar', '',
443            dict(format=1, data=b'\x10obar')
444        ],
445        [
446            'MLL', b'\x00\x01\x00\x00\x02\x00\x00\x03\x04\x08foobar',
447            b'foobar', '',
448            dict(frames=1, bytes=2, milliseconds=3, bits_for_bytes=4,
449                 bits_for_milliseconds=8, data=b'foobar')
450        ],
451        [
452            'LNK', b'TT1http://www.example.org/TIT1.txt\x00',
453            ("TT1", 'http://www.example.org/TIT1.txt', b''), '',
454            dict(frameid='TT1', url='http://www.example.org/TIT1.txt',
455                 data=b'')
456        ],
457        [
458            'CRM', b'foo@example.org\x00test\x00woo', b'woo', '',
459            dict(owner='foo@example.org', desc='test', data=b'woo')
460        ],
461        [
462            'CRM', b'\x00\x00', b'', '',
463            dict(owner='', desc='', data=b'')
464        ],
465    ]
466
467    def _get_frame(self, id_):
468        return getattr(getattr(__import__('mutagen.id3'), "id3"), id_)
469
470    def test_all_tested(self):
471        check = dict.fromkeys(list(Frames.keys()) + list(Frames_2_2.keys()))
472        tested = [l[0] for l in self.DATA]
473        for t in tested:
474            check.pop(t, None)
475        self.assertEqual(list(check.keys()), [])
476
477    def test_tag_repr(self):
478        for frame_id, data, value, intval, info in self.DATA:
479            kind = self._get_frame(frame_id)
480            tag = kind._fromData(_23, 0, data)
481            self.assertTrue(isinstance(tag.__str__(), str))
482            self.assertTrue(isinstance(tag.__repr__(), str))
483            if PY2:
484                if hasattr(tag, "__unicode__"):
485                    self.assertTrue(isinstance(tag.__unicode__(), unicode))
486            else:
487                if hasattr(tag, "__bytes__"):
488                    self.assertTrue(isinstance(tag.__bytes__(), bytes))
489
490    def test_tag_write(self):
491        for frame_id, data, value, intval, info in self.DATA:
492            kind = self._get_frame(frame_id)
493            tag = kind._fromData(_24, 0, data)
494            towrite = tag._writeData()
495            tag2 = kind._fromData(_24, 0, towrite)
496            for spec in kind._framespec:
497                attr = spec.name
498                self.assertEquals(getattr(tag, attr), getattr(tag2, attr))
499            for spec in kind._optionalspec:
500                attr = spec.name
501                other = object()
502                self.assertEquals(
503                    getattr(tag, attr, other), getattr(tag2, attr, other))
504
505    def test_tag_write_v23(self):
506        for frame_id, data, value, intval, info in self.DATA:
507            kind = self._get_frame(frame_id)
508            tag = kind._fromData(_24, 0, data)
509            config = ID3SaveConfig(3, "/")
510            towrite = tag._writeData(config)
511            tag2 = kind._fromData(_23, 0, towrite)
512            tag3 = kind._fromData(_23, 0, tag2._writeData(config))
513            for spec in kind._framespec:
514                attr = spec.name
515                self.assertEquals(getattr(tag2, attr), getattr(tag3, attr))
516            for spec in kind._optionalspec:
517                attr = spec.name
518                other = object()
519                self.assertEquals(
520                    getattr(tag2, attr, other), getattr(tag3, attr, other))
521                self.assertEqual(hasattr(tag, attr), hasattr(tag2, attr))
522
523    def test_tag(self):
524        for frame_id, data, value, intval, info in self.DATA:
525            kind = self._get_frame(frame_id)
526            tag = kind._fromData(_23, 0, data)
527            self.failUnless(tag.HashKey)
528            self.failUnless(tag.pprint())
529            self.assertEquals(value, tag)
530            if 'encoding' not in info:
531                self.assertRaises(AttributeError, getattr, tag, 'encoding')
532            for attr, value in iteritems(info):
533                t = tag
534                if not isinstance(value, list):
535                    value = [value]
536                    t = [t]
537                for value, t in izip(value, iter(t)):
538                    if isinstance(value, float):
539                        self.failUnlessAlmostEqual(value, getattr(t, attr), 5)
540                    else:
541                        self.assertEquals(value, getattr(t, attr))
542
543                    if isinstance(intval, integer_types):
544                        self.assertEquals(intval, operator.pos(t))
545                    else:
546                        self.assertRaises(TypeError, operator.pos, t)
547
548
549class TPCST(TestCase):
550
551    def test_default(self):
552        frame = PCST()
553        self.assertEqual(frame.value, 0)
554
555
556class TETCO(TestCase):
557
558    def test_default(self):
559        frame = ETCO()
560        self.assertEqual(frame.format, 1)
561        self.assertEqual(frame.events, [])
562
563    def test_default_mutable(self):
564        frame = ETCO()
565        frame.events.append(1)
566        self.assertEqual(ETCO().events, [])
567
568
569class TSYTC(TestCase):
570
571    def test_default(self):
572        frame = SYTC()
573        self.assertEqual(frame.format, 1)
574        self.assertEqual(frame.data, b"")
575
576
577class TCRA(TestCase):
578
579    def test_upgrade(self):
580        frame = CRA(owner="a", preview_start=1, preview_length=2, data=b"foo")
581        new = AENC(frame)
582        self.assertEqual(new.owner, "a")
583        self.assertEqual(new.preview_start, 1)
584        self.assertEqual(new.preview_length, 2)
585        self.assertEqual(new.data, b"foo")
586
587
588class TPIC(TestCase):
589
590    def test_default(self):
591        frame = PIC()
592        self.assertEqual(frame.encoding, 1)
593        self.assertEqual(frame.mime, u"JPG")
594        self.assertEqual(frame.type, PictureType.COVER_FRONT)
595        self.assertEqual(frame.desc, u"")
596        self.assertEqual(frame.data, b"")
597
598    def test_upgrade(self):
599        frame = PIC(encoding=0, mime="PNG", desc="bla", type=3, data=b"\x00")
600        new = APIC(frame)
601        self.assertEqual(new.encoding, 0)
602        self.assertEqual(new.mime, "PNG")
603        self.assertEqual(new.desc, "bla")
604        self.assertEqual(new.data, b"\x00")
605
606        frame = PIC(encoding=0, mime="foo",
607                    desc="bla", type=3, data=b"\x00")
608        self.assertEqual(frame.mime, "foo")
609        new = APIC(frame)
610        self.assertEqual(new.mime, "foo")
611
612
613class TLNK(TestCase):
614
615    def test_default(self):
616        frame = LNK()
617        self.assertEqual(frame.frameid, u"XXX")
618        self.assertEqual(frame.url, u"")
619
620    def test_upgrade(self):
621        url = "http://foo.bar"
622
623        frame = LNK(frameid="PIC", url=url, data=b"\x00")
624        new = LINK(frame)
625        self.assertEqual(new.frameid, "APIC")
626        self.assertEqual(new.url, url)
627        self.assertEqual(new.data, b"\x00")
628
629        frame = LNK(frameid="XYZ")
630        new = LINK(frame)
631        self.assertEqual(new.frameid, "XYZ ")
632
633
634class TSIGN(TestCase):
635
636    def test_default(self):
637        frame = SIGN()
638        self.assertEqual(frame.group, 0x80)
639        self.assertEqual(frame.sig, b"")
640
641    def test_hash(self):
642        frame = SIGN(group=1, sig=b"foo")
643        self.assertEqual(frame.HashKey, "SIGN:1:foo")
644
645    def test_pprint(self):
646        frame = SIGN(group=1, sig=b"foo")
647        frame._pprint()
648
649
650class TCRM(TestCase):
651
652    def test_default(self):
653        frame = CRM()
654        self.assertEqual(frame.owner, u"")
655        self.assertEqual(frame.desc, u"")
656        self.assertEqual(frame.data, b"")
657
658
659class TPRIV(TestCase):
660
661    def test_default(self):
662        frame = PRIV()
663        self.assertEqual(frame.owner, u"")
664        self.assertEqual(frame.data, b"")
665
666    def test_hash(self):
667        frame = PRIV(owner="foo", data=b"foo")
668        self.assertEqual(frame.HashKey, "PRIV:foo:foo")
669        frame._pprint()
670
671        frame = PRIV(owner="foo", data=b"\x00\xff")
672        self.assertEqual(frame.HashKey, u"PRIV:foo:\x00\xff")
673        frame._pprint()
674
675
676class TGRID(TestCase):
677
678    def test_default(self):
679        frame = GRID()
680        self.assertEqual(frame.owner, u"")
681        self.assertEqual(frame.group, 0x80)
682
683    def test_hash(self):
684        frame = GRID(owner="foo", group=42)
685        self.assertEqual(frame.HashKey, "GRID:42")
686        frame._pprint()
687
688
689class TENCR(TestCase):
690
691    def test_default(self):
692        frame = ENCR()
693        self.assertEqual(frame.owner, u"")
694        self.assertEqual(frame.method, 0x80)
695        self.assertEqual(frame.data, b"")
696
697    def test_hash(self):
698        frame = ENCR(owner="foo", method=42, data=b"\xff")
699        self.assertEqual(frame.HashKey, "ENCR:foo")
700        frame._pprint()
701
702
703class TOWNE(TestCase):
704
705    def test_default(self):
706        frame = OWNE()
707        self.assertEqual(frame.encoding, 1)
708        self.assertEqual(frame.price, u"")
709        self.assertEqual(frame.date, u"19700101")
710        self.assertEqual(frame.seller, u"")
711
712
713class TCOMR(TestCase):
714
715    def test_default(self):
716        frame = COMR()
717        self.assertEqual(frame.encoding, 1)
718        self.assertEqual(frame.price, u"")
719        self.assertEqual(frame.valid_until, u"19700101")
720        self.assertEqual(frame.contact, u"")
721        self.assertEqual(frame.format, 0)
722        self.assertEqual(frame.seller, u"")
723        self.assertEqual(frame.desc, u"")
724
725    def test_hash(self):
726        frame = COMR(
727            encoding=0, price="p", valid_until="v" * 8, contact="c",
728            format=42, seller="s", desc="d", mime="m", logo=b"\xff")
729        self.assertEqual(
730            frame.HashKey, u"COMR:\x00p\x00vvvvvvvvc\x00*s\x00d\x00m\x00\xff")
731        frame._pprint()
732
733
734class TBinaryFrame(TestCase):
735
736    def test_default(self):
737        frame = BinaryFrame()
738        self.assertEqual(frame.data, b"")
739
740
741class TUSER(TestCase):
742
743    def test_default(self):
744        frame = USER()
745        self.assertEqual(frame.encoding, 1)
746        self.assertEqual(frame.lang, u"XXX")
747        self.assertEqual(frame.text, u"")
748
749    def test_hash(self):
750        frame = USER(encoding=0, lang="foo", text="bla")
751        self.assertEqual(frame.HashKey, "USER:foo")
752        frame._pprint()
753
754        self.assertEquals(USER(text="a").HashKey, USER(text="b").HashKey)
755        self.assertNotEquals(
756            USER(lang="abc").HashKey, USER(lang="def").HashKey)
757
758
759class TMLLT(TestCase):
760
761    def test_default(self):
762        frame = MLLT()
763        self.assertEqual(frame.frames, 0)
764        self.assertEqual(frame.bytes, 0)
765        self.assertEqual(frame.milliseconds, 0)
766        self.assertEqual(frame.bits_for_bytes, 0)
767        self.assertEqual(frame.bits_for_milliseconds, 0)
768        self.assertEqual(frame.data, b"")
769
770
771class TTIT2(TestCase):
772
773    def test_hash(self):
774        self.assertEquals(TIT2(text="a").HashKey, TIT2(text="b").HashKey)
775
776
777class TUFID(TestCase):
778
779    def test_default(self):
780        frame = UFID()
781        self.assertEqual(frame.owner, u"")
782        self.assertEqual(frame.data, b"")
783
784    def test_hash(self):
785        frame = UFID(owner="foo", data=b"\x42")
786        self.assertEqual(frame.HashKey, "UFID:foo")
787        frame._pprint()
788
789        self.assertEquals(UFID(data=b"1").HashKey, UFID(data=b"2").HashKey)
790        self.assertNotEquals(UFID(owner="a").HashKey, UFID(owner="b").HashKey)
791
792
793class TPairedTextFrame(TestCase):
794
795    def test_default(self):
796        frame = PairedTextFrame()
797        self.assertEqual(frame.encoding, 1)
798        self.assertEqual(frame.people, [])
799
800
801class TRVAD(TestCase):
802
803    def test_default(self):
804        frame = RVAD()
805        self.assertEqual(frame.adjustments, [0, 0])
806
807    def test_hash(self):
808        frame = RVAD()
809        self.assertEqual(frame.HashKey, "RVAD")
810
811    def test_upgrade(self):
812        rva = RVA(adjustments=[1, 2])
813        self.assertEqual(RVAD(rva).adjustments, [1, 2])
814
815
816class TLINK(TestCase):
817
818    def test_read(self):
819        frame = LINK()
820        frame._readData(_24, b"XXX\x00Foo\x00")
821        # either we can read invalid frame ids or we fail properly, atm we read
822        # them.
823        self.assertEqual(frame.frameid, "XXX\x00")
824
825    def test_default(self):
826        frame = LINK()
827        self.assertEqual(frame.frameid, u"XXXX")
828        self.assertEqual(frame.url, u"")
829
830    def test_hash(self):
831        frame = LINK(frameid="TPE1", url="http://foo.bar", data=b"\x42")
832        self.assertEqual(frame.HashKey, "LINK:TPE1:http://foo.bar:B")
833        frame._pprint()
834
835        frame = LINK(frameid="TPE1", url="http://foo.bar")
836        self.assertEqual(frame.HashKey, "LINK:TPE1:http://foo.bar:")
837
838
839class TAENC(TestCase):
840
841    def test_default(self):
842        frame = AENC()
843        self.assertEqual(frame.owner, u"")
844        self.assertEqual(frame.preview_start, 0)
845        self.assertEqual(frame.preview_length, 0)
846
847    def test_hash(self):
848        frame = AENC(
849            owner="foo", preview_start=1, preview_length=2, data=b"\x42")
850        self.assertEqual(frame.HashKey, "AENC:foo")
851        frame._pprint()
852
853
854class TGEOB(TestCase):
855
856    def test_default(self):
857        frame = GEOB()
858        self.assertEqual(frame.encoding, 1)
859        self.assertEqual(frame.mime, u"")
860        self.assertEqual(frame.filename, u"")
861        self.assertEqual(frame.desc, u"")
862        self.assertEqual(frame.data, b"")
863
864    def test_hash(self):
865        frame = GEOB(
866            encoding=0, mtime="m", filename="f", desc="d", data=b"\x42")
867        self.assertEqual(frame.HashKey, "GEOB:d")
868        frame._pprint()
869
870        self.assertEquals(GEOB(data=b"1").HashKey, GEOB(data=b"2").HashKey)
871        self.assertNotEquals(GEOB(desc="a").HashKey, GEOB(desc="b").HashKey)
872
873
874class TPOPM(TestCase):
875
876    def test_default(self):
877        frame = POPM()
878        self.assertEqual(frame.email, u"")
879        self.assertEqual(frame.rating, 0)
880        self.assertFalse(hasattr(frame, "count"))
881
882    def test_hash(self):
883        frame = POPM(email="e", rating=42)
884        self.assertEqual(frame.HashKey, "POPM:e")
885        frame._pprint()
886
887        self.assertEquals(POPM(count=1).HashKey, POPM(count=2).HashKey)
888        self.assertNotEquals(POPM(email="a").HashKey, POPM(email="b").HashKey)
889
890
891class TEQU2(TestCase):
892
893    def test_default(self):
894        frame = EQU2()
895        self.assertEqual(frame.method, 0)
896        self.assertEqual(frame.desc, u"")
897        self.assertEqual(frame.adjustments, [])
898
899    def test_default_mutable(self):
900        frame = EQU2()
901        frame.adjustments.append(1)
902        self.assertEqual(EQU2(), [])
903
904    def test_hash(self):
905        frame = EQU2(method=42, desc="d", adjustments=[(0, 0)])
906        self.assertEqual(frame.HashKey, "EQU2:d")
907        frame._pprint()
908
909
910class TSEEK(TestCase):
911
912    def test_default(self):
913        frame = SEEK()
914        self.assertEqual(frame.offset, 0)
915
916
917class TPOSS(TestCase):
918
919    def test_default(self):
920        frame = POSS()
921        self.assertEqual(frame.format, 1)
922        self.assertEqual(frame.position, 0)
923
924
925class TCOMM(TestCase):
926
927    def test_default(self):
928        frame = COMM()
929        self.assertEqual(frame.encoding, 1)
930        self.assertEqual(frame.lang, u"XXX")
931        self.assertEqual(frame.desc, u"")
932        self.assertEqual(frame.text, [])
933
934    def test_hash(self):
935        frame = COMM(encoding=0, lang="foo", desc="d")
936        self.assertEqual(frame.HashKey, "COMM:d:foo")
937        frame._pprint()
938
939        self.assertEquals(COMM(text="a").HashKey, COMM(text="b").HashKey)
940        self.assertNotEquals(COMM(desc="a").HashKey, COMM(desc="b").HashKey)
941        self.assertNotEquals(
942            COMM(lang="abc").HashKey, COMM(lang="def").HashKey)
943
944    def test_bad_unicodedecode(self):
945        # 7 bytes of "UTF16" data.
946        data = b'\x01\x00\x00\x00\xff\xfe\x00\xff\xfeh\x00'
947        self.assertRaises(ID3JunkFrameError, COMM._fromData, _24, 0x00, data)
948
949
950class TSYLT(TestCase):
951
952    def test_default(self):
953        frame = SYLT()
954        self.assertEqual(frame.encoding, 1)
955        self.assertEqual(frame.lang, u"XXX")
956        self.assertEqual(frame.format, 1)
957        self.assertEqual(frame.type, 0)
958        self.assertEqual(frame.desc, u"")
959        self.assertEqual(frame.text, u"")
960
961    def test_hash(self):
962        frame = SYLT(encoding=0, lang="foo", format=1, type=2,
963                     desc="d", text=[("t", 0)])
964        self.assertEqual(frame.HashKey, "SYLT:d:foo")
965        frame._pprint()
966
967    def test_bad_sylt(self):
968        self.assertRaises(
969            ID3JunkFrameError, SYLT._fromData, _24, 0x0,
970            b"\x00eng\x01description\x00foobar")
971        self.assertRaises(
972            ID3JunkFrameError, SYLT._fromData, _24, 0x0,
973            b"\x00eng\x01description\x00foobar\x00\xFF\xFF\xFF")
974
975
976class TRVRB(TestCase):
977
978    def test_default(self):
979        frame = RVRB()
980        self.assertEqual(frame.left, 0)
981        self.assertEqual(frame.right, 0)
982        self.assertEqual(frame.bounce_left, 0)
983        self.assertEqual(frame.bounce_right, 0)
984        self.assertEqual(frame.feedback_ltl, 0)
985        self.assertEqual(frame.feedback_ltr, 0)
986        self.assertEqual(frame.feedback_rtr, 0)
987        self.assertEqual(frame.feedback_rtl, 0)
988        self.assertEqual(frame.premix_ltr, 0)
989        self.assertEqual(frame.premix_rtl, 0)
990
991    def test_extradata(self):
992        self.assertEqual(RVRB()._readData(_24, b'L1R1BBFFFFPP#xyz'), b'#xyz')
993
994
995class TRBUF(TestCase):
996
997    def test_default(self):
998        frame = RBUF()
999        self.assertEqual(frame.size, 0)
1000        self.assertFalse(hasattr(frame, "info"))
1001        self.assertFalse(hasattr(frame, "offset"))
1002
1003    def test_extradata(self):
1004        self.assertEqual(
1005            RBUF()._readData(
1006                _24, b'\x00\x01\x00\x01\x00\x00\x00\x00#xyz'), b'#xyz')
1007
1008
1009class TUSLT(TestCase):
1010
1011    def test_default(self):
1012        frame = USLT()
1013        self.assertEqual(frame.encoding, 1)
1014        self.assertEqual(frame.lang, u"XXX")
1015        self.assertEqual(frame.desc, u"")
1016        self.assertEqual(frame.text, u"")
1017
1018    def test_hash(self):
1019        frame = USLT(encoding=0, lang="foo", desc="d", text="t")
1020        self.assertEqual(frame.HashKey, "USLT:d:foo")
1021        assert frame._pprint() == "d=foo=t"
1022
1023
1024class TWXXX(TestCase):
1025
1026    def test_default(self):
1027        frame = WXXX()
1028        self.assertEqual(frame.encoding, 1)
1029        self.assertEqual(frame.desc, u"")
1030        self.assertEqual(frame.url, u"")
1031
1032    def test_hash(self):
1033        self.assert_(isinstance(WXXX(url='durl'), WXXX))
1034
1035        frame = WXXX(encoding=0, desc="d", url="u")
1036        self.assertEqual(frame.HashKey, "WXXX:d")
1037        frame._pprint()
1038
1039        self.assertEquals(WXXX(text="a").HashKey, WXXX(text="b").HashKey)
1040        self.assertNotEquals(WXXX(desc="a").HashKey, WXXX(desc="b").HashKey)
1041
1042
1043class TTXXX(TestCase):
1044
1045    def test_default(self):
1046        self.assertEqual(TXXX(), TXXX(desc=u"", encoding=1, text=[]))
1047
1048    def test_hash(self):
1049        frame = TXXX(encoding=0, desc="d", text=[])
1050        self.assertEqual(frame.HashKey, "TXXX:d")
1051        frame._pprint()
1052
1053        self.assertEquals(TXXX(text="a").HashKey, TXXX(text="b").HashKey)
1054        self.assertNotEquals(TXXX(desc="a").HashKey, TXXX(desc="b").HashKey)
1055
1056
1057class TWCOM(TestCase):
1058
1059    def test_hash(self):
1060        frame = WCOM(url="u")
1061        self.assertEqual(frame.HashKey, "WCOM:u")
1062        frame._pprint()
1063
1064
1065class TUrlFrame(TestCase):
1066
1067    def test_default(self):
1068        self.assertEqual(UrlFrame(), UrlFrame(url=u""))
1069
1070    def test_main(self):
1071        self.assertEqual(UrlFrame("url").url, "url")
1072
1073
1074class TNumericTextFrame(TestCase):
1075
1076    def test_default(self):
1077        self.assertEqual(
1078            NumericTextFrame(), NumericTextFrame(encoding=1, text=[]))
1079
1080    def test_main(self):
1081        self.assertEqual(NumericTextFrame(text='1').text, ["1"])
1082        self.assertEqual(+NumericTextFrame(text='1'), 1)
1083
1084
1085class TNumericPartTextFrame(TestCase):
1086
1087    def test_default(self):
1088        self.assertEqual(
1089            NumericPartTextFrame(),
1090            NumericPartTextFrame(encoding=1, text=[]))
1091
1092    def test_main(self):
1093        self.assertEqual(NumericPartTextFrame(text='1/2').text, ["1/2"])
1094        self.assertEqual(+NumericPartTextFrame(text='1/2'), 1)
1095
1096
1097class Tread_frames_load_frame(TestCase):
1098
1099    def test_detect_23_ints_in_24_frames(self):
1100        head = b'TIT1\x00\x00\x01\x00\x00\x00\x00'
1101        tail = b'TPE1\x00\x00\x00\x05\x00\x00\x00Yay!'
1102
1103        tagsgood = read_frames(_24, head + b'a' * 127 + tail, Frames)[0]
1104        tagsbad = read_frames(_24, head + b'a' * 255 + tail, Frames)[0]
1105        self.assertEquals(2, len(tagsgood))
1106        self.assertEquals(2, len(tagsbad))
1107        self.assertEquals('a' * 127, tagsgood[0])
1108        self.assertEquals('a' * 255, tagsbad[0])
1109        self.assertEquals('Yay!', tagsgood[1])
1110        self.assertEquals('Yay!', tagsbad[1])
1111
1112        tagsgood = read_frames(_24, head + b'a' * 127, Frames)[0]
1113        tagsbad = read_frames(_24, head + b'a' * 255, Frames)[0]
1114        self.assertEquals(1, len(tagsgood))
1115        self.assertEquals(1, len(tagsbad))
1116        self.assertEquals('a' * 127, tagsgood[0])
1117        self.assertEquals('a' * 255, tagsbad[0])
1118
1119    def test_zerolength_framedata(self):
1120        tail = b'\x00' * 6
1121        for head in b'WOAR TENC TCOP TOPE WXXX'.split():
1122            data = head + tail
1123            self.assertEquals(
1124                0, len(list(read_frames(_24, data, Frames)[1])))
1125
1126    def test_drops_truncated_frames(self):
1127        tail = b'\x00\x00\x00\x03\x00\x00' b'\x01\x02\x03'
1128        for head in b'RVA2 TXXX APIC'.split():
1129            data = head + tail
1130            self.assertEquals(
1131                0, len(read_frames(_24, data, Frames)[1]))
1132
1133    def test_drops_nonalphanum_frames(self):
1134        tail = b'\x00\x00\x00\x03\x00\x00' b'\x01\x02\x03'
1135        for head in [b'\x06\xaf\xfe\x20', b'ABC\x00', b'A   ']:
1136            data = head + tail
1137            self.assertEquals(
1138                0, len(read_frames(_24, data, Frames)[0]))
1139
1140    def test_frame_too_small(self):
1141        self.assertEquals([], read_frames(_24, b'012345678', Frames)[0])
1142        self.assertEquals([], read_frames(_23, b'012345678', Frames)[0])
1143        self.assertEquals([], read_frames(_22, b'01234', Frames_2_2)[0])
1144        self.assertEquals(
1145            [], read_frames(_22, b'TT1' + b'\x00' * 3, Frames_2_2)[0])
1146
1147    def test_unknown_22_frame(self):
1148        data = b'XYZ\x00\x00\x01\x00'
1149        self.assertEquals([data], read_frames(_22, data, {})[1])
1150
1151    def test_22_uses_direct_ints(self):
1152        data = b'TT1\x00\x00\x83\x00' + (b'123456789abcdef' * 16)
1153        tag = read_frames(_22, data, Frames_2_2)[0][0]
1154        self.assertEquals(data[7:7 + 0x82].decode('latin1'), tag.text[0])
1155
1156    def test_load_write(self):
1157        artists = [s.decode('utf8') for s in
1158                   [b'\xc2\xb5', b'\xe6\x97\xa5\xe6\x9c\xac']]
1159        artist = TPE1(encoding=3, text=artists)
1160        config = ID3SaveConfig()
1161        tag = read_frames(_24, save_frame(artist, config=config), Frames)[0][0]
1162        self.assertEquals('TPE1', type(tag).__name__)
1163        self.assertEquals(artist.text, tag.text)
1164
1165
1166class TTPE2(TestCase):
1167
1168    def test_unsynch(self):
1169        header = ID3Header()
1170        header.version = (2, 4, 0)
1171        header._flags = 0x80
1172        badsync = b'\x00\xff\x00ab\x00'
1173
1174        self.assertEquals(TPE2._fromData(header, 0, badsync), [u"\xffab"])
1175
1176        header._flags = 0x00
1177        self.assertEquals(TPE2._fromData(header, 0x02, badsync), [u"\xffab"])
1178
1179        tag = TPE2._fromData(header, 0, badsync)
1180        self.assertEquals(tag, [u"\xff", u"ab"])
1181
1182
1183class TTPE1(TestCase):
1184
1185    def test_badencoding(self):
1186        self.assertRaises(
1187            ID3JunkFrameError, TPE1._fromData, _24, 0, b"\x09ab")
1188        self.assertRaises(ValueError, TPE1, encoding=9, text="ab")
1189
1190    def test_badsync(self):
1191        frame = TPE1._fromData(_24, 0x02, b"\x00\xff\xfe")
1192        self.assertEqual(frame.text, [u'\xff\xfe'])
1193
1194    def test_noencrypt(self):
1195        self.assertRaises(
1196            NotImplementedError, TPE1._fromData, _24, 0x04, b"\x00")
1197        self.assertRaises(
1198            NotImplementedError, TPE1._fromData, _23, 0x40, b"\x00")
1199
1200    def test_badcompress(self):
1201        self.assertRaises(
1202            ID3JunkFrameError, TPE1._fromData, _24, 0x08,
1203            b"\x00\x00\x00\x00#")
1204        self.assertRaises(
1205            ID3JunkFrameError, TPE1._fromData, _23, 0x80,
1206            b"\x00\x00\x00\x00#")
1207
1208    def test_junkframe(self):
1209        self.assertRaises(
1210            ID3JunkFrameError, TPE1._fromData, _24, 0, b"")
1211        self.assertRaises(
1212            ID3JunkFrameError, TPE1._fromData, _24, 0, b'\x03A\xff\xfe')
1213
1214    def test_lengthone_utf16(self):
1215        tpe1 = TPE1._fromData(_24, 0, b'\x01\x00')
1216        self.assertEquals(u'', tpe1)
1217        tpe1 = TPE1._fromData(_24, 0, b'\x01\x00\x00\x00\x00')
1218        self.assertEquals([u'', u''], tpe1)
1219
1220    def test_utf16_wrongnullterm(self):
1221        # issue 169
1222        tpe1 = TPE1._fromData(
1223            _24, 0, b'\x01\xff\xfeH\x00e\x00l\x00l\x00o\x00\x00')
1224        self.assertEquals(tpe1, [u'Hello'])
1225
1226        tpe1 = TPE1._fromData(
1227            _24, 0, b'\x02\x00H\x00e\x00l\x00l\x00o\x00')
1228        self.assertEquals(tpe1, [u'Hello'])
1229
1230    def test_utf_16_missing_bom(self):
1231        tpe1 = TPE1._fromData(
1232            _24, 0, b'\x01H\x00e\x00l\x00l\x00o\x00\x00\x00')
1233        self.assertEquals(tpe1, [u'Hello'])
1234
1235    def test_utf_16_missing_bom_wrong_nullterm(self):
1236        tpe1 = TPE1._fromData(
1237            _24, 0, b'\x01H\x00e\x00l\x00l\x00o\x00\x00')
1238        self.assertEquals(tpe1, [u'Hello'])
1239
1240        tpe1 = TPE1._fromData(
1241            _24, 0, b'\x01f\x00o\x00o\x00\x00\x00b\x00a\x00r\x00\x00')
1242        self.assertEquals(tpe1, [u"foo", u"bar"])
1243
1244    def test_zlib_bpi(self):
1245        tpe1 = TPE1(encoding=0, text="a" * (0xFFFF - 2))
1246        data = save_frame(tpe1)
1247        datalen_size = data[4 + 4 + 2:4 + 4 + 2 + 4]
1248        self.failIf(
1249            max(datalen_size) >= b'\x80'[0], "data is not syncsafe: %r" % data)
1250
1251    def test_ql_0_12_missing_uncompressed_size(self):
1252        tag = TPE1._fromData(
1253            _24, 0x08,
1254            b'x\x9cc\xfc\xff\xaf\x84!\x83!\x93'
1255            b'\xa1\x98A\x01J&2\xe83\x940\xa4\x02\xd9%\x0c\x00\x87\xc6\x07#'
1256        )
1257        self.assertEquals(tag.encoding, 1)
1258        self.assertEquals(tag, ['this is a/test'])
1259
1260    def test_zlib_latin1_missing_datalen(self):
1261        tag = TPE1._fromData(
1262            _24, 0x8,
1263            b'\x00\x00\x00\x0f'
1264            b'x\x9cc(\xc9\xc8,V\x00\xa2D\xfd\x92\xd4\xe2\x12\x00&\x7f\x05%'
1265        )
1266        self.assertEquals(tag.encoding, 0)
1267        self.assertEquals(tag, ['this is a/test'])
1268
1269
1270class TTCON(TestCase):
1271
1272    def _g(self, s):
1273        return TCON(text=s).genres
1274
1275    def test_empty(self):
1276        self.assertEquals(self._g(""), [])
1277
1278    def test_num(self):
1279        for i in xrange(len(GENRES)):
1280            self.assertEquals(self._g("%02d" % i), [GENRES[i]])
1281
1282    def test_parened_num(self):
1283        for i in xrange(len(GENRES)):
1284            self.assertEquals(self._g("(%02d)" % i), [GENRES[i]])
1285
1286    def test_unknown(self):
1287        self.assertEquals(self._g("(255)"), ["Unknown"])
1288        self.assertEquals(self._g("199"), ["Unknown"])
1289        self.assertNotEqual(self._g("256"), ["Unknown"])
1290
1291    def test_parened_multi(self):
1292        self.assertEquals(self._g("(00)(02)"), ["Blues", "Country"])
1293
1294    def test_coverremix(self):
1295        self.assertEquals(self._g("CR"), ["Cover"])
1296        self.assertEquals(self._g("(CR)"), ["Cover"])
1297        self.assertEquals(self._g("RX"), ["Remix"])
1298        self.assertEquals(self._g("(RX)"), ["Remix"])
1299
1300    def test_parened_text(self):
1301        self.assertEquals(
1302            self._g("(00)(02)Real Folk Blues"),
1303            ["Blues", "Country", "Real Folk Blues"])
1304
1305    def test_escape(self):
1306        self.assertEquals(self._g("(0)((A genre)"), ["Blues", "(A genre)"])
1307        self.assertEquals(self._g("(10)((20)"), ["New Age", "(20)"])
1308
1309    def test_nullsep(self):
1310        self.assertEquals(self._g("0\x00A genre"), ["Blues", "A genre"])
1311
1312    def test_nullsep_empty(self):
1313        self.assertEquals(self._g("\x000\x00A genre"), ["Blues", "A genre"])
1314
1315    def test_crazy(self):
1316        self.assertEquals(
1317            self._g("(20)(CR)\x0030\x00\x00Another\x00(51)Hooray"),
1318            ['Alternative', 'Cover', 'Fusion', 'Another',
1319             'Techno-Industrial', 'Hooray'])
1320
1321    def test_repeat(self):
1322        self.assertEquals(self._g("(20)Alternative"), ["Alternative"])
1323        self.assertEquals(
1324            self._g("(20)\x00Alternative"), ["Alternative", "Alternative"])
1325
1326    def test_set_genre(self):
1327        gen = TCON(encoding=0, text="")
1328        self.assertEquals(gen.genres, [])
1329        gen.genres = ["a genre", "another"]
1330        self.assertEquals(gen.genres, ["a genre", "another"])
1331
1332    def test_set_string(self):
1333        gen = TCON(encoding=0, text="")
1334        gen.genres = "foo"
1335        self.assertEquals(gen.genres, ["foo"])
1336
1337    def test_nodoubledecode(self):
1338        gen = TCON(encoding=1, text=u"(255)genre")
1339        gen.genres = gen.genres
1340        self.assertEquals(gen.genres, [u"Unknown", u"genre"])
1341
1342
1343class TID3TimeStamp(TestCase):
1344
1345    def test_Y(self):
1346        s = ID3TimeStamp('1234')
1347        self.assertEquals(s.year, 1234)
1348        self.assertEquals(s.text, '1234')
1349
1350    def test_yM(self):
1351        s = ID3TimeStamp('1234-56')
1352        self.assertEquals(s.year, 1234)
1353        self.assertEquals(s.month, 56)
1354        self.assertEquals(s.text, '1234-56')
1355
1356    def test_ymD(self):
1357        s = ID3TimeStamp('1234-56-78')
1358        self.assertEquals(s.year, 1234)
1359        self.assertEquals(s.month, 56)
1360        self.assertEquals(s.day, 78)
1361        self.assertEquals(s.text, '1234-56-78')
1362
1363    def test_ymdH(self):
1364        s = ID3TimeStamp('1234-56-78T12')
1365        self.assertEquals(s.year, 1234)
1366        self.assertEquals(s.month, 56)
1367        self.assertEquals(s.day, 78)
1368        self.assertEquals(s.hour, 12)
1369        self.assertEquals(s.text, '1234-56-78 12')
1370
1371    def test_ymdhM(self):
1372        s = ID3TimeStamp('1234-56-78T12:34')
1373        self.assertEquals(s.year, 1234)
1374        self.assertEquals(s.month, 56)
1375        self.assertEquals(s.day, 78)
1376        self.assertEquals(s.hour, 12)
1377        self.assertEquals(s.minute, 34)
1378        self.assertEquals(s.text, '1234-56-78 12:34')
1379
1380    def test_ymdhmS(self):
1381        s = ID3TimeStamp('1234-56-78T12:34:56')
1382        self.assertEquals(s.year, 1234)
1383        self.assertEquals(s.month, 56)
1384        self.assertEquals(s.day, 78)
1385        self.assertEquals(s.hour, 12)
1386        self.assertEquals(s.minute, 34)
1387        self.assertEquals(s.second, 56)
1388        self.assertEquals(s.text, '1234-56-78 12:34:56')
1389
1390    def test_Ymdhms(self):
1391        s = ID3TimeStamp('1234-56-78T12:34:56')
1392        s.month = None
1393        self.assertEquals(s.text, '1234')
1394
1395    def test_alternate_reprs(self):
1396        s = ID3TimeStamp('1234-56.78 12:34:56')
1397        self.assertEquals(s.text, '1234-56-78 12:34:56')
1398
1399    def test_order(self):
1400        s = ID3TimeStamp('1234')
1401        t = ID3TimeStamp('1233-12')
1402        u = ID3TimeStamp('1234-01')
1403
1404        self.assert_(t < s < u)
1405        self.assert_(u > s > t)
1406
1407    def test_types(self):
1408        if PY3:
1409            self.assertRaises(TypeError, ID3TimeStamp, b"blah")
1410        self.assertEquals(
1411            text_type(ID3TimeStamp(u"2000-01-01")), u"2000-01-01")
1412        self.assertEquals(
1413            bytes(ID3TimeStamp(u"2000-01-01")), b"2000-01-01")
1414
1415
1416class TFrameTest(object):
1417
1418    FRAME = None
1419
1420    def test_has_doc(self):
1421        self.failUnless(self.FRAME.__doc__, "%s has no docstring" % self.FRAME)
1422
1423    def test_fake_zlib(self):
1424        header = ID3Header()
1425        header.version = (2, 4, 0)
1426        self.assertRaises(ID3JunkFrameError, self.FRAME._fromData, header,
1427                          Frame.FLAG24_COMPRESS, b'\x03abcdefg')
1428
1429    def test_no_hash(self):
1430        self.failUnlessRaises(
1431            TypeError, {}.__setitem__, self.FRAME(), None)
1432
1433    def test_is_valid_frame_id(self):
1434        self.assertTrue(is_valid_frame_id(self.FRAME.__name__))
1435
1436    def test_all_specs_have_default(self):
1437        for spec in self.FRAME._framespec:
1438            self.assertTrue(
1439                spec.default is not None,
1440                msg="%r:%r" % (self.FRAME, spec.name))
1441
1442    @classmethod
1443    def create_frame_tests(cls):
1444        for kind in (list(Frames.values()) + list(Frames_2_2.values())):
1445            new_type = type(cls.__name__ + kind.__name__,
1446                            (cls, TestCase), {"FRAME": kind})
1447            assert new_type.__name__ not in globals()
1448            globals()[new_type.__name__] = new_type
1449
1450
1451TFrameTest.create_frame_tests()
1452
1453
1454class FrameIDValidate(TestCase):
1455
1456    def test_valid(self):
1457        self.failUnless(is_valid_frame_id("APIC"))
1458        self.failUnless(is_valid_frame_id("TPE2"))
1459
1460    def test_invalid(self):
1461        self.failIf(is_valid_frame_id("MP3e"))
1462        self.failIf(is_valid_frame_id("+ABC"))
1463
1464
1465class TTimeStampTextFrame(TestCase):
1466
1467    def test_default(self):
1468        self.assertEqual(
1469            TimeStampTextFrame(), TimeStampTextFrame(encoding=1, text=[]))
1470
1471    def test_compare_to_unicode(self):
1472        frame = TimeStampTextFrame(encoding=0, text=[u'1987', u'1988'])
1473        self.failUnlessEqual(frame, text_type(frame))
1474
1475
1476class TTextFrame(TestCase):
1477
1478    def test_defaults(self):
1479        self.assertEqual(TextFrame(), TextFrame(encoding=1, text=[]))
1480
1481    def test_default_default_mutable(self):
1482        frame = TextFrame()
1483        frame.text.append("foo")
1484        self.assertEqual(TextFrame().text, [])
1485
1486    def test_main(self):
1487        self.assertEqual(TextFrame(text='text').text, ["text"])
1488        self.assertEqual(TextFrame(text=['a', 'b']).text, ["a", "b"])
1489
1490    def test_multi_value(self):
1491        frame = TextFrame(
1492            text=[u"foo", u"", u"", u"bar", u"", u""], encoding=0)
1493        config = ID3SaveConfig(3, None)
1494        data = frame._writeData(config)
1495
1496        frame = frame._fromData(_24, 0x0, data)
1497        self.assertEqual(frame.text, [u"foo", u"", u"", u"bar", u"", u""])
1498        frame = frame._fromData(_23, 0x0, data)
1499        self.assertEqual(frame.text, [u"foo", u"", u"", u"bar"])
1500        frame = frame._fromData(_22, 0x0, data)
1501        self.assertEqual(frame.text, [u"foo", u"", u"", u"bar"])
1502
1503    def test_list_iface(self):
1504        frame = TextFrame()
1505        frame.append("a")
1506        frame.extend(["b", "c"])
1507        self.assertEqual(frame.text, ["a", "b", "c"])
1508
1509    def test_zlib_latin1(self):
1510        tag = TextFrame._fromData(
1511            _24, 0x9, b'\x00\x00\x00\x0f'
1512            b'x\x9cc(\xc9\xc8,V\x00\xa2D\xfd\x92\xd4\xe2\x12\x00&\x7f\x05%'
1513        )
1514        self.assertEquals(tag.encoding, 0)
1515        self.assertEquals(tag, ['this is a/test'])
1516
1517    def test_datalen_but_not_compressed(self):
1518        tag = TextFrame._fromData(_24, 0x01, b'\x00\x00\x00\x06\x00A test')
1519        self.assertEquals(tag.encoding, 0)
1520        self.assertEquals(tag, ['A test'])
1521
1522    def test_utf8(self):
1523        tag = TextFrame._fromData(_23, 0x00, b'\x03this is a test')
1524        self.assertEquals(tag.encoding, 3)
1525        self.assertEquals(tag, 'this is a test')
1526
1527    def test_zlib_utf16(self):
1528        data = (b'\x00\x00\x00\x1fx\x9cc\xfc\xff\xaf\x84!\x83!\x93\xa1\x98A'
1529                b'\x01J&2\xe83\x940\xa4\x02\xd9%\x0c\x00\x87\xc6\x07#')
1530        tag = TextFrame._fromData(_23, 0x80, data)
1531        self.assertEquals(tag.encoding, 1)
1532        self.assertEquals(tag, ['this is a/test'])
1533
1534        tag = TextFrame._fromData(_24, 0x08, data)
1535        self.assertEquals(tag.encoding, 1)
1536        self.assertEquals(tag, ['this is a/test'])
1537
1538
1539class TRVA2(TestCase):
1540
1541    def test_default(self):
1542        frame = RVA2()
1543        self.assertEqual(frame.desc, u"")
1544        self.assertEqual(frame.channel, 1)
1545        self.assertEqual(frame.gain, 1)
1546        self.assertEqual(frame.peak, 1)
1547
1548    def test_basic(self):
1549        r = RVA2(gain=1, channel=1, peak=1)
1550        self.assertEqual(r, r)
1551        self.assertNotEqual(r, 42)
1552
1553    def test_hash_key(self):
1554        frame = RVA2(method=42, desc="d", channel=1, gain=1, peak=1)
1555        self.assertEqual(frame.HashKey, "RVA2:d")
1556
1557        self.assertEquals(RVA2(gain=1).HashKey, RVA2(gain=2).HashKey)
1558        self.assertNotEquals(RVA2(desc="a").HashKey, RVA2(desc="b").HashKey)
1559
1560    def test_pprint(self):
1561        frame = RVA2(method=42, desc="d", channel=1, gain=1, peak=1)
1562        frame._pprint()
1563
1564    def test_wacky_truncated(self):
1565        data = b'\x01{\xf0\x10\xff\xff\x00'
1566        self.assertRaises(ID3JunkFrameError, RVA2._fromData, _24, 0x00, data)
1567
1568    def test_bad_number_of_bits(self):
1569        data = b'\x00\x00\x01\xe6\xfc\x10{\xd7'
1570        self.assertRaises(ID3JunkFrameError, RVA2._fromData, _24, 0x00, data)
1571
1572
1573class TCTOC(TestCase):
1574
1575    def test_defaults(self):
1576        self.assertEqual(CTOC(), CTOC(element_id=u"", flags=0,
1577                                      child_element_ids=[], sub_frames=[]))
1578
1579    def test_hash(self):
1580        frame = CTOC(element_id=u"foo", flags=3,
1581                     child_element_ids=[u"ch0"],
1582                     sub_frames=[TPE2(encoding=3, text=[u"foo"])])
1583        self.assertEqual(frame.HashKey, "CTOC:foo")
1584
1585    def test_pprint(self):
1586        frame = CTOC(element_id=u"foo", flags=3,
1587                     child_element_ids=[u"ch0"],
1588                     sub_frames=[TPE2(encoding=3, text=[u"foo"])])
1589        self.assertEqual(
1590            frame.pprint(),
1591            "CTOC=foo flags=3 child_element_ids=ch0\n    TPE2=foo")
1592
1593    def test_write(self):
1594        frame = CTOC(element_id=u"foo", flags=3,
1595                     child_element_ids=[u"ch0"],
1596                     sub_frames=[TPE2(encoding=3, text=[u"f", u"b"])])
1597        config = ID3SaveConfig(3, "/")
1598        data = (b"foo\x00\x03\x01ch0\x00TPE2\x00\x00\x00\x0b\x00\x00\x01"
1599                b"\xff\xfef\x00/\x00b\x00\x00\x00")
1600        self.assertEqual(frame._writeData(config), data)
1601
1602    def test_eq(self):
1603        self.assertEqual(CTOC(), CTOC())
1604        self.assertNotEqual(CTOC(), object())
1605
1606
1607class TASPI(TestCase):
1608
1609    def test_default(self):
1610        frame = ASPI()
1611        self.assertEqual(frame.S, 0)
1612        self.assertEqual(frame.L, 0)
1613        self.assertEqual(frame.N, 0)
1614        self.assertEqual(frame.b, 0)
1615        self.assertEqual(frame.Fi, [])
1616
1617    def test_default_default_mutable(self):
1618        frame = ASPI()
1619        frame.Fi.append(1)
1620        self.assertEqual(ASPI().Fi, [])
1621
1622
1623class TCHAP(TestCase):
1624
1625    def test_default(self):
1626        frame = CHAP()
1627        self.assertEqual(frame.element_id, u"")
1628        self.assertEqual(frame.start_time, 0)
1629        self.assertEqual(frame.end_time, 0)
1630        self.assertEqual(frame.start_offset, 0xffffffff)
1631        self.assertEqual(frame.end_offset, 0xffffffff)
1632        self.assertEqual(frame.sub_frames, ID3Tags())
1633
1634    def test_hash(self):
1635        frame = CHAP(element_id=u"foo", start_time=0, end_time=0,
1636                     start_offset=0, end_offset=0,
1637                     sub_frames=[TPE2(encoding=3, text=[u"foo"])])
1638        self.assertEqual(frame.HashKey, "CHAP:foo")
1639
1640    def test_pprint(self):
1641        frame = CHAP(element_id=u"foo", start_time=0, end_time=0,
1642                     start_offset=0, end_offset=0,
1643                     sub_frames=[TPE2(encoding=3, text=[u"foo"])])
1644        self.assertEqual(
1645            frame.pprint(), "CHAP=foo time=0..0 offset=0..0\n    TPE2=foo")
1646
1647    def test_eq(self):
1648        self.assertEqual(CHAP(), CHAP())
1649        self.assertNotEqual(CHAP(), object())
1650
1651
1652class TPCNT(TestCase):
1653
1654    def test_default(self):
1655        frame = PCNT()
1656        self.assertEqual(frame.count, 0)
1657
1658
1659class TAPIC(TestCase):
1660
1661    def test_default(self):
1662        frame = APIC()
1663        self.assertEqual(frame.encoding, 1)
1664        self.assertEqual(frame.mime, u"")
1665        self.assertEqual(frame.type, 3)
1666        self.assertEqual(frame.desc, u"")
1667        self.assertEqual(frame.data, b"")
1668
1669    def test_hash(self):
1670        frame = APIC(encoding=0, mime=u"m", type=3, desc=u"d", data=b"\x42")
1671        self.assertEqual(frame.HashKey, "APIC:d")
1672
1673    def test_pprint(self):
1674        frame = APIC(
1675            encoding=0, mime=u"mime", type=3, desc=u"desc", data=b"\x42")
1676        self.assertEqual(frame._pprint(), u"cover front, desc (mime, 1 bytes)")
1677
1678    def test_multi(self):
1679        self.assertEquals(APIC(data=b"1").HashKey, APIC(data=b"2").HashKey)
1680        self.assertNotEquals(APIC(desc="a").HashKey, APIC(desc="b").HashKey)
1681
1682    def test_repr(self):
1683        frame = APIC(encoding=0, mime=u"m", type=3, desc=u"d", data=b"\x42")
1684        if PY2:
1685            expected = (
1686                "APIC(encoding=<Encoding.LATIN1: 0>, mime=u'm', "
1687                "type=<PictureType.COVER_FRONT: 3>, desc=u'd', data='B')")
1688        else:
1689            expected = (
1690                "APIC(encoding=<Encoding.LATIN1: 0>, mime='m', "
1691                "type=<PictureType.COVER_FRONT: 3>, desc='d', data=b'B')")
1692
1693        self.assertEqual(repr(frame), expected)
1694        new_frame = APIC()
1695        new_frame._readData(_24, frame._writeData())
1696        self.assertEqual(repr(new_frame), expected)
1697