#!/usr/bin/python

#Audio Tools, a module and set of tools for manipulating audio data
#Copyright (C) 2007-2011  Brian Langenberger

#This program is free software; you can redistribute it and/or modify
#it under the terms of the GNU General Public License as published by
#the Free Software Foundation; either version 2 of the License, or
#(at your option) any later version.

#This program is distributed in the hope that it will be useful,
#but WITHOUT ANY WARRANTY; without even the implied warranty of
#MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#GNU General Public License for more details.

#You should have received a copy of the GNU General Public License
#along with this program; if not, write to the Free Software
#Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA

from audiotools import (MetaData, Con, re, os, cStringIO,
                        Image, InvalidImage, config)
import codecs
import gettext

gettext.install("audiotools", unicode=True)


class UCS2Codec(codecs.Codec):
    """A special unicode codec for UCS-2.

    This is a subset of UTF-16 with no support for surrogate pairs,
    limiting it to U+0000-U+FFFF."""

    @classmethod
    def fix_char(cls, c):
        """A filter which changes overly large c values to "unknown"."""

        if (ord(c) <= 0xFFFF):
            return c
        else:
            return u"\ufffd"

    def encode(self, input, errors='strict'):
        """Encodes unicode input to plain UCS-2 strings."""

        return codecs.utf_16_encode(u"".join(map(self.fix_char, input)),
                                    errors)

    def decode(self, input, errors='strict'):
        """Decodes plain UCS-2 strings to unicode."""

        (chars, size) = codecs.utf_16_decode(input, errors, True)
        return (u"".join(map(self.fix_char, chars)), size)


class UCS2CodecStreamWriter(UCS2Codec, codecs.StreamWriter):
    pass


class UCS2CodecStreamReader(UCS2Codec, codecs.StreamReader):
    pass


def __reg_ucs2__(name):
    if (name == 'ucs2'):
        return (UCS2Codec().encode,
                UCS2Codec().decode,
                UCS2CodecStreamReader,
                UCS2CodecStreamWriter)
    else:
        return None

codecs.register(__reg_ucs2__)


class UnsupportedID3v2Version(Exception):
    """Raised if one encounters an ID3v2 tag not version .2, .3 or .4."""

    pass


class Syncsafe32(Con.Adapter):
    """An adapter for padding 24 bit values to 32 bits."""

    def __init__(self, name):
        Con.Adapter.__init__(self,
                             Con.StrictRepeater(4, Con.UBInt8(name)))

    def _encode(self, value, context):
        data = []
        for i in xrange(4):
            data.append(value & 0x7F)
            value = value >> 7
        data.reverse()
        return data

    def _decode(self, obj, context):
        i = 0
        for x in obj:
            i = (i << 7) | (x & 0x7F)
        return i


class __24BitsBE__(Con.Adapter):
    def _encode(self, value, context):
        return chr((value & 0xFF0000) >> 16) + \
               chr((value & 0x00FF00) >> 8) + \
               chr(value & 0x0000FF)

    def _decode(self, obj, context):
        return (ord(obj[0]) << 16) | (ord(obj[1]) << 8) | ord(obj[2])


def UBInt24(name):
    """An unsigned, big-endian, 24-bit struct."""

    return __24BitsBE__(Con.Bytes(name, 3))


#UTF16CString and UTF16BECString implement a null-terminated string
#of UTF-16 characters by reading them as unsigned 16-bit integers,
#looking for the null terminator (0x0000) and then converting the integers
#back before decoding.  It's a little half-assed, but it seems to work.
#Even large UTF-16 characters with surrogate pairs (those above U+FFFF)
#shouldn't have embedded 0x0000 bytes in them,
#which ID3v2.2/2.3 aren't supposed to use anyway since they're limited
#to UCS-2 encoding.

class WidecharCStringAdapter(Con.Adapter):
    """An adapter for handling NULL-terminated UTF-16/UCS-2 strings."""

    def __init__(self, obj, encoding):
        Con.Adapter.__init__(self, obj)
        self.encoding = encoding

    def _encode(self, obj, context):
        return Con.GreedyRepeater(Con.UBInt16("s")).parse(obj.encode(
                self.encoding)) + [0]

    def _decode(self, obj, context):
        c = Con.UBInt16("s")

        return "".join([c.build(s) for s in obj[0:-1]]).decode(self.encoding)


def UCS2CString(name):
    """A UCS-2 encoded, NULL-terminated string."""

    return WidecharCStringAdapter(Con.RepeatUntil(lambda obj, ctx: obj == 0x0,
                                                  Con.UBInt16(name)),
                                  encoding='ucs2')


def UTF16CString(name):
    """A UTF-16 encoded, NULL-terminated string."""

    return WidecharCStringAdapter(Con.RepeatUntil(lambda obj, ctx: obj == 0x0,
                                                  Con.UBInt16(name)),
                                  encoding='utf-16')


def UTF16BECString(name):
    """A UTF-16BE encoded, NULL-terminated string."""

    return WidecharCStringAdapter(Con.RepeatUntil(lambda obj, ctx: obj == 0x0,
                                                  Con.UBInt16(name)),
                                  encoding='utf-16be')


def __attrib_equals__(attributes, o1, o2):
    import operator

    try:
        return reduce(operator.and_,
                      [getattr(o1, attrib) == getattr(o2, attrib)
                       for attrib in attributes])
    except AttributeError:
        return False


#takes a pair of integers for the current and total values
#returns a unicode string of their combined pair
#for example, __number_pair__(2,3) returns u"2/3"
#whereas      __number_pair__(4,0) returns u"4"

def __padded_number_pair__(current, total):
    if (total == 0):
        return u"%2.2d" % (current)
    else:
        return u"%2.2d/%2.2d" % (current, total)


def __unpadded_number_pair__(current, total):
    if (total == 0):
        return u"%d" % (current)
    else:
        return u"%d/%d" % (current, total)


if (config.getboolean_default("ID3", "pad", False)):
    __number_pair__ = __padded_number_pair__
else:
    __number_pair__ = __unpadded_number_pair__


#######################
#ID3v2.2
#######################


