#
# $Source: /home/blais/repos/cvsroot/arubomu/lib/python/arubomu/id3tags.py,v $
# $Id: id3tags.py,v 1.15 2005/02/07 23:10:40 blais Exp $
#

"""Python interface for ID3V2 tags in MP3 files.

This library uses the external id3info and id3tag programs that comes with
id3lib.

Note: this should get replaced by pyid3lib at some point (2004-03-02)

"""

__version__ = "$Revision: 1.15 $"
__author__ = "Martin Blais <blais@furius.ca>"


import sys, os
from os.path import getsize
import string, StringIO, re
import types
import commands

import pyid3lib



tagdocs = {

'AENC' : 'Audio encryption',
'APIC' : 'Attached picture',

'COMM' : 'Comments',
'COMR' : 'Commercial frame',

'ENCR' : 'Encryption method registration',
'EQUA' : 'Equalization',
'ETCO' : 'Event timing codes',

'GEOB' : 'General encapsulated object',
'GRID' : 'Group identification registration',

'IPLS' : 'Involved people list',

'LINK' : 'Linked information',

'MCDI' : 'Music CD identifier',
'MLLT' : 'MPEG location lookup table',

'OWNE' : 'Ownership frame',

'PRIV' : 'Private frame',
'PCNT' : 'Play counter',
'POPM' : 'Popularimeter',
'POSS' : 'Position synchronisation frame',

'RBUF' : 'Recommended buffer size',
'RVAD' : 'Relative volume adjustment',
'RVRB' : 'Reverb',

'SYLT' : 'Synchronized lyric/text',
'SYTC' : 'Synchronized tempo codes',

'TALB' : 'Album/Movie/Show title',
'TBPM' : 'BPM beats per minute)',
'TCOM' : 'Composer',
'TCON' : 'Content type',
'TCOP' : 'Copyright message',
'TDAT' : 'Date',
'TDLY' : 'Playlist delay',
'TENC' : 'Encoded by',
'TEXT' : 'Lyricist/Text writer',
'TFLT' : 'File type',
'TIME' : 'Time',
'TIT1' : 'Content group description',
'TIT2' : 'Title/songname/content description',
'TIT3' : 'Subtitle/Description refinement',
'TKEY' : 'Initial key',
'TLAN' : 'Languages)',
'TLEN' : 'Length',
'TMED' : 'Media type',
'TOAL' : 'Original album/movie/show title',
'TOFN' : 'Original filename',
'TOLY' : 'Original lyricists)/text writers)',
'TOPE' : 'Original artists)/performers)',
'TORY' : 'Original release year',
'TOWN' : 'File owner/licensee',
'TPE1' : 'Lead performers)/Soloists)',
'TPE2' : 'Band/orchestra/accompaniment',
'TPE3' : 'Conductor/performer refinement',
'TPE4' : 'Interpreted, remixed, or otherwise modified by',
'TPOS' : 'Part of a set',
'TPUB' : 'Publisher',
'TRCK' : 'Track number/Position in set',
'TRDA' : 'Recording dates',
'TRSN' : 'Internet radio station name',
'TRSO' : 'Internet radio station owner',
'TSIZ' : 'Size',
'TSRC' : 'ISRC international standard recording code)',
'TSSE' : 'Software/Hardware and settings used for encoding',
'TYER' : 'Year',
'TXXX' : 'User defined text information frame',

'UFID' : 'Unique file identifier',
'USER' : 'Terms of use',
'USLT' : 'Unsychronized lyric/text transcription',

'WCOM' : 'Commercial information',
'WCOP' : 'Copyright/Legal information',
'WOAF' : 'Official audio file webpage',
'WOAR' : 'Official artist/performer webpage',
'WOAS' : 'Official audio source webpage',
'WORS' : 'Official internet radio station homepage',
'WPAY' : 'Payment',
'WPUB' : 'Publishers official webpage',
'WXXX' : 'User defined URL link frame',

'TT22' : '(unknown field encountered in practice, content irrelevant)',
'NCON' : '(unknown field encountered in practice, content irrelevant)',

}

active_tags = {
'TALB' : 'album',
#'TIME' : 'duration', ... cannot be set.
'TIT2' : 'title',
'TPE1' : 'artist',
'TRCK' : 'track',
'TYER' : 'year',
'COMM' : 'comments',
}

int_tags = [ 'TRCK', 'TYER' ]


class ID3Tags:
    """Class that represents the set of tags."""

    def __init__(self, filename=None):
        for t in active_tags.values():
            setattr(self, t, None)

        self.tagmap = {}
        if filename:
            self.tagmap = gettags( filename, self )

    def __repr__(self):
        self.dump()

    def dump(self, pfx=''):
        s = StringIO.StringIO()
        for t in active_tags.values():
            a = getattr(self, t)
            if a:
                print >> s, pfx, '%s: "%s"' % (t, a)
        return s.getvalue()

    def compare(self, other):
        for t in active_tags.values():
            if getattr(self, t) != getattr(other, t):
                return 1
        return 0
    compare = staticmethod(compare)


vlre = re.compile('^=== ([A-Z0-9]*) \(.*\): (.*)$')

