~ubuntu-branches/ubuntu/utopic/mutagen/utopic-proposed

« back to all changes in this revision

Viewing changes to mutagen/id3.py

  • Committer: Package Import Robot
  • Author(s): Daniel T Chen
  • Date: 2013-11-27 22:10:48 UTC
  • mfrom: (8.1.17 sid)
  • Revision ID: package-import@ubuntu.com-20131127221048-ae2f5j42ak2ox3kw
Tags: 1.22-1ubuntu1
* Merge from Debian unstable.  Remaining changes:
  - debian/control: Drop faad and oggz-tools build dependencies (in
    universe).

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
1
# id3 support for mutagen
2
2
# Copyright (C) 2005  Michael Urman
 
3
#               2006  Lukas Lalinsky
 
4
#               2013  Christoph Reiter
3
5
#
4
6
# This program is free software; you can redistribute it and/or modify
5
7
# it under the terms of version 2 of the GNU General Public License as
6
8
# published by the Free Software Foundation.
7
 
#
8
 
# $Id: id3.py 4285 2008-09-06 08:01:31Z piman $
9
9
 
10
10
"""ID3v2 reading and writing.
11
11
 
12
12
This is based off of the following references:
13
 
   http://www.id3.org/id3v2.4.0-structure.txt
14
 
   http://www.id3.org/id3v2.4.0-frames.txt
15
 
   http://www.id3.org/id3v2.3.0.html
16
 
   http://www.id3.org/id3v2-00.txt
17
 
   http://www.id3.org/id3v1.html
 
13
 
 
14
* http://id3.org/id3v2.4.0-structure
 
15
* http://id3.org/id3v2.4.0-frames
 
16
* http://id3.org/id3v2.3.0
 
17
* http://id3.org/id3v2-00
 
18
* http://id3.org/ID3v1
18
19
 
19
20
Its largest deviation from the above (versions 2.3 and 2.2) is that it
20
21
will not interpret the / characters as a separator, and will almost
25
26
frame's documentation contains a list of its attributes.
26
27
 
27
28
Since this file's documentation is a little unwieldy, you are probably
28
 
interested in the 'ID3' class to start with.
 
29
interested in the :class:`ID3` class to start with.
29
30
"""
30
31
 
31
32
__all__ = ['ID3', 'ID3FileType', 'Frames', 'Open', 'delete']
33
34
import struct
34
35
 
35
36
from struct import unpack, pack, error as StructError
36
 
from zlib import error as zlibError
37
 
from warnings import warn
38
37
 
39
38
import mutagen
40
39
from mutagen._util import insert_bytes, delete_bytes, DictProxy
41
40
 
42
 
class error(Exception): pass
43
 
class ID3NoHeaderError(error, ValueError): pass
44
 
class ID3BadUnsynchData(error, ValueError): pass
45
 
class ID3BadCompressedData(error, ValueError): pass
46
 
class ID3TagError(error, ValueError): pass
47
 
class ID3UnsupportedVersionError(error, NotImplementedError): pass
48
 
class ID3EncryptionUnsupportedError(error, NotImplementedError): pass
49
 
class ID3JunkFrameError(error, ValueError): pass
50
 
 
51
 
class ID3Warning(error, UserWarning): pass
52
 
 
53
 
def is_valid_frame_id(frame_id):
54
 
    return frame_id.isalnum() and frame_id.isupper()
 
41
from mutagen._id3util import *
 
42
from mutagen._id3frames import *
 
43
from mutagen._id3specs import *
 
44
 
55
45
 
56
46
class ID3(DictProxy, mutagen.Metadata):
57
47
    """A file with an ID3v2 tag.
58
48
 
59
49
    Attributes:
60
 
    version -- ID3 tag version as a tuple
61
 
    unknown_frames -- raw frame data of any unknown frames found
62
 
    size -- the total size of the ID3 tag, including the header
 
50
 
 
51
    * version -- ID3 tag version as a tuple
 
52
    * unknown_frames -- raw frame data of any unknown frames found
 
53
    * size -- the total size of the ID3 tag, including the header
63
54
    """
64
55
 
65
56
    PEDANTIC = True
70
61
    __flags = 0
71
62
    __readbytes = 0
72
63
    __crc = None
73
 
    __unknown_updated = False
 
64
    __unknown_version = None
 
65
 
 
66
    _V24 = (2, 4, 0)
 
67
    _V23 = (2, 3, 0)
 
68
    _V22 = (2, 2, 0)
 
69
    _V11 = (1, 1)
74
70
 
75
71
    def __init__(self, *args, **kwargs):
76
72
        self.unknown_frames = []
81
77
            if size < 0:
82
78
                raise ValueError('Requested bytes (%s) less than zero' % size)
83
79
            if size > self.__filesize:
84
 
                raise EOFError('Requested %#x of %#x (%s)' % 
85
 
                        (long(size), long(self.__filesize), self.filename))
86
 
        except AttributeError: pass
 
80
                raise EOFError('Requested %#x of %#x (%s)' % (
 
81
                    long(size), long(self.__filesize), self.filename))
 
82
        except AttributeError:
 
83
            pass
87
84
        data = self.__fileobj.read(size)
88
 
        if len(data) != size: raise EOFError
 
85
        if len(data) != size:
 
86
            raise EOFError
89
87
        self.__readbytes += size
90
88
        return data
91
89
 
92
 
    def load(self, filename, known_frames=None, translate=True):
 
90
    def load(self, filename, known_frames=None, translate=True, v2_version=4):
93
91
        """Load tags from a filename.
94
92
 
95
93
        Keyword arguments:
96
 
        filename -- filename to load tag data from
97
 
        known_frames -- dict mapping frame IDs to Frame objects
98
 
        translate -- Update all tags to ID3v2.4 internally. Mutagen is
99
 
                     only capable of writing ID3v2.4 tags, so if you
100
 
                     intend to save, this must be true.
101
 
 
102
 
        Example of loading a custom frame:
 
94
 
 
95
        * filename -- filename to load tag data from
 
96
        * known_frames -- dict mapping frame IDs to Frame objects
 
97
        * translate -- Update all tags to ID3v2.3/4 internally. If you
 
98
                       intend to save, this must be true or you have to
 
99
                       call update_to_v23() / update_to_v24() manually.
 
100
        * v2_version -- if update_to_v23 or update_to_v24 get called (3 or 4)
 
101
 
 
102
        Example of loading a custom frame::
 
103
 
103
104
            my_frames = dict(mutagen.id3.Frames)
104
105
            class XMYF(Frame): ...
105
106
            my_frames["XMYF"] = XMYF
106
107
            mutagen.id3.ID3(filename, known_frames=my_frames)
107
108
        """
108
109
 
 
110
        if not v2_version in (3, 4):
 
111
            raise ValueError("Only 3 and 4 possible for v2_version")
 
112
 
109
113
        from os.path import getsize
 
114
 
110
115
        self.filename = filename
111
116
        self.__known_frames = known_frames
112
117
        self.__fileobj = open(filename, 'rb')
116
121
                self.__load_header()
117
122
            except EOFError:
118
123
                self.size = 0