class ID3v22Frame:
    """A container for individual ID3v2.2 frames."""

    FRAME = Con.Struct("id3v22_frame",
                       Con.Bytes("frame_id", 3),
                       Con.PascalString("data", length_field=UBInt24("size")))
    #we use TEXT_TYPE to differentiate frames which are
    #supposed to return text unicode when __unicode__ is called
    #from those that just return summary data
    TEXT_TYPE = False

    def __init__(self, frame_id, data):
        """frame_id is the 3 byte ID.  data is a binary string."""

        self.id = frame_id
        self.data = data

    def __len__(self):
        return len(self.data)

    def __eq__(self, o):
        return __attrib_equals__(["frame_id", "data"], self, o)

    def build(self):
        """Returns a binary string of ID3v2.2 frame data."""

        return self.FRAME.build(Con.Container(frame_id=self.id,
                                              data=self.data))

    def __unicode__(self):
        if (self.id.startswith('W')):
            return self.data.rstrip(chr(0)).decode('iso-8859-1', 'replace')
        else:
            if (len(self.data) <= 20):
                return unicode(self.data.encode('hex').upper())
            else:
                return (unicode(self.data[0:19].encode('hex').upper()) +
                        u"\u2026")

    @classmethod
    def parse(cls, container):
        """Returns the appropriate ID3v22Frame subclass from a Container.

        Container is parsed from ID3v22Frame.FRAME
        and contains "frame_id and "data" attributes.
        """

        if (container.frame_id.startswith('T')):
            try:
                encoding_byte = ord(container.data[0])
                return ID3v22TextFrame(container.frame_id,
                                       encoding_byte,
                                       container.data[1:].decode(
                        ID3v22TextFrame.ENCODING[encoding_byte]))
            except IndexError:
                return ID3v22TextFrame(container.frame_id,
                                       0,
                                       u"")
        elif (container.frame_id == 'PIC'):
            frame_data = cStringIO.StringIO(container.data)
            pic_header = ID3v22PicFrame.FRAME_HEADER.parse_stream(frame_data)
            return ID3v22PicFrame(
                frame_data.read(),
                pic_header.format.decode('ascii', 'replace'),
                pic_header.description,
                pic_header.picture_type)
        elif (container.frame_id == 'COM'):
            com_data = cStringIO.StringIO(container.data)
            try:
                com = ID3v22ComFrame.COMMENT_HEADER.parse_stream(com_data)
                return ID3v22ComFrame(
                    com.encoding,
                    com.language,
                    com.short_description,
                    com_data.read().decode(
                        ID3v22TextFrame.ENCODING[com.encoding], 'replace'))
            except Con.core.ArrayError:
                return cls(frame_id=container.frame_id, data=container.data)
            except Con.core.FieldError:
                return cls(frame_id=container.frame_id, data=container.data)
        else:
            return cls(frame_id=container.frame_id,
                       data=container.data)


class ID3v22TextFrame(ID3v22Frame):
    """A container for individual ID3v2.2 text frames."""

    ENCODING = {0x00: "latin-1",
                0x01: "ucs2"}

    TEXT_TYPE = True

    def __init__(self, frame_id, encoding, s):
        """frame_id is a 3 byte ID, encoding is 0/1, s is a unicode string."""

        self.id = frame_id
        self.encoding = encoding
        self.string = s

    def __eq__(self, o):
        return __attrib_equals__(["id", "encoding", "string"], self, o)

    def __len__(self):
        return len(self.string)

    def __unicode__(self):
        return self.string

    def __int__(self):
        try:
            return int(re.findall(r'\d+', self.string)[0])
        except IndexError:
            return 0

    def total(self):
        """If the frame is number/total formatted, return the "total" int."""

        try:
            return int(re.findall(r'\d+/(\d+)', self.string)[0])
        except IndexError:
            return 0

    @classmethod
    def from_unicode(cls, frame_id, s):
        """Builds an ID3v22TextFrame from 3 byte frame_id and unicode s."""

        if (frame_id == 'COM'):
            return ID3v22ComFrame.from_unicode(s)

        for encoding in 0x00, 0x01:
            try:
                s.encode(cls.ENCODING[encoding])
                return cls(frame_id, encoding, s)
            except UnicodeEncodeError:
                continue

    def build(self):
        """Returns a binary string of ID3v2.2 frame data."""

        return self.FRAME.build(Con.Container(
                frame_id=self.id,
                data=chr(self.encoding) + \
                    self.string.encode(self.ENCODING[self.encoding],
                                       'replace')))


class ID3v22ComFrame(ID3v22TextFrame):
    """A container for ID3v2.2 comment (COM) frames."""

    COMMENT_HEADER = Con.Struct(
        "com_frame",
        Con.Byte("encoding"),
        Con.String("language", 3),
        Con.Switch("short_description",
                   lambda ctx: ctx.encoding,
                   {0x00: Con.CString("s", encoding='latin-1'),
                    0x01: UCS2CString("s")}))

    TEXT_TYPE = True

    def __init__(self, encoding, language, short_description, content):
        """encoding is 0/1, language is a string, the rest are unicode.

        We're mostly interested in encoding and content.
        The language and short_description fields are rarely used."""

        self.encoding = encoding
        self.language = language
        self.short_description = short_description
        self.content = content
        self.id = 'COM'

    def __len__(self):
        return len(self.content)

    def __eq__(self, o):
        return __attrib_equals__(["encoding", "language",
                                  "short_description", "content"], self, o)

    def __unicode__(self):
        return self.content

    def __int__(self):
        return 0

    @classmethod
    def from_unicode(cls, s):
        """Builds an ID3v22ComFrame from a unicode string."""

        for encoding in 0x00, 0x01:
            try:
                s.encode(cls.ENCODING[encoding])
                return cls(encoding, 'eng', u'', s)
            except UnicodeEncodeError:
                continue

    def build(self):
        """Returns a binary string of ID3v2.2 frame data."""

        return self.FRAME.build(Con.Container(
                frame_id=self.id,
                data=self.COMMENT_HEADER.build(Con.Container(
                        encoding=self.encoding,
                        language=self.language,
                        short_description=self.short_description)) +
                  self.content.encode(self.ENCODING[self.encoding],
                                      'replace')))


