__id3__.py :  » Media-Sound-Audio » Python-Audio-Tools » audiotools-2.14 » audiotools » Python Open Source

Home
Python Open Source
1.3.1.2 Python
2.Ajax
3.Aspect Oriented
4.Blog
5.Build
6.Business Application
7.Chart Report
8.Content Management Systems
9.Cryptographic
10.Database
11.Development
12.Editor
13.Email
14.ERP
15.Game 2D 3D
16.GIS
17.GUI
18.IDE
19.Installer
20.IRC
21.Issue Tracker
22.Language Interface
23.Log
24.Math
25.Media Sound Audio
26.Mobile
27.Network
28.Parser
29.PDF
30.Project Management
31.RSS
32.Search
33.Security
34.Template Engines
35.Test
36.UML
37.USB Serial
38.Web Frameworks
39.Web Server
40.Web Services
41.Web Unit
42.Wiki
43.Windows
44.XML
Python Open Source » Media Sound Audio » Python Audio Tools 
Python Audio Tools » audiotools 2.14 » audiotools » __id3__.py
#!/usr/bin/python

#Audio Tools, a module and set of tools for manipulating audio data
#Copyright (C) 2007-2010  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
import codecs
import gettext

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

class UCS2Codec(codecs.Codec):
    @classmethod
    def fix_char(cls,c):
        if (ord(c) <= 0xFFFF):
            return c
        else:
            return u"\ufffd"

    def encode(self, input, errors='strict'):
        return codecs.utf_16_encode(u"".join(map(self.fix_char,input)),errors)

    def decode(self, input, errors='strict'):
        (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): pass

class Syncsafe32(Con.Adapter):
    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):
    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):
    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):
    return WidecharCStringAdapter(Con.RepeatUntil(lambda obj, ctx: obj == 0x0,
                                                  Con.UBInt16(name)),
                                  encoding='ucs2')


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



def UTF16BECString(name):
    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 __number_pair__(current,total):
    if (total == 0):
        return u"%d" % (current)
    else:
        return u"%d/%d" % (current,total)

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

class ID3v22Frame:
    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):
        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):
        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):
        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):
    ENCODING = {0x00:"latin-1",
                0x01:"ucs2"}

    TEXT_TYPE = True

    #encoding is an encoding byte
    #s is a unicode string
    def __init__(self,frame_id,encoding,s):
        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):
        try:
            return int(re.findall(r'\d+/(\d+)',self.string)[0])
        except IndexError:
            return 0

    @classmethod
    def from_unicode(cls,frame_id,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):
        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):
    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

    #encoding should be an integer
    #language should be a standard string
    #short_description and content should be unicode strings
    def __init__(self,encoding,language,short_description,content):
        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):
        for encoding in 0x00,0x01:
            try:
                s.encode(cls.ENCODING[encoding])
                return cls(encoding,'eng',u'',s)
            except UnicodeEncodeError:
                continue

    def build(self):
        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):
    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")}))

    #format and description are unicode strings
    #pic_type is an int
    #data is a string
    def __init__(self, data, format, description, pic_type):
        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):
        #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):
        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):
        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):
    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')

    #frames should be a list of ID3v22Frame-compatible objects
    def __init__(self,frames):
        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):
        image = self.PictureFrame.converted(image)
        self.frames.setdefault('PIC',[]).append(image)

    def delete_image(self, image):
        del(self.frames['PIC'][self['PIC'].index(image)])

    def images(self):
        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):
        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):
        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))

        return cls(frames)

    def merge(self, metadata):
        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):
        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

    #takes a file stream
    #checks that stream for an ID3v2 comment
    #if found, repositions the stream past it
    #if not, leaves the stream in the current location
    @classmethod
    def skip(cls, file):
        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

    #takes a filename
    #returns an ID3v2Comment-based object
    @classmethod
    def read_id3v2_comment(cls, filename):
        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):
    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):
        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):
        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):
    ENCODING = {0x00:"latin-1",
                0x01:"ucs2"}

    TEXT_TYPE = True

    #encoding is an encoding byte
    #s is a unicode string
    def __init__(self,frame_id,encoding,s):
        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):
        try:
            return int(re.findall(r'\d+/(\d+)',self.string)[0])
        except IndexError:
            return 0

    @classmethod
    def from_unicode(cls,frame_id,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):
        return ID3v23Frame.build(
            self,
            chr(self.encoding) + \
                self.string.encode(self.ENCODING[self.encoding],
                                   'replace'))

class ID3v23PicFrame(ID3v23Frame,Image):
    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):
        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):
        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):
        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):
    COMMENT_HEADER = ID3v22ComFrame.COMMENT_HEADER

    TEXT_TYPE = True

    def __init__(self,encoding,language,short_description,content):
        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):
        for encoding in 0x00,0x01:
            try:
                s.encode(cls.ENCODING[encoding])
                return cls(encoding,'eng',u'',s)
            except UnicodeEncodeError:
                continue

    def build(self):
        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):
    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')

    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):
        image = self.PictureFrame.converted(image)
        self.frames.setdefault('APIC',[]).append(image)

    def delete_image(self, image):
        del(self.frames['APIC'][self['APIC'].index(image)])

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

    def build(self):
        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):
    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):
        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):
        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):
    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):
        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):
        try:
            return int(re.findall(r'\d+/(\d+)',self.string)[0])
        except IndexError:
            return 0

    @classmethod
    def from_unicode(cls,frame_id,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):
        return ID3v24Frame.build(
            self,
            chr(self.encoding) + \
                self.string.encode(self.ENCODING[self.encoding],
                                   'replace'))


class ID3v24PicFrame(ID3v24Frame,Image):
    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):
        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):
        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):
        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):
    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):
        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):
        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):
        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):
    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):
        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):
    #id3v2 and id3v1 are ID3v2Comment and ID3v1Comment objects or None
    #values in ID3v2 take precendence over ID3v1, if present
    def __init__(self, id3v2_comment, id3v1_comment):
        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"))

        fields = dict([(field,getattr(base_comment,field))
                       for field in self.__FIELDS__])

        MetaData.__init__(self,**fields)

    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):
        if ((metadata is None) or (isinstance(metadata,ID3CommentPair))):
            return metadata

        if (isinstance(metadata,ID3v2Comment)):
            return ID3CommentPair(metadata,
                                  ID3v1Comment.converted(metadata))
        else:
            return ID3CommentPair(
                ID3v23Comment.converted(metadata),
                ID3v1Comment.converted(metadata))

    def merge(self, metadata):
        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):
        if (self.id3v2 is not None):
            return self.id3v2.images()
        else:
            return []

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

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

    @classmethod
    def supports_images(cls):
        return True
www.java2java.com | Contact Us
Copyright 2009 - 12 Demo Source and Support. All rights reserved.
All other trademarks are property of their respective owners.