119
 
                raise ID3NoHeaderError("%s: too small (%d bytes)" %(
 
124
                raise ID3NoHeaderError("%s: too small (%d bytes)" % (
120
125
                    filename, self.__filesize))
121
126
            except (ID3NoHeaderError, ID3UnsupportedVersionError), err:
122
127
                self.size = 0
123
128
                import sys
124
129
                stack = sys.exc_info()[2]
125
 
                try: self.__fileobj.seek(-128, 2)
126
 
                except EnvironmentError: raise err, None, stack
 
130
                try:
 
131
                    self.__fileobj.seek(-128, 2)
 
132
                except EnvironmentError:
 
133
                    raise err, None, stack
127
134
                else:
128
135
                    frames = ParseID3v1(self.__fileobj.read(128))
129
136
                    if frames is not None:
130
 
                        self.version = (1, 1)
 
137
                        self.version = self._V11
131
138
                        map(self.add, frames.values())
132
 
                    else: raise err, None, stack
 
139
                    else:
 
140
                        raise err, None, stack
133
141
            else:
134
142
                frames = self.__known_frames
135
143
                if frames is None:
136
 
                    if (2,3,0) <= self.version: frames = Frames
137
 
                    elif (2,2,0) <= self.version: frames = Frames_2_2
 
144
                    if self._V23 <= self.version:
 
145
                        frames = Frames
 
146
                    elif self._V22 <= self.version:
 
147
                        frames = Frames_2_2
138
148
                data = self.__fullread(self.size - 10)
139
149
                for frame in self.__read_frames(data, frames=frames):
140
 
                    if isinstance(frame, Frame): self.add(frame)
141
 
                    else: self.unknown_frames.append(frame)
 
150
                    if isinstance(frame, Frame):
 
151
                        self.add(frame)
 
152
                    else:
 
153
                        self.unknown_frames.append(frame)
 
154
                self.__unknown_version = self.version
142
155
        finally:
143
156
            self.__fileobj.close()
144
157
            del self.__fileobj
145
158
            del self.__filesize
146
159
            if translate:
147
 
                self.update_to_v24()
 
160
                if v2_version == 3:
 
161
                    self.update_to_v23()
 
162
                else:
 
163
                    self.update_to_v24()
148
164
 
149
165
    def getall(self, key):
150
166
        """Return all frames with a given name (the list may be empty).
151
167
 
152
 
        This is best explained by examples:
 
168
        This is best explained by examples::
 
169
 
153
170
            id3.getall('TIT2') == [id3['TIT2']]
154
171
            id3.getall('TTTT') == []
155
172
            id3.getall('TXXX') == [TXXX(desc='woo', text='bar'),
157
174
 
158
175
        Since this is based on the frame's HashKey, which is
159
176
        colon-separated, you can use it to do things like
160
 
        getall('COMM:MusicMatch') or getall('TXXX:QuodLibet:').
 
177
        ``getall('COMM:MusicMatch')`` or ``getall('TXXX:QuodLibet:')``.
161
178
        """
162
 
        if key in self: return [self[key]]
 
179
        if key in self:
 
180
            return [self[key]]
163
181
        else:
164
182
            key = key + ":"
165
 
            return [v for s,v in self.items() if s.startswith(key)]
 
183
            return [v for s, v in self.items() if s.startswith(key)]
166
184
 
167
185
    def delall(self, key):
168
186
        """Delete all tags of a given kind; see getall."""
169
 
        if key in self: del(self[key])
 
187
        if key in self:
 
188
            del(self[key])
170
189
        else:
171
190
            key = key + ":"
172
191
            for k in filter(lambda s: s.startswith(key), self.keys()):
183
202
 
184
203
        "Human-readable" is used loosely here. The format is intended
185
204
        to mirror that used for Vorbis or APEv2 output, e.g.
186
 
            TIT2=My Title
 
205
 
 
206
            ``TIT2=My Title``
 
207
 
187
208
        However, ID3 frames can have multiple keys:
188
 
            POPM=user@example.org=3 128/255
 
209
 
 
210
            ``POPM=user@example.org=3 128/255``
189
211
        """
190
212
        frames = list(map(Frame.pprint, self.values()))
191
213
        frames.sort()
194
216
    def loaded_frame(self, tag):
195
217
        """Deprecated; use the add method."""
196
218
        # turn 2.2 into 2.3/2.4 tags
197
 
        if len(type(tag).__name__) == 3: tag = type(tag).__base__(tag)
 
219
        if len(type(tag).__name__) == 3:
 
220
            tag = type(tag).__base__(tag)
198
221
        self[tag.HashKey] = tag
199
222
 
200
223
    # add = loaded_frame (and vice versa) break applications that
216
239
            raise ID3NoHeaderError("'%s' doesn't start with an ID3 tag" % fn)
217
240
        if vmaj not in [2, 3, 4]:
218
241
            raise ID3UnsupportedVersionError("'%s' ID3v2.%d not supported"
219
 
                    % (fn, vmaj))
 
242
                                             % (fn, vmaj))
220
243
 
221
244
        if self.PEDANTIC:
222
 
            if (2,4,0) <= self.version and (flags & 0x0f):
 
245
            if not BitPaddedInt.has_valid_padding(size):
 
246
                raise ValueError("Header size not synchsafe")
 
247
 
 
248
            if self._V24 <= self.version and (flags & 0x0f):
223
249
                raise ValueError("'%s' has invalid flags %#02x" % (fn, flags))
224
 
            elif (2,3,0) <= self.version < (2,4,0) and (flags & 0x1f):
 
250
            elif self._V23 <= self.version < self._V24 and (flags & 0x1f):
225
251
                raise ValueError("'%s' has invalid flags %#02x" % (fn, flags))
226
252
 
227
253
        if self.f_extended:
239
265
                self.__extsize = 0
240
266
                self.__fileobj.seek(-4, 1)
241
267
                self.__readbytes -= 4
242
 
            elif self.version >= (2,4,0):
 
268
            elif self.version >= self._V24:
243
269
                # "Where the 'Extended header size' is the size of the whole
244
270
                # extended header, stored as a 32 bit synchsafe integer."
245
271
                self.__extsize = BitPaddedInt(extsize) - 4
 
272
                if self.PEDANTIC:
 
273
                    if not BitPaddedInt.has_valid_padding(extsize):
 
274
                        raise ValueError("Extended header size not synchsafe")
246
275
            else:
247
276
                # "Where the 'Extended header size', currently 6 or 10 bytes,
248
277
                # excludes itself."
253
282
                self.__extdata = ""
254
283
 
255
284
    def __determine_bpi(self, data, frames, EMPTY="\x00" * 10):
256
 
        if self.version < (2, 4, 0):
 
285
        if self.version < self._V24:
257
286
            return int
258
287
        # have to special case whether to use bitpaddedints here
259
288
        # spec says to use them, but iTunes has it wrong
295
324
        return BitPaddedInt
296
325
 
297
326
    def __read_frames(self, data, frames):
298
 
        if self.version < (2,4,0) and self.f_unsynch:
299
 
            try: data = unsynch.decode(data)
300
 
            except ValueError: pass
 
327
        if self.version < self._V24 and self.f_unsynch:
 
328
            try:
 
329
                data = unsynch.decode(data)
 
330
            except ValueError:
 
331
                pass
301
332
 
302
 
        if (2,3,0) <= self.version:
 
333
        if self._V23 <= self.version:
303
334
            bpi = self.__determine_bpi(data, frames)
304
335
            while data:
305
336
                header = data[:10]
306
 
                try: name, size, flags = unpack('>4sLH', header)
307
 
                except struct.error: return # not enough header
308
 
                if name.strip('\x00') == '': return
 
337
                try:
 
338
                    name, size, flags = unpack('>4sLH', header)
 
339
                except struct.error:
 
340
                    return  # not enough header
 
341
                if name.strip('\x00') == '':
 
342
                    return
309
343
                size = bpi(size)
310
344
                framedata = data[10:10+size]
311
345
                data = data[10+size:]
312
 
                if size == 0: continue # drop empty frames
313
 
                try: tag = frames[name]
314
 
                except KeyError: 
315
 
                    if is_valid_frame_id(name): yield header + framedata
 
346
                if size == 0:
 
347
                    continue  # drop empty frames
 
348
                try:
 
349
                    tag = frames[name]
 
350
                except KeyError:
 
351
                    if is_valid_frame_id(name):
 
352
                        yield header + framedata
316
353
                else:
317
 
                    try: yield self.__load_framedata(tag, flags, framedata)
318
 
                    except NotImplementedError: yield header + framedata
319
 
                    except ID3JunkFrameError: pass
 
354
                    try:
 
355
                        yield self.__load_framedata(tag, flags, framedata)
 
356
                    except NotImplementedError:
 
357
                        yield header + framedata
 
358
                    except ID3JunkFrameError:
 
359
                        pass
320
360
 
321
 
        elif (2,2,0) <= self.version:
 
361
        elif self._V22 <= self.version:
322
362
            while data:
323
363
                header = data[0:6]
324
 
                try: name, size = unpack('>3s3s', header)
325
 
                except struct.error: return # not enough header
 
364
                try:
 
365
                    name, size = unpack('>3s3s', header)
 
366
                except struct.error:
 
367
                    return  # not enough header
326
368
                size, = struct.unpack('>L', '\x00'+size)
327
 
                if name.strip('\x00') == '': return
 
369
                if name.strip('\x00') == '':
 
370
                    return
328
371
                framedata = data[6:6+size]
329
372
                data = data[6+size:]
330
 
                if size == 0: continue # drop empty frames
331
 
                try: tag = frames[name]
 
373
                if size == 0:
 
374
                    continue  # drop empty frames
 
375
                try:
 
376
                    tag = frames[name]
332
377
                except KeyError:
333
 
                    if is_valid_frame_id(name): yield header + framedata
 
378
                    if is_valid_frame_id(name):
 
379
                        yield header + framedata
334
380
                else:
335
 
                    try: yield self.__load_framedata(tag, 0, framedata)
336
 
                    except NotImplementedError: yield header + framedata
337
 
                    except ID3JunkFrameError: pass
 
381
                    try:
 
382
                        yield self.__load_framedata(tag, 0, framedata)
 
383
                    except NotImplementedError:
 
384
                        yield header + framedata
 
385
                    except ID3JunkFrameError:
 
386
                        pass
338
387
 
339
388
    def __load_framedata(self, tag, flags, framedata):
340
389
        return tag.fromData(self, flags, framedata)
341
 
            
 
390
 
342
391
    f_unsynch = property(lambda s: bool(s.__flags & 0x80))
343
392
    f_extended = property(lambda s: bool(s.__flags & 0x40))
344
393
    f_experimental = property(lambda s: bool(s.__flags & 0x20))
346
395
 
347
396
    #f_crc = property(lambda s: bool(s.__extflags & 0x8000))
348
397
 
349
 
    def save(self, filename=None, v1=1):
 
398
    def save(self, filename=None, v1=1, v2_version=4, v23_sep='/'):
350
399
        """Save changes to a file.
351
400
 
352
401
        If no filename is given, the one most recently loaded is used.
355
404
        v1 -- if 0, ID3v1 tags will be removed
356
405
              if 1, ID3v1 tags will be updated but not added
357
406
              if 2, ID3v1 tags will be created and/or updated
 
407
        v2 -- version of ID3v2 tags (3 or 4).
 
408
 
 
409
        By default Mutagen saves ID3v2.4 tags. If you want to save ID3v2.3
 
410
        tags, you must call method update_to_v23 before saving the file.
 
411
 
 
412
        v23_sep -- the separator used to join multiple text values
 
413
                   if v2_version == 3. Defaults to '/' but if it's None
 
414
                   will be the ID3v2v2.4 null separator.
358
415
 
359
416
        The lack of a way to update only an ID3v1 tag is intentional.
360
417
        """
361
418
 
 
419
        if v2_version == 3:
 
420
            version = self._V23
 
421
        elif v2_version == 4:
 
422
            version = self._V24
 
423
        else:
 
424
            raise ValueError("Only 3 or 4 allowed for v2_version")
 
425
 
362
426
        # Sort frames by 'importance'
363
427
        order = ["TIT2", "TPE1", "TRCK", "TALB", "TPOS", "TDRC", "TCON"]
364
428
        order = dict(zip(order, range(len(order))))
367
431
        frames.sort(lambda a, b: cmp(order.get(a[0][:4], last),
368
432
                                     order.get(b[0][:4], last)))
369
433
 
370
 
        framedata = [self.__save_frame(frame) for (key, frame) in frames]
371
 
        framedata.extend([data for data in self.unknown_frames
372
 
                if len(data) > 10])
 
434
        framedata = [self.__save_frame(frame, version=version, v23_sep=v23_sep)
 
435
                     for (key, frame) in frames]
 
436
 
 
437
        # only write unknown frames if they were loaded from the version
 
438
        # we are saving with or upgraded to it
 
439
        if self.__unknown_version == version:
 
440
            framedata.extend([data for data in self.unknown_frames
 
441
                              if len(data) > 10])
 
442
 
373
443
        if not framedata:
374
444
            try:
375
445
                self.delete(filename)
376
446
            except EnvironmentError, err:
377
447
                from errno import ENOENT
378
 
                if err.errno != ENOENT: raise
 
448
                if err.errno != ENOENT:
 
449
                    raise
379
450
            return
380
451
 
381
452
        framedata = ''.join(framedata)
382
453
        framesize = len(framedata)
383
454
 
384
 
        if filename is None: filename = self.filename
385
 
        try: f = open(filename, 'rb+')
 
455
        if filename is None:
 
456
            filename = self.filename
 
457
        try:
 
458
            f = open(filename, 'rb+')
386
459
        except IOError, err:
387
460
            from errno import ENOENT
388
 
            if err.errno != ENOENT: raise
389
 
            f = open(filename, 'ab') # create, then reopen
 
461
            if err.errno != ENOENT:
 
462
                raise
 
463
            f = open(filename, 'ab')  # create, then reopen
390
464
            f = open(filename, 'rb+')
391
465
        try:
392
466
            idata = f.read(10)
393
 
            try: id3, vmaj, vrev, flags, insize = unpack('>3sBBB4s', idata)
394
 
            except struct.error: id3, insize = '', 0
 
467
            try:
 
468
                id3, vmaj, vrev, flags, insize = unpack('>3sBBB4s', idata)
 
469
            except struct.error:
 
470
                id3, insize = '', 0
395
471
            insize = BitPaddedInt(insize)
396
 
            if id3 != 'ID3': insize = -10
 
472
            if id3 != 'ID3':
 
473
                insize = -10
397
474
 
398
 
            if insize >= framesize: outsize = insize
399
 
            else: outsize = (framesize + 1023) & ~0x3FF
 
475
            if insize >= framesize:
 
476
                outsize = insize
 
477
            else:
 
478
                outsize = (framesize + 1023) & ~0x3FF
400
479
            framedata += '\x00' * (outsize - framesize)
401
480
 
402
481
            framesize = BitPaddedInt.to_str(outsize, width=4)
403
482
            flags = 0
404
 
            header = pack('>3sBBB4s', 'ID3', 4, 0, flags, framesize)
 
483
            header = pack('>3sBBB4s', 'ID3', v2_version, 0, flags, framesize)
405
484
            data = header + framedata
406
485
 
407
486
            if (insize < outsize):
430
509
            else:
431
510
                offset = idx - len(data)
432
511
                has_v1 = True
433
 
                
 
512
 
434
513
            f.seek(offset, 2)
435
514
            if v1 == 1 and has_v1 or v1 == 2:
436
515
                f.write(MakeID3v1(self))
446
525
        If no filename is given, the one most recently loaded is used.
447
526
 
448
527
        Keyword arguments:
449
 
        delete_v1 -- delete any ID3v1 tag
450
 
        delete_v2 -- delete any ID3v2 tag
 
528
 
 
529
        * delete_v1 -- delete any ID3v1 tag
 
530
        * delete_v2 -- delete any ID3v2 tag
451
531
        """
452
532
        if filename is None:
453
533
            filename = self.filename
454
534
        delete(filename, delete_v1, delete_v2)
455
535
        self.clear()
456
536
 
457
 
    def __save_frame(self, frame, name=None):
 
537
    def __save_frame(self, frame, name=None, version=_V24, v23_sep=None):
458
538
        flags = 0
459
539
        if self.PEDANTIC and isinstance(frame, TextFrame):
460
 
            if len(str(frame)) == 0: return ''
461
 
        framedata = frame._writeData()
 
540
            if len(str(frame)) == 0:
 
541
                return ''
 
542
 
 
543
        if version == self._V23:
 
544
            framev23 = frame._get_v23_frame(sep=v23_sep)
 
545
            framedata = framev23._writeData()
 
546
        else:
 
547
            framedata = frame._writeData()
 
548
 
462
549
        usize = len(framedata)
463
550
        if usize > 2048:
464
551
            # Disabled as this causes iTunes and other programs
467
554
            #framedata = BitPaddedInt.to_str(usize) + framedata.encode('zlib')
468
555
            #flags |= Frame.FLAG24_COMPRESS | Frame.FLAG24_DATALEN
469
556
            pass
470
 
        datasize = BitPaddedInt.to_str(len(framedata), width=4)
 
557
 
 
558
        if version == self._V24:
 
559
            bits = 7
 
560
        elif version == self._V23:
 
561
            bits = 8
 
562
        else:
 
563
            raise ValueError
 
564
 
 
565
        datasize = BitPaddedInt.to_str(len(framedata), width=4, bits=bits)
471
566
        header = pack('>4s4sH', name or type(frame).__name__, datasize, flags)
472
567
        return header + framedata
473
568
 
 
569
    def __update_common(self):
 
570
        """Updates done by both v23 and v24 update"""
 
571
 
 
572
        if "TCON" in self:
 
573
            # Get rid of "(xx)Foobr" format.
 
574
            self["TCON"].genres = self["TCON"].genres
 
575
 
 
576
        if self.version < self._V23:
 
577
            # ID3v2.2 PIC frames are slightly different.
 
578
            pics = self.getall("APIC")
 
579
            mimes = {"PNG": "image/png", "JPG": "image/jpeg"}
 
580
            self.delall("APIC")
 
581
            for pic in pics:
 
582
                newpic = APIC(
 
583
                    encoding=pic.encoding, mime=mimes.get(pic.mime, pic.mime),
 
584
                    type=pic.type, desc=pic.desc, data=pic.data)
 
585
                self.add(newpic)
 
586
 
 
587
            # ID3v2.2 LNK frames are just way too different to upgrade.
 
588
            self.delall("LINK")
 
589
 
474
590
    def update_to_v24(self):
475
591
        """Convert older tags into an ID3v2.4 tag.
476
592
 
479
595
        at some point; it is called by default when loading the tag.
480
596
        """
481
597
 
482
 
        if self.version < (2,3,0):
483
 
            # unsafe to write
484
 
            del self.unknown_frames[:]
485
 
        elif self.version == (2,3,0) and not self.__unknown_updated:
 
598
        self.__update_common()
 
599
 
 
600
        if self.__unknown_version == (2, 3, 0):
486
601
            # convert unknown 2.3 frames (flags/size) to 2.4
487
602
            converted = []
488
603
            for frame in self.unknown_frames:
493
608
                    continue
494
609
                converted.append(self.__save_frame(frame, name=name))
495
610
            self.unknown_frames[:] = converted
496
 
            self.__unknown_updated = True
 
611
            self.__unknown_version = (2, 4, 0)
497
612
 
498
613
        # TDAT, TYER, and TIME have been turned into TDRC.
499
614
        try:
527
642
            if "TIPL" not in self:
528
643
                self.add(TIPL(encoding=f.encoding, people=f.people))
529
644
 
530
 
        if "TCON" in self:
531
 
            # Get rid of "(xx)Foobr" format.
532
 
            self["TCON"].genres = self["TCON"].genres
533
 
 
534
 
        if self.version < (2, 3):
535
 
            # ID3v2.2 PIC frames are slightly different.
536
 
            pics = self.getall("APIC")
537
 
            mimes = { "PNG": "image/png", "JPG": "image/jpeg" }
538
 
            self.delall("APIC")
539
 
            for pic in pics:
540
 
                newpic = APIC(
541
 
                    encoding=pic.encoding, mime=mimes.get(pic.mime, pic.mime),
542
 
                    type=pic.type, desc=pic.desc, data=pic.data)
543
 
                self.add(newpic)
544
 
 
545
 
            # ID3v2.2 LNK frames are just way too different to upgrade.
546
 
            self.delall("LINK")
547
 
 
548
645
        # These can't be trivially translated to any ID3v2.4 tags, or
549
646
        # should have been removed already.
550
647
        for key in ["RVAD", "EQUA", "TRDA", "TSIZ", "TDAT", "TIME", "CRM"]:
551
 
            if key in self: del(self[key])
 
648
            if key in self:
 
649
                del(self[key])
 
650
 
 
651
    def update_to_v23(self):
 
652
        """Convert older (and newer) tags into an ID3v2.3 tag.
 
653
 
 
654
        This updates incompatible ID3v2 frames to ID3v2.3 ones. If you
 
655
        intend to save tags as ID3v2.3, you must call this function
 
656
        at some point.
 
657
 
 
658
        If you want to to go off spec and include some v2.4 frames
 
659
        in v2.3, remove them before calling this and add them back afterwards.
 
660
        """
 
661
 
 
662
        self.__update_common()
 
663
 
 
664
        # we could downgrade unknown v2.4 frames here, but given that
 
665
        # the main reason to save v2.3 is compatibility and this
 
666
        # might increase the chance of some parser breaking.. better not
 
667
 
 
668
        # TMCL, TIPL -> TIPL
 
669
        if "TIPL" in self or "TMCL" in self:
 
670
            people = []
 
671
            if "TIPL" in self:
 
672
                f = self.pop("TIPL")
 
673
                people.extend(f.people)
 
674
            if "TMCL" in self:
 
675
                f = self.pop("TMCL")
 
676
                people.extend(f.people)
 
677
            if "IPLS" not in self:
 
678
                self.add(IPLS(encoding=f.encoding, people=people))
 
679
 
 
680
        # TDOR -> TORY
 
681
        if "TDOR" in self:
 
682
            f = self.pop("TDOR")
 
683
            if f.text:
 
684
                d = f.text[0]
 
685
                if d.year and "TORY" not in self:
 
686
                    self.add(TORY(encoding=f.encoding, text="%04d" % d.year))
 
687
 
 
688
        # TDRC -> TYER, TDAT, TIME
 
689
        if "TDRC" in self:
 
690
            f = self.pop("TDRC")
 
691
            if f.text:
 
692
                d = f.text[0]
 
693
                if d.year and "TYER" not in self:
 
694
                    self.add(TYER(encoding=f.encoding, text="%04d" % d.year))
 
695
                if d.month and d.day and "TDAT" not in self:
 
696
                    self.add(TDAT(encoding=f.encoding,
 
697
                                  text="%02d%02d" % (d.day, d.month)))
 
698
                if d.hour and d.minute and "TIME" not in self:
 
699
                    self.add(TIME(encoding=f.encoding,
 
700
                                  text="%02d%02d" % (d.hour, d.minute)))
 
701
 
 
702
        # New frames added in v2.4
 
703
        v24_frames = [
 
704
            'ASPI', 'EQU2', 'RVA2', 'SEEK', 'SIGN', 'TDEN', 'TDOR',
 
705
            'TDRC', 'TDRL', 'TDTG', 'TIPL', 'TMCL', 'TMOO', 'TPRO',
 
706
            'TSOA', 'TSOP', 'TSOT', 'TSST',
 
707
        ]
 
708
 
 
709
        for key in v24_frames:
 
710
            if key in self:
 
711
                del(self[key])
 
712
 
552
713
 
553
714
def delete(filename, delete_v1=True, delete_v2=True):
554
715
    """Remove tags from a file.
555
716
 
556
717
    Keyword arguments:
557
 
    delete_v1 -- delete any ID3v1 tag
558
 
    delete_v2 -- delete any ID3v2 tag
 
718
 
 
719
    * delete_v1 -- delete any ID3v1 tag
 
720
    * delete_v2 -- delete any ID3v2 tag
559
721
    """
560
722
 
561
723
    f = open(filename, 'rb+')
563
725
    if delete_v1:
564
726
        try:
565
727
            f.seek(-128, 2)
566
 
        except IOError: pass
 
728
        except IOError:
 
729
            pass
567
730
        else:
568
731
            if f.read(3) == "TAG":
569
732
                f.seek(-128, 2)
574
737
    if delete_v2:
575
738
        f.seek(0, 0)
576
739
        idata = f.read(10)
577
 
        try: id3, vmaj, vrev, flags, insize = unpack('>3sBBB4s', idata)
578
 
        except struct.error: id3, insize = '', -1
 
740
        try:
 
741
            id3, vmaj, vrev, flags, insize = unpack('>3sBBB4s', idata)
 
742
        except struct.error:
 
743
            id3, insize = '', -1
579
744
        insize = BitPaddedInt(insize)
580
745
        if id3 == 'ID3' and insize >= 0:
581
746
            delete_bytes(f, insize + 10, 0)
582
747
 
583
 
class BitPaddedInt(int):
584
 
    def __new__(cls, value, bits=7, bigendian=True):
585
 
        "Strips 8-bits bits out of every byte"
586
 
        mask = (1<<(bits))-1
587
 
        if isinstance(value, (int, long)):
588
 
            bytes = []
589
 
            while value:
590
 
                bytes.append(value & ((1<<bits)-1))
591
 
                value = value >> 8
592
 
        if isinstance(value, str):
593
 
            bytes = [ord(byte) & mask for byte in value]
594
 
            if bigendian: bytes.reverse()
595
 
        numeric_value = 0
596
 
        for shift, byte in zip(range(0, len(bytes)*bits, bits), bytes):
597
 
            numeric_value += byte << shift
598
 
        if isinstance(numeric_value, long):
599
 
            self = long.__new__(BitPaddedLong, numeric_value)
600
 
        else:
601
 
            self = int.__new__(BitPaddedInt, numeric_value)
602
 
        self.bits = bits
603
 
        self.bigendian = bigendian
604
 
        return self
605
 
 
606
 
    def as_str(value, bits=7, bigendian=True, width=4):
607
 
        bits = getattr(value, 'bits', bits)
608
 
        bigendian = getattr(value, 'bigendian', bigendian)
609
 
        value = int(value)
610
 
        mask = (1<<bits)-1
611
 
        bytes = []
612
 
        while value:
613
 
            bytes.append(value & mask)
614
 
            value = value >> bits
615
 
        # PCNT and POPM use growing integers of at least 4 bytes as counters.
616
 
        if width == -1: width = max(4, len(bytes))
617
 
        if len(bytes) > width:
618
 
            raise ValueError, 'Value too wide (%d bytes)' % len(bytes)
619
 
        else: bytes.extend([0] * (width-len(bytes)))
620
 
        if bigendian: bytes.reverse()
621
 
        return ''.join(map(chr, bytes))
622
 
    to_str = staticmethod(as_str)
623
 
 
624
 
class BitPaddedLong(long):
625
 
    def as_str(value, bits=7, bigendian=True, width=4):
626
 
        return BitPaddedInt.to_str(value, bits, bigendian, width)
627
 
    to_str = staticmethod(as_str)
628
 
 
629
 
class unsynch(object):
630
 
    def decode(value):
631
 
        output = []
632
 
        safe = True
633
 
        append = output.append
634
 
        for val in value:
635
 
            if safe:
636
 
                append(val)
637
 
                safe = val != '\xFF'
638
 
            else:
639
 
                if val >= '\xE0': raise ValueError('invalid sync-safe string')
640
 
                elif val != '\x00': append(val)
641
 
                safe = True
642
 
        if not safe: raise ValueError('string ended unsafe')
643
 
        return ''.join(output)
644
 
    decode = staticmethod(decode)
645
 
 
646
 
    def encode(value):
647
 
        output = []
648
 
        safe = True
649
 
        append = output.append
650
 
        for val in value:
651
 
            if safe:
652
 
                append(val)
653
 
                if val == '\xFF': safe = False
654
 
            elif val == '\x00' or val >= '\xE0':
655
 
                append('\x00')
656
 
                append(val)
657
 
                safe = val != '\xFF'
658
 
            else:
659
 
                append(val)
660
 
                safe = True
661
 
        if not safe: append('\x00')
662
 
        return ''.join(output)
663
 
    encode = staticmethod(encode)
664
 
 
665
 
class Spec(object):
666
 
    def __init__(self, name): self.name = name
667
 
    def __hash__(self): raise TypeError("Spec objects are unhashable")
668
 
 
669
 
class ByteSpec(Spec):
670
 
    def read(self, frame, data): return ord(data[0]), data[1:]
671
 
    def write(self, frame, value): return chr(value)
672
 
    def validate(self, frame, value): return value
673
 
 
674
 
class IntegerSpec(Spec):
675
 
    def read(self, frame, data):
676
 
        return int(BitPaddedInt(data, bits=8)), ''
677
 
    def write(self, frame, value):
678
 
        return BitPaddedInt.to_str(value, bits=8, width=-1)
679
 
    def validate(self, frame, value):
680
 
        return value
681
 
 
682
 
class SizedIntegerSpec(Spec):
683
 
    def __init__(self, name, size):
684
 
        self.name, self.__sz = name, size
685
 
    def read(self, frame, data):
686
 
        return int(BitPaddedInt(data[:self.__sz], bits=8)), data[self.__sz:]
687
 
    def write(self, frame, value):
688
 
        return BitPaddedInt.to_str(value, bits=8, width=self.__sz)
689
 
    def validate(self, frame, value):
690
 
        return value
691
 
 
692
 
class EncodingSpec(ByteSpec):
693
 
    def read(self, frame, data):
694
 
        enc, data = super(EncodingSpec, self).read(frame, data)
695
 
        if enc < 16: return enc, data
696
 
        else: return 0, chr(enc)+data
697
 
 
698
 
    def validate(self, frame, value):
699
 
        if 0 <= value <= 3: return value
700
 
        if value is None: return None
701
 
        raise ValueError, 'Invalid Encoding: %r' % value
702
 
 
703
 
class StringSpec(Spec):
704
 
    def __init__(self, name, length):
705
 
        super(StringSpec, self).__init__(name)
706
 
        self.len = length
707
 
    def read(s, frame, data): return data[:s.len], data[s.len:]
708
 
    def write(s, frame, value):
709
 
        if value is None: return '\x00' * s.len
710
 
        else: return (str(value) + '\x00' * s.len)[:s.len]
711
 
    def validate(s, frame, value):
712
 
        if value is None: return None
713
 
        if isinstance(value, basestring) and len(value) == s.len: return value
714
 
        raise ValueError, 'Invalid StringSpec[%d] data: %r' % (s.len, value)
715
 
 
716
 
class BinaryDataSpec(Spec):
717
 
    def read(self, frame, data): return data, ''
718
 
    def write(self, frame, value): return str(value)
719
 
    def validate(self, frame, value): return str(value)
720
 
 
721
 
class EncodedTextSpec(Spec):
722
 
    # Okay, seriously. This is private and defined explicitly and
723
 
    # completely by the ID3 specification. You can't just add
724
 
    # encodings here however you want.
725
 
    _encodings = ( ('latin1', '\x00'), ('utf16', '\x00\x00'),
726
 
                   ('utf_16_be', '\x00\x00'), ('utf8', '\x00') )
727
 
 
728
 
    def read(self, frame, data):
729
 
        enc, term = self._encodings[frame.encoding]
730
 
        ret = ''
731
 
        if len(term) == 1:
732
 
            if term in data:
733
 
                data, ret = data.split(term, 1)
734
 
        else:
735
 
            offset = -1
736
 
            try:
737
 
                while True:
738
 
                    offset = data.index(term, offset+1)
739
 
                    if offset & 1: continue
740
 
                    data, ret = data[0:offset], data[offset+2:]; break
741
 
            except ValueError: pass
742
 
 
743
 
        if len(data) < len(term): return u'', ret
744
 
        return data.decode(enc), ret
745
 
 
746
 
    def write(self, frame, value):
747
 
        enc, term = self._encodings[frame.encoding]
748
 
        return value.encode(enc) + term
749
 
 
750
 
    def validate(self, frame, value): return unicode(value)
751
 
 
752
 
class MultiSpec(Spec):
753
 
    def __init__(self, name, *specs, **kw):
754
 
        super(MultiSpec, self).__init__(name)
755
 
        self.specs = specs
756
 
        self.sep = kw.get('sep')
757
 
 
758
 
    def read(self, frame, data):
759
 
        values = []
760
 
        while data:
761
 
            record = []
762
 
            for spec in self.specs:
763
 
                value, data = spec.read(frame, data)
764
 
                record.append(value)
765
 
            if len(self.specs) != 1: values.append(record)
766
 
            else: values.append(record[0])
767
 
        return values, data
768
 
 
769
 
    def write(self, frame, value):
770
 
        data = []
771
 
        if len(self.specs) == 1:
772
 
            for v in value:
773
 
                data.append(self.specs[0].write(frame, v))
774
 
        else:
775
 
            for record in value:
776
 
                for v, s in zip(record, self.specs):
777
 
                    data.append(s.write(frame, v))
778
 
        return ''.join(data)
779
 
 
780
 
    def validate(self, frame, value):
781
 
        if value is None: return []
782
 
        if self.sep and isinstance(value, basestring):
783
 
            value = value.split(self.sep)
784
 
        if isinstance(value, list):
785
 
            if len(self.specs) == 1:
786
 
                return [self.specs[0].validate(frame, v) for v in value]
787
 
            else:
788
 
                return [ 
789
 
                    [s.validate(frame, v) for (v,s) in zip(val, self.specs)]
790
 
                    for val in value ]
791
 
        raise ValueError, 'Invalid MultiSpec data: %r' % value
792
 
 
793
 
class EncodedNumericTextSpec(EncodedTextSpec): pass
794
 
class EncodedNumericPartTextSpec(EncodedTextSpec): pass
795
 
 
796
 
class Latin1TextSpec(EncodedTextSpec):
797
 
    def read(self, frame, data):
798
 
        if '\x00' in data: data, ret = data.split('\x00',1)
799
 
        else: ret = ''
800
 
        return data.decode('latin1'), ret
801
 
 
802
 
    def write(self, data, value):
803
 
        return value.encode('latin1') + '\x00'
804
 
 
805
 
    def validate(self, frame, value): return unicode(value)
806
 
 
807
 
class ID3TimeStamp(object):
808
 
    """A time stamp in ID3v2 format.
809
 
 
810
 
    This is a restricted form of the ISO 8601 standard; time stamps
811
 
    take the form of:
812
 
        YYYY-MM-DD HH:MM:SS
813
 
    Or some partial form (YYYY-MM-DD HH, YYYY, etc.).
814
 
 
815
 
    The 'text' attribute contains the raw text data of the time stamp.
816
 
    """
817
 
 
818
 
    import re
819
 
    def __init__(self, text):
820
 
        if isinstance(text, ID3TimeStamp): text = text.text
821
 
        self.text = text
822
 
 
823
 
    __formats = ['%04d'] + ['%02d'] * 5
824
 
    __seps = ['-', '-', ' ', ':', ':', 'x']
825
 
    def get_text(self):
826
 
        parts = [self.year, self.month, self.day,
827
 
                self.hour, self.minute, self.second]
828
 
        pieces = []
829
 
        for i, part in enumerate(iter(iter(parts).next, None)):
830
 
            pieces.append(self.__formats[i]%part + self.__seps[i])
831
 
        return u''.join(pieces)[:-1]
832
 
 
833
 
    def set_text(self, text, splitre=re.compile('[-T:/.]|\s+')):
834
 
        year, month, day, hour, minute, second = \
835
 
                splitre.split(text + ':::::')[:6]
836
 
        for a in 'year month day hour minute second'.split():
837
 
            try: v = int(locals()[a])
838
 
            except ValueError: v = None
839
 
            setattr(self, a, v)
840
 
 
841
 
    text = property(get_text, set_text, doc="ID3v2.4 date and time.")
842
 
 
843
 
    def __str__(self): return self.text
844
 
    def __repr__(self): return repr(self.text)
845
 
    def __cmp__(self, other): return cmp(self.text, other.text)
846
 
    __hash__ = object.__hash__
847
 
    def encode(self, *args): return self.text.encode(*args)
848
 
 
849
 
class TimeStampSpec(EncodedTextSpec):
850
 
    def read(self, frame, data):
851
 
        value, data = super(TimeStampSpec, self).read(frame, data)
852
 
        return self.validate(frame, value), data
853
 
 
854
 
    def write(self, frame, data):
855
 
        return super(TimeStampSpec, self).write(frame,
856
 
                data.text.replace(' ', 'T'))
857
 
 
858
 
    def validate(self, frame, value):
859
 
        try: return ID3TimeStamp(value)
860
 
        except TypeError: raise ValueError, "Invalid ID3TimeStamp: %r" % value
861
 
 
862
 
class ChannelSpec(ByteSpec):
863
 
    (OTHER, MASTER, FRONTRIGHT, FRONTLEFT, BACKRIGHT, BACKLEFT, FRONTCENTRE,
864
 
     BACKCENTRE, SUBWOOFER) = range(9)
865
 
 
866
 
class VolumeAdjustmentSpec(Spec):
867
 
    def read(self, frame, data):
868
 
        value, = unpack('>h', data[0:2])
869
 
        return value/512.0, data[2:]
870
 
 
871
 
    def write(self, frame, value):
872
 
        return pack('>h', int(round(value * 512)))
873
 
 
874
 
    def validate(self, frame, value): return value
875
 
 
876
 
class VolumePeakSpec(Spec):
877
 
    def read(self, frame, data):
878
 
        # http://bugs.xmms.org/attachment.cgi?id=113&action=view
879
 
        peak = 0
880
 
        bits = ord(data[0])
881
 
        bytes = min(4, (bits + 7) >> 3)
882
 
        # not enough frame data
883
 
        if bytes + 1 > len(data): raise ID3JunkFrameError
884
 
        shift = ((8 - (bits & 7)) & 7) + (4 - bytes) * 8
885
 
        for i in range(1, bytes+1):
886
 
            peak *= 256
887
 
            peak += ord(data[i])
888
 
        peak *= 2**shift
889
 
        return (float(peak) / (2**31-1)), data[1+bytes:]
890
 
 
891
 
    def write(self, frame, value):
892
 
        # always write as 16 bits for sanity.
893
 
        return "\x10" + pack('>H', int(round(value * 32768)))
894
 
 
895
 
    def validate(self, frame, value): return value
896
 
 
897
 
class SynchronizedTextSpec(EncodedTextSpec):
898
 
    def read(self, frame, data):
899
 
        texts = []
900
 
        encoding, term = self._encodings[frame.encoding]
901
 
        while data:
902
 
            l = len(term)
903
 
            try:
904
 
                value_idx = data.index(term)
905
 
            except ValueError:
906
 
                raise ID3JunkFrameError
907
 
            value = data[:value_idx].decode(encoding)
908
 
            if len(data) < value_idx + l + 4:
909
 
                raise ID3JunkFrameError
910
 
            time, = struct.unpack(">I", data[value_idx+l:value_idx+l+4])
911
 
            texts.append((value, time))
912
 
            data = data[value_idx+l+4:]
913
 
        return texts, ""
914
 
 
915
 
    def write(self, frame, value):
916
 
        data = []
917
 
        encoding, term = self._encodings[frame.encoding]
918
 
        for text, time in frame.text:
919
 
            text = text.encode(encoding) + term
920
 
            data.append(text + struct.pack(">I", time))
921
 
        return "".join(data)
922
 
 
923
 
    def validate(self, frame, value):
924
 
        return value
925
 
 
926
 
class KeyEventSpec(Spec):
927
 
    def read(self, frame, data):
928
 
        events = []
929
 
        while len(data) >= 5:
930
 
            events.append(struct.unpack(">bI", data[:5]))
931
 
            data = data[5:]
932
 
        return events, data
933
 
 
934
 
    def write(self, frame, value):
935
 
        return "".join([struct.pack(">bI", *event) for event in value])
936
 
 
937
 
    def validate(self, frame, value):
938
 
        return value
939
 
 
940
 
class VolumeAdjustmentsSpec(Spec):
941
 
    # Not to be confused with VolumeAdjustmentSpec.
942
 
    def read(self, frame, data):
943
 
        adjustments = {}
944
 
        while len(data) >= 4:
945
 
            freq, adj = struct.unpack(">Hh", data[:4])
946
 
            data = data[4:]
947
 
            freq /= 2.0
948
 
            adj /= 512.0
949
 
            adjustments[freq] = adj
950
 
        adjustments = adjustments.items()
951
 
        adjustments.sort()
952
 
        return adjustments, data
953
 
 
954
 
    def write(self, frame, value):
955
 
        value.sort()
956
 
        return "".join([struct.pack(">Hh", int(freq * 2), int(adj * 512))
957
 
                        for (freq, adj) in value])
958
 
 
959
 
    def validate(self, frame, value):
960
 
        return value
961
 
 
962
 
class ASPIIndexSpec(Spec):
963
 
    def read(self, frame, data):
964
 
        if frame.b == 16:
965
 
            format = "H"
966
 
            size = 2
967
 
        elif frame.b == 8:
968
 
            format = "B"
969
 
            size = 1
970
 
        else:
971
 
            warn("invalid bit count in ASPI (%d)" % frame.b, ID3Warning)
972
 
            return [], data
973
 
        
974
 
        indexes = data[:frame.N * size]
975
 
        data = data[frame.N * size:]
976
 
        return list(struct.unpack(">" + format * frame.N, indexes)), data
977
 
 
978
 
    def write(self, frame, values):
979
 
        if frame.b == 16: format = "H"
980
 
        elif frame.b == 8: format = "B"
981
 
        else: raise ValueError("frame.b must be 8 or 16")
982
 
        return struct.pack(">" + format * frame.N, *values)
983
 
 
984
 
    def validate(self, frame, values):
985
 
        return values
986
 
 
987
 
class Frame(object):
988
 
    """Fundamental unit of ID3 data.
989
 
 
990
 
    ID3 tags are split into frames. Each frame has a potentially
991
 
    different structure, and so this base class is not very featureful.
992
 
    """
993
 
 
994
 
    FLAG23_ALTERTAG     = 0x8000
995
 
    FLAG23_ALTERFILE    = 0x4000
996
 
    FLAG23_READONLY     = 0x2000
997
 
    FLAG23_COMPRESS     = 0x0080
998
 
    FLAG23_ENCRYPT      = 0x0040
999
 
    FLAG23_GROUP        = 0x0020
1000
 
 
1001
 
    FLAG24_ALTERTAG     = 0x4000
1002
 
    FLAG24_ALTERFILE    = 0x2000
1003
 
    FLAG24_READONLY     = 0x1000
1004
 
    FLAG24_GROUPID      = 0x0040
1005
 
    FLAG24_COMPRESS     = 0x0008
1006
 
    FLAG24_ENCRYPT      = 0x0004
1007
 
    FLAG24_UNSYNCH      = 0x0002
1008
 
    FLAG24_DATALEN      = 0x0001
1009
 
 
1010
 
    _framespec = []
1011
 
    def __init__(self, *args, **kwargs):
1012
 
        if len(args)==1 and len(kwargs)==0 and isinstance(args[0], type(self)):
1013
 
            other = args[0]
1014
 
            for checker in self._framespec:
1015
 
                val = checker.validate(self, getattr(other, checker.name))
1016
 
                setattr(self, checker.name, val)
1017
 
        else:
1018
 
            for checker, val in zip(self._framespec, args):
1019
 
                setattr(self, checker.name, checker.validate(self, val))
1020
 
            for checker in self._framespec[len(args):]:
1021
 
                validated = checker.validate(
1022
 
                    self, kwargs.get(checker.name, None))
1023
 
                setattr(self, checker.name, validated)
1024
 
 
1025
 
    HashKey = property(
1026
 
        lambda s: s.FrameID,
1027
 
        doc="an internal key used to ensure frame uniqueness in a tag")
1028
 
    FrameID = property(
1029
 
        lambda s: type(s).__name__,
1030
 
        doc="ID3v2 three or four character frame ID")
1031
 
 
1032
 
    def __repr__(self):
1033
 
        """Python representation of a frame.
1034
 
 
1035
 
        The string returned is a valid Python expression to construct
1036
 
        a copy of this frame.
1037
 
        """
1038
 
        kw = []
1039
 
        for attr in self._framespec:
1040
 
            kw.append('%s=%r' % (attr.name, getattr(self, attr.name)))
1041
 
        return '%s(%s)' % (type(self).__name__, ', '.join(kw))
1042
 
 
1043
 
    def _readData(self, data):
1044
 
        odata = data
1045
 
        for reader in self._framespec:
1046
 
            if len(data):
1047
 
                try: value, data = reader.read(self, data)
1048
 
                except UnicodeDecodeError:
1049
 
                    raise ID3JunkFrameError
1050
 
            else: raise ID3JunkFrameError
1051
 
            setattr(self, reader.name, value)
1052
 
        if data.strip('\x00'):
1053
 
            warn('Leftover data: %s: %r (from %r)' % (
1054
 
                    type(self).__name__, data, odata),
1055
 
                    ID3Warning)
1056
 
 
1057
 
    def _writeData(self):
1058
 
        data = []
1059
 
        for writer in self._framespec:
1060
 
            data.append(writer.write(self, getattr(self, writer.name)))
1061
 
        return ''.join(data)
1062
 
 
1063
 
    def pprint(self):
1064
 
        """Return a human-readable representation of the frame."""
1065
 
        return "%s=%s" % (type(self).__name__, self._pprint())
1066
 
 
1067
 
    def _pprint(self):
1068
 
        return "[unrepresentable data]"
1069
 
 
1070
 
    def fromData(cls, id3, tflags, data):
1071
 
        """Construct this ID3 frame from raw string data."""
1072
 
 
1073
 
        if (2,4,0) <= id3.version:
1074
 
            if tflags & (Frame.FLAG24_COMPRESS | Frame.FLAG24_DATALEN):
1075
 
                # The data length int is syncsafe in 2.4 (but not 2.3).
1076
 
                # However, we don't actually need the data length int,
1077
 
                # except to work around a QL 0.12 bug, and in that case
1078
 
                # all we need are the raw bytes.
1079
 
                datalen_bytes = data[:4]
1080
 
                data = data[4:]
1081
 
            if tflags & Frame.FLAG24_UNSYNCH or id3.f_unsynch:
1082
 
                try: data = unsynch.decode(data)
1083
 
                except ValueError, err:
1084
 
                    if id3.PEDANTIC:
1085
 
                        raise ID3BadUnsynchData, '%s: %r' % (err, data)
1086
 
            if tflags & Frame.FLAG24_ENCRYPT:
1087
 
                raise ID3EncryptionUnsupportedError
1088
 
            if tflags & Frame.FLAG24_COMPRESS:
1089
 
                try: data = data.decode('zlib')
1090
 
                except zlibError, err:
1091
 
                    # the initial mutagen that went out with QL 0.12 did not
1092
 
                    # write the 4 bytes of uncompressed size. Compensate.
1093
 
                    data = datalen_bytes + data
1094
 
                    try: data = data.decode('zlib')
1095
 
                    except zlibError, err:
1096
 
                        if id3.PEDANTIC:
1097
 
                            raise ID3BadCompressedData, '%s: %r' % (err, data)
1098
 
 
1099
 
        elif (2,3,0) <= id3.version:
1100
 
            if tflags & Frame.FLAG23_COMPRESS:
1101
 
                usize, = unpack('>L', data[:4])
1102
 
                data = data[4:]
1103
 
            if tflags & Frame.FLAG23_ENCRYPT:
1104
 
                raise ID3EncryptionUnsupportedError
1105
 
            if tflags & Frame.FLAG23_COMPRESS:
1106
 
                try: data = data.decode('zlib')
1107
 
                except zlibError, err:
1108
 
                    if id3.PEDANTIC:
1109
 
                        raise ID3BadCompressedData, '%s: %r' % (err, data)
1110
 
 
1111
 
        frame = cls()
1112
 
        frame._rawdata = data
1113
 
        frame._flags = tflags
1114
 
        frame._readData(data)
1115
 
        return frame
1116
 
    fromData = classmethod(fromData)
1117
 
 
1118
 
    def __hash__(self):
1119
 
        raise TypeError("Frame objects are unhashable")
1120
 
 
1121
 
class FrameOpt(Frame):
1122
 
    """A frame with optional parts.
1123
 
 
1124
 
    Some ID3 frames have optional data; this class extends Frame to
1125
 
    provide support for those parts.
1126
 
    """
1127
 
    _optionalspec = []
1128
 
 
1129
 
    def __init__(self, *args, **kwargs):
1130
 
        super(FrameOpt, self).__init__(*args, **kwargs)
1131
 
        for spec in self._optionalspec:
1132
 
            if spec.name in kwargs:
1133
 
                validated = spec.validate(self, kwargs[spec.name])
1134
 
                setattr(self, spec.name, validated)
1135
 
            else: break
1136
 
 
1137
 
    def _readData(self, data):
1138
 
        odata = data
1139
 
        for reader in self._framespec:
1140
 
            if len(data): value, data = reader.read(self, data)
1141
 
            else: raise ID3JunkFrameError
1142
 
            setattr(self, reader.name, value)
1143
 
        if data:
1144
 
            for reader in self._optionalspec:
1145
 
                if len(data): value, data = reader.read(self, data)
1146
 
                else: break
1147
 
                setattr(self, reader.name, value)
1148
 
        if data.strip('\x00'):
1149
 
            warn('Leftover data: %s: %r (from %r)' % (
1150
 
                    type(self).__name__, data, odata),
1151
 
                    ID3Warning)
1152
 
 
1153
 
    def _writeData(self):
1154
 
        data = []
1155
 
        for writer in self._framespec:
1156
 
            data.append(writer.write(self, getattr(self, writer.name)))
1157
 
        for writer in self._optionalspec:
1158
 
            try: data.append(writer.write(self, getattr(self, writer.name)))
1159
 
            except AttributeError: break
1160
 
        return ''.join(data)
1161
 
 
1162
 
    def __repr__(self):
1163
 
        kw = []
1164
 
        for attr in self._framespec:
1165
 
            kw.append('%s=%r' % (attr.name, getattr(self, attr.name)))
1166
 
        for attr in self._optionalspec:
1167
 
            if hasattr(self, attr.name):
1168
 
                kw.append('%s=%r' % (attr.name, getattr(self, attr.name)))
1169
 
        return '%s(%s)' % (type(self).__name__, ', '.join(kw))
1170
 
 
1171
 
 
1172
 
class TextFrame(Frame):
1173
 
    """Text strings.
1174
 
 
1175
 
    Text frames support casts to unicode or str objects, as well as
1176
 
    list-like indexing, extend, and append.
1177
 
 
1178
 
    Iterating over a TextFrame iterates over its strings, not its
1179
 
    characters.
1180
 
 
1181
 
    Text frames have a 'text' attribute which is the list of strings,
1182
 
    and an 'encoding' attribute; 0 for ISO-8859 1, 1 UTF-16, 2 for
1183
 
    UTF-16BE, and 3 for UTF-8. If you don't want to worry about
1184
 
    encodings, just set it to 3.
1185
 
    """
1186
 
 
1187
 
    _framespec = [ EncodingSpec('encoding'),
1188
 
        MultiSpec('text', EncodedTextSpec('text'), sep=u'\u0000') ]
1189
 
    def __str__(self): return self.__unicode__().encode('utf-8')
1190
 
    def __unicode__(self): return u'\u0000'.join(self.text)
1191
 
    def __eq__(self, other):
1192
 
        if isinstance(other, str): return str(self) == other
1193
 
        elif isinstance(other, unicode): return unicode(self) == other
1194
 
        return self.text == other
1195
 
    __hash__ = Frame.__hash__
1196
 
    def __getitem__(self, item): return self.text[item]
1197
 
    def __iter__(self): return iter(self.text)
1198
 
    def append(self, value): return self.text.append(value)
1199
 
    def extend(self, value): return self.text.extend(value)
1200
 
    def _pprint(self): return " / ".join(self.text)
1201
 
 
1202
 
class NumericTextFrame(TextFrame):
1203
 
    """Numerical text strings.
1204
 
 
1205
 
    The numeric value of these frames can be gotten with unary plus, e.g.
1206
 
        frame = TLEN('12345')
1207
 
        length = +frame
1208
 
    """
1209
 
 
1210
 
    _framespec = [ EncodingSpec('encoding'),
1211
 
        MultiSpec('text', EncodedNumericTextSpec('text'), sep=u'\u0000') ]
1212
 
 
1213
 
    def __pos__(self):
1214
 
        """Return the numerical value of the string."""
1215
 
        return int(self.text[0])
1216
 
 
1217
 
class NumericPartTextFrame(TextFrame):
1218
 
    """Multivalue numerical text strings.
1219
 
 
1220
 
    These strings indicate 'part (e.g. track) X of Y', and unary plus
1221
 
    returns the first value:
1222
 
        frame = TRCK('4/15')
1223
 
        track = +frame # track == 4
1224
 
    """
1225
 
 
1226
 
    _framespec = [ EncodingSpec('encoding'),
1227
 
        MultiSpec('text', EncodedNumericPartTextSpec('text'), sep=u'\u0000') ]
1228
 
    def __pos__(self):
1229
 
        return int(self.text[0].split("/")[0])
1230
 
 
1231
 
class TimeStampTextFrame(TextFrame):
1232
 
    """A list of time stamps.
1233
 
 
1234
 
    The 'text' attribute in this frame is a list of ID3TimeStamp
1235
 
    objects, not a list of strings.
1236
 
    """
1237
 
 
1238
 
    _framespec = [ EncodingSpec('encoding'),
1239
 
        MultiSpec('text', TimeStampSpec('stamp'), sep=u',') ]
1240
 
    def __str__(self): return self.__unicode__().encode('utf-8')
1241
 
    def __unicode__(self): return ','.join([stamp.text for stamp in self.text])
1242
 
    def _pprint(self):
1243
 
        return " / ".join([stamp.text for stamp in self.text])
1244
 
 
1245
 
class UrlFrame(Frame):
1246
 
    """A frame containing a URL string.
1247
 
 
1248
 
    The ID3 specification is silent about IRIs and normalized URL
1249
 
    forms. Mutagen assumes all URLs in files are encoded as Latin 1,
1250
 
    but string conversion of this frame returns a UTF-8 representation
1251
 
    for compatibility with other string conversions.
1252
 
 
1253
 
    The only sane way to handle URLs in MP3s is to restrict them to
1254
 
    ASCII.
1255
 
    """
1256
 
 
1257
 
    _framespec = [ Latin1TextSpec('url') ]
1258
 
    def __str__(self): return self.url.encode('utf-8')
1259
 
    def __unicode__(self): return self.url
1260
 
    def __eq__(self, other): return self.url == other
1261
 
    __hash__ = Frame.__hash__
1262
 
    def _pprint(self): return self.url
1263
 
 
1264
 
class UrlFrameU(UrlFrame):
1265
 
    HashKey = property(lambda s: '%s:%s' % (s.FrameID, s.url))
1266
 
 
1267
 
class TALB(TextFrame): "Album"
1268
 
class TBPM(NumericTextFrame): "Beats per minute"
1269
 
class TCOM(TextFrame): "Composer"
1270
 
 
1271
 
class TCON(TextFrame):
1272
 
    """Content type (Genre)
1273
 
 
1274
 
    ID3 has several ways genres can be represented; for convenience,
1275
 
    use the 'genres' property rather than the 'text' attribute.
1276
 
    """
1277
 
 
1278
 
    from mutagen._constants import GENRES
1279
 
 
1280
 
    def __get_genres(self):
1281
 
        genres = []
1282
 
        import re
1283
 
        genre_re = re.compile(r"((?:\((?P<id>[0-9]+|RX|CR)\))*)(?P<str>.+)?")
1284
 
        for value in self.text:
1285
 
            if value.isdigit():
1286
 
                try: genres.append(self.GENRES[int(value)])
1287
 
                except IndexError: genres.append(u"Unknown")
1288
 
            elif value == "CR": genres.append(u"Cover")
1289
 
            elif value == "RX": genres.append(u"Remix")
1290
 
            elif value:
1291
 
                newgenres = []
1292
 
                genreid, dummy, genrename = genre_re.match(value).groups()
1293
 
 
1294
 
                if genreid:
1295
 
                    for gid in genreid[1:-1].split(")("):
1296
 
                        if gid.isdigit() and int(gid) < len(self.GENRES):
1297
 
                            gid = unicode(self.GENRES[int(gid)])
1298
 
                            newgenres.append(gid)
1299
 
                        elif gid == "CR": newgenres.append(u"Cover")
1300
 
                        elif gid == "RX": newgenres.append(u"Remix")
1301
 
                        else: newgenres.append(u"Unknown")
1302
 
 
1303
 
                if genrename:
1304
 
                    # "Unescaping" the first parenthesis
1305
 
                    if genrename.startswith("(("): genrename = genrename[1:]
1306
 
                    if genrename not in newgenres: newgenres.append(genrename)
1307
 
 
1308
 
                genres.extend(newgenres)
1309
 
 
1310
 
        return genres
1311
 
 
1312
 
    def __set_genres(self, genres):
1313
 
        if isinstance(genres, basestring): genres = [genres]
1314
 
        self.text = map(self.__decode, genres)
1315
 
 
1316
 
    def __decode(self, value):
1317
 
        if isinstance(value, str):
1318
 
            enc = EncodedTextSpec._encodings[self.encoding][0]
1319
 
            return value.decode(enc)
1320
 
        else: return value
1321
 
 
1322
 
    genres = property(__get_genres, __set_genres, None,
1323
 
                      "A list of genres parsed from the raw text data.")
1324
 
 
1325
 
    def _pprint(self):
1326
 
        return " / ".join(self.genres)
1327
 
 
1328
 
class TCOP(TextFrame): "Copyright (c)"
1329
 
class TCMP(NumericTextFrame): "iTunes Compilation Flag"
1330
 
class TDAT(TextFrame): "Date of recording (DDMM)"
1331
 
class TDEN(TimeStampTextFrame): "Encoding Time"
1332
 
class TDOR(TimeStampTextFrame): "Original Release Time"
1333
 
class TDLY(NumericTextFrame): "Audio Delay (ms)"
1334
 
class TDRC(TimeStampTextFrame): "Recording Time"
1335
 
class TDRL(TimeStampTextFrame): "Release Time"
1336
 
class TDTG(TimeStampTextFrame): "Tagging Time"
1337
 
class TENC(TextFrame): "Encoder"
1338
 
class TEXT(TextFrame): "Lyricist"
1339
 
class TFLT(TextFrame): "File type"
1340
 
class TIME(TextFrame): "Time of recording (HHMM)"
1341
 
class TIT1(TextFrame): "Content group description"
1342
 
class TIT2(TextFrame): "Title"
1343
 
class TIT3(TextFrame): "Subtitle/Description refinement"
1344
 
class TKEY(TextFrame): "Starting Key"
1345
 
class TLAN(TextFrame): "Audio Languages"
1346
 
class TLEN(NumericTextFrame): "Audio Length (ms)"
1347
 
class TMED(TextFrame): "Source Media Type"
1348
 
class TMOO(TextFrame): "Mood"
1349
 
class TOAL(TextFrame): "Original Album"
1350
 
class TOFN(TextFrame): "Original Filename"
1351
 
class TOLY(TextFrame): "Original Lyricist"
1352
 
class TOPE(TextFrame): "Original Artist/Performer"
1353
 
class TORY(NumericTextFrame): "Original Release Year"
1354
 
class TOWN(TextFrame): "Owner/Licensee"
1355
 
class TPE1(TextFrame): "Lead Artist/Performer/Soloist/Group"
1356
 
class TPE2(TextFrame): "Band/Orchestra/Accompaniment"
1357
 
class TPE3(TextFrame): "Conductor"
1358
 
class TPE4(TextFrame): "Interpreter/Remixer/Modifier"
1359
 
class TPOS(NumericPartTextFrame): "Part of set"
1360
 
class TPRO(TextFrame): "Produced (P)"
1361
 
class TPUB(TextFrame): "Publisher"
1362
 
class TRCK(NumericPartTextFrame): "Track Number"
1363
 
class TRDA(TextFrame): "Recording Dates"
1364
 
class TRSN(TextFrame): "Internet Radio Station Name"
1365
 
class TRSO(TextFrame): "Internet Radio Station Owner"
1366
 
class TSIZ(NumericTextFrame): "Size of audio data (bytes)"
1367
 
class TSO2(TextFrame): "iTunes Album Artist Sort"
1368
 
class TSOA(TextFrame): "Album Sort Order key"
1369
 
class TSOC(TextFrame): "iTunes Composer Sort"
1370
 
class TSOP(TextFrame): "Perfomer Sort Order key"
1371
 
class TSOT(TextFrame): "Title Sort Order key"
1372
 
class TSRC(TextFrame): "International Standard Recording Code (ISRC)"
1373
 
class TSSE(TextFrame): "Encoder settings"
1374
 
class TSST(TextFrame): "Set Subtitle"
1375
 
class TYER(NumericTextFrame): "Year of recording"
1376
 
 
1377
 
class TXXX(TextFrame):
1378
 
    """User-defined text data.
1379
 
 
1380
 
    TXXX frames have a 'desc' attribute which is set to any Unicode
1381
 
    value (though the encoding of the text and the description must be
1382
 
    the same). Many taggers use this frame to store freeform keys.
1383
 
    """
1384
 
    _framespec = [ EncodingSpec('encoding'), EncodedTextSpec('desc'),
1385
 
        MultiSpec('text', EncodedTextSpec('text'), sep=u'\u0000') ]
1386
 
    HashKey = property(lambda s: '%s:%s' % (s.FrameID, s.desc))
1387
 
    def _pprint(self): return "%s=%s" % (self.desc, " / ".join(self.text))
1388
 
 
1389
 
class WCOM(UrlFrameU): "Commercial Information"
1390
 
class WCOP(UrlFrame): "Copyright Information"
1391
 
class WOAF(UrlFrame): "Official File Information"
1392
 
class WOAR(UrlFrameU): "Official Artist/Performer Information"
1393
 
class WOAS(UrlFrame): "Official Source Information"
1394
 
class WORS(UrlFrame): "Official Internet Radio Information"
1395
 
class WPAY(UrlFrame): "Payment Information"
1396
 
class WPUB(UrlFrame): "Official Publisher Information"
1397
 
 
1398
 
class WXXX(UrlFrame):
1399
 
    """User-defined URL data.
1400
 
 
1401
 
    Like TXXX, this has a freeform description associated with it.
1402
 
    """
1403
 
    _framespec = [ EncodingSpec('encoding'), EncodedTextSpec('desc'),
1404
 
        Latin1TextSpec('url') ]
1405
 
    HashKey = property(lambda s: '%s:%s' % (s.FrameID, s.desc))
1406
 
 
1407
 
class PairedTextFrame(Frame):
1408
 
    """Paired text strings.
1409
 
 
1410
 
    Some ID3 frames pair text strings, to associate names with a more
1411
 
    specific involvement in the song. The 'people' attribute of these
1412
 
    frames contains a list of pairs:
1413
 
        [['trumpet', 'Miles Davis'], ['bass', 'Paul Chambers']]
1414
 
 
1415
 
    Like text frames, these frames also have an encoding attribute.
1416
 
    """
1417
 
 
1418
 
    _framespec = [ EncodingSpec('encoding'), MultiSpec('people',
1419
 
        EncodedTextSpec('involvement'), EncodedTextSpec('person')) ]
1420
 
    def __eq__(self, other):
1421
 
        return self.people == other
1422
 
    __hash__ = Frame.__hash__
1423
 
 
1424
 
class TIPL(PairedTextFrame): "Involved People List"
1425
 
class TMCL(PairedTextFrame): "Musicians Credits List"
1426
 
class IPLS(TIPL): "Involved People List"
1427
 
 
1428
 
class BinaryFrame(Frame):
1429
 
    """Binary data
1430
 
 
1431
 
    The 'data' attribute contains the raw byte string.
1432
 
    """
1433
 
    _framespec = [ BinaryDataSpec('data') ]
1434
 
    def __eq__(self, other): return self.data == other
1435
 
    __hash__ = Frame.__hash__
1436
 
 
1437
 
class MCDI(BinaryFrame): "Binary dump of CD's TOC"
1438
 
 
1439
 
class ETCO(Frame):
1440
 
    """Event timing codes."""
1441
 
    _framespec = [ ByteSpec("format"), KeyEventSpec("events") ]
1442
 
    def __eq__(self, other): return self.events == other
1443
 
    __hash__ = Frame.__hash__
1444
 
 
1445
 
class MLLT(Frame):
1446
 
    """MPEG location lookup table.
1447
 
 
1448
 
    This frame's attributes may be changed in the future based on
1449
 
    feedback from real-world use.
1450
 
    """
1451
 
    _framespec = [ SizedIntegerSpec('frames', 2),
1452
 
                   SizedIntegerSpec('bytes', 3),
1453
 
                   SizedIntegerSpec('milliseconds', 3),
1454
 
                   ByteSpec('bits_for_bytes'),
1455
 
                   ByteSpec('bits_for_milliseconds'),
1456
 
                   BinaryDataSpec('data') ]
1457
 
    def __eq__(self, other): return self.data == other
1458
 
    __hash__ = Frame.__hash__
1459
 
 
1460
 
class SYTC(Frame):
1461
 
    """Synchronised tempo codes.
1462
 
 
1463
 
    This frame's attributes may be changed in the future based on
1464
 
    feedback from real-world use.
1465
 
    """
1466
 
    _framespec = [ ByteSpec("format"), BinaryDataSpec("data") ]
1467
 
    def __eq__(self, other): return self.data == other
1468
 
    __hash__ = Frame.__hash__
1469
 
 
1470
 
class USLT(Frame):
1471
 
    """Unsynchronised lyrics/text transcription.
1472
 
 
1473
 
    Lyrics have a three letter ISO language code ('lang'), a
1474
 
    description ('desc'), and a block of plain text ('text').
1475
 
    """
1476
 
 
1477
 
    _framespec = [ EncodingSpec('encoding'), StringSpec('lang', 3),
1478
 
        EncodedTextSpec('desc'), EncodedTextSpec('text') ]
1479
 
    HashKey = property(lambda s: '%s:%s:%r' % (s.FrameID, s.desc, s.lang))
1480
 
 
1481
 
    def __str__(self): return self.text.encode('utf-8')
1482
 
    def __unicode__(self): return self.text
1483
 
    def __eq__(self, other): return self.text == other
1484
 
    __hash__ = Frame.__hash__
1485
 
    
1486
 
class SYLT(Frame):
1487
 
    """Synchronised lyrics/text."""
1488
 
 
1489
 
    _framespec = [ EncodingSpec('encoding'), StringSpec('lang', 3),
1490
 
        ByteSpec('format'), ByteSpec('type'), EncodedTextSpec('desc'),
1491
 
        SynchronizedTextSpec('text') ]
1492
 
    HashKey = property(lambda s: '%s:%s:%r' % (s.FrameID, s.desc, s.lang))
1493
 
 
1494
 
    def __eq__(self, other):
1495
 
        return str(self) == other
1496
 
    __hash__ = Frame.__hash__
1497
 
 
1498
 
    def __str__(self):
1499
 
        return "".join([text for (text, time) in self.text]).encode('utf-8')
1500
 
 
1501
 
class COMM(TextFrame):
1502
 
    """User comment.
1503
 
 
1504
 
    User comment frames have a descrption, like TXXX, and also a three
1505
 
    letter ISO language code in the 'lang' attribute.
1506
 
    """
1507
 
    _framespec = [ EncodingSpec('encoding'), StringSpec('lang', 3),
1508
 
        EncodedTextSpec('desc'),
1509
 
        MultiSpec('text', EncodedTextSpec('text'), sep=u'\u0000') ]
1510
 
    HashKey = property(lambda s: '%s:%s:%r' % (s.FrameID, s.desc, s.lang))
1511
 
    def _pprint(self): return "%s=%r=%s" % (
1512
 
        self.desc, self.lang, " / ".join(self.text))
1513
 
 
1514
 
class RVA2(Frame):
1515
 
    """Relative volume adjustment (2).
1516
 
 
1517
 
    This frame is used to implemented volume scaling, and in
1518
 
    particular, normalization using ReplayGain.
1519
 
 
1520
 
    Attributes:
1521
 
    desc -- description or context of this adjustment
1522
 
    channel -- audio channel to adjust (master is 1)
1523
 
    gain -- a + or - dB gain relative to some reference level
1524
 
    peak -- peak of the audio as a floating point number, [0, 1]
1525
 
 
1526
 
    When storing ReplayGain tags, use descriptions of 'album' and
1527
 
    'track' on channel 1.
1528
 
    """
1529
 
 
1530
 
    _framespec = [ Latin1TextSpec('desc'), ChannelSpec('channel'),
1531
 
        VolumeAdjustmentSpec('gain'), VolumePeakSpec('peak') ]
1532
 
    _channels = ["Other", "Master volume", "Front right", "Front left",
1533
 
                 "Back right", "Back left", "Front centre", "Back centre",
1534
 
                 "Subwoofer"]
1535
 
    HashKey = property(lambda s: '%s:%s' % (s.FrameID, s.desc))
1536
 
 
1537
 
    def __eq__(self, other):
1538
 
        return ((str(self) == other) or
1539
 
                (self.desc == other.desc and
1540
 
                 self.channel == other.channel and
1541
 
                 self.gain == other.gain and
1542
 
                 self.peak == other.peak))
1543
 
    __hash__ = Frame.__hash__
1544
 
 
1545
 
    def __str__(self):
1546
 
        return "%s: %+0.4f dB/%0.4f" % (
1547
 
            self._channels[self.channel], self.gain, self.peak)
1548
 
 
1549
 
class EQU2(Frame):
1550
 
    """Equalisation (2).
1551
 
 
1552
 
    Attributes:
1553
 
    method -- interpolation method (0 = band, 1 = linear)
1554
 
    desc -- identifying description
1555
 
    adjustments -- list of (frequency, vol_adjustment) pairs
1556
 
    """
1557
 
    _framespec = [ ByteSpec("method"), Latin1TextSpec("desc"),
1558
 
                   VolumeAdjustmentsSpec("adjustments") ]
1559
 
    def __eq__(self, other): return self.adjustments == other
1560
 
    __hash__ = Frame.__hash__
1561
 
    HashKey = property(lambda s: '%s:%s' % (s.FrameID, s.desc))
1562
 
 
1563
 
# class RVAD: unsupported
1564
 
# class EQUA: unsupported
1565
 
 
1566
 
class RVRB(Frame):
1567
 
    """Reverb."""
1568
 
    _framespec = [ SizedIntegerSpec('left', 2), SizedIntegerSpec('right', 2),
1569
 
                   ByteSpec('bounce_left'), ByteSpec('bounce_right'),
1570
 
                   ByteSpec('feedback_ltl'), ByteSpec('feedback_ltr'),
1571
 
                   ByteSpec('feedback_rtr'), ByteSpec('feedback_rtl'),
1572
 
                   ByteSpec('premix_ltr'), ByteSpec('premix_rtl') ]
1573
 
 
1574
 
    def __eq__(self, other): return (self.left, self.right) == other
1575
 
    __hash__ = Frame.__hash__
1576
 
 
1577
 
class APIC(Frame):
1578
 
    """Attached (or linked) Picture.
1579
 
 
1580
 
    Attributes:
1581
 
    encoding -- text encoding for the description
1582
 
    mime -- a MIME type (e.g. image/jpeg) or '-->' if the data is a URI
1583
 
    type -- the source of the image (3 is the album front cover)
1584
 
    desc -- a text description of the image
1585
 
    data -- raw image data, as a byte string
1586
 
 
1587
 
    Mutagen will automatically compress large images when saving tags.
1588
 
    """
1589
 
    _framespec = [ EncodingSpec('encoding'), Latin1TextSpec('mime'),
1590
 
        ByteSpec('type'), EncodedTextSpec('desc'), BinaryDataSpec('data') ]
1591
 
    def __eq__(self, other): return self.data == other
1592
 
    __hash__ = Frame.__hash__
1593
 
    HashKey = property(lambda s: '%s:%s' % (s.FrameID, s.desc))
1594
 
    def _pprint(self):
1595
 
        return "%s (%s, %d bytes)" % (
1596
 
            self.desc, self.mime, len(self.data))
1597
 
 
1598
 
class PCNT(Frame):
1599
 
    """Play counter.
1600
 
 
1601
 
    The 'count' attribute contains the (recorded) number of times this
1602
 
    file has been played.
1603
 
 
1604
 
    This frame is basically obsoleted by POPM.
1605
 
    """
1606
 
    _framespec = [ IntegerSpec('count') ]
1607
 
 
1608
 
    def __eq__(self, other): return self.count == other
1609
 
    __hash__ = Frame.__hash__
1610
 
    def __pos__(self): return self.count
1611
 
    def _pprint(self): return unicode(self.count)
1612
 
 
1613
 
class POPM(FrameOpt):
1614
 
    """Popularimeter.
1615
 
 
1616
 
    This frame keys a rating (out of 255) and a play count to an email
1617
 
    address.
1618
 
 
1619
 
    Attributes:
1620
 
    email -- email this POPM frame is for
1621
 
    rating -- rating from 0 to 255
1622
 
    count -- number of times the files has been played (optional)
1623
 
    """
1624
 
    _framespec = [ Latin1TextSpec('email'), ByteSpec('rating') ]
1625
 
    _optionalspec = [ IntegerSpec('count') ]
1626
 
                   
1627
 
    HashKey = property(lambda s: '%s:%s' % (s.FrameID, s.email))
1628
 
 
1629
 
    def __eq__(self, other): return self.rating == other
1630
 
    __hash__ = FrameOpt.__hash__
1631
 
    def __pos__(self): return self.rating
1632
 
    def _pprint(self): return "%s=%r %r/255" % (
1633
 
        self.email, getattr(self, 'count', None), self.rating)
1634
 
 
1635
 
class GEOB(Frame):
1636
 
    """General Encapsulated Object.
1637
 
 
1638
 
    A blob of binary data, that is not a picture (those go in APIC).
1639
 
 
1640
 
    Attributes:
1641
 
    encoding -- encoding of the description
1642
 
    mime -- MIME type of the data or '-->' if the data is a URI
1643
 
    filename -- suggested filename if extracted
1644
 
    desc -- text description of the data
1645
 
    data -- raw data, as a byte string
1646
 
    """
1647
 
    _framespec = [ EncodingSpec('encoding'), Latin1TextSpec('mime'),
1648
 
        EncodedTextSpec('filename'), EncodedTextSpec('desc'), 
1649
 
        BinaryDataSpec('data') ]
1650
 
    HashKey = property(lambda s: '%s:%s' % (s.FrameID, s.desc))
1651
 
 
1652
 
    def __eq__(self, other): return self.data == other
1653
 
    __hash__ = Frame.__hash__
1654
 
 
1655
 
class RBUF(FrameOpt):
1656
 
    """Recommended buffer size.
1657
 
 
1658
 
    Attributes:
1659
 
    size -- recommended buffer size in bytes
1660
 
    info -- if ID3 tags may be elsewhere in the file (optional)
1661
 
    offset -- the location of the next ID3 tag, if any
1662
 
 
1663
 
    Mutagen will not find the next tag itself.
1664
 
    """
1665
 
    _framespec = [ SizedIntegerSpec('size', 3) ]
1666
 
    _optionalspec = [ ByteSpec('info'), SizedIntegerSpec('offset', 4) ]
1667
 
 
1668
 
    def __eq__(self, other): return self.size == other
1669
 
    __hash__ = FrameOpt.__hash__
1670
 
    def __pos__(self): return self.size
1671
 
 
1672
 
class AENC(FrameOpt):
1673
 
    """Audio encryption.
1674
 
 
1675
 
    Attributes:
1676
 
    owner -- key identifying this encryption type
1677
 
    preview_start -- unencrypted data block offset
1678
 
    preview_length -- number of unencrypted blocks
1679
 
    data -- data required for decryption (optional)
1680
 
 
1681
 
    Mutagen cannot decrypt files.
1682
 
    """
1683
 
    _framespec = [ Latin1TextSpec('owner'),
1684
 
                   SizedIntegerSpec('preview_start', 2),
1685
 
                   SizedIntegerSpec('preview_length', 2) ]
1686
 
    _optionalspec = [ BinaryDataSpec('data') ]
1687
 
    HashKey = property(lambda s: '%s:%s' % (s.FrameID, s.owner))
1688
 
 
1689
 
    def __str__(self): return self.owner.encode('utf-8')
1690
 
    def __unicode__(self): return self.owner
1691
 
    def __eq__(self, other): return self.owner == other
1692
 
    __hash__ = FrameOpt.__hash__
1693
 
 
1694
 
class LINK(FrameOpt):
1695
 
    """Linked information.
1696
 
 
1697
 
    Attributes:
1698
 
    frameid -- the ID of the linked frame
1699
 
    url -- the location of the linked frame
1700
 
    data -- further ID information for the frame
1701
 
    """
1702
 
 
1703
 
    _framespec = [ StringSpec('frameid', 4), Latin1TextSpec('url') ]
1704
 
    _optionalspec = [ BinaryDataSpec('data') ]
1705
 
    def __HashKey(self):
1706
 
        try:
1707
 
            return "%s:%s:%s:%r" % (
1708
 
                self.FrameID, self.frameid, self.url, self.data)
1709
 
        except AttributeError:
1710
 
            return "%s:%s:%s" % (self.FrameID, self.frameid, self.url)
1711
 
    HashKey = property(__HashKey)
1712
 
    def __eq__(self, other):
1713
 
        try: return (self.frameid, self.url, self.data) == other
1714
 
        except AttributeError: return (self.frameid, self.url) == other
1715
 
    __hash__ = FrameOpt.__hash__
1716
 
 
1717
 
class POSS(Frame):
1718
 
    """Position synchronisation frame
1719
 
 
1720
 
    Attribute:
1721
 
    format -- format of the position attribute (frames or milliseconds)
1722
 
    position -- current position of the file
1723
 
    """
1724
 
    _framespec = [ ByteSpec('format'), IntegerSpec('position') ]
1725
 
 
1726
 
    def __pos__(self): return self.position
1727
 
    def __eq__(self, other): return self.position == other
1728
 
    __hash__ = Frame.__hash__
1729
 
 
1730
 
class UFID(Frame):
1731
 
    """Unique file identifier.
1732
 
 
1733
 
    Attributes:
1734
 
    owner -- format/type of identifier
1735
 
    data -- identifier
1736
 
    """
1737
 
 
1738
 
    _framespec = [ Latin1TextSpec('owner'), BinaryDataSpec('data') ]
1739
 
    HashKey = property(lambda s: '%s:%s' % (s.FrameID, s.owner))
1740
 
    def __eq__(s, o):
1741
 
        if isinstance(o, UFI): return s.owner == o.owner and s.data == o.data
1742
 
        else: return s.data == o
1743
 
    __hash__ = Frame.__hash__
1744
 
    def _pprint(self):
1745
 
        isascii = ord(max(self.data)) < 128
1746
 
        if isascii: return "%s=%s" % (self.owner, self.data)
1747
 
        else: return "%s (%d bytes)" % (self.owner, len(self.data))
1748
 
 
1749
 
class USER(Frame):
1750
 
    """Terms of use.
1751
 
 
1752
 
    Attributes:
1753
 
    encoding -- text encoding
1754
 
    lang -- ISO three letter language code
1755
 
    text -- licensing terms for the audio
1756
 
    """
1757
 
    _framespec = [ EncodingSpec('encoding'), StringSpec('lang', 3),
1758
 
        EncodedTextSpec('text') ]
1759
 
    HashKey = property(lambda s: '%s:%r' % (s.FrameID, s.lang))
1760
 
 
1761
 
    def __str__(self): return self.text.encode('utf-8')
1762
 
    def __unicode__(self): return self.text
1763
 
    def __eq__(self, other): return self.text == other
1764
 
    __hash__ = Frame.__hash__
1765
 
    def _pprint(self): return "%r=%s" % (self.lang, self.text)
1766
 
 
1767
 
class OWNE(Frame):
1768
 
    """Ownership frame."""
1769
 
    _framespec = [ EncodingSpec('encoding'), Latin1TextSpec('price'),
1770
 
                   StringSpec('date', 8), EncodedTextSpec('seller') ]
1771
 
 
1772
 
    def __str__(self): return self.seller.encode('utf-8')
1773
 
    def __unicode__(self): return self.seller
1774
 
    def __eq__(self, other): return self.seller == other
1775
 
    __hash__ = Frame.__hash__
1776
 
 
1777
 
class COMR(FrameOpt):
1778
 
    """Commercial frame."""
1779
 
    _framespec = [ EncodingSpec('encoding'), Latin1TextSpec('price'),
1780
 
                   StringSpec('valid_until', 8), Latin1TextSpec('contact'),
1781
 
                   ByteSpec('format'), EncodedTextSpec('seller'),
1782
 
                   EncodedTextSpec('desc')]
1783
 
    _optionalspec = [ Latin1TextSpec('mime'), BinaryDataSpec('logo') ]
1784
 
    HashKey = property(lambda s: '%s:%s' % (s.FrameID, s._writeData()))
1785
 
    def __eq__(self, other): return self._writeData() == other._writeData()
1786
 
    __hash__ = FrameOpt.__hash__
1787
 
 
1788
 
class ENCR(Frame):
1789
 
    """Encryption method registration.
1790
 
 
1791
 
    The standard does not allow multiple ENCR frames with the same owner
1792
 
    or the same method. Mutagen only verifies that the owner is unique.
1793
 
    """
1794
 
    _framespec = [ Latin1TextSpec('owner'), ByteSpec('method'),
1795
 
                   BinaryDataSpec('data') ]
1796
 
    HashKey = property(lambda s: "%s:%s" % (s.FrameID, s.owner))
1797
 
    def __str__(self): return self.data
1798
 
    def __eq__(self, other): return self.data == other
1799
 
    __hash__ = Frame.__hash__
1800
 
 
1801
 
class GRID(FrameOpt):
1802
 
    """Group identification registration."""
1803
 
    _framespec = [ Latin1TextSpec('owner'), ByteSpec('group') ]
1804
 
    _optionalspec = [ BinaryDataSpec('data') ]
1805
 
    HashKey = property(lambda s: '%s:%s' % (s.FrameID, s.group))
1806
 
    def __pos__(self): return self.group
1807
 
    def __str__(self): return self.owner.encode('utf-8')
1808
 
    def __unicode__(self): return self.owner
1809
 
    def __eq__(self, other): return self.owner == other or self.group == other
1810
 
    __hash__ = FrameOpt.__hash__
1811
 
    
1812
 
 
1813
 
class PRIV(Frame):
1814
 
    """Private frame."""
1815
 
    _framespec = [ Latin1TextSpec('owner'), BinaryDataSpec('data') ]
1816
 
    HashKey = property(lambda s: '%s:%s:%s' % (
1817
 
        s.FrameID, s.owner, s.data.decode('latin1')))
1818
 
    def __str__(self): return self.data
1819
 
    def __eq__(self, other): return self.data == other
1820
 
    def _pprint(self):
1821
 
        isascii = ord(max(self.data)) < 128
1822
 
        if isascii: return "%s=%s" % (self.owner, self.data)
1823
 
        else: return "%s (%d bytes)" % (self.owner, len(self.data))
1824
 
    __hash__ = Frame.__hash__
1825
 
 
1826
 
class SIGN(Frame):
1827
 
    """Signature frame."""
1828
 
    _framespec = [ ByteSpec('group'), BinaryDataSpec('sig') ]
1829
 
    HashKey = property(lambda s: '%s:%c:%s' % (s.FrameID, s.group, s.sig))
1830
 
    def __str__(self): return self.sig
1831
 
    def __eq__(self, other): return self.sig == other
1832
 
    __hash__ = Frame.__hash__
1833
 
 
1834
 
class SEEK(Frame):
1835
 
    """Seek frame.
1836
 
 
1837
 
    Mutagen does not find tags at seek offsets.
1838
 
    """
1839
 
    _framespec = [ IntegerSpec('offset') ]
1840
 
    def __pos__(self): return self.offset
1841
 
    def __eq__(self, other): return self.offset == other
1842
 
    __hash__ = Frame.__hash__
1843
 
 
1844
 
class ASPI(Frame):
1845
 
    """Audio seek point index.
1846
 
 
1847
 
    Attributes: S, L, N, b, and Fi. For the meaning of these, see
1848
 
    the ID3v2.4 specification. Fi is a list of integers.
1849
 
    """
1850
 
    _framespec = [ SizedIntegerSpec("S", 4), SizedIntegerSpec("L", 4),
1851
 
                   SizedIntegerSpec("N", 2), ByteSpec("b"),
1852
 
                   ASPIIndexSpec("Fi") ]
1853
 
    def __eq__(self, other): return self.Fi == other
1854
 
    __hash__ = Frame.__hash__
1855
 
 
1856
 
Frames = dict([(k,v) for (k,v) in globals().items()
1857
 
        if len(k)==4 and isinstance(v, type) and issubclass(v, Frame)])
1858
 
"""All supported ID3v2 frames, keyed by frame name."""
1859
 
del(k); del(v)
1860
 
 
1861
 
# ID3v2.2 frames
1862
 
class UFI(UFID): "Unique File Identifier"
1863
 
 
1864
 
class TT1(TIT1): "Content group description"
1865
 
class TT2(TIT2): "Title"
1866
 
class TT3(TIT3): "Subtitle/Description refinement"
1867
 
class TP1(TPE1): "Lead Artist/Performer/Soloist/Group"
1868
 
class TP2(TPE2): "Band/Orchestra/Accompaniment"
1869
 
class TP3(TPE3): "Conductor"
1870
 
class TP4(TPE4): "Interpreter/Remixer/Modifier"
1871
 
class TCM(TCOM): "Composer"
1872
 
class TXT(TEXT): "Lyricist"
1873
 
class TLA(TLAN): "Audio Language(s)"
1874
 
class TCO(TCON): "Content Type (Genre)"
1875
 
class TAL(TALB): "Album"
1876
 
class TPA(TPOS): "Part of set"
1877
 
class TRK(TRCK): "Track Number"
1878
 
class TRC(TSRC): "International Standard Recording Code (ISRC)"
1879
 
class TYE(TYER): "Year of recording"
1880
 
class TDA(TDAT): "Date of recording (DDMM)"
1881
 
class TIM(TIME): "Time of recording (HHMM)"
1882
 
class TRD(TRDA): "Recording Dates"
1883
 
class TMT(TMED): "Source Media Type"
1884
 
class TFT(TFLT): "File Type"
1885
 
class TBP(TBPM): "Beats per minute"
1886
 
class TCP(TCMP): "iTunes Compilation Flag"
1887
 
class TCR(TCOP): "Copyright (C)"
1888
 
class TPB(TPUB): "Publisher"
1889
 
class TEN(TENC): "Encoder"
1890
 
class TSS(TSSE): "Encoder settings"
1891
 
class TOF(TOFN): "Original Filename"
1892
 
class TLE(TLEN): "Audio Length (ms)"
1893
 
class TSI(TSIZ): "Audio Data size (bytes)"
1894
 
class TDY(TDLY): "Audio Delay (ms)"
1895
 
class TKE(TKEY): "Starting Key"
1896
 
class TOT(TOAL): "Original Album"
1897
 
class TOA(TOPE): "Original Artist/Perfomer"
1898
 
class TOL(TOLY): "Original Lyricist"
1899
 
class TOR(TORY): "Original Release Year"
1900
 
 
1901
 
class TXX(TXXX): "User-defined Text"
1902
 
 
1903
 
class WAF(WOAF): "Official File Information"
1904
 
class WAR(WOAR): "Official Artist/Performer Information"
1905
 
class WAS(WOAS): "Official Source Information"
1906
 
class WCM(WCOM): "Commercial Information"
1907
 
class WCP(WCOP): "Copyright Information"
1908
 
class WPB(WPUB): "Official Publisher Information"
1909
 
 
1910
 
class WXX(WXXX): "User-defined URL"
1911
 
 
1912
 
class IPL(IPLS): "Involved people list"
1913
 
class MCI(MCDI): "Binary dump of CD's TOC"
1914
 
class ETC(ETCO): "Event timing codes"
1915
 
class MLL(MLLT): "MPEG location lookup table"
1916
 
class STC(SYTC): "Synced tempo codes"
1917
 
class ULT(USLT): "Unsychronised lyrics/text transcription"
1918
 
class SLT(SYLT): "Synchronised lyrics/text"
1919
 
class COM(COMM): "Comment"
1920
 
#class RVA(RVAD)
1921
 
#class EQU(EQUA)
1922
 
class REV(RVRB): "Reverb"
1923
 
class PIC(APIC):
1924
 
    """Attached Picture.
1925
 
 
1926
 
    The 'mime' attribute of an ID3v2.2 attached picture must be either
1927
 
    'PNG' or 'JPG'.
1928
 
    """
1929
 
    _framespec = [ EncodingSpec('encoding'), StringSpec('mime', 3),
1930
 
        ByteSpec('type'), EncodedTextSpec('desc'), BinaryDataSpec('data') ]
1931
 
class GEO(GEOB): "General Encapsulated Object"
1932
 
class CNT(PCNT): "Play counter"
1933
 
class POP(POPM): "Popularimeter"
1934
 
class BUF(RBUF): "Recommended buffer size"
1935
 
 
1936
 
class CRM(Frame):
1937
 
    """Encrypted meta frame"""
1938
 
    _framespec = [ Latin1TextSpec('owner'), Latin1TextSpec('desc'),
1939
 
                   BinaryDataSpec('data') ]
1940
 
    def __eq__(self, other): return self.data == other
1941
 
    __hash__ = Frame.__hash__
1942
 
 
1943
 
class CRA(AENC): "Audio encryption"
1944
 
 
1945
 
class LNK(LINK):
1946
 
    """Linked information"""
1947
 
    _framespec = [ StringSpec('frameid', 3), Latin1TextSpec('url') ]
1948
 
    _optionalspec = [ BinaryDataSpec('data') ]
1949
 
 
1950
 
Frames_2_2 = dict([(k,v) for (k,v) in globals().items()
1951
 
        if len(k)==3 and isinstance(v, type) and issubclass(v, Frame)])
1952
748
 
1953
749
# support open(filename) as interface
1954
750
Open = ID3
1955
751
 
 
752
 
1956
753
# ID3v1.1 support.
1957
754
def ParseID3v1(string):
1958
755
    """Parse an ID3v1 tag, returning a list of ID3v2.4 frames."""
1987
784
        fix, [title, artist, album, year, comment])
1988
785
 
1989
786
    frames = {}
1990
 
    if title: frames["TIT2"] = TIT2(encoding=0, text=title)
1991
 
    if artist: frames["TPE1"] = TPE1(encoding=0, text=[artist])
1992
 
    if album: frames["TALB"] = TALB(encoding=0, text=album)
1993
 
    if year: frames["TDRC"] = TDRC(encoding=0, text=year)
1994
 
    if comment: frames["COMM"] = COMM(
1995
 
        encoding=0, lang="eng", desc="ID3v1 Comment", text=comment)
 
787
    if title:
 
788
        frames["TIT2"] = TIT2(encoding=0, text=title)
 
789
    if artist:
 
790
        frames["TPE1"] = TPE1(encoding=0, text=[artist])
 
791
    if album:
 
792
        frames["TALB"] = TALB(encoding=0, text=album)
 
793
    if year:
 
794
        frames["TDRC"] = TDRC(encoding=0, text=year)
 
795
    if comment:
 
796
        frames["COMM"] = COMM(
 
797
            encoding=0, lang="eng", desc="ID3v1 Comment", text=comment)
1996
798
    # Don't read a track number if it looks like the comment was
1997
799
    # padded with spaces instead of nulls (thanks, WinAmp).
1998
800
    if track and (track != 32 or string[-3] == '\x00'):
1999
801
        frames["TRCK"] = TRCK(encoding=0, text=str(track))
2000
 
    if genre != 255: frames["TCON"] = TCON(encoding=0, text=str(genre))
 
802
    if genre != 255:
 
803
        frames["TCON"] = TCON(encoding=0, text=str(genre))
2001
804
    return frames
2002
805
 
 
806
 
2003
807
def MakeID3v1(id3):
2004
808
    """Return an ID3v1.1 tag string from a dict of ID3v2.4 frames."""
2005
809
 
2020
824
    v1["comment"] = cmnt + ("\x00" * (29 - len(cmnt)))
2021
825
 
2022
826
    if "TRCK" in id3:
2023
 
        try: v1["track"] = chr(+id3["TRCK"])
2024
 
        except ValueError: v1["track"] = "\x00"
2025
 
    else: v1["track"] = "\x00"
 
827
        try:
 
828
            v1["track"] = chr(+id3["TRCK"])
 
829
        except ValueError:
 
830
            v1["track"] = "\x00"
 
831
    else:
 
832
        v1["track"] = "\x00"
2026
833
 
2027
834
    if "TCON" in id3:
2028
 
        try: genre = id3["TCON"].genres[0]
2029
 
        except IndexError: pass
 
835
        try:
 
836
            genre = id3["TCON"].genres[0]
 
837
        except IndexError:
 
838
            pass
2030
839
        else:
2031
840
            if genre in TCON.GENRES:
2032
841
                v1["genre"] = chr(TCON.GENRES.index(genre))
2042
851
    v1["year"] = (year + "\x00\x00\x00\x00")[:4]
2043
852
 
2044
853
    return ("TAG%(title)s%(artist)s%(album)s%(year)s%(comment)s"
2045
 
            "%(track)s%(genre)s") % v1 
 
854
            "%(track)s%(genre)s") % v1
 