class ID3v22PicFrame(ID3v22Frame, Image):
    """A container for ID3v2.2 image (PIC) frames."""

    FRAME_HEADER = Con.Struct('pic_frame',
                              Con.Byte('text_encoding'),
                              Con.String('format', 3),
                              Con.Byte('picture_type'),
                              Con.Switch("description",
                                         lambda ctx: ctx.text_encoding,
                                         {0x00: Con.CString(
                    "s",  encoding='latin-1'),
                                          0x01: UCS2CString("s")}))

    def __init__(self, data, format, description, pic_type):
        """Fields are as follows:

        data        - a binary string of raw image data
        format      - a unicode string
        description - a unicode string
        pic_type    - an integer
        """

        ID3v22Frame.__init__(self, 'PIC', None)

        try:
            img = Image.new(data, u'', 0)
        except InvalidImage:
            img = Image(data=data, mime_type=u'',
                        width=0, height=0, color_depth=0, color_count=0,
                        description=u'', type=0)

        self.pic_type = pic_type
        self.format = format
        Image.__init__(self,
                       data=data,
                       mime_type=img.mime_type,
                       width=img.width,
                       height=img.height,
                       color_depth=img.color_depth,
                       color_count=img.color_count,
                       description=description,
                       type={3: 0, 4: 1, 5: 2, 6: 3}.get(pic_type, 4))

    def type_string(self):
        """Returns the image's type as a human readable plain string.

        For example, an image of type 0 returns "Front Cover"""

        #FIXME - these should be internationalized
        return {0: "Other",
                1: "32x32 pixels 'file icon' (PNG only)",
                2: "Other file icon",
                3: "Cover (front)",
                4: "Cover (back)",
                5: "Leaflet page",
                6: "Media (e.g. label side of CD)",
                7: "Lead artist/lead performer/soloist",
                8: "Artist / Performer",
                9: "Conductor",
                10: "Band / Orchestra",
                11: "Composer",
                12: "Lyricist / Text writer",
                13: "Recording Location",
                14: "During recording",
                15: "During performance",
                16: "Movie/Video screen capture",
                17: "A bright coloured fish",
                18: "Illustration",
                19: "Band/Artist logotype",
                20: "Publisher/Studio logotype"}.get(self.pic_type, "Other")

    def __unicode__(self):
        return u"%s (%d\u00D7%d,'%s')" % \
               (self.type_string(),
                self.width, self.height, self.mime_type)

    def __eq__(self, i):
        return Image.__eq__(self, i)

    def build(self):
        """Returns a binary string of ID3v2.2 frame data."""

        try:
            self.description.encode('latin-1')
            text_encoding = 0
        except UnicodeEncodeError:
            text_encoding = 1

        return ID3v22Frame.FRAME.build(
            Con.Container(frame_id='PIC',
                          data=self.FRAME_HEADER.build(
                    Con.Container(text_encoding=text_encoding,
                                  format=self.format.encode('ascii'),
                                  picture_type=self.pic_type,
                                  description=self.description)) + self.data))

    @classmethod
    def converted(cls, image):
        """Given an Image object, returns an ID3v22PicFrame object."""

        return cls(data=image.data,
                   format={u"image/png": u"PNG",
                           u"image/jpeg": u"JPG",
                           u"image/jpg": u"JPG",
                           u"image/x-ms-bmp": u"BMP",
                           u"image/gif": u"GIF",
                           u"image/tiff": u"TIF"}.get(image.mime_type,
                                                     u"JPG"),
                   description=image.description,
                   pic_type={0: 3, 1: 4, 2: 5, 3: 6}.get(image.type, 0))


