Usunięcie biblioteki z repozytorium i dodanie jej za to do requirements.txt.
[wolnelektury.git] / lib / mutagen / id3.py
diff --git a/lib/mutagen/id3.py b/lib/mutagen/id3.py
deleted file mode 100644 (file)
index fb1357e..0000000
+++ /dev/null
@@ -1,1956 +0,0 @@
-# id3 support for mutagen
-# Copyright (C) 2005  Michael Urman
-#
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of version 2 of the GNU General Public License as
-# published by the Free Software Foundation.
-#
-# $Id: id3.py 4275 2008-06-01 06:32:37Z piman $
-
-"""ID3v2 reading and writing.
-
-This is based off of the following references:
-   http://www.id3.org/id3v2.4.0-structure.txt
-   http://www.id3.org/id3v2.4.0-frames.txt
-   http://www.id3.org/id3v2.3.0.html
-   http://www.id3.org/id3v2-00.txt
-   http://www.id3.org/id3v1.html
-
-Its largest deviation from the above (versions 2.3 and 2.2) is that it
-will not interpret the / characters as a separator, and will almost
-always accept null separators to generate multi-valued text frames.
-
-Because ID3 frame structure differs between frame types, each frame is
-implemented as a different class (e.g. TIT2 as mutagen.id3.TIT2). Each
-frame's documentation contains a list of its attributes.
-
-Since this file's documentation is a little unwieldy, you are probably
-interested in the 'ID3' class to start with.
-"""
-
-__all__ = ['ID3', 'ID3FileType', 'Frames', 'Open', 'delete']
-
-import struct; from struct import unpack, pack
-from zlib import error as zlibError
-from warnings import warn
-
-import mutagen
-from mutagen._util import insert_bytes, delete_bytes, DictProxy
-
-class error(Exception): pass
-class ID3NoHeaderError(error, ValueError): pass
-class ID3BadUnsynchData(error, ValueError): pass
-class ID3BadCompressedData(error, ValueError): pass
-class ID3TagError(error, ValueError): pass
-class ID3UnsupportedVersionError(error, NotImplementedError): pass
-class ID3EncryptionUnsupportedError(error, NotImplementedError): pass
-class ID3JunkFrameError(error, ValueError): pass
-
-class ID3Warning(error, UserWarning): pass
-
-def is_valid_frame_id(frame_id):
-    return frame_id.isalnum() and frame_id.isupper()
-
-class ID3(DictProxy, mutagen.Metadata):
-    """A file with an ID3v2 tag.
-
-    Attributes:
-    version -- ID3 tag version as a tuple
-    unknown_frames -- raw frame data of any unknown frames found
-    size -- the total size of the ID3 tag, including the header
-    """
-
-    PEDANTIC = True
-    version = (2, 4, 0)
-
-    filename = None
-    size = 0
-    __flags = 0
-    __readbytes = 0
-    __crc = None
-
-    def __init__(self, *args, **kwargs):
-        self.unknown_frames = []
-        super(ID3, self).__init__(*args, **kwargs)
-
-    def __fullread(self, size):
-        try:
-            if size < 0:
-                raise ValueError('Requested bytes (%s) less than zero' % size)
-            if size > self.__filesize:
-                raise EOFError('Requested %#x of %#x (%s)' % 
-                        (long(size), long(self.__filesize), self.filename))
-        except AttributeError: pass
-        data = self.__fileobj.read(size)
-        if len(data) != size: raise EOFError
-        self.__readbytes += size
-        return data
-
-    def load(self, filename, known_frames=None, translate=True):
-        """Load tags from a filename.
-
-        Keyword arguments:
-        filename -- filename to load tag data from
-        known_frames -- dict mapping frame IDs to Frame objects
-        translate -- Update all tags to ID3v2.4 internally. Mutagen is
-                     only capable of writing ID3v2.4 tags, so if you
-                     intend to save, this must be true.
-
-        Example of loading a custom frame:
-            my_frames = dict(mutagen.id3.Frames)
-            class XMYF(Frame): ...
-            my_frames["XMYF"] = XMYF
-            mutagen.id3.ID3(filename, known_frames=my_frames)
-        """
-
-        from os.path import getsize
-        self.filename = filename
-        self.__known_frames = known_frames
-        self.__fileobj = file(filename, 'rb')
-        self.__filesize = getsize(filename)
-        try:
-            try:
-                self.__load_header()
-            except EOFError:
-                self.size = 0
-                raise ID3NoHeaderError("%s: too small (%d bytes)" %(
-                    filename, self.__filesize))
-            except (ID3NoHeaderError, ID3UnsupportedVersionError), err:
-                self.size = 0
-                import sys
-                stack = sys.exc_info()[2]
-                try: self.__fileobj.seek(-128, 2)
-                except EnvironmentError: raise err, None, stack
-                else:
-                    frames = ParseID3v1(self.__fileobj.read(128))
-                    if frames is not None:
-                        self.version = (1, 1)
-                        map(self.add, frames.values())
-                    else: raise err, None, stack
-            else:
-                frames = self.__known_frames
-                if frames is None:
-                    if (2,3,0) <= self.version: frames = Frames
-                    elif (2,2,0) <= self.version: frames = Frames_2_2
-                data = self.__fullread(self.size - 10)
-                for frame in self.__read_frames(data, frames=frames):
-                    if isinstance(frame, Frame): self.add(frame)
-                    else: self.unknown_frames.append(frame)
-        finally:
-            self.__fileobj.close()
-            del self.__fileobj
-            del self.__filesize
-            if translate:
-                self.update_to_v24()
-
-    def getall(self, key):
-        """Return all frames with a given name (the list may be empty).
-
-        This is best explained by examples:
-            id3.getall('TIT2') == [id3['TIT2']]
-            id3.getall('TTTT') == []
-            id3.getall('TXXX') == [TXXX(desc='woo', text='bar'),
-                                   TXXX(desc='baz', text='quuuux'), ...]
-
-        Since this is based on the frame's HashKey, which is
-        colon-separated, you can use it to do things like
-        getall('COMM:MusicMatch') or getall('TXXX:QuodLibet:').
-        """
-        if key in self: return [self[key]]
-        else:
-            key = key + ":"
-            return [v for s,v in self.items() if s.startswith(key)]
-
-    def delall(self, key):
-        """Delete all tags of a given kind; see getall."""
-        if key in self: del(self[key])
-        else:
-            key = key + ":"
-            for k in filter(lambda s: s.startswith(key), self.keys()):
-                del(self[k])
-
-    def setall(self, key, values):
-        """Delete frames of the given type and add frames in 'values'."""
-        self.delall(key)
-        for tag in values:
-            self[tag.HashKey] = tag
-
-    def pprint(self):
-        """Return tags in a human-readable format.
-
-        "Human-readable" is used loosely here. The format is intended
-        to mirror that used for Vorbis or APEv2 output, e.g.
-            TIT2=My Title
-        However, ID3 frames can have multiple keys:
-            POPM=user@example.org=3 128/255
-        """
-        return "\n".join(map(Frame.pprint, self.values()))
-
-    def loaded_frame(self, tag):
-        """Deprecated; use the add method."""
-        # turn 2.2 into 2.3/2.4 tags
-        if len(type(tag).__name__) == 3: tag = type(tag).__base__(tag)
-        self[tag.HashKey] = tag
-
-    # add = loaded_frame (and vice versa) break applications that
-    # expect to be able to override loaded_frame (e.g. Quod Libet),
-    # as does making loaded_frame call add.
-    def add(self, frame):
-        """Add a frame to the tag."""
-        return self.loaded_frame(frame)
-
-    def __load_header(self):
-        fn = self.filename
-        data = self.__fullread(10)
-        id3, vmaj, vrev, flags, size = unpack('>3sBBB4s', data)
-        self.__flags = flags
-        self.size = BitPaddedInt(size) + 10
-        self.version = (2, vmaj, vrev)
-
-        if id3 != 'ID3':
-            raise ID3NoHeaderError("'%s' doesn't start with an ID3 tag" % fn)
-        if vmaj not in [2, 3, 4]:
-            raise ID3UnsupportedVersionError("'%s' ID3v2.%d not supported"
-                    % (fn, vmaj))
-
-        if self.PEDANTIC:
-            if (2,4,0) <= self.version and (flags & 0x0f):
-                raise ValueError("'%s' has invalid flags %#02x" % (fn, flags))
-            elif (2,3,0) <= self.version and (flags & 0x1f):
-                raise ValueError("'%s' has invalid flags %#02x" % (fn, flags))
-
-        if self.f_extended:
-            if self.version >= (2,4,0):
-                # "Where the 'Extended header size' is the size of the whole
-                # extended header, stored as a 32 bit synchsafe integer."
-                self.__extsize = BitPaddedInt(self.__fullread(4)) - 4
-            else:
-                # "Where the 'Extended header size', currently 6 or 10 bytes,
-                # excludes itself."
-                self.__extsize = unpack('>L', self.__fullread(4))[0]
-            self.__extdata = self.__fullread(self.__extsize)
-
-    def __determine_bpi(self, data, frames):
-        if self.version < (2,4,0): return int
-        # have to special case whether to use bitpaddedints here
-        # spec says to use them, but iTunes has it wrong
-
-        # count number of tags found as BitPaddedInt and how far past
-        o = 0
-        asbpi = 0
-        while o < len(data)-10:
-            name, size, flags = unpack('>4sLH', data[o:o+10])
-            size = BitPaddedInt(size)
-            o += 10+size
-            if name in frames: asbpi += 1
-        bpioff = o - len(data)
-
-        # count number of tags found as int and how far past
-        o = 0
-        asint = 0
-        while o < len(data)-10:
-            name, size, flags = unpack('>4sLH', data[o:o+10])
-            o += 10+size
-            if name in frames: asint += 1
-        intoff = o - len(data)
-
-        # if more tags as int, or equal and bpi is past and int is not
-        if asint > asbpi or (asint == asbpi and (bpioff >= 1 and intoff <= 1)):
-            return int
-        return BitPaddedInt
-
-    def __read_frames(self, data, frames):
-        if self.version < (2,4,0) and self.f_unsynch:
-            try: data = unsynch.decode(data)
-            except ValueError: pass
-
-        if (2,3,0) <= self.version:
-            bpi = self.__determine_bpi(data, frames)
-            while data:
-                header = data[:10]
-                try: name, size, flags = unpack('>4sLH', header)
-                except struct.error: return # not enough header
-                if name.strip('\x00') == '': return
-                size = bpi(size)
-                framedata = data[10:10+size]
-                data = data[10+size:]
-                if size == 0: continue # drop empty frames
-                try: tag = frames[name]
-                except KeyError: 
-                    if is_valid_frame_id(name): yield header + framedata
-                else:
-                    try: yield self.__load_framedata(tag, flags, framedata)
-                    except NotImplementedError: yield header + framedata
-                    except ID3JunkFrameError: pass
-
-        elif (2,2,0) <= self.version:
-            while data:
-                header = data[0:6]
-                try: name, size = unpack('>3s3s', header)
-                except struct.error: return # not enough header
-                size, = struct.unpack('>L', '\x00'+size)
-                if name.strip('\x00') == '': return
-                framedata = data[6:6+size]
-                data = data[6+size:]
-                if size == 0: continue # drop empty frames
-                try: tag = frames[name]
-                except KeyError:
-                    if is_valid_frame_id(name): yield header + framedata
-                else:
-                    try: yield self.__load_framedata(tag, 0, framedata)
-                    except NotImplementedError: yield header + framedata
-                    except ID3JunkFrameError: pass
-
-    def __load_framedata(self, tag, flags, framedata):
-        return tag.fromData(self, flags, framedata)
-            
-    f_unsynch = property(lambda s: bool(s.__flags & 0x80))
-    f_extended = property(lambda s: bool(s.__flags & 0x40))
-    f_experimental = property(lambda s: bool(s.__flags & 0x20))
-    f_footer = property(lambda s: bool(s.__flags & 0x10))
-
-    #f_crc = property(lambda s: bool(s.__extflags & 0x8000))
-
-    def save(self, filename=None, v1=1):
-        """Save changes to a file.
-
-        If no filename is given, the one most recently loaded is used.
-
-        Keyword arguments:
-        v1 -- if 0, ID3v1 tags will be removed
-              if 1, ID3v1 tags will be updated but not added
-              if 2, ID3v1 tags will be created and/or updated
-
-        The lack of a way to update only an ID3v1 tag is intentional.
-        """
-
-        # Sort frames by 'importance'
-        order = ["TIT2", "TPE1", "TRCK", "TALB", "TPOS", "TDRC", "TCON"]
-        order = dict(zip(order, range(len(order))))
-        last = len(order)
-        frames = self.items()
-        frames.sort(lambda a, b: cmp(order.get(a[0][:4], last),
-                                     order.get(b[0][:4], last)))
-
-        framedata = [self.__save_frame(frame) for (key, frame) in frames]
-        framedata.extend([data for data in self.unknown_frames
-                if len(data) > 10])
-        if not framedata:
-            try:
-                self.delete(filename)
-            except EnvironmentError, err:
-                from errno import ENOENT
-                if err.errno != ENOENT: raise
-            return
-
-        framedata = ''.join(framedata)
-        framesize = len(framedata)
-
-        if filename is None: filename = self.filename
-        try: f = open(filename, 'rb+')
-        except IOError, err:
-            from errno import ENOENT
-            if err.errno != ENOENT: raise
-            f = open(filename, 'ab') # create, then reopen
-            f = open(filename, 'rb+')
-        try:
-            idata = f.read(10)
-            try: id3, vmaj, vrev, flags, insize = unpack('>3sBBB4s', idata)
-            except struct.error: id3, insize = '', 0
-            insize = BitPaddedInt(insize)
-            if id3 != 'ID3': insize = -10
-
-            if insize >= framesize: outsize = insize
-            else: outsize = (framesize + 1023) & ~0x3FF
-            framedata += '\x00' * (outsize - framesize)
-
-            framesize = BitPaddedInt.to_str(outsize, width=4)
-            flags = 0
-            header = pack('>3sBBB4s', 'ID3', 4, 0, flags, framesize)
-            data = header + framedata
-
-            if (insize < outsize):
-                insert_bytes(f, outsize-insize, insize+10)
-            f.seek(0)
-            f.write(data)
-
-            try:
-                f.seek(-128, 2)
-            except IOError, err:
-                from errno import EINVAL
-                if err.errno != EINVAL: raise
-                f.seek(0, 2) # ensure read won't get "TAG"
-
-            if f.read(3) == "TAG":
-                f.seek(-128, 2)
-                if v1 > 0: f.write(MakeID3v1(self))
-                else: f.truncate()
-            elif v1 == 2:
-                f.seek(0, 2)
-                f.write(MakeID3v1(self))
-
-        finally:
-            f.close()
-
-    def delete(self, filename=None, delete_v1=True, delete_v2=True):
-        """Remove tags from a file.
-
-        If no filename is given, the one most recently loaded is used.
-
-        Keyword arguments:
-        delete_v1 -- delete any ID3v1 tag
-        delete_v2 -- delete any ID3v2 tag
-        """
-        if filename is None:
-            filename = self.filename
-        delete(filename, delete_v1, delete_v2)
-        self.clear()
-
-    def __save_frame(self, frame):
-        flags = 0
-        if self.PEDANTIC and isinstance(frame, TextFrame):
-            if len(str(frame)) == 0: return ''
-        framedata = frame._writeData()
-        usize = len(framedata)
-        if usize > 2048:
-            framedata = BitPaddedInt.to_str(usize) + framedata.encode('zlib')
-            flags |= Frame.FLAG24_COMPRESS | Frame.FLAG24_DATALEN
-        datasize = BitPaddedInt.to_str(len(framedata), width=4)
-        header = pack('>4s4sH', type(frame).__name__, datasize, flags)
-        return header + framedata
-
-    def update_to_v24(self):
-        """Convert older tags into an ID3v2.4 tag.
-
-        This updates old ID3v2 frames to ID3v2.4 ones (e.g. TYER to
-        TDRC). If you intend to save tags, you must call this function
-        at some point; it is called by default when loading the tag.
-        """
-
-        if self.version < (2,3,0): del self.unknown_frames[:]
-        # unsafe to write
-
-        # TDAT, TYER, and TIME have been turned into TDRC.
-        try:
-            if str(self.get("TYER", "")).strip("\x00"):
-                date = str(self.pop("TYER"))
-                if str(self.get("TDAT", "")).strip("\x00"):
-                    dat = str(self.pop("TDAT"))
-                    date = "%s-%s-%s" % (date, dat[2:], dat[:2])
-                    if str(self.get("TIME", "")).strip("\x00"):
-                        time = str(self.pop("TIME"))
-                        date += "T%s:%s:00" % (time[:2], time[2:])
-                if "TDRC" not in self:
-                    self.add(TDRC(encoding=0, text=date))
-        except UnicodeDecodeError:
-            # Old ID3 tags have *lots* of Unicode problems, so if TYER
-            # is bad, just chuck the frames.
-            pass
-
-        # TORY can be the first part of a TDOR.
-        if "TORY" in self:
-            f = self.pop("TORY")
-            if "TDOR" not in self:
-                try:
-                    self.add(TDOR(encoding=0, text=str(f)))
-                except UnicodeDecodeError:
-                    pass
-
-        # IPLS is now TIPL.
-        if "IPLS" in self:
-            f = self.pop("IPLS")
-            if "TIPL" not in self:
-                self.add(TIPL(encoding=f.encoding, people=f.people))
-
-        if "TCON" in self:
-            # Get rid of "(xx)Foobr" format.
-            self["TCON"].genres = self["TCON"].genres
-
-        if self.version < (2, 3):
-            # ID3v2.2 PIC frames are slightly different.
-            pics = self.getall("APIC")
-            mimes = { "PNG": "image/png", "JPG": "image/jpeg" }
-            self.delall("APIC")
-            for pic in pics:
-                newpic = APIC(
-                    encoding=pic.encoding, mime=mimes.get(pic.mime, pic.mime),
-                    type=pic.type, desc=pic.desc, data=pic.data)
-                self.add(newpic)
-
-            # ID3v2.2 LNK frames are just way too different to upgrade.
-            self.delall("LINK")
-
-        # These can't be trivially translated to any ID3v2.4 tags, or
-        # should have been removed already.
-        for key in ["RVAD", "EQUA", "TRDA", "TSIZ", "TDAT", "TIME", "CRM"]:
-            if key in self: del(self[key])
-
-def delete(filename, delete_v1=True, delete_v2=True):
-    """Remove tags from a file.
-
-    Keyword arguments:
-    delete_v1 -- delete any ID3v1 tag
-    delete_v2 -- delete any ID3v2 tag
-    """
-
-    f = open(filename, 'rb+')
-
-    if delete_v1:
-        try:
-            f.seek(-128, 2)
-        except IOError: pass
-        else:
-            if f.read(3) == "TAG":
-                f.seek(-128, 2)
-                f.truncate()
-
-    # technically an insize=0 tag is invalid, but we delete it anyway
-    # (primarily because we used to write it)
-    if delete_v2:
-        f.seek(0, 0)
-        idata = f.read(10)
-        try: id3, vmaj, vrev, flags, insize = unpack('>3sBBB4s', idata)
-        except struct.error: id3, insize = '', -1
-        insize = BitPaddedInt(insize)
-        if id3 == 'ID3' and insize >= 0:
-            delete_bytes(f, insize + 10, 0)
-
-class BitPaddedInt(int):
-    def __new__(cls, value, bits=7, bigendian=True):
-        "Strips 8-bits bits out of every byte"
-        mask = (1<<(bits))-1
-        if isinstance(value, (int, long)):
-            bytes = []
-            while value:
-                bytes.append(value & ((1<<bits)-1))
-                value = value >> 8
-        if isinstance(value, str):
-            bytes = [ord(byte) & mask for byte in value]
-            if bigendian: bytes.reverse()
-        numeric_value = 0
-        for shift, byte in zip(range(0, len(bytes)*bits, bits), bytes):
-            numeric_value += byte << shift
-        if isinstance(numeric_value, long):
-            self = long.__new__(BitPaddedLong, numeric_value)
-        else:
-            self = int.__new__(BitPaddedInt, numeric_value)
-        self.bits = bits
-        self.bigendian = bigendian
-        return self
-
-    def as_str(value, bits=7, bigendian=True, width=4):
-        bits = getattr(value, 'bits', bits)
-        bigendian = getattr(value, 'bigendian', bigendian)
-        value = int(value)
-        mask = (1<<bits)-1
-        bytes = []
-        while value:
-            bytes.append(value & mask)
-            value = value >> bits
-        # PCNT and POPM use growing integers of at least 4 bytes as counters.
-        if width == -1: width = max(4, len(bytes))
-        if len(bytes) > width:
-            raise ValueError, 'Value too wide (%d bytes)' % len(bytes)
-        else: bytes.extend([0] * (width-len(bytes)))
-        if bigendian: bytes.reverse()
-        return ''.join(map(chr, bytes))
-    to_str = staticmethod(as_str)
-
-class BitPaddedLong(long):
-    def as_str(value, bits=7, bigendian=True, width=4):
-        return BitPaddedInt.to_str(value, bits, bigendian, width)
-    to_str = staticmethod(as_str)
-
-class unsynch(object):
-    def decode(value):
-        output = []
-        safe = True
-        append = output.append
-        for val in value:
-            if safe:
-                append(val)
-                safe = val != '\xFF'
-            else:
-                if val >= '\xE0': raise ValueError('invalid sync-safe string')
-                elif val != '\x00': append(val)
-                safe = True
-        if not safe: raise ValueError('string ended unsafe')
-        return ''.join(output)
-    decode = staticmethod(decode)
-
-    def encode(value):
-        output = []
-        safe = True
-        append = output.append
-        for val in value:
-            if safe:
-                append(val)
-                if val == '\xFF': safe = False
-            elif val == '\x00' or val >= '\xE0':
-                append('\x00')
-                append(val)
-                safe = val != '\xFF'
-            else:
-                append(val)
-                safe = True
-        if not safe: append('\x00')
-        return ''.join(output)
-    encode = staticmethod(encode)
-
-class Spec(object):
-    def __init__(self, name): self.name = name
-    def __hash__(self): raise TypeError("Spec objects are unhashable")
-
-class ByteSpec(Spec):
-    def read(self, frame, data): return ord(data[0]), data[1:]
-    def write(self, frame, value): return chr(value)
-    def validate(self, frame, value): return value
-
-class IntegerSpec(Spec):
-    def read(self, frame, data):
-        return int(BitPaddedInt(data, bits=8)), ''
-    def write(self, frame, value):
-        return BitPaddedInt.to_str(value, bits=8, width=-1)
-    def validate(self, frame, value):
-        return value
-
-class SizedIntegerSpec(Spec):
-    def __init__(self, name, size):
-        self.name, self.__sz = name, size
-    def read(self, frame, data):
-        return int(BitPaddedInt(data[:self.__sz], bits=8)), data[self.__sz:]
-    def write(self, frame, value):
-        return BitPaddedInt.to_str(value, bits=8, width=self.__sz)
-    def validate(self, frame, value):
-        return value
-
-class EncodingSpec(ByteSpec):
-    def read(self, frame, data):
-        enc, data = super(EncodingSpec, self).read(frame, data)
-        if enc < 16: return enc, data
-        else: return 0, chr(enc)+data
-
-    def validate(self, frame, value):
-        if 0 <= value <= 3: return value
-        if value is None: return None
-        raise ValueError, 'Invalid Encoding: %r' % value
-
-class StringSpec(Spec):
-    def __init__(self, name, length):
-        super(StringSpec, self).__init__(name)
-        self.len = length
-    def read(s, frame, data): return data[:s.len], data[s.len:]
-    def write(s, frame, value):
-        if value is None: return '\x00' * s.len
-        else: return (str(value) + '\x00' * s.len)[:s.len]
-    def validate(s, frame, value):
-        if value is None: return None
-        if isinstance(value, basestring) and len(value) == s.len: return value
-        raise ValueError, 'Invalid StringSpec[%d] data: %r' % (s.len, value)
-
-class BinaryDataSpec(Spec):
-    def read(self, frame, data): return data, ''
-    def write(self, frame, value): return str(value)
-    def validate(self, frame, value): return str(value)
-
-class EncodedTextSpec(Spec):
-    # Okay, seriously. This is private and defined explicitly and
-    # completely by the ID3 specification. You can't just add
-    # encodings here however you want.
-    _encodings = ( ('latin1', '\x00'), ('utf16', '\x00\x00'),
-                   ('utf_16_be', '\x00\x00'), ('utf8', '\x00') )
-
-    def read(self, frame, data):
-        enc, term = self._encodings[frame.encoding]
-        ret = ''
-        if len(term) == 1:
-            if term in data:
-                data, ret = data.split(term, 1)
-        else:
-            offset = -1
-            try:
-                while True:
-                    offset = data.index(term, offset+1)
-                    if offset & 1: continue
-                    data, ret = data[0:offset], data[offset+2:]; break
-            except ValueError: pass
-
-        if len(data) < len(term): return u'', ret
-        return data.decode(enc), ret
-
-    def write(self, frame, value):
-        enc, term = self._encodings[frame.encoding]
-        return value.encode(enc) + term
-
-    def validate(self, frame, value): return unicode(value)
-
-class MultiSpec(Spec):
-    def __init__(self, name, *specs, **kw):
-        super(MultiSpec, self).__init__(name)
-        self.specs = specs
-        self.sep = kw.get('sep')
-
-    def read(self, frame, data):
-        values = []
-        while data:
-            record = []
-            for spec in self.specs:
-                value, data = spec.read(frame, data)
-                record.append(value)
-            if len(self.specs) != 1: values.append(record)
-            else: values.append(record[0])
-        return values, data
-
-    def write(self, frame, value):
-        data = []
-        if len(self.specs) == 1:
-            for v in value:
-                data.append(self.specs[0].write(frame, v))
-        else:
-            for record in value:
-                for v, s in zip(record, self.specs):
-                    data.append(s.write(frame, v))
-        return ''.join(data)
-
-    def validate(self, frame, value):
-        if value is None: return []
-        if self.sep and isinstance(value, basestring):
-            value = value.split(self.sep)
-        if isinstance(value, list):
-            if len(self.specs) == 1:
-                return [self.specs[0].validate(frame, v) for v in value]
-            else:
-                return [ 
-                    [s.validate(frame, v) for (v,s) in zip(val, self.specs)]
-                    for val in value ]
-        raise ValueError, 'Invalid MultiSpec data: %r' % value
-
-class EncodedNumericTextSpec(EncodedTextSpec): pass
-class EncodedNumericPartTextSpec(EncodedTextSpec): pass
-
-class Latin1TextSpec(EncodedTextSpec):
-    def read(self, frame, data):
-        if '\x00' in data: data, ret = data.split('\x00',1)
-        else: ret = ''
-        return data.decode('latin1'), ret
-
-    def write(self, data, value):
-        return value.encode('latin1') + '\x00'
-
-    def validate(self, frame, value): return unicode(value)
-
-class ID3TimeStamp(object):
-    """A time stamp in ID3v2 format.
-
-    This is a restricted form of the ISO 8601 standard; time stamps
-    take the form of:
-        YYYY-MM-DD HH:MM:SS
-    Or some partial form (YYYY-MM-DD HH, YYYY, etc.).
-
-    The 'text' attribute contains the raw text data of the time stamp.
-    """
-
-    import re
-    def __init__(self, text):
-        if isinstance(text, ID3TimeStamp): text = text.text
-        self.text = text
-
-    __formats = ['%04d'] + ['%02d'] * 5
-    __seps = ['-', '-', ' ', ':', ':', 'x']
-    def get_text(self):
-        parts = [self.year, self.month, self.day,
-                self.hour, self.minute, self.second]
-        pieces = []
-        for i, part in enumerate(iter(iter(parts).next, None)):
-            pieces.append(self.__formats[i]%part + self.__seps[i])
-        return u''.join(pieces)[:-1]
-
-    def set_text(self, text, splitre=re.compile('[-T:/.]|\s+')):
-        year, month, day, hour, minute, second = \
-                splitre.split(text + ':::::')[:6]
-        for a in 'year month day hour minute second'.split():
-            try: v = int(locals()[a])
-            except ValueError: v = None
-            setattr(self, a, v)
-
-    text = property(get_text, set_text, doc="ID3v2.4 date and time.")
-
-    def __str__(self): return self.text
-    def __repr__(self): return repr(self.text)
-    def __cmp__(self, other): return cmp(self.text, other.text)
-    def encode(self, *args): return self.text.encode(*args)
-
-class TimeStampSpec(EncodedTextSpec):
-    def read(self, frame, data):
-        value, data = super(TimeStampSpec, self).read(frame, data)
-        return self.validate(frame, value), data
-
-    def write(self, frame, data):
-        return super(TimeStampSpec, self).write(frame,
-                data.text.replace(' ', 'T'))
-
-    def validate(self, frame, value):
-        try: return ID3TimeStamp(value)
-        except TypeError: raise ValueError, "Invalid ID3TimeStamp: %r" % value
-
-class ChannelSpec(ByteSpec):
-    (OTHER, MASTER, FRONTRIGHT, FRONTLEFT, BACKRIGHT, BACKLEFT, FRONTCENTRE,
-     BACKCENTRE, SUBWOOFER) = range(9)
-
-class VolumeAdjustmentSpec(Spec):
-    def read(self, frame, data):
-        value, = unpack('>h', data[0:2])
-        return value/512.0, data[2:]
-
-    def write(self, frame, value):
-        return pack('>h', int(round(value * 512)))
-
-    def validate(self, frame, value): return value
-
-class VolumePeakSpec(Spec):
-    def read(self, frame, data):
-        # http://bugs.xmms.org/attachment.cgi?id=113&action=view
-        peak = 0
-        bits = ord(data[0])
-        bytes = min(4, (bits + 7) >> 3)
-        # not enough frame data
-        if bytes + 1 > len(data): raise ID3JunkFrameError
-        shift = ((8 - (bits & 7)) & 7) + (4 - bytes) * 8
-        for i in range(1, bytes+1):
-            peak *= 256
-            peak += ord(data[i])
-        peak *= 2**shift
-        return (float(peak) / (2**31-1)), data[1+bytes:]
-
-    def write(self, frame, value):
-        # always write as 16 bits for sanity.
-        return "\x10" + pack('>H', int(round(value * 32768)))
-
-    def validate(self, frame, value): return value
-
-class SynchronizedTextSpec(EncodedTextSpec):
-    def read(self, frame, data):
-        texts = []
-        encoding, term = self._encodings[frame.encoding]
-        while data:
-            l = len(term)
-            value_idx = data.index(term)
-            value = data[:value_idx].decode(encoding)
-            time, = struct.unpack(">I", data[value_idx+l:value_idx+l+4])
-            texts.append((value, time))
-            data = data[value_idx+l+4:]
-        return texts, ""
-
-    def write(self, frame, value):
-        data = []
-        encoding, term = self._encodings[frame.encoding]
-        for text, time in frame.text:
-            text = text.encode(encoding) + term
-            data.append(text + struct.pack(">I", time))
-        return "".join(data)
-
-    def validate(self, frame, value):
-        return value
-
-class KeyEventSpec(Spec):
-    def read(self, frame, data):
-        events = []
-        while len(data) >= 5:
-            events.append(struct.unpack(">bI", data[:5]))
-            data = data[5:]
-        return events, data
-
-    def write(self, frame, value):
-        return "".join([struct.pack(">bI", *event) for event in value])
-
-    def validate(self, frame, value):
-        return value
-
-class VolumeAdjustmentsSpec(Spec):
-    # Not to be confused with VolumeAdjustmentSpec.
-    def read(self, frame, data):
-        adjustments = {}
-        while len(data) >= 4:
-            freq, adj = struct.unpack(">Hh", data[:4])
-            data = data[4:]
-            freq /= 2.0
-            adj /= 512.0
-            adjustments[freq] = adj
-        adjustments = adjustments.items()
-        adjustments.sort()
-        return adjustments, data
-
-    def write(self, frame, value):
-        value.sort()
-        return "".join([struct.pack(">Hh", int(freq * 2), int(adj * 512))
-                        for (freq, adj) in value])
-
-    def validate(self, frame, value):
-        return value
-
-class ASPIIndexSpec(Spec):
-    def read(self, frame, data):
-        if frame.b == 16:
-            format = "H"
-            size = 2
-        elif frame.b == 8:
-            format = "B"
-            size = 1
-        else:
-            warn("invalid bit count in ASPI (%d)" % frame.b, ID3Warning)
-            return [], data
-        
-        indexes = data[:frame.N * size]
-        data = data[frame.N * size:]
-        return list(struct.unpack(">" + format * frame.N, indexes)), data
-
-    def write(self, frame, values):
-        if frame.b == 16: format = "H"
-        elif frame.b == 8: format = "B"
-        else: raise ValueError("frame.b must be 8 or 16")
-        return struct.pack(">" + format * frame.N, *values)
-
-    def validate(self, frame, values):
-        return values
-
-class Frame(object):
-    """Fundamental unit of ID3 data.
-
-    ID3 tags are split into frames. Each frame has a potentially
-    different structure, and so this base class is not very featureful.
-    """
-
-    FLAG23_ALTERTAG     = 0x8000
-    FLAG23_ALTERFILE    = 0x4000
-    FLAG23_READONLY     = 0x2000
-    FLAG23_COMPRESS     = 0x0080
-    FLAG23_ENCRYPT      = 0x0040
-    FLAG23_GROUP        = 0x0020
-
-    FLAG24_ALTERTAG     = 0x4000
-    FLAG24_ALTERFILE    = 0x2000
-    FLAG24_READONLY     = 0x1000
-    FLAG24_GROUPID      = 0x0040
-    FLAG24_COMPRESS     = 0x0008
-    FLAG24_ENCRYPT      = 0x0004
-    FLAG24_UNSYNCH      = 0x0002
-    FLAG24_DATALEN      = 0x0001
-
-    _framespec = []
-    def __init__(self, *args, **kwargs):
-        if len(args)==1 and len(kwargs)==0 and isinstance(args[0], type(self)):
-            other = args[0]
-            for checker in self._framespec:
-                val = checker.validate(self, getattr(other, checker.name))
-                setattr(self, checker.name, val)
-        else:
-            for checker, val in zip(self._framespec, args):
-                setattr(self, checker.name, checker.validate(self, val))
-            for checker in self._framespec[len(args):]:
-                validated = checker.validate(
-                    self, kwargs.get(checker.name, None))
-                setattr(self, checker.name, validated)
-
-    HashKey = property(
-        lambda s: s.FrameID,
-        doc="an internal key used to ensure frame uniqueness in a tag")
-    FrameID = property(
-        lambda s: type(s).__name__,
-        doc="ID3v2 three or four character frame ID")
-
-    def __repr__(self):
-        """Python representation of a frame.
-
-        The string returned is a valid Python expression to construct
-        a copy of this frame.
-        """
-        kw = []
-        for attr in self._framespec:
-            kw.append('%s=%r' % (attr.name, getattr(self, attr.name)))
-        return '%s(%s)' % (type(self).__name__, ', '.join(kw))
-
-    def _readData(self, data):
-        odata = data
-        for reader in self._framespec:
-            if len(data):
-                try: value, data = reader.read(self, data)
-                except UnicodeDecodeError:
-                    raise ID3JunkFrameError
-            else: raise ID3JunkFrameError
-            setattr(self, reader.name, value)
-        if data.strip('\x00'):
-            warn('Leftover data: %s: %r (from %r)' % (
-                    type(self).__name__, data, odata),
-                    ID3Warning)
-
-    def _writeData(self):
-        data = []
-        for writer in self._framespec:
-            data.append(writer.write(self, getattr(self, writer.name)))
-        return ''.join(data)
-
-    def pprint(self):
-        """Return a human-readable representation of the frame."""
-        return "%s=%s" % (type(self).__name__, self._pprint())
-
-    def _pprint(self):
-        return "[unrepresentable data]"
-
-    def fromData(cls, id3, tflags, data):
-        """Construct this ID3 frame from raw string data."""
-
-        if (2,4,0) <= id3.version:
-            if tflags & (Frame.FLAG24_COMPRESS | Frame.FLAG24_DATALEN):
-                # The data length int is syncsafe in 2.4 (but not 2.3).
-                # However, we don't actually need the data length int,
-                # except to work around a QL 0.12 bug, and in that case
-                # all we need are the raw bytes.
-                datalen_bytes = data[:4]
-                data = data[4:]
-            if tflags & Frame.FLAG24_UNSYNCH or id3.f_unsynch:
-                try: data = unsynch.decode(data)
-                except ValueError, err:
-                    if id3.PEDANTIC:
-                        raise ID3BadUnsynchData, '%s: %r' % (err, data)
-            if tflags & Frame.FLAG24_ENCRYPT:
-                raise ID3EncryptionUnsupportedError
-            if tflags & Frame.FLAG24_COMPRESS:
-                try: data = data.decode('zlib')
-                except zlibError, err:
-                    # the initial mutagen that went out with QL 0.12 did not
-                    # write the 4 bytes of uncompressed size. Compensate.
-                    data = datalen_bytes + data
-                    try: data = data.decode('zlib')
-                    except zlibError, err:
-                        if id3.PEDANTIC:
-                            raise ID3BadCompressedData, '%s: %r' % (err, data)
-
-        elif (2,3,0) <= id3.version:
-            if tflags & Frame.FLAG23_COMPRESS:
-                usize, = unpack('>L', data[:4])
-                data = data[4:]
-            if tflags & Frame.FLAG23_ENCRYPT:
-                raise ID3EncryptionUnsupportedError
-            if tflags & Frame.FLAG23_COMPRESS:
-                try: data = data.decode('zlib')
-                except zlibError, err:
-                    if id3.PEDANTIC:
-                        raise ID3BadCompressedData, '%s: %r' % (err, data)
-
-        frame = cls()
-        frame._rawdata = data
-        frame._flags = tflags
-        frame._readData(data)
-        return frame
-    fromData = classmethod(fromData)
-
-    def __hash__(self):
-        raise TypeError("Frame objects are unhashable")
-
-class FrameOpt(Frame):
-    """A frame with optional parts.
-
-    Some ID3 frames have optional data; this class extends Frame to
-    provide support for those parts.
-    """
-    _optionalspec = []
-
-    def __init__(self, *args, **kwargs):
-        super(FrameOpt, self).__init__(*args, **kwargs)
-        for spec in self._optionalspec:
-            if spec.name in kwargs:
-                validated = spec.validate(self, kwargs[spec.name])
-                setattr(self, spec.name, validated)
-            else: break
-
-    def _readData(self, data):
-        odata = data
-        for reader in self._framespec:
-            if len(data): value, data = reader.read(self, data)
-            else: raise ID3JunkFrameError
-            setattr(self, reader.name, value)
-        if data:
-            for reader in self._optionalspec:
-                if len(data): value, data = reader.read(self, data)
-                else: break
-                setattr(self, reader.name, value)
-        if data.strip('\x00'):
-            warn('Leftover data: %s: %r (from %r)' % (
-                    type(self).__name__, data, odata),
-                    ID3Warning)
-
-    def _writeData(self):
-        data = []
-        for writer in self._framespec:
-            data.append(writer.write(self, getattr(self, writer.name)))
-        for writer in self._optionalspec:
-            try: data.append(writer.write(self, getattr(self, writer.name)))
-            except AttributeError: break
-        return ''.join(data)
-
-    def __repr__(self):
-        kw = []
-        for attr in self._framespec:
-            kw.append('%s=%r' % (attr.name, getattr(self, attr.name)))
-        for attr in self._optionalspec:
-            if hasattr(self, attr.name):
-                kw.append('%s=%r' % (attr.name, getattr(self, attr.name)))
-        return '%s(%s)' % (type(self).__name__, ', '.join(kw))
-
-
-class TextFrame(Frame):
-    """Text strings.
-
-    Text frames support casts to unicode or str objects, as well as
-    list-like indexing, extend, and append.
-
-    Iterating over a TextFrame iterates over its strings, not its
-    characters.
-
-    Text frames have a 'text' attribute which is the list of strings,
-    and an 'encoding' attribute; 0 for ISO-8859 1, 1 UTF-16, 2 for
-    UTF-16BE, and 3 for UTF-8. If you don't want to worry about
-    encodings, just set it to 3.
-    """
-
-    _framespec = [ EncodingSpec('encoding'),
-        MultiSpec('text', EncodedTextSpec('text'), sep=u'\u0000') ]
-    def __str__(self): return self.__unicode__().encode('utf-8')
-    def __unicode__(self): return u'\u0000'.join(self.text)
-    def __eq__(self, other):
-        if isinstance(other, str): return str(self) == other
-        elif isinstance(other, unicode):
-            return u'\u0000'.join(self.text) == other
-        return self.text == other
-    def __getitem__(self, item): return self.text[item]
-    def __iter__(self): return iter(self.text)
-    def append(self, value): return self.text.append(value)
-    def extend(self, value): return self.text.extend(value)
-    def _pprint(self): return " / ".join(self.text)
-
-class NumericTextFrame(TextFrame):
-    """Numerical text strings.
-
-    The numeric value of these frames can be gotten with unary plus, e.g.
-        frame = TLEN('12345')
-        length = +frame
-    """
-
-    _framespec = [ EncodingSpec('encoding'),
-        MultiSpec('text', EncodedNumericTextSpec('text'), sep=u'\u0000') ]
-
-    def __pos__(self):
-        """Return the numerical value of the string."""
-        return int(self.text[0])
-
-class NumericPartTextFrame(TextFrame):
-    """Multivalue numerical text strings.
-
-    These strings indicate 'part (e.g. track) X of Y', and unary plus
-    returns the first value:
-        frame = TRCK('4/15')
-        track = +frame # track == 4
-    """
-
-    _framespec = [ EncodingSpec('encoding'),
-        MultiSpec('text', EncodedNumericPartTextSpec('text'), sep=u'\u0000') ]
-    def __pos__(self):
-        return int(self.text[0].split("/")[0])
-
-class TimeStampTextFrame(TextFrame):
-    """A list of time stamps.
-
-    The 'text' attribute in this frame is a list of ID3TimeStamp
-    objects, not a list of strings.
-    """
-
-    _framespec = [ EncodingSpec('encoding'),
-        MultiSpec('text', TimeStampSpec('stamp'), sep=u',') ]
-    def __str__(self): return self.__unicode__().encode('utf-8')
-    def __unicode__(self): return ','.join([stamp.text for stamp in self.text])
-    def _pprint(self):
-        return " / ".join([stamp.text for stamp in self.text])
-
-class UrlFrame(Frame):
-    """A frame containing a URL string.
-
-    The ID3 specification is silent about IRIs and normalized URL
-    forms. Mutagen assumes all URLs in files are encoded as Latin 1,
-    but string conversion of this frame returns a UTF-8 representation
-    for compatibility with other string conversions.
-
-    The only sane way to handle URLs in MP3s is to restrict them to
-    ASCII.
-    """
-
-    _framespec = [ Latin1TextSpec('url') ]
-    def __str__(self): return self.url.encode('utf-8')
-    def __unicode__(self): return self.url
-    def __eq__(self, other): return self.url == other
-    def _pprint(self): return self.url
-
-class UrlFrameU(UrlFrame):
-    HashKey = property(lambda s: '%s:%s' % (s.FrameID, s.url))
-
-class TALB(TextFrame): "Album"
-class TBPM(NumericTextFrame): "Beats per minute"
-class TCOM(TextFrame): "Composer"
-
-class TCON(TextFrame):
-    """Content type (Genre)
-
-    ID3 has several ways genres can be represented; for convenience,
-    use the 'genres' property rather than the 'text' attribute.
-    """
-
-    from mutagen._constants import GENRES
-
-    def __get_genres(self):
-        genres = []
-        import re
-        genre_re = re.compile(r"((?:\((?P<id>[0-9]+|RX|CR)\))*)(?P<str>.+)?")
-        for value in self.text:
-            if value.isdigit():
-                try: genres.append(self.GENRES[int(value)])
-                except IndexError: genres.append(u"Unknown")
-            elif value == "CR": genres.append(u"Cover")
-            elif value == "RX": genres.append(u"Remix")
-            elif value:
-                newgenres = []
-                genreid, dummy, genrename = genre_re.match(value).groups()
-
-                if genreid:
-                    for gid in genreid[1:-1].split(")("):
-                        if gid.isdigit() and int(gid) < len(self.GENRES):
-                            gid = unicode(self.GENRES[int(gid)])
-                            newgenres.append(gid)
-                        elif gid == "CR": newgenres.append(u"Cover")
-                        elif gid == "RX": newgenres.append(u"Remix")
-                        else: newgenres.append(u"Unknown")
-
-                if genrename:
-                    # "Unescaping" the first parenthesis
-                    if genrename.startswith("(("): genrename = genrename[1:]
-                    if genrename not in newgenres: newgenres.append(genrename)
-
-                genres.extend(newgenres)
-
-        return genres
-
-    def __set_genres(self, genres):
-        if isinstance(genres, basestring): genres = [genres]
-        self.text = map(self.__decode, genres)
-
-    def __decode(self, value):
-        if isinstance(value, str):
-            enc = EncodedTextSpec._encodings[self.encoding][0]
-            return value.decode(enc)
-        else: return value
-
-    genres = property(__get_genres, __set_genres, None,
-                      "A list of genres parsed from the raw text data.")
-
-    def _pprint(self):
-        return " / ".join(self.genres)
-
-class TCOP(TextFrame): "Copyright (c)"
-class TCMP(NumericTextFrame): "iTunes Compilation Flag"
-class TDAT(TextFrame): "Date of recording (DDMM)"
-class TDEN(TimeStampTextFrame): "Encoding Time"
-class TDOR(TimeStampTextFrame): "Original Release Time"
-class TDLY(NumericTextFrame): "Audio Delay (ms)"
-class TDRC(TimeStampTextFrame): "Recording Time"
-class TDRL(TimeStampTextFrame): "Release Time"
-class TDTG(TimeStampTextFrame): "Tagging Time"
-class TENC(TextFrame): "Encoder"
-class TEXT(TextFrame): "Lyricist"
-class TFLT(TextFrame): "File type"
-class TIME(TextFrame): "Time of recording (HHMM)"
-class TIT1(TextFrame): "Content group description"
-class TIT2(TextFrame): "Title"
-class TIT3(TextFrame): "Subtitle/Description refinement"
-class TKEY(TextFrame): "Starting Key"
-class TLAN(TextFrame): "Audio Languages"
-class TLEN(NumericTextFrame): "Audio Length (ms)"
-class TMED(TextFrame): "Source Media Type"
-class TMOO(TextFrame): "Mood"
-class TOAL(TextFrame): "Original Album"
-class TOFN(TextFrame): "Original Filename"
-class TOLY(TextFrame): "Original Lyricist"
-class TOPE(TextFrame): "Original Artist/Performer"
-class TORY(NumericTextFrame): "Original Release Year"
-class TOWN(TextFrame): "Owner/Licensee"
-class TPE1(TextFrame): "Lead Artist/Performer/Soloist/Group"
-class TPE2(TextFrame): "Band/Orchestra/Accompaniment"
-class TPE3(TextFrame): "Conductor"
-class TPE4(TextFrame): "Interpreter/Remixer/Modifier"
-class TPOS(NumericPartTextFrame): "Part of set"
-class TPRO(TextFrame): "Produced (P)"
-class TPUB(TextFrame): "Publisher"
-class TRCK(NumericPartTextFrame): "Track Number"
-class TRDA(TextFrame): "Recording Dates"
-class TRSN(TextFrame): "Internet Radio Station Name"
-class TRSO(TextFrame): "Internet Radio Station Owner"
-class TSIZ(NumericTextFrame): "Size of audio data (bytes)"
-class TSOA(TextFrame): "Album Sort Order key"
-class TSOP(TextFrame): "Perfomer Sort Order key"
-class TSOT(TextFrame): "Title Sort Order key"
-class TSRC(TextFrame): "International Standard Recording Code (ISRC)"
-class TSSE(TextFrame): "Encoder settings"
-class TSST(TextFrame): "Set Subtitle"
-class TYER(NumericTextFrame): "Year of recording"
-
-class TXXX(TextFrame):
-    """User-defined text data.
-
-    TXXX frames have a 'desc' attribute which is set to any Unicode
-    value (though the encoding of the text and the description must be
-    the same). Many taggers use this frame to store freeform keys.
-    """
-    _framespec = [ EncodingSpec('encoding'), EncodedTextSpec('desc'),
-        MultiSpec('text', EncodedTextSpec('text'), sep=u'\u0000') ]
-    HashKey = property(lambda s: '%s:%s' % (s.FrameID, s.desc))
-    def _pprint(self): return "%s=%s" % (self.desc, " / ".join(self.text))
-
-class WCOM(UrlFrameU): "Commercial Information"
-class WCOP(UrlFrame): "Copyright Information"
-class WOAF(UrlFrame): "Official File Information"
-class WOAR(UrlFrameU): "Official Artist/Performer Information"
-class WOAS(UrlFrame): "Official Source Information"
-class WORS(UrlFrame): "Official Internet Radio Information"
-class WPAY(UrlFrame): "Payment Information"
-class WPUB(UrlFrame): "Official Publisher Information"
-
-class WXXX(UrlFrame):
-    """User-defined URL data.
-
-    Like TXXX, this has a freeform description associated with it.
-    """
-    _framespec = [ EncodingSpec('encoding'), EncodedTextSpec('desc'),
-        Latin1TextSpec('url') ]
-    HashKey = property(lambda s: '%s:%s' % (s.FrameID, s.desc))
-
-class PairedTextFrame(Frame):
-    """Paired text strings.
-
-    Some ID3 frames pair text strings, to associate names with a more
-    specific involvement in the song. The 'people' attribute of these
-    frames contains a list of pairs:
-        [['trumpet', 'Miles Davis'], ['bass', 'Paul Chambers']]
-
-    Like text frames, these frames also have an encoding attribute.
-    """
-
-    _framespec = [ EncodingSpec('encoding'), MultiSpec('people',
-        EncodedTextSpec('involvement'), EncodedTextSpec('person')) ]
-    def __eq__(self, other):
-        return self.people == other
-
-class TIPL(PairedTextFrame): "Involved People List"
-class TMCL(PairedTextFrame): "Musicians Credits List"
-class IPLS(TIPL): "Involved People List"
-
-class MCDI(Frame):
-    """Binary dump of CD's TOC.
-
-    The 'data' attribute contains the raw byte string.
-    """
-    _framespec = [ BinaryDataSpec('data') ]
-    def __eq__(self, other): return self.data == other
-
-class ETCO(Frame):
-    """Event timing codes."""
-    _framespec = [ ByteSpec("format"), KeyEventSpec("events") ]
-    def __eq__(self, other): return self.events == other
-
-class MLLT(Frame):
-    """MPEG location lookup table.
-
-    This frame's attributes may be changed in the future based on
-    feedback from real-world use.
-    """
-    _framespec = [ SizedIntegerSpec('frames', 2),
-                   SizedIntegerSpec('bytes', 3),
-                   SizedIntegerSpec('milliseconds', 3),
-                   ByteSpec('bits_for_bytes'),
-                   ByteSpec('bits_for_milliseconds'),
-                   BinaryDataSpec('data') ]
-    def __eq__(self, other): return self.data == other
-
-class SYTC(Frame):
-    """Synchronised tempo codes.
-
-    This frame's attributes may be changed in the future based on
-    feedback from real-world use.
-    """
-    _framespec = [ ByteSpec("format"), BinaryDataSpec("data") ]
-    def __eq__(self, other): return self.data == other
-
-class USLT(Frame):
-    """Unsynchronised lyrics/text transcription.
-
-    Lyrics have a three letter ISO language code ('lang'), a
-    description ('desc'), and a block of plain text ('text').
-    """
-
-    _framespec = [ EncodingSpec('encoding'), StringSpec('lang', 3),
-        EncodedTextSpec('desc'), EncodedTextSpec('text') ]
-    HashKey = property(lambda s: '%s:%s:%r' % (s.FrameID, s.desc, s.lang))
-
-    def __str__(self): return self.text.encode('utf-8')
-    def __unicode__(self): return self.text
-    def __eq__(self, other): return self.text == other
-    
-class SYLT(Frame):
-    """Synchronised lyrics/text."""
-
-    _framespec = [ EncodingSpec('encoding'), StringSpec('lang', 3),
-        ByteSpec('format'), ByteSpec('type'), EncodedTextSpec('desc'),
-        SynchronizedTextSpec('text') ]
-    HashKey = property(lambda s: '%s:%s:%r' % (s.FrameID, s.desc, s.lang))
-
-    def __eq__(self, other):
-        return str(self) == other
-
-    def __str__(self):
-        return "".join([text for (text, time) in self.text]).encode('utf-8')
-
-class COMM(TextFrame):
-    """User comment.
-
-    User comment frames have a descrption, like TXXX, and also a three
-    letter ISO language code in the 'lang' attribute.
-    """
-    _framespec = [ EncodingSpec('encoding'), StringSpec('lang', 3),
-        EncodedTextSpec('desc'),
-        MultiSpec('text', EncodedTextSpec('text'), sep=u'\u0000') ]
-    HashKey = property(lambda s: '%s:%s:%r' % (s.FrameID, s.desc, s.lang))
-    def _pprint(self): return "%s=%r=%s" % (
-        self.desc, self.lang, " / ".join(self.text))
-
-class RVA2(Frame):
-    """Relative volume adjustment (2).
-
-    This frame is used to implemented volume scaling, and in
-    particular, normalization using ReplayGain.
-
-    Attributes:
-    desc -- description or context of this adjustment
-    channel -- audio channel to adjust (master is 1)
-    gain -- a + or - dB gain relative to some reference level
-    peak -- peak of the audio as a floating point number, [0, 1]
-
-    When storing ReplayGain tags, use descriptions of 'album' and
-    'track' on channel 1.
-    """
-
-    _framespec = [ Latin1TextSpec('desc'), ChannelSpec('channel'),
-        VolumeAdjustmentSpec('gain'), VolumePeakSpec('peak') ]
-    _channels = ["Other", "Master volume", "Front right", "Front left",
-                 "Back right", "Back left", "Front centre", "Back centre",
-                 "Subwoofer"]
-    HashKey = property(lambda s: '%s:%s' % (s.FrameID, s.desc))
-
-    def __eq__(self, other):
-        return ((str(self) == other) or
-                (self.desc == other.desc and
-                 self.channel == other.channel and
-                 self.gain == other.gain and
-                 self.peak == other.peak))
-
-    def __str__(self):
-        return "%s: %+0.4f dB/%0.4f" % (
-            self._channels[self.channel], self.gain, self.peak)
-
-class EQU2(Frame):
-    """Equalisation (2).
-
-    Attributes:
-    method -- interpolation method (0 = band, 1 = linear)
-    desc -- identifying description
-    adjustments -- list of (frequency, vol_adjustment) pairs
-    """
-    _framespec = [ ByteSpec("method"), Latin1TextSpec("desc"),
-                   VolumeAdjustmentsSpec("adjustments") ]
-    def __eq__(self, other): return self.adjustments == other
-    HashKey = property(lambda s: '%s:%s' % (s.FrameID, s.desc))
-
-# class RVAD: unsupported
-# class EQUA: unsupported
-
-class RVRB(Frame):
-    """Reverb."""
-    _framespec = [ SizedIntegerSpec('left', 2), SizedIntegerSpec('right', 2),
-                   ByteSpec('bounce_left'), ByteSpec('bounce_right'),
-                   ByteSpec('feedback_ltl'), ByteSpec('feedback_ltr'),
-                   ByteSpec('feedback_rtr'), ByteSpec('feedback_rtl'),
-                   ByteSpec('premix_ltr'), ByteSpec('premix_rtl') ]
-
-    def __eq__(self, other): return (self.left, self.right) == other
-
-class APIC(Frame):
-    """Attached (or linked) Picture.
-
-    Attributes:
-    encoding -- text encoding for the description
-    mime -- a MIME type (e.g. image/jpeg) or '-->' if the data is a URI
-    type -- the source of the image (3 is the album front cover)
-    desc -- a text description of the image
-    data -- raw image data, as a byte string
-
-    Mutagen will automatically compress large images when saving tags.
-    """
-    _framespec = [ EncodingSpec('encoding'), Latin1TextSpec('mime'),
-        ByteSpec('type'), EncodedTextSpec('desc'), BinaryDataSpec('data') ]
-    def __eq__(self, other): return self.data == other
-    HashKey = property(lambda s: '%s:%s' % (s.FrameID, s.desc))
-    def _pprint(self):
-        return "%s (%s, %d bytes)" % (
-            self.desc, self.mime, len(self.data))
-
-class PCNT(Frame):
-    """Play counter.
-
-    The 'count' attribute contains the (recorded) number of times this
-    file has been played.
-
-    This frame is basically obsoleted by POPM.
-    """
-    _framespec = [ IntegerSpec('count') ]
-
-    def __eq__(self, other): return self.count == other
-    def __pos__(self): return self.count
-    def _pprint(self): return unicode(self.count)
-
-class POPM(Frame):
-    """Popularimeter.
-
-    This frame keys a rating (out of 255) and a play count to an email
-    address.
-
-    Attributes:
-    email -- email this POPM frame is for
-    rating -- rating from 0 to 255
-    count -- number of times the files has been played
-    """
-    _framespec = [ Latin1TextSpec('email'), ByteSpec('rating'),
-        IntegerSpec('count') ]
-    HashKey = property(lambda s: '%s:%s' % (s.FrameID, s.email))
-
-    def __eq__(self, other): return self.rating == other
-    def __pos__(self): return self.rating
-    def _pprint(self): return "%s=%s %s/255" % (
-        self.email, self.count, self.rating)
-
-class GEOB(Frame):
-    """General Encapsulated Object.
-
-    A blob of binary data, that is not a picture (those go in APIC).
-
-    Attributes:
-    encoding -- encoding of the description
-    mime -- MIME type of the data or '-->' if the data is a URI
-    filename -- suggested filename if extracted
-    desc -- text description of the data
-    data -- raw data, as a byte string
-    """
-    _framespec = [ EncodingSpec('encoding'), Latin1TextSpec('mime'),
-        EncodedTextSpec('filename'), EncodedTextSpec('desc'), 
-        BinaryDataSpec('data') ]
-    HashKey = property(lambda s: '%s:%s' % (s.FrameID, s.desc))
-
-    def __eq__(self, other): return self.data == other
-
-class RBUF(FrameOpt):
-    """Recommended buffer size.
-
-    Attributes:
-    size -- recommended buffer size in bytes
-    info -- if ID3 tags may be elsewhere in the file (optional)
-    offset -- the location of the next ID3 tag, if any
-
-    Mutagen will not find the next tag itself.
-    """
-    _framespec = [ SizedIntegerSpec('size', 3) ]
-    _optionalspec = [ ByteSpec('info'), SizedIntegerSpec('offset', 4) ]
-
-    def __eq__(self, other): return self.size == other
-    def __pos__(self): return self.size
-
-class AENC(FrameOpt):
-    """Audio encryption.
-
-    Attributes:
-    owner -- key identifying this encryption type
-    preview_start -- unencrypted data block offset
-    preview_length -- number of unencrypted blocks
-    data -- data required for decryption (optional)
-
-    Mutagen cannot decrypt files.
-    """
-    _framespec = [ Latin1TextSpec('owner'),
-                   SizedIntegerSpec('preview_start', 2),
-                   SizedIntegerSpec('preview_length', 2) ]
-    _optionalspec = [ BinaryDataSpec('data') ]
-    HashKey = property(lambda s: '%s:%s' % (s.FrameID, s.owner))
-
-    def __str__(self): return self.owner.encode('utf-8')
-    def __unicode__(self): return self.owner
-    def __eq__(self, other): return self.owner == other
-
-class LINK(FrameOpt):
-    """Linked information.
-
-    Attributes:
-    frameid -- the ID of the linked frame
-    url -- the location of the linked frame
-    data -- further ID information for the frame
-    """
-
-    _framespec = [ StringSpec('frameid', 4), Latin1TextSpec('url') ]
-    _optionalspec = [ BinaryDataSpec('data') ]
-    def __HashKey(self):
-        try:
-            return "%s:%s:%s:%r" % (
-                self.FrameID, self.frameid, self.url, self.data)
-        except AttributeError:
-            return "%s:%s:%s" % (self.FrameID, self.frameid, self.url)
-    HashKey = property(__HashKey)
-    def __eq__(self, other):
-        try: return (self.frameid, self.url, self.data) == other
-        except AttributeError: return (self.frameid, self.url) == other
-
-class POSS(Frame):
-    """Position synchronisation frame
-
-    Attribute:
-    format -- format of the position attribute (frames or milliseconds)
-    position -- current position of the file
-    """
-    _framespec = [ ByteSpec('format'), IntegerSpec('position') ]
-
-    def __pos__(self): return self.position
-    def __eq__(self, other): return self.position == other
-
-class UFID(Frame):
-    """Unique file identifier.
-
-    Attributes:
-    owner -- format/type of identifier
-    data -- identifier
-    """
-
-    _framespec = [ Latin1TextSpec('owner'), BinaryDataSpec('data') ]
-    HashKey = property(lambda s: '%s:%s' % (s.FrameID, s.owner))
-    def __eq__(s, o):
-        if isinstance(o, UFI): return s.owner == o.owner and s.data == o.data
-        else: return s.data == o
-    def _pprint(self):
-        isascii = ord(max(self.data)) < 128
-        if isascii: return "%s=%s" % (self.owner, self.data)
-        else: return "%s (%d bytes)" % (self.owner, len(self.data))
-
-class USER(Frame):
-    """Terms of use.
-
-    Attributes:
-    encoding -- text encoding
-    lang -- ISO three letter language code
-    text -- licensing terms for the audio
-    """
-    _framespec = [ EncodingSpec('encoding'), StringSpec('lang', 3),
-        EncodedTextSpec('text') ]
-    HashKey = property(lambda s: '%s:%r' % (s.FrameID, s.lang))
-
-    def __str__(self): return self.text.encode('utf-8')
-    def __unicode__(self): return self.text
-    def __eq__(self, other): return self.text == other
-    def _pprint(self): return "%r=%s" % (self.lang, self.text)
-
-class OWNE(Frame):
-    """Ownership frame."""
-    _framespec = [ EncodingSpec('encoding'), Latin1TextSpec('price'),
-                   StringSpec('date', 8), EncodedTextSpec('seller') ]
-
-    def __str__(self): return self.seller.encode('utf-8')
-    def __unicode__(self): return self.seller
-    def __eq__(self, other): return self.seller == other
-
-class COMR(FrameOpt):
-    """Commercial frame."""
-    _framespec = [ EncodingSpec('encoding'), Latin1TextSpec('price'),
-                   StringSpec('valid_until', 8), Latin1TextSpec('contact'),
-                   ByteSpec('format'), EncodedTextSpec('seller'),
-                   EncodedTextSpec('desc')]
-    _optionalspec = [ Latin1TextSpec('mime'), BinaryDataSpec('logo') ]
-    HashKey = property(lambda s: '%s:%s' % (s.FrameID, s._writeData()))
-    def __eq__(self, other): return self._writeData() == other._writeData()
-
-class ENCR(Frame):
-    """Encryption method registration.
-
-    The standard does not allow multiple ENCR frames with the same owner
-    or the same method. Mutagen only verifies that the owner is unique.
-    """
-    _framespec = [ Latin1TextSpec('owner'), ByteSpec('method'),
-                   BinaryDataSpec('data') ]
-    HashKey = property(lambda s: "%s:%s" % (s.FrameID, s.owner))
-    def __str__(self): return self.data
-    def __eq__(self, other): return self.data == other
-
-class GRID(FrameOpt):
-    """Group identification registration."""
-    _framespec = [ Latin1TextSpec('owner'), ByteSpec('group') ]
-    _optionalspec = [ BinaryDataSpec('data') ]
-    HashKey = property(lambda s: '%s:%s' % (s.FrameID, s.group))
-    def __pos__(self): return self.group
-    def __str__(self): return self.owner.encode('utf-8')
-    def __unicode__(self): return self.owner
-    def __eq__(self, other): return self.owner == other or self.group == other
-    
-
-class PRIV(Frame):
-    """Private frame."""
-    _framespec = [ Latin1TextSpec('owner'), BinaryDataSpec('data') ]
-    HashKey = property(lambda s: '%s:%s:%s' % (
-        s.FrameID, s.owner, s.data.decode('latin1')))
-    def __str__(self): return self.data
-    def __eq__(self, other): return self.data == other
-    def _pprint(self):
-        isascii = ord(max(self.data)) < 128
-        if isascii: return "%s=%s" % (self.owner, self.data)
-        else: return "%s (%d bytes)" % (self.owner, len(self.data))
-
-class SIGN(Frame):
-    """Signature frame."""
-    _framespec = [ ByteSpec('group'), BinaryDataSpec('sig') ]
-    HashKey = property(lambda s: '%s:%c:%s' % (s.FrameID, s.group, s.sig))
-    def __str__(self): return self.sig
-    def __eq__(self, other): return self.sig == other
-
-class SEEK(Frame):
-    """Seek frame.
-
-    Mutagen does not find tags at seek offsets.
-    """
-    _framespec = [ IntegerSpec('offset') ]
-    def __pos__(self): return self.offset
-    def __eq__(self, other): return self.offset == other
-
-class ASPI(Frame):
-    """Audio seek point index.
-
-    Attributes: S, L, N, b, and Fi. For the meaning of these, see
-    the ID3v2.4 specification. Fi is a list of integers.
-    """
-    _framespec = [ SizedIntegerSpec("S", 4), SizedIntegerSpec("L", 4),
-                   SizedIntegerSpec("N", 2), ByteSpec("b"),
-                   ASPIIndexSpec("Fi") ]
-    def __eq__(self, other): return self.Fi == other
-
-Frames = dict([(k,v) for (k,v) in globals().items()
-        if len(k)==4 and isinstance(v, type) and issubclass(v, Frame)])
-"""All supported ID3v2 frames, keyed by frame name."""
-del(k); del(v)
-
-# ID3v2.2 frames
-class UFI(UFID): "Unique File Identifier"
-
-class TT1(TIT1): "Content group description"
-class TT2(TIT2): "Title"
-class TT3(TIT3): "Subtitle/Description refinement"
-class TP1(TPE1): "Lead Artist/Performer/Soloist/Group"
-class TP2(TPE2): "Band/Orchestra/Accompaniment"
-class TP3(TPE3): "Conductor"
-class TP4(TPE4): "Interpreter/Remixer/Modifier"
-class TCM(TCOM): "Composer"
-class TXT(TEXT): "Lyricist"
-class TLA(TLAN): "Audio Language(s)"
-class TCO(TCON): "Content Type (Genre)"
-class TAL(TALB): "Album"
-class TPA(TPOS): "Part of set"
-class TRK(TRCK): "Track Number"
-class TRC(TSRC): "International Standard Recording Code (ISRC)"
-class TYE(TYER): "Year of recording"
-class TDA(TDAT): "Date of recording (DDMM)"
-class TIM(TIME): "Time of recording (HHMM)"
-class TRD(TRDA): "Recording Dates"
-class TMT(TMED): "Source Media Type"
-class TFT(TFLT): "File Type"
-class TBP(TBPM): "Beats per minute"
-class TCP(TCMP): "iTunes Compilation Flag"
-class TCR(TCOP): "Copyright (C)"
-class TPB(TPUB): "Publisher"
-class TEN(TENC): "Encoder"
-class TSS(TSSE): "Encoder settings"
-class TOF(TOFN): "Original Filename"
-class TLE(TLEN): "Audio Length (ms)"
-class TSI(TSIZ): "Audio Data size (bytes)"
-class TDY(TDLY): "Audio Delay (ms)"
-class TKE(TKEY): "Starting Key"
-class TOT(TOAL): "Original Album"
-class TOA(TOPE): "Original Artist/Perfomer"
-class TOL(TOLY): "Original Lyricist"
-class TOR(TORY): "Original Release Year"
-
-class TXX(TXXX): "User-defined Text"
-
-class WAF(WOAF): "Official File Information"
-class WAR(WOAR): "Official Artist/Performer Information"
-class WAS(WOAS): "Official Source Information"
-class WCM(WCOM): "Commercial Information"
-class WCP(WCOP): "Copyright Information"
-class WPB(WPUB): "Official Publisher Information"
-
-class WXX(WXXX): "User-defined URL"
-
-class IPL(IPLS): "Involved people list"
-class MCI(MCDI): "Binary dump of CD's TOC"
-class ETC(ETCO): "Event timing codes"
-class MLL(MLLT): "MPEG location lookup table"
-class STC(SYTC): "Synced tempo codes"
-class ULT(USLT): "Unsychronised lyrics/text transcription"
-class SLT(SYLT): "Synchronised lyrics/text"
-class COM(COMM): "Comment"
-#class RVA(RVAD)
-#class EQU(EQUA)
-class REV(RVRB): "Reverb"
-class PIC(APIC):
-    """Attached Picture.
-
-    The 'mime' attribute of an ID3v2.2 attached picture must be either
-    'PNG' or 'JPG'.
-    """
-    _framespec = [ EncodingSpec('encoding'), StringSpec('mime', 3),
-        ByteSpec('type'), EncodedTextSpec('desc'), BinaryDataSpec('data') ]
-class GEO(GEOB): "General Encapsulated Object"
-class CNT(PCNT): "Play counter"
-class POP(POPM): "Popularimeter"
-class BUF(RBUF): "Recommended buffer size"
-
-class CRM(Frame):
-    """Encrypted meta frame"""
-    _framespec = [ Latin1TextSpec('owner'), Latin1TextSpec('desc'),
-                   BinaryDataSpec('data') ]
-    def __eq__(self, other): return self.data == other
-
-class CRA(AENC): "Audio encryption"
-
-class LNK(LINK):
-    """Linked information"""
-    _framespec = [ StringSpec('frameid', 3), Latin1TextSpec('url') ]
-    _optionalspec = [ BinaryDataSpec('data') ]
-
-Frames_2_2 = dict([(k,v) for (k,v) in globals().items()
-        if len(k)==3 and isinstance(v, type) and issubclass(v, Frame)])
-
-# support open(filename) as interface
-Open = ID3
-
-# ID3v1.1 support.
-def ParseID3v1(string):
-    """Parse an ID3v1 tag, returning a list of ID3v2.4 frames."""
-    from struct import error as StructError
-    frames = {}
-    try:
-        tag, title, artist, album, year, comment, track, genre = unpack(
-            "3s30s30s30s4s29sBB", string)
-    except StructError: return None
-
-    if tag != "TAG": return None
-    def fix(string):
-        return string.split("\x00")[0].strip().decode('latin1')
-    title, artist, album, year, comment = map(
-        fix, [title, artist, album, year, comment])
-
-    if title: frames["TIT2"] = TIT2(encoding=0, text=title)
-    if artist: frames["TPE1"] = TPE1(encoding=0, text=[artist])
-    if album: frames["TALB"] = TALB(encoding=0, text=album)
-    if year: frames["TDRC"] = TDRC(encoding=0, text=year)
-    if comment: frames["COMM"] = COMM(
-        encoding=0, lang="eng", desc="ID3v1 Comment", text=comment)
-    # Don't read a track number if it looks like the comment was
-    # padded with spaces instead of nulls (thanks, WinAmp).
-    if track and (track != 32 or string[-3] == '\x00'):
-        frames["TRCK"] = TRCK(encoding=0, text=str(track))
-    if genre != 255: frames["TCON"] = TCON(encoding=0, text=str(genre))
-    return frames
-
-def MakeID3v1(id3):
-    """Return an ID3v1.1 tag string from a dict of ID3v2.4 frames."""
-
-    v1 = {}
-
-    for v2id, name in {"TIT2": "title", "TPE1": "artist",
-                       "TALB": "album"}.items():
-        if v2id in id3:
-            text = id3[v2id].text[0].encode('latin1', 'replace')[:30]
-        else: text = ""
-        v1[name] = text + ("\x00" * (30 - len(text)))
-
-    if "COMM" in id3:
-        cmnt = id3["COMM"].text[0].encode('latin1', 'replace')[:28]
-    else: cmnt = ""
-    v1["comment"] = cmnt + ("\x00" * (29 - len(cmnt)))
-
-    if "TRCK" in id3:
-        try: v1["track"] = chr(+id3["TRCK"])
-        except ValueError: v1["track"] = "\x00"
-    else: v1["track"] = "\x00"
-
-    if "TCON" in id3:
-        try: genre = id3["TCON"].genres[0]
-        except IndexError: pass
-        else:
-            if genre in TCON.GENRES:
-                v1["genre"] = chr(TCON.GENRES.index(genre))
-    if "genre" not in v1: v1["genre"] = "\xff"
-
-    if "TDRC" in id3: v1["year"] = str(id3["TDRC"])[:4]
-    else: v1["year"] = "\x00\x00\x00\x00"
-
-    return ("TAG%(title)s%(artist)s%(album)s%(year)s%(comment)s"
-            "%(track)s%(genre)s") % v1 
-
-class ID3FileType(mutagen.FileType):
-    """An unknown type of file with ID3 tags."""
-
-    class _Info(object):
-        length = 0
-        def __init__(self, fileobj, offset): pass
-        pprint = staticmethod(lambda: "Unknown format with ID3 tag")
-
-    def score(filename, fileobj, header):
-        return header.startswith("ID3")
-    score = staticmethod(score)
-
-    def add_tags(self, ID3=ID3):
-        """Add an empty ID3 tag to the file.
-
-        A custom tag reader may be used in instead of the default
-        mutagen.id3.ID3 object, e.g. an EasyID3 reader.
-        """
-        if self.tags is None:
-            self.tags = ID3()
-        else:
-            raise error("an ID3 tag already exists")
-
-    def load(self, filename, ID3=ID3, **kwargs):
-        """Load stream and tag information from a file.
-
-        A custom tag reader may be used in instead of the default
-        mutagen.id3.ID3 object, e.g. an EasyID3 reader.
-        """
-        self.filename = filename
-        try: self.tags = ID3(filename, **kwargs)
-        except error: self.tags = None
-        if self.tags is not None:
-            try: offset = self.tags.size
-            except AttributeError: offset = None
-        else: offset = None
-        try:
-            fileobj = file(filename, "rb")
-            self.info = self._Info(fileobj, offset)
-        finally:
-            fileobj.close()
-