855
 
2046
856
 
2047
857
class ID3FileType(mutagen.FileType):
2048
858
    """An unknown type of file with ID3 tags."""
2049
859
 
2050
860
    ID3 = ID3
2051
 
    
 
861
 
2052
862
    class _Info(object):
2053
863
        length = 0
2054
 
        def __init__(self, fileobj, offset): pass
2055
 
        pprint = staticmethod(lambda: "Unknown format with ID3 tag")
2056
 
 
 
864
 
 
865
        def __init__(self, fileobj, offset):
 
866
            pass
 
867
 
 
868
        @staticmethod
 
869
        def pprint():
 
870
            return "Unknown format with ID3 tag"
 
871
 
 
872
    @staticmethod
2057
873
    def score(filename, fileobj, header):
2058
874
        return header.startswith("ID3")
2059
 
    score = staticmethod(score)
2060
875
 
2061
876
    def add_tags(self, ID3=None):
2062
877
        """Add an empty ID3 tag to the file.
2078
893
        A custom tag reader may be used in instead of the default
2079
894
        mutagen.id3.ID3 object, e.g. an EasyID3 reader.
2080
895
        """
 
896
 
2081
897
        if ID3 is None:
2082
898
            ID3 = self.ID3
2083
899
        else:
2085
901
            # when tags are auto-instantiated in add_tags.
2086
902
            self.ID3 = ID3
2087
903
        self.filename = filename
2088
 
        try: self.tags = ID3(filename, **kwargs)
2089
 
        except error: self.tags = None
 
904
        try:
 
905
            self.tags = ID3(filename, **kwargs)
 
906
        except error:
 
907
            self.tags = None
2090
908
        if self.tags is not None:
2091
 
            try: offset = self.tags.size
2092
 
            except AttributeError: offset = None
2093
 
        else: offset = None
 
909
            try:
 
910
                offset = self.tags.size
 
911
            except AttributeError:
 
912
                offset = None
 
913
        else:
 
914
            offset = None
2094
915
        try:
2095
916
            fileobj = open(filename, "rb")
2096
917
            self.info = self._Info(fileobj, offset)