class ID3v22Comment(MetaData):
    """A complete ID3v2.2 comment."""

    Frame = ID3v22Frame
    TextFrame = ID3v22TextFrame
    PictureFrame = ID3v22PicFrame
    CommentFrame = ID3v22ComFrame

    TAG_HEADER = Con.Struct("id3v22_header",
                            Con.Const(Con.Bytes("file_id", 3), 'ID3'),
                            Con.Byte("version_major"),
                            Con.Byte("version_minor"),
                            Con.Embed(Con.BitStruct("flags",
                                                    Con.Flag("unsync"),
                                                    Con.Flag("compression"),
                                                    Con.Padding(6))),
                            Syncsafe32("length"))

    ATTRIBUTE_MAP = {'track_name': 'TT2',
                     'track_number': 'TRK',
                     'track_total': 'TRK',
                     'album_name': 'TAL',
                     'artist_name': 'TP1',
                     'performer_name': 'TP2',
                     'conductor_name': 'TP3',
                     'composer_name': 'TCM',
                     'media': 'TMT',
                     'ISRC': 'TRC',
                     'copyright': 'TCR',
                     'publisher': 'TPB',
                     'year': 'TYE',
                     'date': 'TRD',
                     'album_number': 'TPA',
                     'album_total': 'TPA',
                     'comment': 'COM'}

    INTEGER_ITEMS = ('TRK', 'TPA')

    KEY_ORDER = ('TT2', 'TAL', 'TRK', 'TPA', 'TP1', 'TP2', 'TCM', 'TP3',
                 'TPB', 'TRC', 'TYE', 'TRD', None, 'COM', 'PIC')

    def __init__(self, frames):
        """frame should be a list of ID3v2?Frame-compatible objects."""

        self.__dict__["frames"] = {}  # a frame_id->[frame list] mapping

        for frame in frames:
            self.__dict__["frames"].setdefault(frame.id, []).append(frame)

    def __repr__(self):
        return "ID3v22Comment(%s)" % (repr(self.__dict__["frames"]))

    def __comment_name__(self):
        return u'ID3v2.2'

    def __comment_pairs__(self):
        key_order = list(self.KEY_ORDER)

        def by_weight(keyval1, keyval2):
            (key1, key2) = (keyval1[0], keyval2[0])

            if (key1 in key_order):
                order1 = key_order.index(key1)
            else:
                order1 = key_order.index(None)

            if (key2 in key_order):
                order2 = key_order.index(key2)
            else:
                order2 = key_order.index(None)

            return cmp((order1, key1), (order2, key2))

        pairs = []

        for (key, values) in sorted(self.frames.items(), by_weight):
            for value in values:
                pairs.append(('     ' + key, unicode(value)))

        return pairs

    def __unicode__(self):
        comment_pairs = self.__comment_pairs__()
        if (len(comment_pairs) > 0):
            max_key_length = max([len(pair[0]) for pair in comment_pairs])
            line_template = u"%%(key)%(length)d.%(length)ds : %%(value)s" % \
                            {"length": max_key_length}

            return unicode(os.linesep.join(
                [u"%s Comment:" % (self.__comment_name__())] + \
                [line_template % {"key": key, "value": value} for
                 (key, value) in comment_pairs]))
        else:
            return u""

    #if an attribute is updated (e.g. self.track_name)
    #make sure to update the corresponding dict pair
    def __setattr__(self, key, value):
        if (key in self.ATTRIBUTE_MAP):
            if (key == 'track_number'):
                value = __number_pair__(value, self.track_total)
            elif (key == 'track_total'):
                value = __number_pair__(self.track_number, value)
            elif (key == 'album_number'):
                value = __number_pair__(value, self.album_total)
            elif (key == 'album_total'):
                value = __number_pair__(self.album_number, value)

            self.frames[self.ATTRIBUTE_MAP[key]] = [
                self.TextFrame.from_unicode(self.ATTRIBUTE_MAP[key],
                                            unicode(value))]
        elif (key in MetaData.__FIELDS__):
            pass
        else:
            self.__dict__[key] = value

    def __getattr__(self, key):
        if (key in self.ATTRIBUTE_MAP):
            try:
                frame = self.frames[self.ATTRIBUTE_MAP[key]][0]
                if (key in ('track_number', 'album_number')):
                    return int(frame)
                elif (key in ('track_total', 'album_total')):
                    return frame.total()
                else:
                    return unicode(frame)
            except KeyError:
                if (key in MetaData.__INTEGER_FIELDS__):
                    return 0
                else:
                    return u""
        elif (key in MetaData.__FIELDS__):
            return u""
        else:
            raise AttributeError(key)

    def __delattr__(self, key):
        if (key in self.ATTRIBUTE_MAP):
            if (key == 'track_number'):
                setattr(self, 'track_number', 0)
                if ((self.track_number == 0) and (self.track_total == 0)):
                    del(self.frames[self.ATTRIBUTE_MAP[key]])
            elif (key == 'track_total'):
                setattr(self, 'track_total', 0)
                if ((self.track_number == 0) and (self.track_total == 0)):
                    del(self.frames[self.ATTRIBUTE_MAP[key]])
            elif (key == 'album_number'):
                setattr(self, 'album_number', 0)
                if ((self.album_number == 0) and (self.album_total == 0)):
                    del(self.frames[self.ATTRIBUTE_MAP[key]])
            elif (key == 'album_total'):
                setattr(self, 'album_total', 0)
                if ((self.album_number == 0) and (self.album_total == 0)):
                    del(self.frames[self.ATTRIBUTE_MAP[key]])
            elif (self.ATTRIBUTE_MAP[key] in self.frames):
                del(self.frames[self.ATTRIBUTE_MAP[key]])
        elif (key in MetaData.__FIELDS__):
            pass
        else:
            raise AttributeError(key)

    def add_image(self, image):
        """Embeds an Image object in this metadata."""

        image = self.PictureFrame.converted(image)
        self.frames.setdefault('PIC', []).append(image)

    def delete_image(self, image):
        """Deletes an Image object from this metadata."""

        del(self.frames['PIC'][self['PIC'].index(image)])

    def images(self):
        """Returns a list of embedded Image objects."""

        if ('PIC' in self.frames.keys()):
            return self.frames['PIC'][:]
        else:
            return []

    def __getitem__(self, key):
        return self.frames[key]

    #this should always take a list of items,
    #either unicode strings (for text fields)
    #or something Frame-compatible (for everything else)
    #or possibly both in one list
    def __setitem__(self, key, values):
        frames = []
        for value in values:
            if (isinstance(value, unicode)):
                frames.append(self.TextFrame.from_unicode(key, value))
            elif (isinstance(value, int)):
                frames.append(self.TextFrame.from_unicode(key, unicode(value)))
            elif (isinstance(value, self.Frame)):
                frames.append(value)

        self.frames[key] = frames

    def __delitem__(self, key):
        del(self.frames[key])

    def len(self):
        return len(self.frames)

    def keys(self):
        return self.frames.keys()

    def values(self):
        return self.frames.values()

    def items(self):
        return self.frames.items()

    @classmethod
    def parse(cls, stream):
        """Given a file stream, returns an ID3v22Comment object."""

        header = cls.TAG_HEADER.parse_stream(stream)

        #read in the whole tag
        stream = cStringIO.StringIO(stream.read(header.length))

        #read in a collection of parsed Frame containers
        frames = []

        while (stream.tell() < header.length):
            try:
                container = cls.Frame.FRAME.parse_stream(stream)
            except Con.core.FieldError:
                break
            except Con.core.ArrayError:
                break

            if (chr(0) in container.frame_id):
                break
            else:
                try:
                    frames.append(cls.Frame.parse(container))
                except UnicodeDecodeError:
                    break

        return cls(frames)

    @classmethod
    def converted(cls, metadata):
        """Converts a MetaData object to an ID3v22Comment object."""

        if ((metadata is None) or
            (isinstance(metadata, cls) and
             (cls.Frame is metadata.Frame))):
            return metadata

        frames = []

        for (field, key) in cls.ATTRIBUTE_MAP.items():
            value = getattr(metadata, field)
            if (key not in cls.INTEGER_ITEMS):
                if (len(value.strip()) > 0):
                    frames.append(cls.TextFrame.from_unicode(key, value))

        frames.append(cls.TextFrame.from_unicode(
                cls.INTEGER_ITEMS[0],
                __number_pair__(metadata.track_number,
                                metadata.track_total)))

        if ((metadata.album_number != 0) or
            (metadata.album_total != 0)):
            frames.append(cls.TextFrame.from_unicode(
                cls.INTEGER_ITEMS[1],
                __number_pair__(metadata.album_number,
                                metadata.album_total)))

        for image in metadata.images():
            frames.append(cls.PictureFrame.converted(image))

        if (hasattr(cls, 'ITUNES_COMPILATION')):
            frames.append(cls.TextFrame.from_unicode(
                    cls.ITUNES_COMPILATION, u'1'))

        return cls(frames)

    def merge(self, metadata):
        """Updates any currently empty entries from metadata's values."""

        metadata = self.__class__.converted(metadata)
        if (metadata is None):
            return

        for (key, values) in metadata.frames.items():
            if ((key not in self.INTEGER_ITEMS) and
                (len(values) > 0) and
                (len(values[0]) > 0) and
                (len(self.frames.get(key, [])) == 0)):
                self.frames[key] = values

        for attr in ("track_number", "track_total",
                     "album_number", "album_total"):
            if ((getattr(self, attr) == 0) and
                (getattr(metadata, attr) != 0)):
                setattr(self, attr, getattr(metadata, attr))

    def build(self):
        """Returns an ID3v2.2 comment as a binary string."""

        subframes = "".join(["".join([value.build() for value in values])
                             for values in self.frames.values()])

        return self.TAG_HEADER.build(
            Con.Container(file_id='ID3',
                          version_major=0x02,
                          version_minor=0x00,
                          unsync=False,
                          compression=False,
                          length=len(subframes))) + subframes

    @classmethod
    def skip(cls, file):
        """Seeks past an ID3v2 comment if found in the file stream.

        The stream must be seekable, obviously."""

        if (file.read(3) == 'ID3'):
            file.seek(0, 0)
            #parse the header
            h = cls.TAG_HEADER.parse_stream(file)
            #seek to the end of its length
            file.seek(h.length, 1)
            #skip any null bytes after the ID3v2 tag
            c = file.read(1)
            while (c == '\x00'):
                c = file.read(1)
            file.seek(-1, 1)
        else:
            try:
                file.seek(-3, 1)
            except IOError:
                pass

    @classmethod
    def read_id3v2_comment(cls, filename):
        """Given a filename, returns an ID3v22Comment or a subclass.

        For example, if the file is ID3v2.3 tagged,
        this returns an ID3v23Comment.
        """

        import cStringIO

        f = file(filename, "rb")

        try:
            f.seek(0, 0)
            try:
                header = ID3v2Comment.TAG_HEADER.parse_stream(f)
            except Con.ConstError:
                raise UnsupportedID3v2Version()
            if (header.version_major == 0x04):
                comment_class = ID3v24Comment
            elif (header.version_major == 0x03):
                comment_class = ID3v23Comment
            elif (header.version_major == 0x02):
                comment_class = ID3v22Comment
            else:
                raise UnsupportedID3v2Version()

            f.seek(0, 0)
            return comment_class.parse(f)
        finally:
            f.close()


