X-Git-Url: https://git.mdrn.pl/wolnelektury.git/blobdiff_plain/e8390b9f10338ff5ced700f5dbd87ba30a3566bd..1477a9281c6c552b0cb00cf967c1cf1bc8739a54:/lib/mutagen/id3.py?ds=sidebyside diff --git a/lib/mutagen/id3.py b/lib/mutagen/id3.py deleted file mode 100644 index fb1357e43..000000000 --- a/lib/mutagen/id3.py +++ /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<> 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 - # 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[0-9]+|RX|CR)\))*)(?P.+)?") - 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() -