Back to index

eyed3  0.6.18
tag.py
Go to the documentation of this file.
00001 ###############################################################################
00002 #  Copyright (C) 2002-2007  Travis Shirk <travis@pobox.com>
00003 #
00004 #  This program is free software; you can redistribute it and/or modify
00005 #  it under the terms of the GNU General Public License as published by
00006 #  the Free Software Foundation; either version 2 of the License, or
00007 #  (at your option) any later version.
00008 #
00009 #  This program is distributed in the hope that it will be useful,
00010 #  but WITHOUT ANY WARRANTY; without even the implied warranty of
00011 #  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
00012 #  GNU General Public License for more details.
00013 #
00014 #  You should have received a copy of the GNU General Public License
00015 #  along with this program; if not, write to the Free Software
00016 #  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
00017 #
00018 ################################################################################
00019 import re, os, string, stat, shutil, tempfile, binascii;
00020 from stat import *;
00021 from eyeD3 import *;
00022 import eyeD3.utils;
00023 import eyeD3.mp3;
00024 from frames import *;
00025 from binfuncs import *;
00026 import math;
00027 from xml.sax.saxutils import escape
00028 
00029 ID3_V1_COMMENT_DESC = "ID3 v1 Comment";
00030 
00031 ################################################################################
00032 class TagException(Exception):
00033    '''error reading tag'''
00034 
00035 ################################################################################
00036 class TagHeader:
00037    SIZE = 10;
00038 
00039    version = None;
00040    majorVersion = None;
00041    minorVersion = None;
00042    revVersion = None;
00043 
00044    # Flag bits
00045    unsync = 0;
00046    extended = 0;
00047    experimental = 0;
00048    # v2.4 addition
00049    footer = 0;
00050 
00051    # The size in the most recently parsed header.
00052    tagSize = 0;
00053 
00054    # Constructor
00055    def __init__(self):
00056       self.clear();
00057 
00058    def clear(self):
00059       self.setVersion(None);
00060       self.unsync = 0;
00061       self.extended = 0;
00062       self.experimental = 0;
00063       self.tagSize = 0;
00064 
00065    def setVersion(self, v):
00066       if v == None:
00067          self.version = None;
00068          self.majorVersion = None;
00069          self.minorVersion = None;
00070          self.revVersion = None;
00071          return;
00072 
00073       if v == ID3_CURRENT_VERSION:
00074          if self.majorVersion == None or self.minorVersion == None:
00075             v = ID3_DEFAULT_VERSION;
00076          else:
00077             return;
00078       elif v == ID3_ANY_VERSION:
00079          v = ID3_DEFAULT_VERSION;
00080 
00081       # Handle 3-element lists or tuples.
00082       if isinstance(v, tuple) or isinstance(v, list):
00083          self.version = eyeD3.utils.versionsToConstant(v);
00084          (self.majorVersion,
00085           self.minorVersion,
00086           self.revVersion) = v;
00087       # Handle int constants.
00088       elif isinstance(v, int):
00089          (self.majorVersion,
00090           self.minorVersion,
00091           self.revVersion) = eyeD3.utils.constantToVersions(v);
00092          self.version = v;
00093       else:
00094          raise TypeError("Wrong type: %s" % str(type(v)));
00095 
00096    # Given a file handle this method attempts to identify and then parse
00097    # a ID3 v2 header.  If successful, the parsed values are stored in
00098    # the instance variable.  If the files does not contain an ID3v2 tag
00099    # false is returned. A TagException is thrown if a tag is found, but is
00100    # not valid or corrupt.
00101    def parse(self, f):
00102       self.clear();
00103 
00104       # The first three bytes of a v2 header is "ID3".
00105       if f.read(3) != "ID3":
00106          return 0;
00107       TRACE_MSG("Located ID3 v2 tag");
00108 
00109       # The next 2 bytes are the minor and revision versions.
00110       version = f.read(2);
00111       major = 2;
00112       minor = ord(version[0]);
00113       rev = ord(version[1]);
00114       TRACE_MSG("TagHeader [major]: " + str(major));
00115       TRACE_MSG("TagHeader [minor]: " + str(minor));
00116       TRACE_MSG("TagHeader [revis]: " + str(rev));
00117       if not (major == 2 and (minor >= 2 and minor <= 4)):
00118          raise TagException("ID3 v" + str(major) + "." + str(minor) +\
00119                             " is not supported.");
00120       # Get all the version madness in sync.
00121       self.setVersion([major, minor, rev]);
00122 
00123       # The first 4 bits of the next byte are flags.
00124       (self.unsync,
00125        self.extended,
00126        self.experimental,
00127        self.footer) = bytes2bin(f.read(1))[0:4];
00128       TRACE_MSG("TagHeader [flags]: unsync(%d) extended(%d) "\
00129                 "experimental(%d) footer(%d)" % (self.unsync, self.extended,
00130                                                  self.experimental,
00131                                                  self.footer));
00132 
00133       # The size of the extended header (optional), frames, and padding
00134       # afer unsynchronization.  This is a sync safe integer, so only the
00135       # bottom 7 bits of each byte are used.
00136       tagSizeStr = f.read(4);
00137       TRACE_MSG("TagHeader [size string]: 0x%02x%02x%02x%02x" %\
00138                 (ord(tagSizeStr[0]), ord(tagSizeStr[1]),
00139                  ord(tagSizeStr[2]), ord(tagSizeStr[3])));
00140       self.tagSize = bin2dec(bytes2bin(tagSizeStr, 7));
00141       TRACE_MSG("TagHeader [size]: %d (0x%x)" % (self.tagSize, self.tagSize));
00142 
00143       return 1;
00144 
00145    def render(self, tagLen = None):
00146       if tagLen != None:
00147          self.tagSize = tagLen;
00148 
00149       data = "ID3";
00150       data += chr(self.minorVersion) + chr(self.revVersion);
00151       # not not the values so we only get 1's and 0's.
00152       data += bin2bytes([not not self.unsync,
00153                          not not self.extended,
00154                          not not self.experimental,
00155                          not not self.footer,
00156                          0, 0, 0, 0]);
00157       TRACE_MSG("Setting tag size to %d" % tagLen);
00158       szBytes = bin2bytes(bin2synchsafe(dec2bin(tagLen, 32)));
00159       data += szBytes;
00160       TRACE_MSG("TagHeader rendered %d bytes" % len(data));
00161       return data;
00162 
00163 ################################################################################
00164 class ExtendedTagHeader:
00165    size = 0;
00166    flags = 0;
00167    crc = 0;
00168    restrictions = 0;
00169 
00170    def isUpdate(self):
00171        return self.flags & 0x40;
00172    def hasCRC(self):
00173        return self.flags & 0x20;
00174    def hasRestrictions(self, minor_version = None):
00175        return self.flags & 0x10;
00176 
00177    def setSizeRestrictions(self, v):
00178       assert(v >= 0 and v <= 3);
00179       self.restrictions = (v << 6) | (self.restrictions & 0x3f);
00180    def getSizeRestrictions(self):
00181       return self.restrictions >> 6;
00182    def getSizeRestrictionsString(self):
00183       val = self.getSizeRestrictions();
00184       if val == 0x00:
00185          return "No more than 128 frames and 1 MB total tag size.";
00186       elif val == 0x01:
00187          return "No more than 64 frames and 128 KB total tag size.";
00188       elif val == 0x02:
00189          return "No more than 32 frames and 40 KB total tag size.";
00190       elif val == 0x03:
00191          return "No more than 32 frames and 4 KB total tag size.";
00192 
00193    def setTextEncodingRestrictions(self, v):
00194       assert(v == 0 or v == 1);
00195       self.restrictions ^= 0x20;
00196    def getTextEncodingRestrictions(self):
00197       return self.restrictions & 0x20;
00198    def getTextEncodingRestrictionsString(self):
00199       if self.getTextEncodingRestrictions():
00200          return "Strings are only encoded with ISO-8859-1 [ISO-8859-1] or "\
00201                 "UTF-8 [UTF-8].";
00202       else:
00203          return "None";
00204 
00205    def setTextFieldSizeRestrictions(self, v):
00206       assert(v >= 0 and v <= 3);
00207       self.restrictions = (v << 3) | (self.restrictions & 0xe7);
00208    def getTextFieldSizeRestrictions(self):
00209       return (self.restrictions >> 3) & 0x03;
00210    def getTextFieldSizeRestrictionsString(self):
00211       val = self.getTextFieldSizeRestrictions();
00212       if val == 0x00:
00213          return "None";
00214       elif val == 0x01:
00215          return "No string is longer than 1024 characters.";
00216       elif val == 0x02:
00217          return "No string is longer than 128 characters.";
00218       elif val == 0x03:
00219          return "No string is longer than 30 characters.";
00220 
00221    def setImageEncodingRestrictions(self, v):
00222       assert(v == 0 or v == 1);
00223       self.restrictions ^= 0x04;
00224    def getImageEncodingRestrictions(self):
00225       return self.restrictions & 0x04;
00226    def getImageEncodingRestrictionsString(self):
00227       if self.getImageEncodingRestrictions():
00228          return "Images are encoded only with PNG [PNG] or JPEG [JFIF].";
00229       else:
00230          return "None";
00231 
00232    def setImageSizeRestrictions(self, v):
00233       assert(v >= 0 and v <= 3);
00234       self.restrictions = v | (self.restrictions & 0xfc);
00235    def getImageSizeRestrictions(self):
00236       return self.restrictions & 0x03;
00237    def getImageSizeRestrictionsString(self):
00238       val = self.getImageSizeRestrictions();
00239       if val == 0x00:
00240          return "None";
00241       elif val == 0x01:
00242          return "All images are 256x256 pixels or smaller.";
00243       elif val == 0x02:
00244          return "All images are 64x64 pixels or smaller.";
00245       elif val == 0x03:
00246          return "All images are exactly 64x64 pixels, unless required "\
00247                 "otherwise.";
00248 
00249    def _syncsafeCRC(self):
00250        bites = ""
00251        bites += chr((self.crc >> 28) & 0x7f);
00252        bites += chr((self.crc >> 21) & 0x7f);
00253        bites += chr((self.crc >> 14) & 0x7f);
00254        bites += chr((self.crc >>  7) & 0x7f);
00255        bites += chr((self.crc >>  0) & 0x7f);
00256        return bites;
00257 
00258 
00259    def render(self, header, frameData, padding=0):
00260       assert(header.majorVersion == 2);
00261 
00262       data = "";
00263       crc = None;
00264       if header.minorVersion == 4:
00265          # Version 2.4
00266          size = 6;
00267          # Extended flags.
00268          if self.isUpdate():
00269             data += "\x00";
00270          if self.hasCRC():
00271             data += "\x05";
00272             # XXX: Using the absolute value of the CRC.  The spec is unclear
00273             # about the type of this data.
00274             self.crc = int(math.fabs(binascii.crc32(frameData +\
00275                                                     ("\x00" * padding))));
00276             crc_data = self._syncsafeCRC();
00277             if len(crc_data) < 5:
00278                 crc_data = ("\x00" * (5 - len(crc_data))) + crc_data
00279             assert(len(crc_data) == 5)
00280             data += crc_data
00281          if self.hasRestrictions():
00282             data += "\x01";
00283             assert(len(self.restrictions) == 1);
00284             data += self.restrictions;
00285          TRACE_MSG("Rendered extended header data (%d bytes)" % len(data));
00286 
00287          # Extended header size.
00288          size = bin2bytes(bin2synchsafe(dec2bin(len(data) + 6, 32)))
00289          assert(len(size) == 4);
00290 
00291          data = size + "\x01" + bin2bytes(dec2bin(self.flags)) + data;
00292          TRACE_MSG("Rendered extended header of size %d" % len(data));
00293       else:
00294          # Version 2.3
00295          size = 6;  # Note, the 4 size bytes are not included in the size
00296          # Extended flags.
00297          f = [0] * 16;
00298          if self.hasCRC():
00299             f[0] = 1;
00300             # XXX: Using the absolute value of the CRC.  The spec is unclear
00301             # about the type of this value.
00302             self.crc = int(math.fabs(binascii.crc32(frameData +\
00303                                                     ("\x00" * padding))));
00304             crc = bin2bytes(dec2bin(self.crc));
00305             assert(len(crc) == 4);
00306             size += 4;
00307          flags = bin2bytes(f);
00308          assert(len(flags) == 2);
00309          # Extended header size.
00310          size = bin2bytes(dec2bin(size, 32))
00311          assert(len(size) == 4);
00312          # Padding size
00313          paddingSize = bin2bytes(dec2bin(padding, 32));
00314 
00315          data = size + flags + paddingSize;
00316          if crc:
00317             data += crc;
00318       return data;
00319 
00320    # Only call this when you *know* there is an extened header.
00321    def parse(self, fp, header):
00322       assert(header.majorVersion == 2);
00323 
00324       TRACE_MSG("Parsing extended header @ 0x%x" % fp.tell());
00325       # First 4 bytes is the size of the extended header.
00326       data = fp.read(4);
00327       if header.minorVersion == 4:
00328          # sync-safe
00329          sz = bin2dec(bytes2bin(data, 7));
00330          self.size = sz
00331          TRACE_MSG("Extended header size (includes the 4 size bytes): %d" % sz);
00332          data = fp.read(sz - 4);
00333 
00334          if ord(data[0]) != 1 or (ord(data[1]) & 0x8f):
00335             # As of 2.4 the first byte is 1 and the second can only have
00336             # bits 6, 5, and 4 set.
00337             raise TagException("Invalid Extended Header");
00338 
00339          offset = 2;
00340          self.flags = ord(data[1]);
00341          TRACE_MSG("Extended header flags: %x" % self.flags);
00342 
00343          if self.isUpdate():
00344             TRACE_MSG("Extended header has update bit set");
00345             assert(ord(data[offset]) == 0);
00346             offset += 1;
00347          if self.hasCRC():
00348             TRACE_MSG("Extended header has CRC bit set");
00349             assert(ord(data[offset]) == 5);
00350             offset += 1;
00351             crcData = data[offset:offset + 5];
00352             # This is sync-safe.
00353             self.crc = bin2dec(bytes2bin(crcData, 7));
00354             TRACE_MSG("Extended header CRC: %d" % self.crc);
00355             offset += 5;
00356          if self.hasRestrictions():
00357             TRACE_MSG("Extended header has restrictions bit set");
00358             assert(ord(data[offset]) == 5);
00359             offset += 1;
00360             self.restrictions = ord(data[offset]);
00361             offset += 1;
00362       else:
00363          # v2.3 is totally different... *sigh*
00364          sz = bin2dec(bytes2bin(data));
00365          TRACE_MSG("Extended header size (not including 4 size bytes): %d" % sz)
00366          self.size = sz + 4  # +4 to include size bytes
00367          tmpFlags = fp.read(2);
00368          # Read the padding size, but it'll be computed during the parse.
00369          ps = fp.read(4);
00370          TRACE_MSG("Extended header says there is %d bytes of padding" %
00371                    bin2dec(bytes2bin(ps)));
00372          # Make this look like a v2.4 mask.
00373          self.flags = ord(tmpFlags[0]) >> 2;
00374          if self.hasCRC():
00375             TRACE_MSG("Extended header has CRC bit set");
00376             crcData = fp.read(4);
00377             self.crc = bin2dec(bytes2bin(crcData));
00378             TRACE_MSG("Extended header CRC: %d" % self.crc);
00379 
00380 
00381 ################################################################################
00382 # ID3 tag class.  The class is capable of reading v1 and v2 tags.  ID3 v1.x
00383 # are converted to v2 frames.
00384 class Tag:
00385    # Latin1 is the default (0x00)
00386    encoding = DEFAULT_ENCODING;
00387 
00388    # ID3v1 tags do not contain a header.  The only ID3v1 values stored
00389    # in this header are the major/minor version.
00390    header = TagHeader();
00391 
00392    # Optional in v2 tags.
00393    extendedHeader = ExtendedTagHeader();
00394 
00395    # Contains the tag's frames.  ID3v1 fields are read and converted
00396    # the the corresponding v2 frame.  
00397    frames = None;
00398 
00399    # Used internally for iterating over frames.
00400    iterIndex = None;
00401 
00402    # If this value is None the tag is not linked to any particular file..
00403    linkedFile = None;
00404 
00405    # add TDTG (or TXXX) - Tagging Time - when saved
00406    do_tdtg = True
00407 
00408    # Constructor.  An empty tag is created and the link method is used
00409    # to read an mp3 file's v1.x or v2.x tag.  You can optionally set a 
00410    # file name, but it will not be read, but may be written to.
00411    def __init__(self, fileName = None):
00412       if fileName:
00413          self.linkedFile = LinkedFile(fileName);
00414       self.clear();
00415 
00416    def clear(self):
00417       self.header = TagHeader();
00418       self.frames = FrameSet(self.header);
00419       self.iterIndex = None;
00420 
00421    # Returns an read-only iterator for all frames.
00422    def __iter__(self):
00423       if len(self.frames):
00424          self.iterIndex = 0;
00425       else:
00426          self.iterIndex = None;
00427       return self;
00428 
00429    def next(self):
00430       if self.iterIndex == None or self.iterIndex == len(self.frames):
00431          raise StopIteration;
00432       frm = self.frames[self.iterIndex];
00433       self.iterIndex += 1;
00434       return frm;
00435 
00436    # Returns true when an ID3 tag is read from f which may be a file name
00437    # or an aleady opened file object.  In the latter case, the file object 
00438    # is not closed when this method returns.
00439    #
00440    # By default, both ID3 v2 and v1 tags are parsed in that order.
00441    # If a v2 tag is found then a v1 parse is not performed.  This behavior
00442    # can be refined by passing ID3_V1 or ID3_V2 as the second argument 
00443    # instead of the default ID3_ANY_VERSION.
00444    #
00445    # Converts all ID3v1 data into ID3v2 frames internally.
00446    # May throw IOError, or TagException if parsing fails.
00447    def link(self, f, v = ID3_ANY_VERSION):
00448       self.linkedFile = None;
00449       self.clear();
00450 
00451       fileName = "";
00452       if isinstance(f, file):
00453          fileName = f.name;
00454       elif isinstance(f, str) or isinstance(f, unicode):
00455          fileName = f;
00456       else:
00457          raise TagException("Invalid type passed to Tag.link: " +
00458                             str(type(f)));
00459 
00460       if v != ID3_V1 and v != ID3_V2 and v != ID3_ANY_VERSION:
00461          raise TagException("Invalid version: " + hex(v));
00462 
00463       tagFound = 0;
00464       padding = 0;
00465       TRACE_MSG("Linking File: " + fileName);
00466       if v == ID3_V1:
00467          if self.__loadV1Tag(f):
00468             tagFound = 1;
00469       elif v == ID3_V2:
00470          padding = self.__loadV2Tag(f);
00471          if padding >= 0:
00472             tagFound = 1;
00473       elif v == ID3_ANY_VERSION:
00474          padding = self.__loadV2Tag(f);
00475          if padding >= 0:
00476             tagFound = 1;
00477          else:
00478             padding = 0;
00479             if self.__loadV1Tag(f):
00480                tagFound = 1;
00481 
00482       self.linkedFile = LinkedFile(fileName);
00483       if tagFound:
00484          # In the case of a v1.x tag this is zero.
00485          self.linkedFile.tagSize = self.header.tagSize;
00486          self.linkedFile.tagPadding = padding;
00487       else:
00488          self.linkedFile.tagSize = 0;
00489          self.linkedFile.tagPadding = 0;
00490       return tagFound;
00491 
00492    # Write the current tag state to the linked file.
00493    # The version of the ID3 file format that should be written can
00494    # be passed as an argument; the default is ID3_CURRENT_VERSION.
00495    def update(self, version = ID3_CURRENT_VERSION, backup = 0):
00496       if not self.linkedFile:
00497          raise TagException("The Tag is not linked to a file.");
00498 
00499       if backup:
00500          shutil.copyfile(self.linkedFile.name, self.linkedFile.name + ".orig");
00501 
00502       self.setVersion(version);
00503       version = self.getVersion();
00504       if version == ID3_V2_2:
00505           raise TagException("Unable to write ID3 v2.2");
00506       # If v1.0 is being requested explicitly then so be it, if not and there is
00507       # a track number then bumping to v1.1 is /probably/ best.
00508       if self.header.majorVersion == 1 and self.header.minorVersion == 0 and\
00509          self.getTrackNum()[0] != None and version != ID3_V1_0:
00510          version = ID3_V1_1;
00511          self.setVersion(version);
00512 
00513       # If there are no frames then simply remove the current tag.
00514       if len(self.frames) == 0:
00515          self.remove(version);
00516          self.header = TagHeader();
00517          self.frames.setTagHeader(self.header);
00518          self.linkedFile.tagPadding = 0;
00519          self.linkedFile.tagSize = 0;
00520          return;
00521 
00522       if version & ID3_V1:
00523          self.__saveV1Tag(version);
00524          return 1;
00525       elif version & ID3_V2:
00526          self.__saveV2Tag(version);
00527          return 1;
00528       else:
00529          raise TagException("Invalid version: %s" % hex(version));
00530       return 0;
00531 
00532    # Remove the tag.  The version argument can selectively remove specific
00533    # ID3 tag versions; the default is ID3_CURRENT_VERSION meaning the version
00534    # of the current tag.  A value of ID3_ANY_VERSION causes all tags to be
00535    # removed.
00536    def remove(self, version = ID3_CURRENT_VERSION):
00537       if not self.linkedFile:
00538          raise TagException("The Tag is not linked to a file; nothing to "\
00539                             "remove.");
00540 
00541       if version == ID3_CURRENT_VERSION:
00542          version = self.getVersion();
00543 
00544       retval = 0;
00545       if version & ID3_V1 or version == ID3_ANY_VERSION:
00546          tagFile = file(self.linkedFile.name, "r+b");
00547          tagFile.seek(-128, 2);
00548          if tagFile.read(3) == "TAG":
00549             TRACE_MSG("Removing ID3 v1.x Tag");
00550             tagFile.seek(-3, 1);
00551             tagFile.truncate();
00552             retval |= 1;
00553          tagFile.close();
00554 
00555       if ((version & ID3_V2) or (version == ID3_ANY_VERSION)) and\
00556           self.header.tagSize:
00557          tagFile = file(self.linkedFile.name, "r+b");
00558          if tagFile.read(3) == "ID3":
00559             TRACE_MSG("Removing ID3 v2.x Tag");
00560             tagSize = self.header.tagSize + self.header.SIZE;
00561             tagFile.seek(tagSize);
00562 
00563             # Open tmp file
00564             tmpName = tempfile.mktemp();
00565             tmpFile = file(tmpName, "w+b");
00566 
00567             # Write audio data in chunks
00568             self.__copyRemaining(tagFile, tmpFile);
00569             tagFile.truncate();
00570             tagFile.close();
00571 
00572             tmpFile.close();
00573 
00574             # Move tmp to orig.
00575             shutil.copyfile(tmpName, self.linkedFile.name);
00576             os.unlink(tmpName);
00577 
00578             retval |= 1;
00579 
00580       return retval;
00581 
00582    # Get artist.  There are a few frames that can contain this information,
00583    # and they are subtley different. 
00584    #   eyeD3.frames.ARTIST_FID - Lead performer(s)/Soloist(s)
00585    #   eyeD3.frames.BAND_FID - Band/orchestra/accompaniment
00586    #   eyeD3.frames.CONDUCTOR_FID - Conductor/performer refinement
00587    #   eyeD3.frames.REMIXER_FID - Interpreted, remixed, or otherwise modified by
00588    #
00589    # Any of these values can be passed as an argument to select the artist
00590    # of interest.  By default, the first one found (searched in the above order)
00591    # is the value returned.  Most tags only have the ARTIST_FID, btw.
00592    # 
00593    # When no artist is found, an empty string is returned.
00594    # 
00595    def getArtist(self, artistID = ARTIST_FIDS):
00596       if isinstance(artistID, list):
00597          frameIDs = artistID;
00598       else:
00599          frameIDs = [artistID];
00600 
00601       for fid in frameIDs:
00602          f = self.frames[fid];
00603          if f:
00604              return f[0].text;
00605       return u"";
00606 
00607    def getAlbum(self):
00608       f = self.frames[ALBUM_FID];
00609       if f:
00610          return f[0].text;
00611       else:
00612          return u"";
00613 
00614    # Get the track title.  By default the main title is returned.  Optionally,
00615    # you can pass:
00616    #   eyeD3.frames.TITLE_FID - The title; the default.
00617    #   eyeD3.frames.SUBTITLE_FID - The subtitle.
00618    #   eyeD3.frames.CONTENT_TITLE_FID - Conten group description???? Rare.
00619    # An empty string is returned when no title exists.
00620    def getTitle(self, titleID = TITLE_FID):
00621       f = self.frames[titleID];
00622       if f:
00623          return f[0].text;
00624       else:
00625          return u"";
00626 
00627    def getDate(self, fid = None):
00628        if not fid:
00629            for fid in DATE_FIDS:
00630                if self.frames[fid]:
00631                    return self.frames[fid];
00632            return None;
00633        return self.frames[fid];
00634 
00635    def getYear(self, fid = None):
00636        dateFrame = self.getDate(fid);
00637        if dateFrame:
00638            return dateFrame[0].getYear();
00639        else:
00640            return None;
00641 
00642    # Throws GenreException when the tag contains an unrecognized genre format.
00643    # Note this method returns a eyeD3.Genre object, not a raw string.
00644    def getGenre(self):
00645       f = self.frames[GENRE_FID];
00646       if f and f[0].text:
00647          g = Genre();
00648          g.parse(f[0].text);
00649          return g;
00650       else:
00651          return None;
00652 
00653    def _getNum(self, fid):
00654       tn = None
00655       tt = None
00656       f = self.frames[fid];
00657       if f:
00658          n = f[0].text.split('/')
00659          if len(n) == 1:
00660             tn = self.toInt(n[0])
00661          elif len(n) == 2:
00662             tn = self.toInt(n[0])
00663             tt = self.toInt(n[1])
00664       return (tn, tt)
00665 
00666    # Returns a tuple with the first value containing the track number and the
00667    # second the total number of tracks.  One or both of these values may be
00668    # None depending on what is available in the tag. 
00669    def getTrackNum(self):
00670       return self._getNum(TRACKNUM_FID)
00671 
00672    # Like TrackNum, except for DiscNum--that is, position in a set. Most
00673    # tags won't have this or it will be 1/1.
00674    def getDiscNum(self):
00675       return self._getNum(DISCNUM_FID)
00676 
00677    # Since multiple comment frames are allowed this returns a list with 0
00678    # or more elements.  The elements are not the comment strings, they are
00679    # eyeD3.frames.CommentFrame objects.
00680    def getComments(self):
00681       return self.frames[COMMENT_FID];
00682 
00683    # Since multiple lyrics frames are allowed this returns a list with 0
00684    # or more elements.  The elements are not the lyrics strings, they are
00685    # eyeD3.frames.LyricsFrame objects.
00686    def getLyrics(self):
00687       return self.frames[LYRICS_FID];
00688 
00689    # Returns a list (possibly empty) of eyeD3.frames.ImageFrame objects.
00690    def getImages(self):
00691       return self.frames[IMAGE_FID];
00692 
00693    # Returns a list (possibly empty) of eyeD3.frames.ObjectFrame objects.
00694    def getObjects(self):
00695       return self.frames[OBJECT_FID];
00696 
00697    # Returns a list (possibly empty) of eyeD3.frames.URLFrame objects.
00698    # Both URLFrame and UserURLFrame objects are returned.  UserURLFrames
00699    # add a description and encoding, and have a different frame ID.
00700    def getURLs(self):
00701        urls = []
00702        for fid in URL_FIDS:
00703            urls.extend(self.frames[fid])
00704        urls.extend(self.frames[USERURL_FID])
00705        return urls
00706 
00707    def getUserTextFrames(self):
00708       return self.frames[USERTEXT_FID];
00709 
00710    def getCDID(self):
00711       return self.frames[CDID_FID];
00712 
00713    def getVersion(self):
00714       return self.header.version;
00715 
00716    def getVersionStr(self):
00717       return versionToString(self.header.version);
00718 
00719    def strToUnicode(self, s):
00720        t = type(s);
00721        if t != unicode and t == str:
00722            s = unicode(s, eyeD3.LOCAL_ENCODING);
00723        elif t != unicode and t != str:
00724            raise TagException("Wrong type passed to strToUnicode: %s" % str(t));
00725        return s;
00726 
00727    # Set the artist name.  Arguments equal to None or "" cause the frame to
00728    # be removed. An optional second argument can be passed to select the
00729    # actual artist frame that should be set.  By default, the main artist frame
00730    # (TPE1) is the value used.
00731    def setArtist(self, a, id = ARTIST_FID):
00732        self.setTextFrame(id, self.strToUnicode(a));
00733 
00734    def setAlbum(self, a):
00735        self.setTextFrame(ALBUM_FID, self.strToUnicode(a));
00736 
00737    def setTitle(self, t, titleID = TITLE_FID):
00738        self.setTextFrame(titleID, self.strToUnicode(t));
00739 
00740    def setDate(self, year, month = None, dayOfMonth = None,
00741                hour = None, minute = None, second = None, fid = None):
00742       if not year and not fid:
00743           dateFrames = self.getDate();
00744           if dateFrames:
00745               self.frames.removeFramesByID(dateFrames[0].header.id)
00746           return
00747       elif not year:
00748           self.frames.removeFramesByID(fid)
00749       else:
00750           self.frames.removeFramesByID(frames.OBSOLETE_YEAR_FID)
00751 
00752       dateStr = self.strToUnicode(str(year));
00753       if len(dateStr) != 4:
00754          raise TagException("Invalid Year field: " + dateStr);
00755       if month:
00756          dateStr += "-" + self.__padDateField(month);
00757          if dayOfMonth:
00758             dateStr += "-" + self.__padDateField(dayOfMonth);
00759             if hour:
00760                dateStr += "T" + self.__padDateField(hour);
00761                if minute:
00762                   dateStr += ":" + self.__padDateField(minute);
00763                   if second:
00764                      dateStr += ":" + self.__padDateField(second);
00765 
00766       if not fid:
00767           fid = "TDRL";
00768       dateFrame = self.frames[fid];
00769       try:
00770          if dateFrame:
00771             dateFrame[0].setDate(self.encoding + dateStr);
00772          else:
00773             header = FrameHeader(self.header);
00774             header.id = fid;
00775             dateFrame = DateFrame(header, encoding = self.encoding,
00776                                   date_str = self.strToUnicode(dateStr));
00777             self.frames.addFrame(dateFrame);
00778       except FrameException, ex:
00779          raise TagException(str(ex));
00780 
00781    # Three types are accepted for the genre parameter.  A Genre object, an
00782    # acceptable (see Genre.parse) genre string, or an integer genre id.
00783    # Arguments equal to None or "" cause the frame to be removed.
00784    def setGenre(self, g):
00785       if g == None or g == "":
00786           self.frames.removeFramesByID(GENRE_FID);
00787           return;
00788 
00789       if isinstance(g, Genre):
00790           self.frames.setTextFrame(GENRE_FID, self.strToUnicode(str(g)),
00791                                    self.encoding);
00792       elif isinstance(g, str):
00793           gObj = Genre();
00794           gObj.parse(g);
00795           self.frames.setTextFrame(GENRE_FID, self.strToUnicode(str(gObj)),
00796                                    self.encoding);
00797       elif isinstance(g, int):
00798           gObj = Genre();
00799           gObj.id = g;
00800           self.frames.setTextFrame(GENRE_FID, self.strToUnicode(str(gObj)),
00801                                    self.encoding);
00802       else:
00803           raise TagException("Invalid type passed to setGenre: %s" +
00804                              str(type(g)));
00805 
00806    # Accepts a tuple with the first value containing the track number and the
00807    # second the total number of tracks.  One or both of these values may be
00808    # None.  If both values are None, the frame is removed.
00809    def setTrackNum(self, n, zeropad = True):
00810       self.setNum(TRACKNUM_FID, n, zeropad)
00811 
00812    def setDiscNum(self, n, zeropad = True):
00813       self.setNum(DISCNUM_FID, n, zeropad)
00814 
00815    def setNum(self, fid, n, zeropad = True):
00816       if n[0] == None and n[1] == None:
00817          self.frames.removeFramesByID(fid);
00818          return;
00819 
00820       totalStr = "";
00821       if n[1] != None:
00822          if zeropad and n[1] >= 0 and n[1] <= 9:
00823             totalStr = "0" + str(n[1]);
00824          else:
00825             totalStr = str(n[1]);
00826 
00827       t = n[0];
00828       if t == None:
00829          t = 0;
00830 
00831       trackStr = str(t);
00832 
00833       # Pad with zeros according to how large the total count is.
00834       if zeropad:
00835          if len(trackStr) == 1:
00836             trackStr = "0" + trackStr;
00837          if len(trackStr) < len(totalStr):
00838             trackStr = ("0" * (len(totalStr) - len(trackStr))) + trackStr;
00839 
00840       s = "";
00841       if trackStr and totalStr:
00842          s = trackStr + "/" + totalStr;
00843       elif trackStr and not totalStr:
00844          s = trackStr;
00845 
00846       self.frames.setTextFrame(fid, self.strToUnicode(s),
00847                                self.encoding);
00848 
00849 
00850    # Add a comment.  This adds a comment unless one is already present with
00851    # the same language and description in which case the current value is
00852    # either changed (cmt != "") or removed (cmt equals "" or None).
00853    def addComment(self, cmt, desc = u"", lang = DEFAULT_LANG):
00854       if not cmt:
00855          # A little more then a call to removeFramesByID is involved since we
00856          # need to look at more than the frame ID.
00857          comments = self.frames[COMMENT_FID];
00858          for c in comments:
00859             if c.lang == lang and c.description == desc:
00860                self.frames.remove(c);
00861                break;
00862       else:
00863          self.frames.setCommentFrame(self.strToUnicode(cmt),
00864                                      self.strToUnicode(desc),
00865                                      lang, self.encoding);
00866 
00867    # Add lyrics.  Semantics similar to addComment
00868    def addLyrics(self, lyr, desc = u"", lang = DEFAULT_LANG):
00869       if not lyr:
00870          # A little more than a call to removeFramesByID is involved since we
00871          # need to look at more than the frame ID.
00872          lyrics = self.frames[LYRICS_FID];
00873          for l in lyrics:
00874             if l.lang == lang and l.description == desc:
00875                self.frames.remove(l);
00876                break;
00877       else:
00878          self.frames.setLyricsFrame(self.strToUnicode(lyr),
00879                                      self.strToUnicode(desc),
00880                                      lang, self.encoding);
00881 
00882    # Semantics similar to addComment
00883    def addUserTextFrame(self, desc, text):
00884       if not text:
00885          u_frames = self.frames[USERTEXT_FID]
00886          for u in u_frames:
00887             if u.description == desc:
00888                self.frames.remove(u);
00889                break
00890       else:
00891          self.frames.setUserTextFrame(self.strToUnicode(text),
00892                                       self.strToUnicode(desc), self.encoding);
00893 
00894    def addUserURLFrame(self, desc, url):
00895       if not url:
00896          u_frames = self.frames[USERURL_FID]
00897          for u in u_frames:
00898             if u.description == desc:
00899                self.frames.remove(u)
00900                break
00901       else:
00902          self.frames.setUserURLFrame(str(url), self.strToUnicode(desc),
00903                                      self.encoding)
00904 
00905    def removeUserTextFrame(self, desc):
00906       self.addUserTextFrame(desc, "")
00907 
00908    def removeUserURLFrame(self, desc):
00909       self.addUserURLFrame(desc, "")
00910 
00911    def removeComments(self):
00912        return self.frames.removeFramesByID(COMMENT_FID);
00913 
00914    def removeLyrics(self):
00915        return self.frames.removeFramesByID(LYRICS_FID);
00916 
00917    def removeImages(self):
00918        return self.frames.removeFramesByID(IMAGE_FID)
00919 
00920    def addImage(self, type, image_file_path, desc = u""):
00921        if image_file_path:
00922            image_frame = ImageFrame.create(type, image_file_path, desc);
00923            self.frames.addFrame(image_frame);
00924        else:
00925            image_frames = self.frames[IMAGE_FID];
00926            for i in image_frames:
00927                if i.pictureType == type:
00928                    self.frames.remove(i);
00929                    break;
00930 
00931    def addObject(self, object_file_path, mime = "", desc = u"",
00932                  filename = None ):
00933        object_frames = self.frames[OBJECT_FID];
00934        for i in object_frames:
00935            if i.description == desc:
00936                self.frames.remove(i);
00937        if object_file_path:
00938            object_frame = ObjectFrame.create(object_file_path, mime, desc,
00939                                              filename);
00940            self.frames.addFrame(object_frame);
00941 
00942    def getPlayCount(self):
00943        if self.frames[PLAYCOUNT_FID]:
00944            pc = self.frames[PLAYCOUNT_FID][0];
00945            assert(isinstance(pc, PlayCountFrame));
00946            return pc.count;
00947        else:
00948            return None;
00949 
00950    def setPlayCount(self, count):
00951        assert(count >= 0);
00952        if self.frames[PLAYCOUNT_FID]:
00953            pc = self.frames[PLAYCOUNT_FID][0];
00954            assert(isinstance(pc, PlayCountFrame));
00955            pc.count = count;
00956        else:
00957            frameHeader = FrameHeader(self.header);
00958            frameHeader.id = PLAYCOUNT_FID;
00959            pc = PlayCountFrame(frameHeader, count = count);
00960            self.frames.addFrame(pc);
00961 
00962    def incrementPlayCount(self, n = 1):
00963        pc = self.getPlayCount();
00964        if pc != None:
00965            self.setPlayCount(pc + n);
00966        else:
00967            self.setPlayCount(n);
00968 
00969    def getUniqueFileIDs(self):
00970        return self.frames[UNIQUE_FILE_ID_FID];
00971 
00972    def addUniqueFileID(self, owner_id, id):
00973       if not id:
00974          ufids = self.frames[UNIQUE_FILE_ID_FID];
00975          for ufid in ufids:
00976              if ufid.owner_id == owner_id:
00977                  self.frames.remove(ufid);
00978                  break;
00979       else:
00980          self.frames.setUniqueFileIDFrame(owner_id, id);
00981 
00982    def getBPM(self):
00983       bpm = self.frames[BPM_FID];
00984       if bpm:
00985           try:
00986               bpm = float(bpm[0].text)
00987           except ValueError:
00988               # Invalid bpm value, in the spirit of not crashing...
00989               bpm = 0.0
00990           finally:
00991               # Round floats since the spec says this is an integer
00992               return int(round(bpm))
00993       else:
00994           return None;
00995 
00996    def setBPM(self, bpm):
00997        self.setTextFrame(BPM_FID, self.strToUnicode(str(bpm)))
00998 
00999    def getPublisher(self):
01000       pub = self.frames[PUBLISHER_FID];
01001       if pub:
01002           return pub[0].text or None;
01003 
01004    def setPublisher(self, p):
01005        self.setTextFrame(PUBLISHER_FID, self.strToUnicode(str(p)));
01006 
01007    # Test ID3 major version.
01008    def isV1(self):
01009       return self.header.majorVersion == 1;
01010    def isV2(self):
01011       return self.header.majorVersion == 2;
01012 
01013    def setVersion(self, v):
01014       if v == ID3_V1:
01015          v = ID3_V1_1;
01016       elif v == ID3_V2:
01017          v = ID3_DEFAULT_VERSION;
01018 
01019       if v != ID3_CURRENT_VERSION:
01020          self.header.setVersion(v);
01021          self.frames.setTagHeader(self.header);
01022 
01023    def setTextFrame(self, fid, txt):
01024        if not txt:
01025           self.frames.removeFramesByID(fid);
01026        else:
01027           self.frames.setTextFrame(fid, self.strToUnicode(txt), self.encoding);
01028 
01029    def setURLFrame(self, fid, url):
01030        if not url:
01031           self.frames.removeFramesByID(fid)
01032        else:
01033           self.frames.setURLFrame(fid, url)
01034 
01035    def setTextEncoding(self, enc):
01036        if enc != LATIN1_ENCODING and enc != UTF_16_ENCODING and\
01037           enc != UTF_16BE_ENCODING and enc != UTF_8_ENCODING:
01038            raise TagException("Invalid encoding")
01039        elif self.getVersion() & ID3_V1 and enc != LATIN1_ENCODING:
01040            raise TagException("ID3 v1.x supports ISO-8859 encoding only")
01041        elif self.getVersion() <= ID3_V2_3 and enc == UTF_8_ENCODING:
01042            # This is unfortunate.
01043            raise TagException("UTF-8 is not supported by ID3 v2.3")
01044 
01045        self.encoding = enc
01046        for f in self.frames:
01047            f.encoding = enc
01048 
01049    def tagToString(self, pattern):
01050        # %A - artist
01051        # %a - album
01052        # %t - title
01053        # %n - track number
01054        # %N - track total
01055        # %Y - year 
01056        # %G - genre
01057        s = self._subst(pattern, "%A", self.getArtist())
01058        s = self._subst(s, "%a", self.getAlbum())
01059        s = self._subst(s, "%t", self.getTitle())
01060        s = self._subst(s, "%n", self._prettyTrack(self.getTrackNum()[0]))
01061        s = self._subst(s, "%N", self._prettyTrack(self.getTrackNum()[1]))
01062        s = self._subst(s, "%Y", self.getYear())
01063        s = self._subst(s, "%G", self.getGenre().name)
01064        return s
01065 
01066    def _prettyTrack(self, track):
01067        if not track:
01068            return None
01069        track_str = str(track)
01070        if len(track_str) == 1:
01071            track_str = "0" + track_str
01072        return track_str
01073 
01074    def _subst(self, name, pattern, repl):
01075        regex = re.compile(pattern)
01076        if regex.search(name) and repl:
01077            # No '/' characters allowed
01078            (repl, subs) = re.compile("/").subn("-", repl);
01079            (name, subs) = regex.subn(repl, name)
01080        return name;
01081 
01082    def __saveV1Tag(self, version):
01083       assert(version & ID3_V1);
01084 
01085       # Build tag buffer.
01086       tag = "TAG";
01087       tag += self._fixToWidth(self.getTitle().encode("latin_1"), 30);
01088       tag += self._fixToWidth(self.getArtist().encode("latin_1"), 30);
01089       tag += self._fixToWidth(self.getAlbum().encode("latin_1"), 30);
01090       y = self.getYear();
01091       if y is None:
01092           y = "";
01093       tag += self._fixToWidth(y.encode("latin_1"), 4);
01094 
01095       cmt = "";
01096       for c in self.getComments():
01097          if c.description == ID3_V1_COMMENT_DESC:
01098             cmt = c.comment;
01099             # We prefer this one over "";
01100             break; 
01101          elif c.description == "":
01102             cmt = c.comment;
01103             # Keep searching in case we find the description eyeD3 uses.
01104       cmt = self._fixToWidth(cmt.encode("latin_1"), 30);
01105       if version != ID3_V1_0:
01106          track = self.getTrackNum()[0];
01107          if track != None:
01108             cmt = cmt[0:28] + "\x00" + chr(int(track) & 0xff);
01109       tag += cmt;
01110 
01111       if not self.getGenre() or self.getGenre().getId() is None:
01112          genre = 0;
01113       else:
01114          genre = self.getGenre().getId();
01115       tag += chr(genre & 0xff);
01116 
01117       assert(len(tag) == 128);
01118 
01119       tagFile = file(self.linkedFile.name, "r+b");
01120       # Write the tag over top an original or append it.
01121       try:
01122          tagFile.seek(-128, 2);
01123          if tagFile.read(3) == "TAG":
01124             tagFile.seek(-128, 2);
01125          else:
01126             tagFile.seek(0, 2);
01127       except IOError:
01128          # File is smaller than 128 bytes.
01129          tagFile.seek(0, 2);
01130 
01131       tagFile.write(tag);
01132       tagFile.flush();
01133       tagFile.close();
01134 
01135    def _fixToWidth(self, s, n):
01136       retval = str(s);
01137       retval = retval[0:n];
01138       retval = retval + ("\x00" * (n - len(retval)));
01139       return retval;
01140 
01141    # Returns false when an ID3 v1 tag is not present, or contains no data.
01142    def __loadV1Tag(self, f):
01143       if isinstance(f, str) or isinstance(f, unicode):
01144          fp = file(f, "rb")
01145          closeFile = 1;
01146       else:
01147          fp = f;
01148          closeFile = 0;
01149 
01150       # Seek to the end of the file where all ID3v1 tags are written.
01151       fp.seek(0, 2);
01152       strip_chars = string.whitespace + "\x00";
01153       if fp.tell() > 127:
01154          fp.seek(-128, 2);
01155          id3tag = fp.read(128);
01156          if id3tag[0:3] == "TAG":
01157             TRACE_MSG("Located ID3 v1 tag");
01158             # 1.0 is implied until a 1.1 feature is recognized.
01159             self.setVersion(ID3_V1_0);
01160 
01161             title = re.sub("\x00+$", "", id3tag[3:33].strip(strip_chars));
01162             TRACE_MSG("Tite: " + title);
01163             if title:
01164                self.setTitle(unicode(title, "latin1"));
01165 
01166             artist = re.sub("\x00+$", "", id3tag[33:63].strip(strip_chars));
01167             TRACE_MSG("Artist: " + artist);
01168             if artist:
01169                self.setArtist(unicode(artist, "latin1"));
01170 
01171             album = re.sub("\x00+$", "", id3tag[63:93].strip(strip_chars));
01172             TRACE_MSG("Album: " + album);
01173             if album:
01174                self.setAlbum(unicode(album, "latin1"));
01175 
01176             year = re.sub("\x00+$", "", id3tag[93:97].strip(strip_chars));
01177             TRACE_MSG("Year: " + year);
01178             try:
01179                if year and int(year):
01180                   self.setDate(year);
01181             except ValueError:
01182                # Bogus year strings.
01183                pass;
01184 
01185             if re.sub("\x00+$", "", id3tag[97:127]):
01186                comment = id3tag[97:127];
01187                TRACE_MSG("Comment: " + comment);
01188                if comment[-2] == "\x00" and comment[-1] != "\x00":
01189                   # Parse track number (added to ID3v1.1) if present.
01190                   TRACE_MSG("Comment contains track number per v1.1 spec");
01191                   track = ord(comment[-1]);
01192                   self.setTrackNum((track, None));
01193                   TRACE_MSG("Track: " + str(track));
01194                   TRACE_MSG("Track Num found, setting version to v1.1s");
01195                   self.setVersion(ID3_V1_1);
01196                   comment = comment[:-2];
01197                else:
01198                   track = None
01199                comment = re.sub("\x00+$", "", comment).rstrip();
01200                TRACE_MSG("Comment: " + comment);
01201                if comment:
01202                   self.addComment(unicode(comment, 'latin1'),
01203                                   ID3_V1_COMMENT_DESC);
01204 
01205             genre = ord(id3tag[127:128])
01206             TRACE_MSG("Genre ID: " + str(genre));
01207             self.setGenre(genre);
01208 
01209       if closeFile:
01210          fp.close()
01211       return len(self.frames);
01212 
01213    def __saveV2Tag(self, version):
01214       assert(version & ID3_V2);
01215       TRACE_MSG("Rendering tag version: " + versionToString(version));
01216 
01217       self.setVersion(version);
01218 
01219       currPadding = 0;
01220       currTagSize = 0
01221       # We may be converting from 1.x to 2.x so we need to find any
01222       # current v2.x tag otherwise we're gonna hork the file.
01223       tmpTag = Tag();
01224       if tmpTag.link(self.linkedFile.name, ID3_V2):
01225          TRACE_MSG("Found current v2.x tag:");
01226          currTagSize = tmpTag.linkedFile.tagSize;
01227          TRACE_MSG("Current tag size: %d" % currTagSize);
01228          currPadding = tmpTag.linkedFile.tagPadding;
01229          TRACE_MSG("Current tag padding: %d" % currPadding);
01230 
01231       if self.do_tdtg:
01232           t = time.strftime("%Y-%m-%dT%H:%M:%S", time.gmtime());
01233           # Tag it!
01234           if self.header.minorVersion == 4:
01235               # TDTG for 2.4
01236               h = FrameHeader(self.header);
01237               h.id = "TDTG";
01238               dateFrame = DateFrame(h, date_str = self.strToUnicode(t),
01239                                     encoding = self.encoding);
01240               self.frames.removeFramesByID("TDTG");
01241               self.frames.addFrame(dateFrame);
01242           else:
01243               # TXXX (Tagging time) for older versions
01244               self.frames.removeFramesByID("TDTG");
01245               self.addUserTextFrame('Tagging time', t)
01246 
01247       # Render all frames first so the data size is known for the tag header.
01248       frameData = "";
01249       for f in self.frames:
01250          TRACE_MSG("Rendering frame: " + f.header.id);
01251          raw_frame = f.render();
01252          TRACE_MSG("Rendered %d bytes" % len(raw_frame));
01253          frameData += raw_frame;
01254       # Handle the overall tag header unsync bit.  Frames themselves duplicate
01255       # this bit.
01256       if self.header.unsync:
01257           TRACE_MSG("Unsyncing all frames (sync-safe)");
01258           frameData = frames.unsyncData(frameData);
01259 
01260       rewriteFile = 0;
01261       paddingSize = 0;
01262       DEFAULT_PADDING = 1024
01263       def compute_padding():
01264           if currPadding <= DEFAULT_PADDING:
01265               return DEFAULT_PADDING
01266           else:
01267               return currPadding
01268 
01269       # Extended header
01270       extHeaderData = "";
01271       if self.header.extended:
01272          # This is sorta lame.  We don't know the total framesize until
01273          # this is rendered, yet we can't render it witout knowing the
01274          # amount of padding for the crc.  Force it.
01275          rewriteFile = 1;
01276          TRACE_MSG("Rendering extended header");
01277          paddingSize = compute_padding()
01278          extHeaderData += self.extendedHeader.render(self.header, frameData,
01279                                                      paddingSize);
01280 
01281       new_size = 10 + len(extHeaderData) + len(frameData) + paddingSize
01282       if rewriteFile or new_size >= currTagSize:
01283          TRACE_MSG("File rewrite required");
01284          rewriteFile = 1;
01285          if paddingSize <= 0:
01286              paddingSize = compute_padding()
01287       elif paddingSize <= 0:
01288          paddingSize = currTagSize - (new_size - 10)
01289       TRACE_MSG("Adding %d bytes of padding" % paddingSize)
01290       frameData += ("\x00" * paddingSize);
01291 
01292       # Recompute with padding
01293       new_size = 10 + len(extHeaderData) + len(frameData)
01294       header_tag_size = new_size - 10
01295 
01296       # Render the tag header.
01297       TRACE_MSG("Rendering %s tag header with size %d" %
01298                 (versionToString(self.getVersion()), header_tag_size))
01299       headerData = self.header.render(header_tag_size)
01300 
01301       # Assemble frame.
01302       tagData = headerData + extHeaderData + frameData;
01303 
01304       # Write the tag.
01305       if not rewriteFile:
01306          tagFile = file(self.linkedFile.name, "r+b");
01307          TRACE_MSG("Writing %d bytes of tag data" % len(tagData));
01308          tagFile.write(tagData);
01309          tagFile.close();
01310       else:
01311          # Open tmp file
01312          tmpName = tempfile.mktemp();
01313          tmpFile = file(tmpName, "w+b");
01314          TRACE_MSG("Writing %d bytes of tag data" % len(tagData));
01315          tmpFile.write(tagData);
01316 
01317          # Write audio data in chunks
01318          tagFile = file(self.linkedFile.name, "rb");
01319          if currTagSize != 0:
01320              seek_point = currTagSize + 10
01321          else:
01322              seek_point = 0
01323          TRACE_MSG("Seeking to beginning of audio data, byte %d (%x)" %
01324                    (seek_point, seek_point))
01325          tagFile.seek(seek_point)
01326          self.__copyRemaining(tagFile, tmpFile);
01327 
01328          tagFile.close();
01329          tmpFile.close();
01330 
01331          # Move tmp to orig.
01332          shutil.copyfile(tmpName, self.linkedFile.name);
01333          os.unlink(tmpName);
01334 
01335       # Update our state.
01336       TRACE_MSG("Tag write complete.  Updating state.");
01337       self.linkedFile.tagPadding = paddingSize;
01338       # XXX: getSize could cache sizes so to prevent rendering again.
01339       self.linkedFile.tagSize = self.frames.getSize();
01340 
01341 
01342    # Returns >= 0 to indicate the padding size of the read frame; -1 returned
01343    # when not tag was found.
01344    def __loadV2Tag(self, f):
01345       if isinstance(f, str) or isinstance(f, unicode):
01346          fp = file(f, "rb")
01347          closeFile = 1;
01348       else:
01349          fp = f;
01350          closeFile = 0;
01351 
01352       padding = -1;
01353       try:
01354          # Look for a tag and if found load it.
01355          if not self.header.parse(fp):
01356             return -1;
01357 
01358          # Read the extended header if present.
01359          if self.header.extended:
01360             self.extendedHeader.parse(fp, self.header);
01361 
01362          # Header is definitely there so at least one frame *must* follow.
01363          self.frames.setTagHeader(self.header);
01364          padding = self.frames.parse(fp, self.header, self.extendedHeader);
01365          TRACE_MSG("Tag contains %d bytes of padding." % padding);
01366       except FrameException, ex:
01367          fp.close();
01368          raise TagException(str(ex));
01369       except TagException:
01370          fp.close();
01371          raise;
01372 
01373       if closeFile:
01374          fp.close();
01375       return padding;
01376 
01377    def toInt(self, s):
01378       try:
01379          return int(s);
01380       except ValueError:
01381          return None;
01382       except TypeError:
01383          return None;
01384 
01385    def __padDateField(self, f):
01386       fStr = str(f);
01387       if len(fStr) == 2:
01388          pass;
01389       elif len(fStr) == 1:
01390          fStr = "0" + fStr;
01391       else:
01392          raise TagException("Invalid date field: " + fStr);
01393       return fStr;
01394 
01395    def __copyRemaining(self, src_fp, dest_fp):
01396        # Write audio data in chunks
01397        done = False
01398        amt = 1024 * 512
01399        while not done:
01400            data = src_fp.read(amt)
01401            if data:
01402                dest_fp.write(data)
01403            else:
01404                done = True
01405            del data
01406 
01407    # DEPRECATED
01408    # This method will return the first comment in the FrameSet
01409    # and not all of them.  Multiple COMM frames are common and useful.  Use
01410    # getComments which returns a list.
01411    def getComment(self):
01412       f = self.frames[COMMENT_FID];
01413       if f:
01414          return f[0].comment;
01415       else:
01416          return None;
01417 
01418 
01419 ################################################################################
01420 class GenreException(Exception):
01421    '''Problem looking up genre'''
01422 
01423 ################################################################################
01424 class Genre:
01425    id = None;
01426    name = None;
01427 
01428    def __init__(self, id = None, name = None):
01429       if id is not None:
01430          self.setId(id);
01431       elif name is not None:
01432          self.setName(name);
01433 
01434    def getId(self):
01435       return self.id;
01436    def getName(self):
01437       return self.name;
01438 
01439    # Sets the genre id.  The objects name field is set to the corresponding
01440    # value obtained from eyeD3.genres.
01441    #
01442    # Throws GenreException when name does not map to a valid ID3 v1.1. id.
01443    # This behavior can be disabled by passing 0 as the second argument.
01444    def setId(self, id):
01445       if not isinstance(id, int):
01446          raise TypeError("Invalid genre id: " + str(id));
01447 
01448       try:
01449           name = genres[id];
01450       except Exception, ex:
01451           if utils.strictID3():
01452               raise GenreException("Invalid genre id: " + str(id));
01453 
01454       if utils.strictID3() and not name:
01455           raise GenreException("Genre id maps to a null name: " + str(id));
01456 
01457       self.id = id;
01458       self.name = name;
01459 
01460    # Sets the genre name.  The objects id field is set to the corresponding
01461    # value obtained from eyeD3.genres.
01462    #
01463    # Throws GenreException when name does not map to a valid ID3 v1.1. name.
01464    # This behavior can be disabled by passing 0 as the second argument.
01465    def setName(self, name):
01466       if not isinstance(name, str):
01467          raise GenreException("Invalid genre name: " + str(name));
01468 
01469       try:
01470          id = genres[name];
01471          if (not utils.itunesCompat()) or (id <= GenreMap.ITUNES_GENRE_MAX):
01472             # Get titled case.
01473             name = genres[id];
01474       except:
01475           if utils.strictID3():
01476               raise GenreException("Invalid genre name: " + name);
01477           id = None;
01478 
01479       self.id = id;
01480       self.name = name;
01481 
01482 
01483    # Sets the genre id and name. 
01484    #
01485    # Throws GenreException when eyeD3.genres[id] != name (case insensitive). 
01486    # This behavior can be disabled by passing 0 as the second argument.
01487    def set(self, id, name):
01488       if not isinstance(id, int):
01489          raise GenreException("Invalid genre id: " + id);
01490       if not isinstance(name, str):
01491          raise GenreException("Invalid genre name: " + str(name));
01492 
01493       if not utils.strictID3():
01494          self.id = id;
01495          self.name = name;
01496       else:
01497          try:
01498             if genres[name] != id:
01499                raise GenreException("eyeD3.genres[" + str(id) + "] " +\
01500                                     "does not match " + name);
01501             self.id = id;
01502             self.name = name;
01503          except:
01504             raise GenreException("eyeD3.genres[" + str(id) + "] " +\
01505                                  "does not match " + name);
01506 
01507    # Parses genre information from genreStr. 
01508    # The following formats are supported:
01509    # 01, 2, 23, 125 - ID3 v1 style.
01510    # (01), (2), (129)Hardcore, (9)Metal - ID3 v2 style with and without
01511    #                                      refinement.
01512    #
01513    # Throws GenreException when an invalid string is passed.
01514    def parse(self, genreStr):
01515       genreStr =\
01516           str(genreStr.encode('utf-8')).strip(string.whitespace + '\x00');
01517       self.id = None;
01518       self.name = None;
01519 
01520       if not genreStr:
01521          return;
01522 
01523       # XXX: Utf-16 conversions leave a null byte at the end of the string.
01524       while genreStr[len(genreStr) - 1] == "\x00":
01525          genreStr = genreStr[:len(genreStr) - 1];
01526          if len(genreStr) == 0:
01527             break;
01528 
01529       # ID3 v1 style.
01530       # Match 03, 34, 129.
01531       regex = re.compile("[0-9][0-9]?[0-9]?$");
01532       if regex.match(genreStr):
01533          if len(genreStr) != 1 and genreStr[0] == '0':
01534             genreStr = genreStr[1:];
01535 
01536          self.setId(int(genreStr));
01537          return;
01538 
01539       # ID3 v2 style.
01540       # Match (03), (0)Blues, (15) Rap
01541       regex = re.compile("\(([0-9][0-9]?[0-9]?)\)(.*)$");
01542       m = regex.match(genreStr);
01543       if m:
01544          (id, name) = m.groups();
01545          if len(id) != 1 and id[0] == '0':
01546             id = id[1:];
01547 
01548          if id and name:
01549             self.set(int(id), name.strip());
01550          else:
01551             self.setId(int(id));
01552          return;
01553 
01554       # Non standard, but witnessed.
01555       # Match genre alone. e.g. Rap, Rock, blues, 'Rock|Punk|Pop-Punk', etc
01556       '''
01557       regex = re.compile("^[A-Z 0-9+/\-\|!&'\.]+\00*$", re.IGNORECASE)
01558       if regex.match(genreStr):
01559          print "boo"
01560          self.setName(genreStr);
01561          return;
01562      '''
01563 
01564       # non standard, but witnessed. 
01565       # Match delimited-separated list of genres alone.
01566       # e.g. 'breaks, electronic', 'hardcore|nyhc' 
01567       regex = re.compile(
01568               r"^([A-Z 0-9+/\-\|!&'\.]+)([,;|][A-Z 0-9+/\-\|!&'\.]+)*$",
01569               re.IGNORECASE)
01570       if regex.match(genreStr):
01571          print "boo"
01572          self.setName(genreStr);
01573          return;
01574 
01575       raise GenreException("Genre string cannot be parsed with '%s': %s" %\
01576                            (regex.pattern, genreStr));
01577 
01578    def __str__(self):
01579       s = "";
01580       if (self.id != None and
01581               ((not utils.itunesCompat()) or
01582                (self.id <= GenreMap.ITUNES_GENRE_MAX))):
01583          s += "(" + str(self.id) + ")"
01584       if self.name:
01585          s += self.name;
01586       return s;
01587 
01588 ################################################################################
01589 class InvalidAudioFormatException(Exception):
01590    '''Problems with audio format'''
01591 
01592 ################################################################################
01593 class TagFile:
01594    fileName = str("");
01595    fileSize = int(0);
01596    tag      = None;
01597    # Number of seconds required to play the audio file.
01598    play_time = int(0);
01599 
01600    def __init__(self, fileName):
01601        self.fileName = fileName;
01602 
01603    def getTag(self):
01604       return self.tag;
01605 
01606    def getSize(self):
01607       if not self.fileSize:
01608          self.fileSize = os.stat(self.fileName)[ST_SIZE];
01609       return self.fileSize;
01610 
01611    def rename(self, name, fsencoding):
01612        base = os.path.basename(self.fileName);
01613        base_ext = os.path.splitext(base)[1];
01614        dir = os.path.dirname(self.fileName);
01615        if not dir:
01616            dir = ".";
01617        new_name = dir + os.sep + name.encode(fsencoding) + base_ext;
01618 
01619        if os.path.exists(new_name):
01620            raise TagException("File '%s' exists, eyeD3 will not overwrite it" %
01621                               new_name)
01622 
01623        try:
01624            os.rename(self.fileName, new_name);
01625            self.fileName = new_name;
01626        except OSError, ex:
01627            raise TagException("Error renaming '%s' to '%s'" % (self.fileName,
01628                                                                new_name));
01629 
01630    def getPlayTime(self):
01631       return self.play_time;
01632 
01633    def getPlayTimeString(self):
01634       from eyeD3.utils import format_track_time
01635       return format_track_time(self.getPlayTime())
01636 
01637 
01638 ################################################################################
01639 class Mp3AudioFile(TagFile):
01640 
01641    def __init__(self, fileName, tagVersion = ID3_ANY_VERSION):
01642       TagFile.__init__(self, fileName)
01643 
01644       self.tag        = None
01645       self.header     = None
01646       self.xingHeader = None
01647       self.lameTag    = None
01648 
01649       if not isMp3File(fileName):
01650          raise InvalidAudioFormatException("File is not mp3");
01651 
01652       # Parse ID3 tag.
01653       f = file(self.fileName, "rb");
01654       self.tag = Tag();
01655       hasTag = self.tag.link(f, tagVersion);
01656       # Find the first mp3 frame.
01657       if self.tag.isV1():
01658          framePos = 0;
01659       elif not hasTag:
01660          framePos = 0;
01661          self.tag = None;
01662       else:
01663          framePos = self.tag.header.SIZE + self.tag.header.tagSize;
01664 
01665       TRACE_MSG("mp3 header search starting @ %x" % framePos)
01666       # Find an mp3 header
01667       header_pos, header, header_bytes = mp3.find_header(f, framePos)
01668       if header:
01669           try:
01670               self.header = mp3.Header(header)
01671           except mp3.Mp3Exception, ex:
01672               self.header = None
01673               raise InvalidAudioFormatException(str(ex));
01674           else:
01675               TRACE_MSG("mp3 header %x found at position: 0x%x" % (header,
01676                                                                    header_pos))
01677       else:
01678         raise InvalidAudioFormatException("Unable to find a valid mp3 frame")
01679 
01680       # Check for Xing/Info header information which will always be in the
01681       # first "null" frame.
01682       f.seek(header_pos)
01683       mp3_frame = f.read(self.header.frameLength)
01684       if re.compile('Xing|Info').search(mp3_frame):
01685           self.xingHeader = mp3.XingHeader();
01686           if not self.xingHeader.decode(mp3_frame):
01687               TRACE_MSG("Ignoring corrupt Xing header")
01688               self.xingHeader = None
01689       # Check for LAME Tag
01690       self.lameTag = mp3.LameTag(mp3_frame)
01691 
01692       # Compute track play time.
01693       tpf = mp3.computeTimePerFrame(self.header);
01694       if self.xingHeader and self.xingHeader.vbr:
01695          self.play_time = int(tpf * self.xingHeader.numFrames);
01696       else:
01697          length = self.getSize();
01698          if self.tag and self.tag.isV2():
01699             length -= self.tag.header.SIZE + self.tag.header.tagSize;
01700             # Handle the case where there is a v2 tag and a v1 tag.
01701             f.seek(-128, 2)
01702             if f.read(3) == "TAG":
01703                length -= 128;
01704          elif self.tag and self.tag.isV1():
01705             length -= 128;
01706          self.play_time = int((length / self.header.frameLength) * tpf);
01707 
01708       f.close();
01709 
01710    # Returns a tuple.  The first value is a boolean which if true means the
01711    # bit rate returned in the second value is variable.
01712    def getBitRate(self):
01713       xHead = self.xingHeader;
01714       if xHead and xHead.vbr:
01715          tpf = eyeD3.mp3.computeTimePerFrame(self.header);
01716          # FIXME: if xHead.numFrames == 0 (Fuoco.mp3), ZeroDivisionError
01717          br = int((xHead.numBytes * 8) / (tpf * xHead.numFrames * 1000));
01718          vbr = 1;
01719       else:
01720          br = self.header.bitRate;
01721          vbr = 0;
01722       return (vbr, br);
01723 
01724    def getBitRateString(self):
01725       (vbr, bitRate) = self.getBitRate();
01726       brs = "%d kb/s" % bitRate;
01727       if vbr:
01728          brs = "~" + brs;
01729       return brs;
01730    def getSampleFreq(self):
01731       return self.header.sampleFreq;
01732 
01733 ################################################################################
01734 def isMp3File(fileName):
01735     type = eyeD3.utils.guess_mime_type(fileName);
01736     return type == "audio/mpeg";
01737 
01738 ################################################################################
01739 class GenreMap(list):
01740    # None value are set in the ctor
01741    GENRE_MIN = 0;
01742    GENRE_MAX = None;
01743    ID3_GENRE_MIN = 0;
01744    ID3_GENRE_MAX = 79;
01745    WINAMP_GENRE_MIN = 80;
01746    WINAMP_GENRE_MAX = 147;
01747    ITUNES_GENRE_MAX = 125;
01748    EYED3_GENRE_MIN = None;
01749    EYED3_GENRE_MAX = None;
01750 
01751    # Accepts both int and string keys. Throws IndexError and TypeError.
01752    def __getitem__(self, key):
01753       if isinstance(key, int):
01754          if key >= 0 and key < len(self):
01755             v = list.__getitem__(self, key);
01756             if v:
01757                return v;
01758             else:
01759                return None;
01760          else:
01761             raise IndexError("genre index out of range");
01762       elif isinstance(key, str):
01763          if self.reverseDict.has_key(key.lower()):
01764             return self.reverseDict[key.lower()];
01765          else:
01766             raise IndexError(key + " genre not found");
01767       else:
01768          raise TypeError("genre key must be type int or string");
01769 
01770    def __init__(self):
01771       self.data = []
01772       self.reverseDict = {}
01773       # ID3 genres as defined by the v1.1 spec with WinAmp extensions.
01774       self.append('Blues');
01775       self.append('Classic Rock');
01776       self.append('Country');
01777       self.append('Dance');
01778       self.append('Disco');
01779       self.append('Funk');
01780       self.append('Grunge');
01781       self.append('Hip-Hop');
01782       self.append('Jazz');
01783       self.append('Metal');
01784       self.append('New Age');
01785       self.append('Oldies');
01786       self.append('Other');
01787       self.append('Pop');
01788       self.append('R&B');
01789       self.append('Rap');
01790       self.append('Reggae');
01791       self.append('Rock');
01792       self.append('Techno');
01793       self.append('Industrial');
01794       self.append('Alternative');
01795       self.append('Ska');
01796       self.append('Death Metal');
01797       self.append('Pranks');
01798       self.append('Soundtrack');
01799       self.append('Euro-Techno');
01800       self.append('Ambient');
01801       self.append('Trip-Hop');
01802       self.append('Vocal');
01803       self.append('Jazz+Funk');
01804       self.append('Fusion');
01805       self.append('Trance');
01806       self.append('Classical');
01807       self.append('Instrumental');
01808       self.append('Acid');
01809       self.append('House');
01810       self.append('Game');
01811       self.append('Sound Clip');
01812       self.append('Gospel');
01813       self.append('Noise');
01814       self.append('AlternRock');
01815       self.append('Bass');
01816       self.append('Soul');
01817       self.append('Punk');
01818       self.append('Space');
01819       self.append('Meditative');
01820       self.append('Instrumental Pop');
01821       self.append('Instrumental Rock');
01822       self.append('Ethnic');
01823       self.append('Gothic');
01824       self.append('Darkwave');
01825       self.append('Techno-Industrial');
01826       self.append('Electronic');
01827       self.append('Pop-Folk');
01828       self.append('Eurodance');
01829       self.append('Dream');
01830       self.append('Southern Rock');
01831       self.append('Comedy');
01832       self.append('Cult');
01833       self.append('Gangsta Rap');
01834       self.append('Top 40');
01835       self.append('Christian Rap');
01836       self.append('Pop / Funk');
01837       self.append('Jungle');
01838       self.append('Native American');
01839       self.append('Cabaret');
01840       self.append('New Wave');
01841       self.append('Psychedelic');
01842       self.append('Rave');
01843       self.append('Showtunes');
01844       self.append('Trailer');
01845       self.append('Lo-Fi');
01846       self.append('Tribal');
01847       self.append('Acid Punk');
01848       self.append('Acid Jazz');
01849       self.append('Polka');
01850       self.append('Retro');
01851       self.append('Musical');
01852       self.append('Rock & Roll');
01853       self.append('Hard Rock');
01854       self.append('Folk');
01855       self.append('Folk-Rock');
01856       self.append('National Folk');
01857       self.append('Swing');
01858       self.append('Fast  Fusion');
01859       self.append('Bebob');
01860       self.append('Latin');
01861       self.append('Revival');
01862       self.append('Celtic');
01863       self.append('Bluegrass');
01864       self.append('Avantgarde');
01865       self.append('Gothic Rock');
01866       self.append('Progressive Rock');
01867       self.append('Psychedelic Rock');
01868       self.append('Symphonic Rock');
01869       self.append('Slow Rock');
01870       self.append('Big Band');
01871       self.append('Chorus');
01872       self.append('Easy Listening');
01873       self.append('Acoustic');
01874       self.append('Humour');
01875       self.append('Speech');
01876       self.append('Chanson');
01877       self.append('Opera');
01878       self.append('Chamber Music');
01879       self.append('Sonata');
01880       self.append('Symphony');
01881       self.append('Booty Bass');
01882       self.append('Primus');
01883       self.append('Porn Groove');
01884       self.append('Satire');
01885       self.append('Slow Jam');
01886       self.append('Club');
01887       self.append('Tango');
01888       self.append('Samba');
01889       self.append('Folklore');
01890       self.append('Ballad');
01891       self.append('Power Ballad');
01892       self.append('Rhythmic Soul');
01893       self.append('Freestyle');
01894       self.append('Duet');
01895       self.append('Punk Rock');
01896       self.append('Drum Solo');
01897       self.append('A Cappella');
01898       self.append('Euro-House');
01899       self.append('Dance Hall');
01900       self.append('Goa');
01901       self.append('Drum & Bass');
01902       self.append('Club-House');
01903       self.append('Hardcore');
01904       self.append('Terror');
01905       self.append('Indie');
01906       self.append('BritPop');
01907       self.append('Negerpunk');
01908       self.append('Polsk Punk');
01909       self.append('Beat');
01910       self.append('Christian Gangsta Rap');
01911       self.append('Heavy Metal');
01912       self.append('Black Metal');
01913       self.append('Crossover');
01914       self.append('Contemporary Christian');
01915       self.append('Christian Rock');
01916       self.append('Merengue');
01917       self.append('Salsa');
01918       self.append('Thrash Metal');
01919       self.append('Anime');
01920       self.append('JPop');
01921       self.append('Synthpop');
01922       # The follow genres I've encountered in the wild.
01923       self.append('Rock/Pop');
01924       self.EYED3_GENRE_MIN = len(self) - 1;
01925       # New genres go here
01926 
01927       self.EYED3_GENRE_MAX = len(self) - 1;
01928       self.GENRE_MAX = len(self) - 1;
01929 
01930       # Pad up to 255 with "Unknown"
01931       count = len(self);
01932       while count < 256:
01933          self.append("Unknown");
01934          count += 1;
01935 
01936       for index in range(len(self)):
01937          if self[index]:
01938             self.reverseDict[string.lower(self[index])] = index
01939 class LinkedFile:
01940    name = "";
01941    tagPadding = 0;
01942    tagSize = 0;  # This includes the padding byte count.
01943 
01944    def __init__(self, fileName):
01945        if isinstance(fileName, str):
01946            try:
01947                self.name = unicode(fileName, sys.getfilesystemencoding());
01948            except:
01949                # Work around the local encoding not matching that of a mounted
01950                # filesystem
01951                self.name = fileName
01952        else:
01953            self.name = fileName;
01954 
01955 def tagToUserTune(tag):
01956     audio_file = None;
01957     if isinstance(tag, Mp3AudioFile):
01958         audio_file = tag;
01959         tag = audio_file.getTag();
01960 
01961     tune =  [u"<tune xmlns='http://jabber.org/protocol/tune'>\n"]
01962     def add(name, value):
01963         if value:
01964             value = escape(value)
01965             tune.append('  <%s>%s</%s>\n' % (name, value, name))
01966   
01967     add('artist', tag.getArtist())
01968     add('title', tag.getTitle())
01969     add('source', tag.getAlbum())
01970     add('track', "file://" + unicode(os.path.abspath(tag.linkedFile.name)))
01971     if audio_file:
01972         add('length', unicode(audio_file.getPlayTime()))
01973     tune.append("</tune>\n")
01974     return ''.join(tune)
01975 
01976 def tagToRfc822(tag):
01977     if isinstance(tag, Mp3AudioFile):
01978         tag = tag.getTag();
01979     lines = []
01980     def add(name, value):
01981         if value:
01982             lines.append(u'%s: %s\n' % (name, value))
01983     add('Filename', tag.linkedFile.name)
01984     add('Artist', tag.getArtist())
01985     add('Album', tag.getAlbum())
01986     for comment in tag.getComments():
01987         add('Comment', comment.comment)
01988     try:
01989         g = tag.getGenre()
01990         if g:
01991             add('Genre', '%s (%s)' % (g.getName(), g.getId() or 0))
01992     except GenreException:
01993         pass
01994     add('Title', tag.getTitle())
01995     tn, tt = tag.getTrackNum()
01996     add('Track', tn)
01997     add('Year', tag.getYear())
01998     return ''.join(lines)
01999 
02000 
02001 #
02002 # Module level globals.
02003 #
02004 genres = GenreMap();
02005