#######################
#ID3v2.3
#######################


class ID3v23Frame(ID3v22Frame):
    """A container for individual ID3v2.3 frames."""

    FRAME = Con.Struct("id3v23_frame",
                       Con.Bytes("frame_id", 4),
                       Con.UBInt32("size"),
                       Con.Embed(Con.BitStruct("flags",
                                               Con.Flag('tag_alter'),
                                               Con.Flag('file_alter'),
                                               Con.Flag('read_only'),
                                               Con.Padding(5),
                                               Con.Flag('compression'),
                                               Con.Flag('encryption'),
                                               Con.Flag('grouping'),
                                               Con.Padding(5))),
                       Con.String("data", length=lambda ctx: ctx["size"]))

    def build(self, data=None):
        """Returns a binary string of ID3v2.3 frame data."""

        if (data is None):
            data = self.data

        return self.FRAME.build(Con.Container(frame_id=self.id,
                                              size=len(data),
                                              tag_alter=False,
                                              file_alter=False,
                                              read_only=False,
                                              compression=False,
                                              encryption=False,
                                              grouping=False,
                                              data=data))

    @classmethod
    def parse(cls, container):
        """Returns the appropriate ID3v23Frame subclass from a Container.

        Container is parsed from ID3v23Frame.FRAME
        and contains "frame_id and "data" attributes.
        """

        if (container.frame_id.startswith('T')):
            try:
                encoding_byte = ord(container.data[0])
                return ID3v23TextFrame(container.frame_id,
                                       encoding_byte,
                                       container.data[1:].decode(
                        ID3v23TextFrame.ENCODING[encoding_byte]))
            except IndexError:
                return ID3v23TextFrame(container.frame_id,
                                       0,
                                       u"")
        elif (container.frame_id == 'APIC'):
            frame_data = cStringIO.StringIO(container.data)
            pic_header = ID3v23PicFrame.FRAME_HEADER.parse_stream(frame_data)
            return ID3v23PicFrame(
                frame_data.read(),
                pic_header.mime_type,
                pic_header.description,
                pic_header.picture_type)
        elif (container.frame_id == 'COMM'):
            com_data = cStringIO.StringIO(container.data)
            try:
                com = ID3v23ComFrame.COMMENT_HEADER.parse_stream(com_data)
                return ID3v23ComFrame(
                    com.encoding,
                    com.language,
                    com.short_description,
                    com_data.read().decode(
                        ID3v23TextFrame.ENCODING[com.encoding], 'replace'))
            except Con.core.ArrayError:
                return cls(frame_id=container.frame_id, data=container.data)
            except Con.core.FieldError:
                return cls(frame_id=container.frame_id, data=container.data)
        else:
            return cls(frame_id=container.frame_id,
                       data=container.data)

    def __unicode__(self):
        if (self.id.startswith('W')):
            return self.data.rstrip(chr(0)).decode('iso-8859-1', 'replace')
        else:
            if (len(self.data) <= 20):
                return unicode(self.data.encode('hex').upper())
            else:
                return (unicode(self.data[0:19].encode('hex').upper()) +
                        u"\u2026")


class ID3v23TextFrame(ID3v23Frame):
    """A container for individual ID3v2.3 text frames."""

    ENCODING = {0x00: "latin-1",
                0x01: "ucs2"}

    TEXT_TYPE = True

    def __init__(self, frame_id, encoding, s):
        """frame_id is a 4 byte ID, encoding is 0/1, s is a unicode string."""

        self.id = frame_id
        self.encoding = encoding
        self.string = s

    def __len__(self):
        return len(self.string)

    def __eq__(self, o):
        return __attrib_equals__(["id", "encoding", "string"], self, o)

    def __unicode__(self):
        return self.string

    def __int__(self):
        try:
            return int(re.findall(r'\d+', self.string)[0])
        except IndexError:
            return 0

    def total(self):
        """If the frame is number/total formatted, return the "total" int."""

        try:
            return int(re.findall(r'\d+/(\d+)', self.string)[0])
        except IndexError:
            return 0

    @classmethod
    def from_unicode(cls, frame_id, s):
        """Builds an ID3v23TextFrame from 4 byte frame_id and unicode s."""

        if (frame_id == 'COMM'):
            return ID3v23ComFrame.from_unicode(s)

        for encoding in 0x00, 0x01:
            try:
                s.encode(cls.ENCODING[encoding])
                return ID3v23TextFrame(frame_id, encoding, s)
            except UnicodeEncodeError:
                continue

    def build(self):
        """Returns a binary string of ID3v2.3 frame data."""

        return ID3v23Frame.build(
            self,
            chr(self.encoding) + \
                self.string.encode(self.ENCODING[self.encoding],
                                   'replace'))


