root/eyeD3/tag.py

Revision 6ab8361a61197bb0a951e5f77d4d1c5c25a9702f, 61.7 kB (checked in by Jason Michalski <armooo@armooo.net>, 2 years ago)

pyTivo
- ID3 suport for music

  • Property mode set to 100644
Line 
1 ###############################################################################
2 #
3 #  Copyright (C) 2002-2005  Travis Shirk <travis@pobox.com>
4 #
5 #  This program is free software; you can redistribute it and/or modify
6 #  it under the terms of the GNU General Public License as published by
7 #  the Free Software Foundation; either version 2 of the License, or
8 #  (at your option) any later version.
9 #
10 #  This program is distributed in the hope that it will be useful,
11 #  but WITHOUT ANY WARRANTY; without even the implied warranty of
12 #  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 #  GNU General Public License for more details.
14 #
15 #  You should have received a copy of the GNU General Public License
16 #  along with this program; if not, write to the Free Software
17 #  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
18 #
19 ################################################################################
20 import re, os, string, stat, shutil, tempfile, binascii;
21 import mimetypes;
22 from stat import *;
23 from eyeD3 import *;
24 import eyeD3.utils;
25 import eyeD3.mp3;
26 from frames import *;
27 from binfuncs import *;
28 import math;
29
30 ID3_V1_COMMENT_DESC = "ID3 v1 Comment";
31
32 ################################################################################
33 class TagException(Exception):
34    '''error reading tag'''
35
36 ################################################################################
37 class TagHeader:
38    SIZE = 10;
39
40    version = None;
41    majorVersion = None;
42    minorVersion = None;
43    revVersion = None;
44
45    # Flag bits
46    unsync = 0;
47    extended = 0;
48    experimental = 0;
49    # v2.4 addition
50    footer = 0;
51
52    # The size in the most recently parsed header.
53    tagSize = 0;
54    
55    # Constructor
56    def __init__(self):
57       self.clear();
58
59    def clear(self):
60       self.setVersion(None);
61       self.unsync = 0;
62       self.extended = 0;
63       self.experimental = 0;
64       self.tagSize = 0;
65
66    def setVersion(self, v):
67       if v == None:
68          self.version = None;
69          self.majorVersion = None;
70          self.minorVersion = None;
71          self.revVersion = None;
72          return;
73
74       if v == ID3_CURRENT_VERSION:
75          if self.majorVersion == None or self.minorVersion == None:
76             v = ID3_DEFAULT_VERSION;
77          else:
78             return;
79       elif v == ID3_ANY_VERSION:
80          v = ID3_DEFAULT_VERSION;
81
82       # Handle 3-element lists or tuples.
83       if isinstance(v, tuple) or isinstance(v, list):
84          self.version = eyeD3.utils.versionsToConstant(v);
85          (self.majorVersion,
86           self.minorVersion,
87           self.revVersion) = v;
88       # Handle int constants.
89       elif isinstance(v, int):
90          (self.majorVersion,
91           self.minorVersion,
92           self.revVersion) = eyeD3.utils.constantToVersions(v);
93          self.version = v;
94       else:
95          raise TypeError("Wrong type: %s" % str(type(v)));
96
97    # Given a file handle this method attempts to identify and then parse
98    # a ID3 v2 header.  If successful, the parsed values are stored in
99    # the instance variable.  If the files does not contain an ID3v2 tag
100    # false is returned. A TagException is thrown if a tag is found, but is
101    # not valid or corrupt.
102    def parse(self, f):
103       self.clear();
104
105       # The first three bytes of a v2 header is "ID3".
106       if f.read(3) != "ID3":
107          return 0;
108       TRACE_MSG("Located ID3 v2 tag");
109
110       # The next 2 bytes are the minor and revision versions.
111       version = f.read(2);
112       major = 2;
113       minor = ord(version[0]);
114       rev = ord(version[1]);
115       TRACE_MSG("TagHeader [major]: " + str(major));
116       TRACE_MSG("TagHeader [minor]: " + str(minor));
117       TRACE_MSG("TagHeader [revis]: " + str(rev));
118       if not (major == 2 and (minor >= 2 and minor <= 4)):
119          raise TagException("ID3 v" + str(major) + "." + str(minor) +\
120                             " is not supported.");
121       # Get all the version madness in sync.
122       self.setVersion([major, minor, rev]);
123
124       # The first 4 bits of the next byte are flags.
125       (self.unsync,
126        self.extended,
127        self.experimental,
128        self.footer) = bytes2bin(f.read(1))[0:4];
129       TRACE_MSG("TagHeader [flags]: unsync(%d) extended(%d) "\
130                 "experimental(%d) footer(%d)" % (self.unsync, self.extended,
131                                                  self.experimental,
132                                                  self.footer));
133
134       # The size of the optional extended header, frames, and padding
135       # afer unsynchronization.  This is a sync safe integer, so only the
136       # bottom 7 bits of each byte are used.
137       tagSizeStr = f.read(4);
138       TRACE_MSG("TagHeader [size string]: 0x%02x%02x%02x%02x" %\
139                 (ord(tagSizeStr[0]), ord(tagSizeStr[1]),
140                  ord(tagSizeStr[2]), ord(tagSizeStr[3])));
141       self.tagSize = bin2dec(bytes2bin(tagSizeStr, 7));
142       TRACE_MSG("TagHeader [size]: %d (0x%x)" % (self.tagSize, self.tagSize));
143
144       return 1;
145
146    def render(self, tagLen = None):
147       if tagLen != None:
148          self.tagSize = tagLen;
149
150       data = "ID3";
151       data += chr(self.minorVersion) + chr(self.revVersion);
152       # not not the values so we only get 1's and 0's.
153       data += bin2bytes([not not self.unsync,
154                          not not self.extended,
155                          not not self.experimental,
156                          not not self.footer,
157                          0, 0, 0, 0]);
158       TRACE_MSG("Setting tag size to %d" % tagLen);
159       szBytes = bin2bytes(bin2synchsafe(dec2bin(tagLen, 32)));
160       data += szBytes;
161       TRACE_MSG("TagHeader Rendered");
162       return data;
163
164 ################################################################################
165 class ExtendedTagHeader:
166    size = 0;
167    flags = 0;
168    crc = 0;
169    restrictions = 0;
170
171    def isUpdate(self):
172        return self.flags & 0x40;
173    def hasCRC(self):
174        return self.flags & 0x20;
175    def hasRestrictions(self, minor_version = None):
176        return self.flags & 0x10;
177
178    def setSizeRestrictions(self, v):
179       assert(v >= 0 and v <= 3);
180       self.restrictions = (v << 6) | (self.restrictions & 0x3f);
181    def getSizeRestrictions(self):
182       return self.restrictions >> 6;
183    def getSizeRestrictionsString(self):
184       val = self.getSizeRestrictions();
185       if val == 0x00:
186          return "No more than 128 frames and 1 MB total tag size.";
187       elif val == 0x01:
188          return "No more than 64 frames and 128 KB total tag size.";
189       elif val == 0x02:
190          return "No more than 32 frames and 40 KB total tag size.";
191       elif val == 0x03:
192          return "No more than 32 frames and 4 KB total tag size.";
193
194    def setTextEncodingRestrictions(self, v):
195       assert(v == 0 or v == 1);
196       self.restrictions ^= 0x20;
197    def getTextEncodingRestrictions(self):
198       return self.restrictions & 0x20;
199    def getTextEncodingRestrictionsString(self):
200       if self.getTextEncodingRestrictions():
201          return "Strings are only encoded with ISO-8859-1 [ISO-8859-1] or "\
202                 "UTF-8 [UTF-8].";
203       else:
204          return "None";
205
206    def setTextFieldSizeRestrictions(self, v):
207       assert(v >= 0 and v <= 3);
208       self.restrictions = (v << 3) | (self.restrictions & 0xe7);
209    def getTextFieldSizeRestrictions(self):
210       return (self.restrictions >> 3) & 0x03;
211    def getTextFieldSizeRestrictionsString(self):
212       val = self.getTextFieldSizeRestrictions();
213       if val == 0x00:
214          return "None";
215       elif val == 0x01:
216          return "No string is longer than 1024 characters.";
217       elif val == 0x02:
218          return "No string is longer than 128 characters.";
219       elif val == 0x03:
220          return "No string is longer than 30 characters.";
221
222    def setImageEncodingRestrictions(self, v):
223       assert(v == 0 or v == 1);
224       self.restrictions ^= 0x04;
225    def getImageEncodingRestrictions(self):
226       return self.restrictions & 0x04;
227    def getImageEncodingRestrictionsString(self):
228       if self.getImageEncodingRestrictions():
229          return "Images are encoded only with PNG [PNG] or JPEG [JFIF].";
230       else:
231          return "None";
232
233    def setImageSizeRestrictions(self, v):
234       assert(v >= 0 and v <= 3);
235       self.restrictions = v | (self.restrictions & 0xfc);
236    def getImageSizeRestrictions(self):
237       return self.restrictions & 0x03;
238    def getImageSizeRestrictionsString(self):
239       val = self.getImageSizeRestrictions();
240       if val == 0x00:
241          return "None";
242       elif val == 0x01:
243          return "All images are 256x256 pixels or smaller.";
244       elif val == 0x02:
245          return "All images are 64x64 pixels or smaller.";
246       elif val == 0x03:
247          return "All images are exactly 64x64 pixels, unless required "\
248                 "otherwise.";
249
250    def _syncsafeCRC(self):
251        bites = ""
252        bites += chr((self.crc >> 28) & 0x7f);
253        bites += chr((self.crc >> 21) & 0x7f);
254        bites += chr((self.crc >> 14) & 0x7f);
255        bites += chr((self.crc >>  7) & 0x7f);
256        bites += chr((self.crc >>  0) & 0x7f);
257        return bites;
258
259
260    def render(self, header, frameData, padding = 0):
261       assert(header.majorVersion == 2);
262
263       data = "";
264       crc = None;
265       if header.minorVersion == 4:
266          # Version 2.4
267          size = 6;
268          # Extended flags.
269          if self.isUpdate():
270             data += "\x00";
271          if self.hasCRC():
272             data += "\x05";
273             # XXX: Using the absolute value of the CRC.  The spec is unclear
274             # about the type of this data.
275             self.crc = int(math.fabs(binascii.crc32(frameData +\
276                                                     ("\x00" * padding))));
277             crc_data = self._syncsafeCRC();
278             if len(crc_data) < 5:
279                 crc_data = ("\x00" * (5 - len(crc_data))) + crc_data
280             assert(len(crc_data) == 5)
281             data += crc_data
282          if self.hasRestrictions():
283             data += "\x01";
284             assert(len(self.restrictions) == 1);
285             data += self.restrictions;
286          TRACE_MSG("Rendered extended header data (%d bytes)" % len(data));
287
288          # Extended header size.
289          size = bin2bytes(bin2synchsafe(dec2bin(len(data) + 6, 32)))
290          assert(len(size) == 4);
291
292          data = size + "\x01" + bin2bytes(dec2bin(self.flags)) + data;
293          TRACE_MSG("Rendered extended header of size %d" % len(data));
294       else:
295          # Version 2.3
296          size = 10;
297          # Extended flags.
298          f = [0] * 16;
299          if self.hasCRC():
300             f[0] = 1;
301             # XXX: Using the absolute value of the CRC.  The spec is unclear
302             # about the type of this type.
303             self.crc = int(math.fabs(binascii.crc32(frameData +\
304                                                     ("\x00" * padding))));
305             crc = bin2bytes(dec2bin(self.crc));
306             assert(len(crc) == 4);
307             size += 4;
308          flags = bin2bytes(f);
309          assert(len(flags) == 2);
310          # Extended header size.
311          size = bin2bytes(dec2bin(size, 32))
312          assert(len(size) == 4);
313          # Padding size
314          paddingSize = bin2bytes(dec2bin(padding, 32));
315
316          data = size + flags + paddingSize;
317          if crc:
318             data += crc;
319       return data;
320
321    # Only call this when you *know* there is an extened header.
322    def parse(self, fp, header):
323       assert(header.majorVersion == 2);
324
325       TRACE_MSG("Parsing extended header @ 0x%x" % fp.tell());
326       # First 4 bytes is the size of the extended header.
327       data = fp.read(4);
328       if header.minorVersion == 4:
329          # sync-safe
330          sz = bin2dec(bytes2bin(data, 7));
331          TRACE_MSG("Extended header size (not including 4 byte size): %d" %\
332                    (sz - 4));
333          data = fp.read(sz - 4);
334
335          if ord(data[0]) != 1 or (ord(data[1]) & 0x8f):
336             # As of 2.4 the first byte is 1 and the second can only have
337             # bits 6, 5, and 4 set.
338             raise TagException("Invalid Extended Header");
339
340          offset = 2;
341          self.flags = ord(data[1]);
342          TRACE_MSG("Extended header flags: %x" % self.flags);
343
344          if self.isUpdate():
345             TRACE_MSG("Extended header has update bit set");
346             assert(ord(data[offset]) == 0);
347             offset += 1;
348          if self.hasCRC():
349             TRACE_MSG("Extended header has CRC bit set");
350             assert(ord(data[offset]) == 5);
351             offset += 1;
352             crcData = data[offset:offset + 5];
353             # This is sync-safe.
354             self.crc = bin2dec(bytes2bin(crcData, 7));
355             TRACE_MSG("Extended header CRC: %d" % self.crc);
356             offset += 5;
357          if self.hasRestrictions():
358             TRACE_MSG("Extended header has restrictions bit set");
359             assert(ord(data[offset]) == 5);
360             offset += 1;
361             self.restrictions = ord(data[offset]);
362             offset += 1;
363       else:
364          # v2.3 is totally different... *sigh*
365          sz = bin2dec(bytes2bin(data));
366          TRACE_MSG("Extended header size (not including 4 bytes size): %d" %\
367                    sz);
368          tmpFlags = fp.read(2);
369          # Read the padding size, but it'll be computed during the parse.
370          ps = fp.read(4);
371          TRACE_MSG("Extended header says there is %d bytes of padding" %\
372                    bin2dec(bytes2bin(ps)));
373          # Make this look like a v2.4 mask.
374          self.flags = ord(tmpFlags[0]) >> 2;
375          if self.hasCRC():
376             TRACE_MSG("Extended header has CRC bit set");
377             crcData = fp.read(4);
378             self.crc = bin2dec(bytes2bin(crcData));
379             TRACE_MSG("Extended header CRC: %d" % self.crc);
380
381
382 ################################################################################
383 # ID3 tag class.  The class is capable of reading v1 and v2 tags.  ID3 v1.x
384 # are converted to v2 frames.
385 class Tag:
386    # Latin1 is the default (0x00)
387    encoding = DEFAULT_ENCODING;
388
389    # ID3v1 tags do not contain a header.  The only ID3v1 values stored
390    # in this header are the major/minor version.
391    header = TagHeader();
392
393    # Optional in v2 tags.
394    extendedHeader = ExtendedTagHeader();
395
396    # Contains the tag's frames.  ID3v1 fields are read and converted
397    # the the corresponding v2 frame. 
398    frames = None;
399
400    # Used internally for iterating over frames.
401    iterIndex = None;
402
403    # If this value is None the tag is not linked to any particular file..
404    linkedFile = None;
405
406    # Constructor.  An empty tag is created and the link method is used
407    # to read an mp3 file's v1.x or v2.x tag.  You can optionally set a
408    # file name, but it will not be read, but may be written to.
409    def __init__(self, fileName = None):
410       if fileName:
411          self.linkedFile = LinkedFile(fileName);
412       self.clear();
413
414    def clear(self):
415       self.header = TagHeader();
416       self.frames = FrameSet(self.header);
417       self.iterIndex = None;
418
419    # Returns an read-only iterator for all frames.
420    def __iter__(self):
421       if len(self.frames):
422          self.iterIndex = 0;
423       else:
424          self.iterIndex = None;
425       return self;
426
427    def next(self):
428       if self.iterIndex == None or self.iterIndex == len(self.frames):
429          raise StopIteration;
430       frm = self.frames[self.iterIndex];
431       self.iterIndex += 1;
432       return frm;
433
434    # Returns true when an ID3 tag is read from f which may be a file name
435    # or an aleady opened file object.  In the latter case, the file object
436    # is not closed when this method returns.
437    #
438    # By default, both ID3 v2 and v1 tags are parsed in that order.
439    # If a v2 tag is found then a v1 parse is not performed.  This behavior
440    # can be refined by passing ID3_V1 or ID3_V2 as the second argument
441    # instead of the default ID3_ANY_VERSION.
442    #
443    # Converts all ID3v1 data into ID3v2 frames internally.
444    # May throw IOError, or TagException if parsing fails.
445    def link(self, f, v = ID3_ANY_VERSION):
446       self.linkedFile = None;
447       self.clear();
448
449       fileName = "";
450       if isinstance(f, file):
451          fileName = f.name;
452       elif isinstance(f, str) or isinstance(f, unicode):
453          fileName = f;
454       else:
455          raise TagException("Invalid type passed to Tag.link: " +
456                             str(type(f)));
457
458       if v != ID3_V1 and v != ID3_V2 and v != ID3_ANY_VERSION:
459          raise TagException("Invalid version: " + hex(v));
460
461       tagFound = 0;
462       padding = 0;
463       TRACE_MSG("Linking File: " + fileName);
464       if v == ID3_V1:
465          if self.__loadV1Tag(f):
466             tagFound = 1;
467       elif v == ID3_V2:
468          padding = self.__loadV2Tag(f);
469          if padding >= 0:
470             tagFound = 1;
471       elif v == ID3_ANY_VERSION:
472          padding = self.__loadV2Tag(f);
473          if padding >= 0:
474             tagFound = 1;
475          else:
476             padding = 0;
477             if self.__loadV1Tag(f):
478                tagFound = 1;
479
480       self.linkedFile = LinkedFile(fileName);
481       if tagFound:
482          # In the case of a v1.x tag this is zero.
483          self.linkedFile.tagSize = self.header.tagSize;
484          self.linkedFile.tagPadding = padding;
485       else:
486          self.linkedFile.tagSize = 0;
487          self.linkedFile.tagPadding = 0;
488       return tagFound;
489
490    # Write the current tag state to the linked file.
491    # The version of the ID3 file format that should be written can
492    # be passed as an argument; the default is ID3_CURRENT_VERSION.
493    def update(self, version = ID3_CURRENT_VERSION, backup = 0):
494       if not self.linkedFile:
495          raise TagException("The Tag is not linked to a file.");
496
497       if backup:
498          shutil.copyfile(self.linkedFile.name, self.linkedFile.name + ".orig");
499
500       self.setVersion(version);
501       version = self.getVersion();
502       if version == ID3_V2_2:
503           raise TagException("Unable to write ID3 v2.2");
504       # If v1.0 is being requested explicitly then so be it, if not and there is
505       # a track number then bumping to v1.1 is /probably/ best.
506       if self.header.majorVersion == 1 and self.header.minorVersion == 0 and\
507          self.getTrackNum()[0] != None and version != ID3_V1_0:
508          version = ID3_V1_1;
509          self.setVersion(version);
510      
511       # If there are no frames then simply remove the current tag.
512       if len(self.frames) == 0:
513          self.remove(version);
514          self.header = TagHeader();
515          self.frames.setTagHeader(self.header);
516          self.linkedFile.tagPadding = 0;
517          self.linkedFile.tagSize = 0;
518          return;
519
520       if version & ID3_V1:
521          self.__saveV1Tag(version);
522          return 1;
523       elif version & ID3_V2:
524          self.__saveV2Tag(version);
525          return 1;
526       else:
527          raise TagException("Invalid version: %s" % hex(version));
528       return 0;
529
530    # Remove the tag.  The version argument can selectively remove specific
531    # ID3 tag versions; the default is ID3_CURRENT_VERSION meaning the version
532    # of the current tag.  A value of ID3_ANY_VERSION causes all tags to be
533    # removed.
534    def remove(self, version = ID3_CURRENT_VERSION):
535       if not self.linkedFile:
536          raise TagException("The Tag is not linked to a file; nothing to "\
537                             "remove.");
538
539       if version == ID3_CURRENT_VERSION:
540          version = self.getVersion();
541
542       retval = 0;   
543       if version & ID3_V1 or version == ID3_ANY_VERSION:
544          tagFile = file(self.linkedFile.name, "r+b");
545          tagFile.seek(-128, 2);
546          if tagFile.read(3) == "TAG":
547             TRACE_MSG("Removing ID3 v1.x Tag");
548             tagFile.seek(-3, 1);
549             tagFile.truncate();
550             retval |= 1;
551          tagFile.close();
552
553       if ((version & ID3_V2) or (version == ID3_ANY_VERSION)) and\
554           self.header.tagSize:
555          tagFile = file(self.linkedFile.name, "r+b");
556          if tagFile.read(3) == "ID3":
557             TRACE_MSG("Removing ID3 v2.x Tag");
558             tagSize = self.header.tagSize + self.header.SIZE;
559             tagFile.seek(tagSize);
560             data = tagFile.read();
561             tagFile.seek(0);
562             tagFile.write(data);
563             tagFile.truncate();
564             tagFile.close();
565             retval |= 1;
566
567       return retval;
568
569
570
571    # Get artist.  There are a few frames that can contain this information,
572    # and they are subtley different.
573    #   eyeD3.frames.ARTIST_FID - Lead performer(s)/Soloist(s)
574    #   eyeD3.frames.BAND_FID - Band/orchestra/accompaniment
575    #   eyeD3.frames.CONDUCTOR_FID - Conductor/performer refinement
576    #   eyeD3.frames.REMIXER_FID - Interpreted, remixed, or otherwise modified by
577    #
578    # Any of these values can be passed as an argument to select the artist
579    # of interest.  By default, the first one found (searched in the above order)
580    # is the value returned.  Most tags only have the ARTIST_FID, btw.
581    #
582    # When no artist is found, an empty string is returned.
583    #
584    def getArtist(self, artistID = ARTIST_FIDS):
585       if isinstance(artistID, list):
586          frameIDs = artistID;
587       else:
588          frameIDs = [artistID];
589
590       for fid in frameIDs:
591          f = self.frames[fid];
592          if f:
593              return f[0].text;
594       return u"";
595  
596    def getAlbum(self):
597       f = self.frames[ALBUM_FID];
598       if f:
599          return f[0].text;
600       else:
601          return u"";
602
603    # Get the track title.  By default the main title is returned.  Optionally,
604    # you can pass:
605    #   eyeD3.frames.TITLE_FID - The title; the default.
606    #   eyeD3.frames.SUBTITLE_FID - The subtitle.
607    #   eyeD3.frames.CONTENT_TITLE_FID - Conten group description???? Rare.
608    # An empty string is returned when no title exists.
609    def getTitle(self, titleID = TITLE_FID):
610       f = self.frames[titleID];
611       if f:
612          return f[0].text;
613       else:
614          return u"";
615
616    def getDate(self, fid = None):
617        if not fid:
618            for fid in DATE_FIDS:
619                if self.frames[fid]:
620                    return self.frames[fid];
621            return None;
622        return self.frames[fid];
623
624    def getYear(self, fid = None):
625        dateFrame = self.getDate(fid);
626        if dateFrame:
627            return dateFrame[0].getYear();
628        else:
629            return None;
630            
631    # Throws GenreException when the tag contains an unrecognized genre format.
632    # Note this method returns a eyeD3.Genre object, not a raw string.
633    def getGenre(self):
634       f = self.frames[GENRE_FID];
635       if f and f[0].text:
636          g = Genre();
637          g.parse(f[0].text);
638          return g;
639       else:
640          return None;
641
642    def _getNum(self, fid):
643       tn = None
644       tt = None
645       f = self.frames[fid];
646       if f:
647          n = f[0].text.split('/')
648          if len(n) == 1:
649             tn = self.toInt(n[0])
650          elif len(n) == 2:
651             tn = self.toInt(n[0])
652             tt = self.toInt(n[1])
653       return (tn, tt)     
654
655    # Returns a tuple with the first value containing the track number and the
656    # second the total number of tracks.  One or both of these values may be
657    # None depending on what is available in the tag.
658    def getTrackNum(self):
659       return self._getNum(TRACKNUM_FID)
660
661    # Like TrackNum, except for DiscNum--that is, position in a set. Most
662    # songs won't have this or it will be 1/1.
663    def getDiscNum(self):
664       return self._getNum(DISCNUM_FID)
665
666    # Since multiple comment frames are allowed this returns a list with 0
667    # or more elements.  The elements are not the comment strings, they are
668    # eyeD3.frames.CommentFrame objects.
669    def getComments(self):
670       return self.frames[COMMENT_FID];
671
672    # Returns a list (possibly empty) of eyeD3.frames.ImageFrame objects.
673    def getImages(self):
674       return self.frames[IMAGE_FID];
675
676    # Returns a list (possibly empty) of eyeD3.frames.URLFrame objects.
677    # Both URLFrame and UserURLFrame objects are returned.  UserURLFrames
678    # add a description and encoding, and have a different frame ID.
679    def getURLs(self):
680       urls = list();
681       for fid in URL_FIDS:
682          urls.extend(self.frames[fid]);
683       urls.extend(self.frames[USERURL_FID]);
684       return urls;
685
686    def getUserTextFrames(self):
687       return self.frames[USERTEXT_FID];
688
689    def getCDID(self):
690       return self.frames[CDID_FID];
691
692    def getVersion(self):
693       return self.header.version;
694
695    def getVersionStr(self):
696       return versionToString(self.header.version);
697
698    def strToUnicode(self, s):
699        t = type(s);
700        if t != unicode and t == str:
701            s = unicode(s, eyeD3.LOCAL_ENCODING);
702        elif t != unicode and t != str:
703            raise TagException("Wrong type passed to strToUnicode: %s" % str(t));
704        return s;
705
706    # Set the artist name.  Arguments equal to None or "" cause the frame to
707    # be removed. An optional second argument can be passed to select the
708    # actual artist frame that should be set.  By default, the main artist frame
709    # (TPE1) is the value used.
710    def setArtist(self, a, id = ARTIST_FID):
711        self.setTextFrame(id, self.strToUnicode(a));
712
713    def setAlbum(self, a):
714        self.setTextFrame(ALBUM_FID, self.strToUnicode(a));
715
716    def setTitle(self, t, titleID = TITLE_FID):
717        self.setTextFrame(titleID, self.strToUnicode(t));
718
719    def setDate(self, year, month = None, dayOfMonth = None,
720                hour = None, minute = None, second = None, fid = None):
721       if not year and not fid:
722           dateFrames = self.getDate();
723           if dateFrames:
724               self.frames.removeFramesByID(dateFrames[0].header.id);
725           return;
726       elif not year:
727           self.frames.removeFramesByID(fid);
728
729       dateStr = self.strToUnicode(str(year));
730       if len(dateStr) != 4:
731          raise TagException("Invalid Year field: " + dateStr);
732       if month:
733          dateStr += "-" + self.__padDateField(month);
734          if dayOfMonth:
735             dateStr += "-" + self.__padDateField(dayOfMonth);
736             if hour:
737                dateStr += "T" + self.__padDateField(hour);
738                if minute:
739                   dateStr += ":" + self.__padDateField(minute);
740                   if second:
741                      dateStr += ":" + self.__padDateField(second);
742
743       if not fid:
744           fid = "TDRL";
745       dateFrame = self.frames[fid];
746       try:
747          if dateFrame:
748             dateFrame[0].setDate(self.encoding + dateStr);
749          else:
750             header = FrameHeader(self.header);
751             header.id = fid;
752             dateFrame = DateFrame(header, encoding = self.encoding,
753                                   date_str = self.strToUnicode(dateStr));
754             self.frames.addFrame(dateFrame);
755       except FrameException, ex:
756          raise TagException(str(ex));
757
758    # Three types are accepted for the genre parameter.  A Genre object, an
759    # acceptable (see Genre.parse) genre string, or an integer genre id.
760    # Arguments equal to None or "" cause the frame to be removed.
761    def setGenre(self, g):
762       if g == None or g == "":
763           self.frames.removeFramesByID(GENRE_FID);
764           return;
765
766       if isinstance(g, Genre):
767           self.frames.setTextFrame(GENRE_FID, self.strToUnicode(str(g)),
768                                    self.encoding);
769       elif isinstance(g, str):
770           gObj = Genre();
771           gObj.parse(g);
772           self.frames.setTextFrame(GENRE_FID, self.strToUnicode(str(gObj)),
773                                    self.encoding);
774       elif isinstance(g, int):
775           gObj = Genre();
776           gObj.id = g;
777           self.frames.setTextFrame(GENRE_FID, self.strToUnicode(str(gObj)),
778                                    self.encoding);
779       else:
780           raise TagException("Invalid type passed to setGenre: %s" +
781                              str(type(g)));
782
783    # Accepts a tuple with the first value containing the track number and the
784    # second the total number of tracks.  One or both of these values may be
785    # None.  If both values are None, the frame is removed.
786    def setTrackNum(self, n):
787       self.setNum(TRACKNUM_FID, n)
788
789    def setDiscNum(self, n):
790       self.setNum(DISCNUM_FID, n)
791    
792    def setNum(self, fid, n):
793       if n[0] == None and n[1] == None:
794          self.frames.removeFramesByID(fid);
795          return;
796
797       totalStr = "";
798       zPadding = 1;
799       if n[1] != None:
800          if n[1] >= 0 and n[1] <= 9:
801             totalStr = "0" + str(n[1]);
802          else:
803             totalStr = str(n[1]);
804          zPadding = len(totalStr) - 1;
805
806       t = n[0];
807       if t == None:
808          t = 0;
809
810       # Pad with zeros according to how large the total count is.
811       trackStr = str(t);
812       if len(trackStr) == 1:
813          trackStr = "0" + trackStr;
814       if len(trackStr) < len(totalStr):
815          trackStr = ("0" * (len(totalStr) - len(trackStr))) + trackStr;
816
817       s = "";
818       if trackStr and totalStr:
819          s = trackStr + "/" + totalStr;
820       elif trackStr and not totalStr:
821          s = trackStr;
822
823       self.frames.setTextFrame(fid, self.strToUnicode(s),
824                                self.encoding);
825
826
827    # Add a comment.  This adds a comment unless one is already present with
828    # the same language and description in which case the current value is
829    # either changed (cmt != "") or removed (cmt equals "" or None).
830    def addComment(self, cmt, desc = u"", lang = DEFAULT_LANG):