def gettags(fn, tagsobj=None):
    """
    Reads the mp3 id tags for a file and returns a map of them.
    This deals correctly with ' chars but depends on pyid3lib.
    """

    tagmap = {}
    tags = pyid3lib.tag(fn)
    for tt in tags:
        iid = tt['frameid']
        if 'text' in tt:
            content = tt['text'].decode('iso-8859-1')

            tagmap[iid] = content

            if tagsobj and (iid in active_tags):
                if iid in int_tags:
                    try:
                        setattr(tagsobj, active_tags[iid], int(content))
                    except ValueError:
                        pass
                else:
                    setattr(tagsobj, active_tags[iid], content)
 
    return tagmap


vlre = re.compile('^=== ([A-Z0-9]*) \(.*\): (.*)$')

def gettags_OLD(fn, tagsobj=None):
    """
    Reads the mp3 id tags for a file and returns a map of them.
    This depends only on id3info but does not deal correctly with ' characters.
    """

    tagmap = {}
    fn = fn.replace("'", "?") # cannot seem to be able to do better than this.
    fn = fn.replace("(", "\\(")
    fn = fn.replace(")", "\\)")
    # Note: the only way to do this right would be to fork() and exec()
    output = commands.getoutput('id3info "%s"' % fn)
    for line in output.splitlines():
        mo = vlre.match(line)
        if mo:
            ttag = mo.group(1)
            content = mo.group(2).decode('iso-8859-1') # to unicode from latin1
            tagmap[ttag] = content
            if not ttag in tagdocs:
                print >> sys.stderr, \
                      "Warning (id3tags): tag '%s' not in known tags." % ttag

            if tagsobj and (ttag in active_tags):
                if ttag in int_tags:
                    try:
                        setattr(tagsobj, active_tags[ttag], int(content))
                    except ValueError:
                        pass
                else:
                    setattr(tagsobj, active_tags[ttag], content)
    return tagmap


trans_set = {
'title' : 'song',
'comments' : 'comment',
}

def settags(fn, tagsobj, reset=0, encoding=None):
    """Sets the recognized tags that are set on the given object.
    If reset is true, nullify fields without content."""

    arglist = []
    for t in active_tags.values():
        content = getattr(tagsobj, t)
        if content or reset:
            if not content:
                content = ''
            if t in trans_set:
                t = trans_set[t]

            arglist.append("--%s" % t)
            ct = content
            if type(ct) not in [types.StringType, types.UnicodeType]:
                ct = str(ct)
            if encoding:
                ct = ct.encode(encoding)
            arglist.append(ct)

    if not os.access(fn, os.W_OK):
        raise IOError('cannot access file "%s" for tagging.' % fn)

    arglist = ['id3tag'] + arglist + [fn]
    stat = os.spawnvp(os.P_WAIT, arglist[0], arglist)

        



#
# FIXME: THIS DOESN'T WORK
#
# #-------------------------------------------------------------------------------
# #
# def gettagssize(fn):
#     "Tries to figure out the total size of the tags in the given file."
# 
#     # ID3v2/file identifier      "ID3"
#     # ID3v2 version              $04 00
#     # ID3v2 flags                %abcd0000
#     # ID3v2 size             4 * %0xxxxxxx
# 
#     import struct
#     def _from_synch_safe(synchsafe):
#         if isinstance(synchsafe, type(1)):
#             (b3, b2, b1, b0) = struct.unpack('!4b', struct.pack('!1i', synchsafe))
#         else:
#             while len(synchsafe) < 4:
#                 synchsafe = (0,) + synchsafe
#             (b3, b2, b1, b0) = synchsafe
#     
#         x = 128
#         return (((b3 * x + b2) * x + b1) * x + b0)
#     
#     size = -1
#     try:
#         f = open(fn, 'r')
#         b = f.read(3)
#         if b == 'TAG':
#             size = 128
#         elif b == 'ID3':
#             f.read(3)
#             size = _from_synch_safe(struct.unpack('!4b', f.read(4)))
#         else:
#             size = -1
#     except IOError, e:
#         raise IOError('error reading mp3 file "%s":' % fn + str(e))
#     
#     return size
# 
# #-------------------------------------------------------------------------------
# #
# def getrawsize(fn):
#     "Returns the size of the mp3 file without tags."
# 
#     return getsize(fn) - gettagssize(fn)
# 
#     
# #-------------------------------------------------------------------------------
# #
# def prafsz(fn):
# 
#     import curses.ascii
# 
#     ts = gettagssize(fn)
# 
#     f = open(fn, 'r')
#     f.seek(ts)
#     print map(ord, f.read(16))
    


def test():
    from pprint import pprint

    if 0:
        #pprint( gettags('/tmp/a.mp3') )
        i = ID3Tags('/tmp/a.mp3')
    
        i2 = ID3Tags()
        i2.title = 'A lo cubano'
        i2.artist = 'Orishas'
        i2.year = 1997
        settags('/tmp/a.mp3', i2, 1)
    
        print ID3Tags.compare(i, i2)

    if 0:
        print gettagssize('/tmp/a.mp3')
        print gettagssize('/tmp/b.mp3')

        print getrawsize('/tmp/a.mp3')
        print getrawsize('/tmp/b.mp3')

        print prafsz('/tmp/a.mp3')
        print prafsz('/tmp/b.mp3')

        # 79703
        f1 = open('/tmp/a.mp3', 'r')
        f1.seek(1000000)
        print map(ord, f1.read(16))
        f2 = open('/tmp/b.mp3', 'r')
        f2.seek(1000000 + 79703)
        print map(ord, f2.read(16))
        f2.seek(1000000 - 79703)
        print map(ord, f2.read(16))


# Run main if loaded as a script
if __name__ == "__main__":
    test()