class ID3v23PicFrame(ID3v23Frame, Image):
    """A container for ID3v2.3 image (APIC) frames."""

    FRAME_HEADER = Con.Struct('apic_frame',
                              Con.Byte('text_encoding'),
                              Con.CString('mime_type'),
                              Con.Byte('picture_type'),
                              Con.Switch("description",
                                         lambda ctx: ctx.text_encoding,
                                         {0x00: Con.CString(
                    "s", encoding='latin-1'),
                                          0x01: UCS2CString("s")}))

    def __init__(self, data, mime_type, description, pic_type):
        """Fields are as follows:

        data        - a binary string of raw image data
        mime_type   - a unicode string
        description - a unicode string
        pic_type    - an integer
        """

        ID3v23Frame.__init__(self, 'APIC', None)

        try:
            img = Image.new(data, u'', 0)
        except InvalidImage:
            img = Image(data=data, mime_type=u'',
                        width=0, height=0, color_depth=0, color_count=0,
                        description=u'', type=0)

        self.pic_type = pic_type
        Image.__init__(self,
                       data=data,
                       mime_type=mime_type,
                       width=img.width,
                       height=img.height,
                       color_depth=img.color_depth,
                       color_count=img.color_count,
                       description=description,
                       type={3: 0, 4: 1, 5: 2, 6: 3}.get(pic_type, 4))

    def __eq__(self, i):
        return Image.__eq__(self, i)

    def __unicode__(self):
        return u"%s (%d\u00D7%d,'%s')" % \
               (self.type_string(),
                self.width, self.height, self.mime_type)

    def build(self):
        """Returns a binary string of ID3v2.3 frame data."""

        try:
            self.description.encode('latin-1')
            text_encoding = 0
        except UnicodeEncodeError:
            text_encoding = 1

        return ID3v23Frame.build(self,
                                 self.FRAME_HEADER.build(
                Con.Container(text_encoding=text_encoding,
                              picture_type=self.pic_type,
                              mime_type=self.mime_type,
                              description=self.description)) + self.data)

    @classmethod
    def converted(cls, image):
        """Given an Image object, returns an ID3v23PicFrame object."""

        return cls(data=image.data,
                   mime_type=image.mime_type,
                   description=image.description,
                   pic_type={0: 3, 1: 4, 2: 5, 3: 6}.get(image.type, 0))


class ID3v23ComFrame(ID3v23TextFrame):
    """A container for ID3v2.3 comment (COMM) frames."""

    COMMENT_HEADER = ID3v22ComFrame.COMMENT_HEADER

    TEXT_TYPE = True

    def __init__(self, encoding, language, short_description, content):
        """Fields are as follows:

        encoding          - a text encoding integer 0/1
        language          - a 3 byte language field
        short_description - a unicode string
        contenxt          - a unicode string
        """

        self.encoding = encoding
        self.language = language
        self.short_description = short_description
        self.content = content
        self.id = 'COMM'

    def __len__(self):
        return len(self.content)

    def __eq__(self, o):
        return __attrib_equals__(["encoding", "language",
                                  "short_description", "content"], self, o)

    def __unicode__(self):
        return self.content

    def __int__(self):
        return 0

    @classmethod
    def from_unicode(cls, s):
        """Builds an ID3v23ComFrame from a unicode string."""

        for encoding in 0x00, 0x01:
            try:
                s.encode(cls.ENCODING[encoding])
                return cls(encoding, 'eng', u'', s)
            except UnicodeEncodeError:
                continue

    def build(self):
        """Returns a binary string of ID3v2.3 frame data."""

        return ID3v23Frame.build(
            self,
            self.COMMENT_HEADER.build(Con.Container(
                    encoding=self.encoding,
                    language=self.language,
                    short_description=self.short_description)) + \
                self.content.encode(self.ENCODING[self.encoding], 'replace'))


class ID3v23Comment(ID3v22Comment):
    """A complete ID3v2.3 comment."""

    Frame = ID3v23Frame
    TextFrame = ID3v23TextFrame
    PictureFrame = ID3v23PicFrame

    TAG_HEADER = Con.Struct("id3v23_header",
                            Con.Const(Con.Bytes("file_id", 3), 'ID3'),
                            Con.Byte("version_major"),
                            Con.Byte("version_minor"),
                            Con.Embed(Con.BitStruct("flags",
                                                    Con.Flag("unsync"),
                                                    Con.Flag("extended"),
                                                    Con.Flag("experimental"),
                                                    Con.Flag("footer"),
                                                    Con.Padding(4))),
                            Syncsafe32("length"))

    ATTRIBUTE_MAP = {'track_name': 'TIT2',
                     'track_number': 'TRCK',
                     'track_total': 'TRCK',
                     'album_name': 'TALB',
                     'artist_name': 'TPE1',
                     'performer_name': 'TPE2',
                     'composer_name': 'TCOM',
                     'conductor_name': 'TPE3',
                     'media': 'TMED',
                     'ISRC': 'TSRC',
                     'copyright': 'TCOP',
                     'publisher': 'TPUB',
                     'year': 'TYER',
                     'date': 'TRDA',
                     'album_number': 'TPOS',
                     'album_total': 'TPOS',
                     'comment': 'COMM'}

    INTEGER_ITEMS = ('TRCK', 'TPOS')

    KEY_ORDER = ('TIT2', 'TALB', 'TRCK', 'TPOS', 'TPE1', 'TPE2', 'TCOM',
                 'TPE3', 'TPUB', 'TSRC', 'TMED', 'TYER', 'TRDA', 'TCOP',
                 None, 'COMM', 'APIC')

    ITUNES_COMPILATION = 'TCMP'

    def __comment_name__(self):
        return u'ID3v2.3'

    def __comment_pairs__(self):
        key_order = list(self.KEY_ORDER)

        def by_weight(keyval1, keyval2):
            (key1, key2) = (keyval1[0], keyval2[0])

            if (key1 in key_order):
                order1 = key_order.index(key1)
            else:
                order1 = key_order.index(None)

            if (key2 in key_order):
                order2 = key_order.index(key2)
            else:
                order2 = key_order.index(None)

            return cmp((order1, key1), (order2, key2))

        pairs = []

        for (key, values) in sorted(self.frames.items(), by_weight):
            for value in values:
                pairs.append(('    ' + key, unicode(value)))

        return pairs

    def add_image(self,  image):
        """Embeds an Image object in this metadata."""

        image = self.PictureFrame.converted(image)
        self.frames.setdefault('APIC', []).append(image)

    def delete_image(self, image):
        """Deletes an Image object from this metadata."""

        del(self.frames['APIC'][self['APIC'].index(image)])

    def images(self):
        """Returns a list of embedded Image objects."""

        if ('APIC' in self.frames.keys()):
            return self.frames['APIC'][:]
        else:
            return []

    def build(self):
        """Returns an ID3v2.3 comment as a binary string."""

        subframes = "".join(["".join([value.build() for value in values])
                             for values in self.frames.values()])

        return self.TAG_HEADER.build(
            Con.Container(file_id='ID3',
                          version_major=0x03,
                          version_minor=0x00,
                          unsync=False,
                          extended=False,
                          experimental=False,
                          footer=False,
                          length=len(subframes))) + subframes


#######################
#ID3v2.4
#######################


class ID3v24Frame(ID3v23Frame):
    """A container for individual ID3v2.4 frames."""

    FRAME = Con.Struct("id3v24_frame",
                       Con.Bytes("frame_id", 4),
                       Syncsafe32("size"),
                       Con.Embed(Con.BitStruct("flags",
                                               Con.Padding(1),
                                               Con.Flag('tag_alter'),
                                               Con.Flag('file_alter'),
                                               Con.Flag('read_only'),
                                               Con.Padding(5),
                                               Con.Flag('grouping'),
                                               Con.Padding(2),
                                               Con.Flag('compression'),
                                               Con.Flag('encryption'),
                                               Con.Flag('unsync'),
                                               Con.Flag('data_length'))),
                       Con.String("data", length=lambda ctx: ctx["size"]))

    def build(self, data=None):
        """Returns a binary string of ID3v2.4 frame data."""

        if (data is None):
            data = self.data

        return self.FRAME.build(Con.Container(frame_id=self.id,
                                              size=len(data),
                                              tag_alter=False,
                                              file_alter=False,
                                              read_only=False,
                                              compression=False,
                                              encryption=False,
                                              grouping=False,
                                              unsync=False,
                                              data_length=False,
                                              data=data))

    @classmethod
    def parse(cls, container):
        """Returns the appropriate ID3v24Frame subclass from a Container.

        Container is parsed from ID3v24Frame.FRAME
        and contains "frame_id and "data" attributes.
        """

        if (container.frame_id.startswith('T')):
            try:
                encoding_byte = ord(container.data[0])
                return ID3v24TextFrame(container.frame_id,
                                       encoding_byte,
                                       container.data[1:].decode(
                        ID3v24TextFrame.ENCODING[encoding_byte]))
            except IndexError:
                return ID3v24TextFrame(container.frame_id,
                                       0,
                                       u"")
        elif (container.frame_id == 'APIC'):
            frame_data = cStringIO.StringIO(container.data)
            pic_header = ID3v24PicFrame.FRAME_HEADER.parse_stream(frame_data)
            return ID3v24PicFrame(
                frame_data.read(),
                pic_header.mime_type,
                pic_header.description,
                pic_header.picture_type)
        elif (container.frame_id == 'COMM'):
            com_data = cStringIO.StringIO(container.data)
            try:
                com = ID3v24ComFrame.COMMENT_HEADER.parse_stream(com_data)
                return ID3v24ComFrame(
                    com.encoding,
                    com.language,
                    com.short_description,
                    com_data.read().decode(
                        ID3v24TextFrame.ENCODING[com.encoding], 'replace'))
            except Con.core.ArrayError:
                return cls(frame_id=container.frame_id, data=container.data)
            except Con.core.FieldError:
                return cls(frame_id=container.frame_id, data=container.data)
        else:
            return cls(frame_id=container.frame_id,
                       data=container.data)

    def __unicode__(self):
        if (self.id.startswith('W')):
            return self.data.rstrip(chr(0)).decode('iso-8859-1', 'replace')
        else:
            if (len(self.data) <= 20):
                return unicode(self.data.encode('hex').upper())
            else:
                return (unicode(self.data[0:19].encode('hex').upper()) +
                        u"\u2026")


class ID3v24TextFrame(ID3v24Frame):
    """A container for individual ID3v2.4 text frames."""

    ENCODING = {0x00: "latin-1",
                0x01: "utf-16",
                0x02: "utf-16be",
                0x03: "utf-8"}

    TEXT_TYPE = True

    #encoding is an encoding byte
    #s is a unicode string
    def __init__(self, frame_id, encoding, s):
        """frame_id is a 4 byte ID, encoding is 0-3, s is a unicode string."""

        self.id = frame_id
        self.encoding = encoding
        self.string = s

    def __eq__(self, o):
        return __attrib_equals__(["id", "encoding", "string"], self, o)

    def __len__(self):
        return len(self.string)

    def __unicode__(self):
        return self.string

    def __int__(self):
        try:
            return int(re.findall(r'\d+', self.string)[0])
        except IndexError:
            return 0

    def total(self):
        """If the frame is number/total formatted, return the "total" int."""

        try:
            return int(re.findall(r'\d+/(\d+)', self.string)[0])
        except IndexError:
            return 0

    @classmethod
    def from_unicode(cls, frame_id, s):
        """Builds an ID3v24TextFrame from 4 byte frame_id and unicode s."""

        if (frame_id == 'COMM'):
            return ID3v24ComFrame.from_unicode(s)

        for encoding in 0x00, 0x03, 0x01, 0x02:
            try:
                s.encode(cls.ENCODING[encoding])
                return ID3v24TextFrame(frame_id, encoding, s)
            except UnicodeEncodeError:
                continue

    def build(self):
        """Returns a binary string of ID3v2.4 frame data."""

        return ID3v24Frame.build(
            self,
            chr(self.encoding) + \
                self.string.encode(self.ENCODING[self.encoding],
                                   'replace'))


class ID3v24PicFrame(ID3v24Frame, Image):
    """A container for ID3v2.4 image (APIC) frames."""

    FRAME_HEADER = Con.Struct('apic_frame',
                              Con.Byte('text_encoding'),
                              Con.CString('mime_type'),
                              Con.Byte('picture_type'),
                              Con.Switch("description",
                                         lambda ctx: ctx.text_encoding,
                                         {0x00: Con.CString(
                    "s", encoding='latin-1'),
                                          0x01: UTF16CString("s"),
                                          0x02: UTF16BECString("s"),
                                          0x03: Con.CString(
                    "s", encoding='utf-8')}))

    def __init__(self, data, mime_type, description, pic_type):
        """Fields are as follows:

        data        - a binary string of raw image data
        mime_type   - a unicode string
        description - a unicode string
        pic_type    - an integer
        """

        ID3v24Frame.__init__(self, 'APIC', None)

        try:
            img = Image.new(data, u'', 0)
        except InvalidImage:
            img = Image(data=data, mime_type=u'',
                        width=0, height=0, color_depth=0, color_count=0,
                        description=u'', type=0)

        self.pic_type = pic_type
        Image.__init__(self,
                       data=data,
                       mime_type=mime_type,
                       width=img.width,
                       height=img.height,
                       color_depth=img.color_depth,
                       color_count=img.color_count,
                       description=description,
                       type={3: 0, 4: 1, 5: 2, 6: 3}.get(pic_type, 4))

    def __eq__(self, i):
        return Image.__eq__(self, i)

    def __unicode__(self):
        return u"%s (%d\u00D7%d,'%s')" % \
               (self.type_string(),
                self.width, self.height, self.mime_type)

    def build(self):
        """Returns a binary string of ID3v2.4 frame data."""

        try:
            self.description.encode('latin-1')
            text_encoding = 0
        except UnicodeEncodeError:
            text_encoding = 1

        return ID3v24Frame.build(self,
                                 self.FRAME_HEADER.build(
                Con.Container(text_encoding=text_encoding,
                              picture_type=self.pic_type,
                              mime_type=self.mime_type,
                              description=self.description)) + self.data)

    @classmethod
    def converted(cls, image):
        """Given an Image object, returns an ID3v24PicFrame object."""

        return cls(data=image.data,
                   mime_type=image.mime_type,
                   description=image.description,
                   pic_type={0: 3, 1: 4, 2: 5, 3: 6}.get(image.type, 0))


class ID3v24ComFrame(ID3v24TextFrame):
    """A container for ID3v2.4 comment (COMM) frames."""

    COMMENT_HEADER = Con.Struct(
        "com_frame",
        Con.Byte("encoding"),
        Con.String("language", 3),
        Con.Switch("short_description",
                   lambda ctx: ctx.encoding,
                   {0x00: Con.CString("s", encoding='latin-1'),
                    0x01: UTF16CString("s"),
                    0x02: UTF16BECString("s"),
                    0x03: Con.CString("s", encoding='utf-8')}))

    TEXT_TYPE = True

    def __init__(self, encoding, language, short_description, content):
        """Fields are as follows:

        encoding          - a text encoding integer 0-3
        language          - a 3 byte language field
        short_description - a unicode string
        contenxt          - a unicode string
        """

        self.encoding = encoding
        self.language = language
        self.short_description = short_description
        self.content = content
        self.id = 'COMM'

    def __eq__(self, o):
        return __attrib_equals__(["encoding", "language",
                                  "short_description", "content"], self, o)

    def __unicode__(self):
        return self.content

    def __int__(self):
        return 0

    @classmethod
    def from_unicode(cls, s):
        """Builds an ID3v24ComFrame from a unicode string."""

        for encoding in 0x00, 0x03, 0x01, 0x02:
            try:
                s.encode(cls.ENCODING[encoding])
                return cls(encoding, 'eng', u'', s)
            except UnicodeEncodeError:
                continue

    def build(self):
        """Returns a binary string of ID3v2.4 frame data."""

        return ID3v24Frame.build(
            self,
            self.COMMENT_HEADER.build(Con.Container(
                    encoding=self.encoding,
                    language=self.language,
                    short_description=self.short_description)) + \
                self.content.encode(self.ENCODING[self.encoding], 'replace'))


class ID3v24Comment(ID3v23Comment):
    """A complete ID3v2.4 comment."""

    Frame = ID3v24Frame
    TextFrame = ID3v24TextFrame
    PictureFrame = ID3v24PicFrame

    def __repr__(self):
        return "ID3v24Comment(%s)" % (repr(self.__dict__["frames"]))

    def __comment_name__(self):
        return u'ID3v2.4'

    def build(self):
        """Returns an ID3v2.4 comment as a binary string."""

        subframes = "".join(["".join([value.build() for value in values])
                             for values in self.frames.values()])

        return self.TAG_HEADER.build(
            Con.Container(file_id='ID3',
                          version_major=0x04,
                          version_minor=0x00,
                          unsync=False,
                          extended=False,
                          experimental=False,
                          footer=False,
                          length=len(subframes))) + subframes


ID3v2Comment = ID3v22Comment

from __id3v1__ import *


class ID3CommentPair(MetaData):
    """A pair of ID3v2/ID3v1 comments.

    These can be manipulated as a set."""

    def __init__(self, id3v2_comment, id3v1_comment):
        """id3v2 and id3v1 are ID3v2Comment and ID3v1Comment objects or None.

        Values in ID3v2 take precendence over ID3v1, if present."""

        self.__dict__['id3v2'] = id3v2_comment
        self.__dict__['id3v1'] = id3v1_comment

        if (self.id3v2 is not None):
            base_comment = self.id3v2
        elif (self.id3v1 is not None):
            base_comment = self.id3v1
        else:
            raise ValueError(_(u"ID3v2 and ID3v1 cannot both be blank"))

    def __getattr__(self, key):
        if (key in self.__INTEGER_FIELDS__):
            if ((self.id3v2 is not None) and
                (getattr(self.id3v2, key) != 0)):
                    return getattr(self.id3v2, key)
            if (self.id3v1 is not None):
                return getattr(self.id3v1, key)
            else:
                raise ValueError(_(u"ID3v2 and ID3v1 cannot both be blank"))
        elif (key in self.__FIELDS__):
            if ((self.id3v2 is not None) and
                (getattr(self.id3v2, key) != u'')):
                    return getattr(self.id3v2, key)
            if (self.id3v1 is not None):
                return getattr(self.id3v1, key)
            else:
                raise ValueError(_(u"ID3v2 and ID3v1 cannot both be blank"))
        else:
            raise AttributeError(key)

    def __setattr__(self, key, value):
        self.__dict__[key] = value

        if (self.id3v2 is not None):
            setattr(self.id3v2, key, value)
        if (self.id3v1 is not None):
            setattr(self.id3v1, key, value)

    def __delattr__(self, key):
        if (self.id3v2 is not None):
            delattr(self.id3v2, key)
        if (self.id3v1 is not None):
            delattr(self.id3v1, key)

    @classmethod
    def converted(cls, metadata,
                  id3v2_class=ID3v23Comment,
                  id3v1_class=ID3v1Comment):
        """Takes a MetaData object and returns an ID3CommentPair object."""

        if ((metadata is None) or (isinstance(metadata, ID3CommentPair))):
            return metadata

        if (isinstance(metadata, ID3v2Comment)):
            return ID3CommentPair(metadata,
                                  id3v1_class.converted(metadata))
        else:
            return ID3CommentPair(
                id3v2_class.converted(metadata),
                id3v1_class.converted(metadata))

    def merge(self, metadata):
        """Updates any currently empty entries from metadata's values."""

        self.id3v2.merge(metadata)
        self.id3v1.merge(metadata)

    def __unicode__(self):
        if ((self.id3v2 is not None) and (self.id3v1 is not None)):
            #both comments present
            return unicode(self.id3v2) + \
                   (os.linesep * 2) + \
                   unicode(self.id3v1)
        elif (self.id3v2 is not None):
            #only ID3v2
            return unicode(self.id3v2)
        elif (self.id3v1 is not None):
            #only ID3v1
            return unicode(self.id3v1)
        else:
            return u''

    #ImageMetaData passthroughs
    def images(self):
        """Returns a list of embedded Image objects."""

        if (self.id3v2 is not None):
            return self.id3v2.images()
        else:
            return []

    def add_image(self, image):
        """Embeds an Image object in this metadata."""

        if (self.id3v2 is not None):
            self.id3v2.add_image(image)

    def delete_image(self, image):
        """Deletes an Image object from this metadata."""

        if (self.id3v2 is not None):
            self.id3v2.delete_image(image)

    @classmethod
    def supports_images(cls):
        """Returns True."""

        return True
