not needed with 20Mi
[pyTivo/wgw.git] / eyeD3 / tag.py
blobfa2bd065c845d34862ef8b48f0b6a2629f0ac719
1 ###############################################################################
2 # Copyright (C) 2002-2007 Travis Shirk <travis@pobox.com>
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 2 of the License, or
7 # (at your option) any later version.
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
18 ################################################################################
19 import re, os, string, stat, shutil, tempfile, binascii;
20 import mimetypes;
21 from stat import *;
22 from eyeD3 import *;
23 import eyeD3.utils;
24 import eyeD3.mp3;
25 from frames import *;
26 from binfuncs import *;
27 import math;
29 ID3_V1_COMMENT_DESC = "ID3 v1 Comment";
31 ################################################################################
32 class TagException(Exception):
33 '''error reading tag'''
35 ################################################################################
36 class TagHeader:
37 SIZE = 10;
39 version = None;
40 majorVersion = None;
41 minorVersion = None;
42 revVersion = None;
44 # Flag bits
45 unsync = 0;
46 extended = 0;
47 experimental = 0;
48 # v2.4 addition
49 footer = 0;
51 # The size in the most recently parsed header.
52 tagSize = 0;
54 # Constructor
55 def __init__(self):
56 self.clear();
58 def clear(self):
59 self.setVersion(None);
60 self.unsync = 0;
61 self.extended = 0;
62 self.experimental = 0;
63 self.tagSize = 0;
65 def setVersion(self, v):
66 if v == None:
67 self.version = None;
68 self.majorVersion = None;
69 self.minorVersion = None;
70 self.revVersion = None;
71 return;
73 if v == ID3_CURRENT_VERSION:
74 if self.majorVersion == None or self.minorVersion == None:
75 v = ID3_DEFAULT_VERSION;
76 else:
77 return;
78 elif v == ID3_ANY_VERSION:
79 v = ID3_DEFAULT_VERSION;
81 # Handle 3-element lists or tuples.
82 if isinstance(v, tuple) or isinstance(v, list):
83 self.version = eyeD3.utils.versionsToConstant(v);
84 (self.majorVersion,
85 self.minorVersion,
86 self.revVersion) = v;
87 # Handle int constants.
88 elif isinstance(v, int):
89 (self.majorVersion,
90 self.minorVersion,
91 self.revVersion) = eyeD3.utils.constantToVersions(v);
92 self.version = v;
93 else:
94 raise TypeError("Wrong type: %s" % str(type(v)));
96 # Given a file handle this method attempts to identify and then parse
97 # a ID3 v2 header. If successful, the parsed values are stored in
98 # the instance variable. If the files does not contain an ID3v2 tag
99 # false is returned. A TagException is thrown if a tag is found, but is
100 # not valid or corrupt.
101 def parse(self, f):
102 self.clear();
104 # The first three bytes of a v2 header is "ID3".
105 if f.read(3) != "ID3":
106 return 0;
107 TRACE_MSG("Located ID3 v2 tag");
109 # The next 2 bytes are the minor and revision versions.
110 version = f.read(2);
111 major = 2;
112 minor = ord(version[0]);
113 rev = ord(version[1]);
114 TRACE_MSG("TagHeader [major]: " + str(major));
115 TRACE_MSG("TagHeader [minor]: " + str(minor));
116 TRACE_MSG("TagHeader [revis]: " + str(rev));
117 if not (major == 2 and (minor >= 2 and minor <= 4)):
118 raise TagException("ID3 v" + str(major) + "." + str(minor) +\
119 " is not supported.");
120 # Get all the version madness in sync.
121 self.setVersion([major, minor, rev]);
123 # The first 4 bits of the next byte are flags.
124 (self.unsync,
125 self.extended,
126 self.experimental,
127 self.footer) = bytes2bin(f.read(1))[0:4];
128 TRACE_MSG("TagHeader [flags]: unsync(%d) extended(%d) "\
129 "experimental(%d) footer(%d)" % (self.unsync, self.extended,
130 self.experimental,
131 self.footer));
133 # The size of the extended header (optional), frames, and padding
134 # afer unsynchronization. This is a sync safe integer, so only the
135 # bottom 7 bits of each byte are used.
136 tagSizeStr = f.read(4);
137 TRACE_MSG("TagHeader [size string]: 0x%02x%02x%02x%02x" %\
138 (ord(tagSizeStr[0]), ord(tagSizeStr[1]),
139 ord(tagSizeStr[2]), ord(tagSizeStr[3])));
140 self.tagSize = bin2dec(bytes2bin(tagSizeStr, 7));
141 TRACE_MSG("TagHeader [size]: %d (0x%x)" % (self.tagSize, self.tagSize));
143 return 1;
145 def render(self, tagLen = None):
146 if tagLen != None:
147 self.tagSize = tagLen;
149 data = "ID3";
150 data += chr(self.minorVersion) + chr(self.revVersion);
151 # not not the values so we only get 1's and 0's.
152 data += bin2bytes([not not self.unsync,
153 not not self.extended,
154 not not self.experimental,
155 not not self.footer,
156 0, 0, 0, 0]);
157 TRACE_MSG("Setting tag size to %d" % tagLen);
158 szBytes = bin2bytes(bin2synchsafe(dec2bin(tagLen, 32)));
159 data += szBytes;
160 TRACE_MSG("TagHeader rendered %d bytes" % len(data));
161 return data;
163 ################################################################################
164 class ExtendedTagHeader:
165 size = 0;
166 flags = 0;
167 crc = 0;
168 restrictions = 0;
170 def isUpdate(self):
171 return self.flags & 0x40;
172 def hasCRC(self):
173 return self.flags & 0x20;
174 def hasRestrictions(self, minor_version = None):
175 return self.flags & 0x10;
177 def setSizeRestrictions(self, v):
178 assert(v >= 0 and v <= 3);
179 self.restrictions = (v << 6) | (self.restrictions & 0x3f);
180 def getSizeRestrictions(self):
181 return self.restrictions >> 6;
182 def getSizeRestrictionsString(self):
183 val = self.getSizeRestrictions();
184 if val == 0x00:
185 return "No more than 128 frames and 1 MB total tag size.";
186 elif val == 0x01:
187 return "No more than 64 frames and 128 KB total tag size.";
188 elif val == 0x02:
189 return "No more than 32 frames and 40 KB total tag size.";
190 elif val == 0x03:
191 return "No more than 32 frames and 4 KB total tag size.";
193 def setTextEncodingRestrictions(self, v):
194 assert(v == 0 or v == 1);
195 self.restrictions ^= 0x20;
196 def getTextEncodingRestrictions(self):
197 return self.restrictions & 0x20;
198 def getTextEncodingRestrictionsString(self):
199 if self.getTextEncodingRestrictions():
200 return "Strings are only encoded with ISO-8859-1 [ISO-8859-1] or "\
201 "UTF-8 [UTF-8].";
202 else:
203 return "None";
205 def setTextFieldSizeRestrictions(self, v):
206 assert(v >= 0 and v <= 3);
207 self.restrictions = (v << 3) | (self.restrictions & 0xe7);
208 def getTextFieldSizeRestrictions(self):
209 return (self.restrictions >> 3) & 0x03;
210 def getTextFieldSizeRestrictionsString(self):
211 val = self.getTextFieldSizeRestrictions();
212 if val == 0x00:
213 return "None";
214 elif val == 0x01:
215 return "No string is longer than 1024 characters.";
216 elif val == 0x02:
217 return "No string is longer than 128 characters.";
218 elif val == 0x03:
219 return "No string is longer than 30 characters.";
221 def setImageEncodingRestrictions(self, v):
222 assert(v == 0 or v == 1);
223 self.restrictions ^= 0x04;
224 def getImageEncodingRestrictions(self):
225 return self.restrictions & 0x04;
226 def getImageEncodingRestrictionsString(self):
227 if self.getImageEncodingRestrictions():
228 return "Images are encoded only with PNG [PNG] or JPEG [JFIF].";
229 else:
230 return "None";
232 def setImageSizeRestrictions(self, v):
233 assert(v >= 0 and v <= 3);
234 self.restrictions = v | (self.restrictions & 0xfc);
235 def getImageSizeRestrictions(self):
236 return self.restrictions & 0x03;
237 def getImageSizeRestrictionsString(self):
238 val = self.getImageSizeRestrictions();
239 if val == 0x00:
240 return "None";
241 elif val == 0x01:
242 return "All images are 256x256 pixels or smaller.";
243 elif val == 0x02:
244 return "All images are 64x64 pixels or smaller.";
245 elif val == 0x03:
246 return "All images are exactly 64x64 pixels, unless required "\
247 "otherwise.";
249 def _syncsafeCRC(self):
250 bites = ""
251 bites += chr((self.crc >> 28) & 0x7f);
252 bites += chr((self.crc >> 21) & 0x7f);
253 bites += chr((self.crc >> 14) & 0x7f);
254 bites += chr((self.crc >> 7) & 0x7f);
255 bites += chr((self.crc >> 0) & 0x7f);
256 return bites;
259 def render(self, header, frameData, padding=0):
260 assert(header.majorVersion == 2);
262 data = "";
263 crc = None;
264 if header.minorVersion == 4:
265 # Version 2.4
266 size = 6;
267 # Extended flags.
268 if self.isUpdate():
269 data += "\x00";
270 if self.hasCRC():
271 data += "\x05";
272 # XXX: Using the absolute value of the CRC. The spec is unclear
273 # about the type of this data.
274 self.crc = int(math.fabs(binascii.crc32(frameData +\
275 ("\x00" * padding))));
276 crc_data = self._syncsafeCRC();
277 if len(crc_data) < 5:
278 crc_data = ("\x00" * (5 - len(crc_data))) + crc_data
279 assert(len(crc_data) == 5)
280 data += crc_data
281 if self.hasRestrictions():
282 data += "\x01";
283 assert(len(self.restrictions) == 1);
284 data += self.restrictions;
285 TRACE_MSG("Rendered extended header data (%d bytes)" % len(data));
287 # Extended header size.
288 size = bin2bytes(bin2synchsafe(dec2bin(len(data) + 6, 32)))
289 assert(len(size) == 4);
291 data = size + "\x01" + bin2bytes(dec2bin(self.flags)) + data;
292 TRACE_MSG("Rendered extended header of size %d" % len(data));
293 else:
294 # Version 2.3
295 size = 6; # Note, the 4 size bytes are not included in the size
296 # Extended flags.
297 f = [0] * 16;
298 if self.hasCRC():
299 f[0] = 1;
300 # XXX: Using the absolute value of the CRC. The spec is unclear
301 # about the type of this value.
302 self.crc = int(math.fabs(binascii.crc32(frameData +\
303 ("\x00" * padding))));
304 crc = bin2bytes(dec2bin(self.crc));
305 assert(len(crc) == 4);
306 size += 4;
307 flags = bin2bytes(f);
308 assert(len(flags) == 2);
309 # Extended header size.
310 size = bin2bytes(dec2bin(size, 32))
311 assert(len(size) == 4);
312 # Padding size
313 paddingSize = bin2bytes(dec2bin(padding, 32));
315 data = size + flags + paddingSize;
316 if crc:
317 data += crc;
318 return data;
320 # Only call this when you *know* there is an extened header.
321 def parse(self, fp, header):
322 assert(header.majorVersion == 2);
324 TRACE_MSG("Parsing extended header @ 0x%x" % fp.tell());
325 # First 4 bytes is the size of the extended header.
326 data = fp.read(4);
327 if header.minorVersion == 4:
328 # sync-safe
329 sz = bin2dec(bytes2bin(data, 7));
330 self.size = sz
331 TRACE_MSG("Extended header size (includes the 4 size bytes): %d" % sz);
332 data = fp.read(sz - 4);
334 if ord(data[0]) != 1 or (ord(data[1]) & 0x8f):
335 # As of 2.4 the first byte is 1 and the second can only have
336 # bits 6, 5, and 4 set.
337 raise TagException("Invalid Extended Header");
339 offset = 2;
340 self.flags = ord(data[1]);
341 TRACE_MSG("Extended header flags: %x" % self.flags);
343 if self.isUpdate():
344 TRACE_MSG("Extended header has update bit set");
345 assert(ord(data[offset]) == 0);
346 offset += 1;
347 if self.hasCRC():
348 TRACE_MSG("Extended header has CRC bit set");
349 assert(ord(data[offset]) == 5);
350 offset += 1;
351 crcData = data[offset:offset + 5];
352 # This is sync-safe.
353 self.crc = bin2dec(bytes2bin(crcData, 7));
354 TRACE_MSG("Extended header CRC: %d" % self.crc);
355 offset += 5;
356 if self.hasRestrictions():
357 TRACE_MSG("Extended header has restrictions bit set");
358 assert(ord(data[offset]) == 5);
359 offset += 1;
360 self.restrictions = ord(data[offset]);
361 offset += 1;
362 else:
363 # v2.3 is totally different... *sigh*
364 sz = bin2dec(bytes2bin(data));
365 TRACE_MSG("Extended header size (not including 4 size bytes): %d" % sz)
366 self.size = sz + 4 # +4 to include size bytes
367 tmpFlags = fp.read(2);
368 # Read the padding size, but it'll be computed during the parse.
369 ps = fp.read(4);
370 TRACE_MSG("Extended header says there is %d bytes of padding" %
371 bin2dec(bytes2bin(ps)));
372 # Make this look like a v2.4 mask.
373 self.flags = ord(tmpFlags[0]) >> 2;
374 if self.hasCRC():
375 TRACE_MSG("Extended header has CRC bit set");
376 crcData = fp.read(4);
377 self.crc = bin2dec(bytes2bin(crcData));
378 TRACE_MSG("Extended header CRC: %d" % self.crc);
381 ################################################################################
382 # ID3 tag class. The class is capable of reading v1 and v2 tags. ID3 v1.x
383 # are converted to v2 frames.
384 class Tag:
385 # Latin1 is the default (0x00)
386 encoding = DEFAULT_ENCODING;
388 # ID3v1 tags do not contain a header. The only ID3v1 values stored
389 # in this header are the major/minor version.
390 header = TagHeader();
392 # Optional in v2 tags.
393 extendedHeader = ExtendedTagHeader();
395 # Contains the tag's frames. ID3v1 fields are read and converted
396 # the the corresponding v2 frame.
397 frames = None;
399 # Used internally for iterating over frames.
400 iterIndex = None;
402 # If this value is None the tag is not linked to any particular file..
403 linkedFile = None;
405 # add TDTG (or TXXX) - Tagging Time - when saved
406 do_tdtg = True
408 # Constructor. An empty tag is created and the link method is used
409 # to read an mp3 file's v1.x or v2.x tag. You can optionally set a
410 # file name, but it will not be read, but may be written to.
411 def __init__(self, fileName = None):
412 if fileName:
413 self.linkedFile = LinkedFile(fileName);
414 self.clear();
416 def clear(self):
417 self.header = TagHeader();
418 self.frames = FrameSet(self.header);
419 self.iterIndex = None;
421 # Returns an read-only iterator for all frames.
422 def __iter__(self):
423 if len(self.frames):
424 self.iterIndex = 0;
425 else:
426 self.iterIndex = None;
427 return self;
429 def next(self):
430 if self.iterIndex == None or self.iterIndex == len(self.frames):
431 raise StopIteration;
432 frm = self.frames[self.iterIndex];
433 self.iterIndex += 1;
434 return frm;
436 # Returns true when an ID3 tag is read from f which may be a file name
437 # or an aleady opened file object. In the latter case, the file object
438 # is not closed when this method returns.
440 # By default, both ID3 v2 and v1 tags are parsed in that order.
441 # If a v2 tag is found then a v1 parse is not performed. This behavior
442 # can be refined by passing ID3_V1 or ID3_V2 as the second argument
443 # instead of the default ID3_ANY_VERSION.
445 # Converts all ID3v1 data into ID3v2 frames internally.
446 # May throw IOError, or TagException if parsing fails.
447 def link(self, f, v = ID3_ANY_VERSION):
448 self.linkedFile = None;
449 self.clear();
451 fileName = "";
452 if isinstance(f, file):
453 fileName = f.name;
454 elif isinstance(f, str) or isinstance(f, unicode):
455 fileName = f;
456 else:
457 raise TagException("Invalid type passed to Tag.link: " +
458 str(type(f)));
460 if v != ID3_V1 and v != ID3_V2 and v != ID3_ANY_VERSION:
461 raise TagException("Invalid version: " + hex(v));
463 tagFound = 0;
464 padding = 0;
465 TRACE_MSG("Linking File: " + fileName);
466 if v == ID3_V1:
467 if self.__loadV1Tag(f):
468 tagFound = 1;
469 elif v == ID3_V2:
470 padding = self.__loadV2Tag(f);
471 if padding >= 0:
472 tagFound = 1;
473 elif v == ID3_ANY_VERSION:
474 padding = self.__loadV2Tag(f);
475 if padding >= 0:
476 tagFound = 1;
477 else:
478 padding = 0;
479 if self.__loadV1Tag(f):
480 tagFound = 1;
482 self.linkedFile = LinkedFile(fileName);
483 if tagFound:
484 # In the case of a v1.x tag this is zero.
485 self.linkedFile.tagSize = self.header.tagSize;
486 self.linkedFile.tagPadding = padding;
487 else:
488 self.linkedFile.tagSize = 0;
489 self.linkedFile.tagPadding = 0;
490 return tagFound;
492 # Write the current tag state to the linked file.
493 # The version of the ID3 file format that should be written can
494 # be passed as an argument; the default is ID3_CURRENT_VERSION.
495 def update(self, version = ID3_CURRENT_VERSION, backup = 0):
496 if not self.linkedFile:
497 raise TagException("The Tag is not linked to a file.");
499 if backup:
500 shutil.copyfile(self.linkedFile.name, self.linkedFile.name + ".orig");
502 self.setVersion(version);
503 version = self.getVersion();
504 if version == ID3_V2_2:
505 raise TagException("Unable to write ID3 v2.2");
506 # If v1.0 is being requested explicitly then so be it, if not and there is
507 # a track number then bumping to v1.1 is /probably/ best.
508 if self.header.majorVersion == 1 and self.header.minorVersion == 0 and\
509 self.getTrackNum()[0] != None and version != ID3_V1_0:
510 version = ID3_V1_1;
511 self.setVersion(version);
513 # If there are no frames then simply remove the current tag.
514 if len(self.frames) == 0:
515 self.remove(version);
516 self.header = TagHeader();
517 self.frames.setTagHeader(self.header);
518 self.linkedFile.tagPadding = 0;
519 self.linkedFile.tagSize = 0;
520 return;
522 if version & ID3_V1:
523 self.__saveV1Tag(version);
524 return 1;
525 elif version & ID3_V2:
526 self.__saveV2Tag(version);
527 return 1;
528 else:
529 raise TagException("Invalid version: %s" % hex(version));
530 return 0;
532 # Remove the tag. The version argument can selectively remove specific
533 # ID3 tag versions; the default is ID3_CURRENT_VERSION meaning the version
534 # of the current tag. A value of ID3_ANY_VERSION causes all tags to be
535 # removed.
536 def remove(self, version = ID3_CURRENT_VERSION):
537 if not self.linkedFile:
538 raise TagException("The Tag is not linked to a file; nothing to "\
539 "remove.");
541 if version == ID3_CURRENT_VERSION:
542 version = self.getVersion();
544 retval = 0;
545 if version & ID3_V1 or version == ID3_ANY_VERSION:
546 tagFile = file(self.linkedFile.name, "r+b");
547 tagFile.seek(-128, 2);
548 if tagFile.read(3) == "TAG":
549 TRACE_MSG("Removing ID3 v1.x Tag");
550 tagFile.seek(-3, 1);
551 tagFile.truncate();
552 retval |= 1;
553 tagFile.close();
555 if ((version & ID3_V2) or (version == ID3_ANY_VERSION)) and\
556 self.header.tagSize:
557 tagFile = file(self.linkedFile.name, "r+b");
558 if tagFile.read(3) == "ID3":
559 TRACE_MSG("Removing ID3 v2.x Tag");
560 tagSize = self.header.tagSize + self.header.SIZE;
561 tagFile.seek(tagSize);
563 # Open tmp file
564 tmpName = tempfile.mktemp();
565 tmpFile = file(tmpName, "w+b");
567 # Write audio data in chunks
568 self.__copyRemaining(tagFile, tmpFile);
569 tagFile.truncate();
570 tagFile.close();
572 tmpFile.close();
574 # Move tmp to orig.
575 shutil.copyfile(tmpName, self.linkedFile.name);
576 os.unlink(tmpName);
578 retval |= 1;
580 return retval;
582 # Get artist. There are a few frames that can contain this information,
583 # and they are subtley different.
584 # eyeD3.frames.ARTIST_FID - Lead performer(s)/Soloist(s)
585 # eyeD3.frames.BAND_FID - Band/orchestra/accompaniment
586 # eyeD3.frames.CONDUCTOR_FID - Conductor/performer refinement
587 # eyeD3.frames.REMIXER_FID - Interpreted, remixed, or otherwise modified by
589 # Any of these values can be passed as an argument to select the artist
590 # of interest. By default, the first one found (searched in the above order)
591 # is the value returned. Most tags only have the ARTIST_FID, btw.
593 # When no artist is found, an empty string is returned.
595 def getArtist(self, artistID = ARTIST_FIDS):
596 if isinstance(artistID, list):
597 frameIDs = artistID;
598 else:
599 frameIDs = [artistID];
601 for fid in frameIDs:
602 f = self.frames[fid];
603 if f:
604 return f[0].text;
605 return u"";
607 def getAlbum(self):
608 f = self.frames[ALBUM_FID];
609 if f:
610 return f[0].text;
611 else:
612 return u"";
614 # Get the track title. By default the main title is returned. Optionally,
615 # you can pass:
616 # eyeD3.frames.TITLE_FID - The title; the default.
617 # eyeD3.frames.SUBTITLE_FID - The subtitle.
618 # eyeD3.frames.CONTENT_TITLE_FID - Conten group description???? Rare.
619 # An empty string is returned when no title exists.
620 def getTitle(self, titleID = TITLE_FID):
621 f = self.frames[titleID];
622 if f:
623 return f[0].text;
624 else:
625 return u"";
627 def getDate(self, fid = None):
628 if not fid:
629 for fid in DATE_FIDS:
630 if self.frames[fid]:
631 return self.frames[fid];
632 return None;
633 return self.frames[fid];
635 def getYear(self, fid = None):
636 dateFrame = self.getDate(fid);
637 if dateFrame:
638 return dateFrame[0].getYear();
639 else:
640 return None;
642 # Throws GenreException when the tag contains an unrecognized genre format.
643 # Note this method returns a eyeD3.Genre object, not a raw string.
644 def getGenre(self):
645 f = self.frames[GENRE_FID];
646 if f and f[0].text:
647 g = Genre();
648 g.parse(f[0].text);
649 return g;
650 else:
651 return None;
653 def _getNum(self, fid):
654 tn = None
655 tt = None
656 f = self.frames[fid];
657 if f:
658 n = f[0].text.split('/')
659 if len(n) == 1:
660 tn = self.toInt(n[0])
661 elif len(n) == 2:
662 tn = self.toInt(n[0])
663 tt = self.toInt(n[1])
664 return (tn, tt)
666 # Returns a tuple with the first value containing the track number and the
667 # second the total number of tracks. One or both of these values may be
668 # None depending on what is available in the tag.
669 def getTrackNum(self):
670 return self._getNum(TRACKNUM_FID)
672 # Like TrackNum, except for DiscNum--that is, position in a set. Most
673 # songs won't have this or it will be 1/1.
674 def getDiscNum(self):
675 return self._getNum(DISCNUM_FID)
677 # Since multiple comment frames are allowed this returns a list with 0
678 # or more elements. The elements are not the comment strings, they are
679 # eyeD3.frames.CommentFrame objects.
680 def getComments(self):
681 return self.frames[COMMENT_FID];
683 # Since multiple lyrics frames are allowed this returns a list with 0
684 # or more elements. The elements are not the lyrics strings, they are
685 # eyeD3.frames.LyricsFrame objects.
686 def getLyrics(self):
687 return self.frames[LYRICS_FID];
689 # Returns a list (possibly empty) of eyeD3.frames.ImageFrame objects.
690 def getImages(self):
691 return self.frames[IMAGE_FID];
693 # Returns a list (possibly empty) of eyeD3.frames.ObjectFrame objects.
694 def getObjects(self):
695 return self.frames[OBJECT_FID];
697 # Returns a list (possibly empty) of eyeD3.frames.URLFrame objects.
698 # Both URLFrame and UserURLFrame objects are returned. UserURLFrames
699 # add a description and encoding, and have a different frame ID.
700 def getURLs(self):
701 urls = list();
702 for fid in URL_FIDS:
703 urls.extend(self.frames[fid]);
704 urls.extend(self.frames[USERURL_FID]);
705 return urls;
707 def getUserTextFrames(self):
708 return self.frames[USERTEXT_FID];
710 def getCDID(self):
711 return self.frames[CDID_FID];
713 def getVersion(self):
714 return self.header.version;
716 def getVersionStr(self):
717 return versionToString(self.header.version);
719 def strToUnicode(self, s):
720 t = type(s);
721 if t != unicode and t == str:
722 s = unicode(s, eyeD3.LOCAL_ENCODING);
723 elif t != unicode and t != str:
724 raise TagException("Wrong type passed to strToUnicode: %s" % str(t));
725 return s;
727 # Set the artist name. Arguments equal to None or "" cause the frame to
728 # be removed. An optional second argument can be passed to select the
729 # actual artist frame that should be set. By default, the main artist frame
730 # (TPE1) is the value used.
731 def setArtist(self, a, id = ARTIST_FID):
732 self.setTextFrame(id, self.strToUnicode(a));
734 def setAlbum(self, a):
735 self.setTextFrame(ALBUM_FID, self.strToUnicode(a));
737 def setTitle(self, t, titleID = TITLE_FID):
738 self.setTextFrame(titleID, self.strToUnicode(t));
740 def setDate(self, year, month = None, dayOfMonth = None,
741 hour = None, minute = None, second = None, fid = None):
742 if not year and not fid:
743 dateFrames = self.getDate();
744 if dateFrames:
745 self.frames.removeFramesByID(dateFrames[0].header.id)
746 return
747 elif not year:
748 self.frames.removeFramesByID(fid)
749 else:
750 self.frames.removeFramesByID(frames.OBSOLETE_YEAR_FID)
752 dateStr = self.strToUnicode(str(year));
753 if len(dateStr) != 4:
754 raise TagException("Invalid Year field: " + dateStr);
755 if month:
756 dateStr += "-" + self.__padDateField(month);
757 if dayOfMonth:
758 dateStr += "-" + self.__padDateField(dayOfMonth);
759 if hour:
760 dateStr += "T" + self.__padDateField(hour);
761 if minute:
762 dateStr += ":" + self.__padDateField(minute);
763 if second:
764 dateStr += ":" + self.__padDateField(second);
766 if not fid:
767 fid = "TDRL";
768 dateFrame = self.frames[fid];
769 try:
770 if dateFrame:
771 dateFrame[0].setDate(self.encoding + dateStr);
772 else:
773 header = FrameHeader(self.header);
774 header.id = fid;
775 dateFrame = DateFrame(header, encoding = self.encoding,
776 date_str = self.strToUnicode(dateStr));
777 self.frames.addFrame(dateFrame);
778 except FrameException, ex:
779 raise TagException(str(ex));
781 # Three types are accepted for the genre parameter. A Genre object, an
782 # acceptable (see Genre.parse) genre string, or an integer genre id.
783 # Arguments equal to None or "" cause the frame to be removed.
784 def setGenre(self, g):
785 if g == None or g == "":
786 self.frames.removeFramesByID(GENRE_FID);
787 return;
789 if isinstance(g, Genre):
790 self.frames.setTextFrame(GENRE_FID, self.strToUnicode(str(g)),
791 self.encoding);
792 elif isinstance(g, str):
793 gObj = Genre();
794 gObj.parse(g);
795 self.frames.setTextFrame(GENRE_FID, self.strToUnicode(str(gObj)),
796 self.encoding);
797 elif isinstance(g, int):
798 gObj = Genre();
799 gObj.id = g;
800 self.frames.setTextFrame(GENRE_FID, self.strToUnicode(str(gObj)),
801 self.encoding);
802 else:
803 raise TagException("Invalid type passed to setGenre: %s" +
804 str(type(g)));
806 # Accepts a tuple with the first value containing the track number and the
807 # second the total number of tracks. One or both of these values may be
808 # None. If both values are None, the frame is removed.
809 def setTrackNum(self, n, zeropad = True):
810 self.setNum(TRACKNUM_FID, n, zeropad)
812 def setDiscNum(self, n, zeropad = True):
813 self.setNum(DISCNUM_FID, n, zeropad)
815 def setNum(self, fid, n, zeropad = True):
816 if n[0] == None and n[1] == None:
817 self.frames.removeFramesByID(fid);
818 return;
820 totalStr = "";
821 if n[1] != None:
822 if zeropad and n[1] >= 0 and n[1] <= 9:
823 totalStr = "0" + str(n[1]);
824 else:
825 totalStr = str(n[1]);
827 t = n[0];
828 if t == None:
829 t = 0;
831 trackStr = str(t);
833 # Pad with zeros according to how large the total count is.
834 if zeropad:
835 if len(trackStr) == 1:
836 trackStr = "0" + trackStr;
837 if len(trackStr) < len(totalStr):
838 trackStr = ("0" * (len(totalStr) - len(trackStr))) + trackStr;
840 s = "";
841 if trackStr and totalStr:
842 s = trackStr + "/" + totalStr;
843 elif trackStr and not totalStr:
844 s = trackStr;
846 self.frames.setTextFrame(fid, self.strToUnicode(s),
847 self.encoding);
850 # Add a comment. This adds a comment unless one is already present with
851 # the same language and description in which case the current value is
852 # either changed (cmt != "") or removed (cmt equals "" or None).
853 def addComment(self, cmt, desc = u"", lang = DEFAULT_LANG):
854 if not cmt:
855 # A little more then a call to removeFramesByID is involved since we
856 # need to look at more than the frame ID.
857 comments = self.frames[COMMENT_FID];
858 for c in comments:
859 if c.lang == lang and c.description == desc:
860 self.frames.remove(c);
861 break;
862 else:
863 self.frames.setCommentFrame(self.strToUnicode(cmt),
864 self.strToUnicode(desc),
865 lang, self.encoding);
867 # Add lyrics. Semantics similar to addComment
868 def addLyrics(self, lyr, desc = u"", lang = DEFAULT_LANG):
869 if not lyr:
870 # A little more than a call to removeFramesByID is involved since we
871 # need to look at more than the frame ID.
872 lyrics = self.frames[LYRICS_FID];
873 for l in lyrics:
874 if l.lang == lang and l.description == desc:
875 self.frames.remove(l);
876 break;
877 else:
878 self.frames.setLyricsFrame(self.strToUnicode(lyr),
879 self.strToUnicode(desc),
880 lang, self.encoding);
882 # Semantics similar to addComment
883 def addUserTextFrame(self, desc, text):
884 if not text:
885 u_frames = self.frames[USERTEXT_FID];
886 for u in u_frames:
887 if u.description == desc:
888 self.frames.remove(u);
889 break;
890 else:
891 self.frames.setUserTextFrame(self.strToUnicode(text),
892 self.strToUnicode(desc), self.encoding);
894 def removeUserTextFrame(self, desc):
895 self.addUserTextFrame(desc, "")
897 def removeComments(self):
898 return self.frames.removeFramesByID(COMMENT_FID);
900 def removeLyrics(self):
901 return self.frames.removeFramesByID(LYRICS_FID);
903 def removeImages(self):
904 return self.frames.removeFramesByID(IMAGE_FID)
906 def addImage(self, type, image_file_path, desc = u""):
907 if image_file_path:
908 image_frame = ImageFrame.create(type, image_file_path, desc);
909 self.frames.addFrame(image_frame);
910 else:
911 image_frames = self.frames[IMAGE_FID];
912 for i in image_frames:
913 if i.pictureType == type:
914 self.frames.remove(i);
915 break;
917 def addObject(self, object_file_path, mime = "", desc = u"",
918 filename = None ):
919 object_frames = self.frames[OBJECT_FID];
920 for i in object_frames:
921 if i.description == desc:
922 self.frames.remove(i);
923 if object_file_path:
924 object_frame = ObjectFrame.create(object_file_path, mime, desc,
925 filename);
926 self.frames.addFrame(object_frame);
928 def getPlayCount(self):
929 if self.frames[PLAYCOUNT_FID]:
930 pc = self.frames[PLAYCOUNT_FID][0];
931 assert(isinstance(pc, PlayCountFrame));
932 return pc.count;
933 else:
934 return None;
936 def setPlayCount(self, count):
937 assert(count >= 0);
938 if self.frames[PLAYCOUNT_FID]:
939 pc = self.frames[PLAYCOUNT_FID][0];
940 assert(isinstance(pc, PlayCountFrame));
941 pc.count = count;
942 else:
943 frameHeader = FrameHeader(self.header);
944 frameHeader.id = PLAYCOUNT_FID;
945 pc = PlayCountFrame(frameHeader, count = count);
946 self.frames.addFrame(pc);
948 def incrementPlayCount(self, n = 1):
949 pc = self.getPlayCount();
950 if pc != None:
951 self.setPlayCount(pc + n);
952 else:
953 self.setPlayCount(n);
955 def getUniqueFileIDs(self):
956 return self.frames[UNIQUE_FILE_ID_FID];
958 def addUniqueFileID(self, owner_id, id):
959 if not id:
960 ufids = self.frames[UNIQUE_FILE_ID_FID];
961 for ufid in ufids:
962 if ufid.owner_id == owner_id:
963 self.frames.remove(ufid);
964 break;
965 else:
966 self.frames.setUniqueFileIDFrame(owner_id, id);
968 def getBPM(self):
969 bpm = self.frames[BPM_FID];
970 if bpm:
971 # Round floats since the spec says this is an integer
972 bpm = int(float(bpm[0].text) + 0.5)
973 return bpm
974 else:
975 return None;
977 def setBPM(self, bpm):
978 self.setTextFrame(BPM_FID, self.strToUnicode(str(bpm)))
980 def getPublisher(self):
981 pub = self.frames[PUBLISHER_FID];
982 if pub:
983 return pub[0].text or None;
985 def setPublisher(self, p):
986 self.setTextFrame(PUBLISHER_FID, self.strToUnicode(str(p)));
988 # Test ID3 major version.
989 def isV1(self):
990 return self.header.majorVersion == 1;
991 def isV2(self):
992 return self.header.majorVersion == 2;
994 def setVersion(self, v):
995 if v == ID3_V1:
996 v = ID3_V1_1;
997 elif v == ID3_V2:
998 v = ID3_DEFAULT_VERSION;
1000 if v != ID3_CURRENT_VERSION:
1001 self.header.setVersion(v);
1002 self.frames.setTagHeader(self.header);
1004 def setTextFrame(self, fid, txt):
1005 if not txt:
1006 self.frames.removeFramesByID(fid);
1007 else:
1008 self.frames.setTextFrame(fid, self.strToUnicode(txt), self.encoding);
1010 def setTextEncoding(self, enc):
1011 if enc != LATIN1_ENCODING and enc != UTF_16_ENCODING and\
1012 enc != UTF_16BE_ENCODING and enc != UTF_8_ENCODING:
1013 raise TagException("Invalid encoding");
1014 elif self.getVersion() & ID3_V1 and enc != LATIN1_ENCODING:
1015 raise TagException("ID3 v1.x supports ISO-8859 encoding only");
1016 elif self.getVersion() <= ID3_V2_3 and enc == UTF_8_ENCODING:
1017 # This is unfortunate.
1018 raise TagException("UTF-8 is not supported by ID3 v2.3");
1020 self.encoding = enc;
1021 for f in self.frames:
1022 f.encoding = enc;
1024 def tagToString(self, pattern):
1025 # %A - artist
1026 # %a - album
1027 # %t - title
1028 # %n - track number
1029 # %N - track total
1030 s = self._subst(pattern, "%A", self.getArtist());
1031 s = self._subst(s, "%a", self.getAlbum());
1032 s = self._subst(s, "%t", self.getTitle());
1033 s = self._subst(s, "%n", self._prettyTrack(self.getTrackNum()[0]));
1034 s = self._subst(s, "%N", self._prettyTrack(self.getTrackNum()[1]));
1035 return s;
1037 def _prettyTrack(self, track):
1038 if not track:
1039 return None;
1040 track_str = str(track);
1041 if len(track_str) == 1:
1042 track_str = "0" + track_str;
1043 return track_str;
1045 def _subst(self, name, pattern, repl):
1046 regex = re.compile(pattern);
1047 if regex.search(name) and repl:
1048 # No '/' characters allowed
1049 (repl, subs) = re.compile("/").subn("-", repl);
1050 (name, subs) = regex.subn(repl, name)
1051 return name;
1053 def __saveV1Tag(self, version):
1054 assert(version & ID3_V1);
1056 # Build tag buffer.
1057 tag = "TAG";
1058 tag += self._fixToWidth(self.getTitle().encode("latin_1"), 30);
1059 tag += self._fixToWidth(self.getArtist().encode("latin_1"), 30);
1060 tag += self._fixToWidth(self.getAlbum().encode("latin_1"), 30);
1061 y = self.getYear();
1062 if y is None:
1063 y = "";
1064 tag += self._fixToWidth(y.encode("latin_1"), 4);
1066 cmt = "";
1067 for c in self.getComments():
1068 if c.description == ID3_V1_COMMENT_DESC:
1069 cmt = c.comment;
1070 # We prefer this one over "";
1071 break;
1072 elif c.description == "":
1073 cmt = c.comment;
1074 # Keep searching in case we find the description eyeD3 uses.
1075 cmt = self._fixToWidth(cmt.encode("latin_1"), 30);
1076 if version != ID3_V1_0:
1077 track = self.getTrackNum()[0];
1078 if track != None:
1079 cmt = cmt[0:28] + "\x00" + chr(int(track) & 0xff);
1080 tag += cmt;
1082 if not self.getGenre() or self.getGenre().getId() is None:
1083 genre = 0;
1084 else:
1085 genre = self.getGenre().getId();
1086 tag += chr(genre & 0xff);
1088 assert(len(tag) == 128);
1090 tagFile = file(self.linkedFile.name, "r+b");
1091 # Write the tag over top an original or append it.
1092 try:
1093 tagFile.seek(-128, 2);
1094 if tagFile.read(3) == "TAG":
1095 tagFile.seek(-128, 2);
1096 else:
1097 tagFile.seek(0, 2);
1098 except IOError:
1099 # File is smaller than 128 bytes.
1100 tagFile.seek(0, 2);
1102 tagFile.write(tag);
1103 tagFile.flush();
1104 tagFile.close();
1106 def _fixToWidth(self, s, n):
1107 retval = str(s);
1108 retval = retval[0:n];
1109 retval = retval + ("\x00" * (n - len(retval)));
1110 return retval;
1112 # Returns false when an ID3 v1 tag is not present, or contains no data.
1113 def __loadV1Tag(self, f):
1114 if isinstance(f, str) or isinstance(f, unicode):
1115 fp = file(f, "rb")
1116 closeFile = 1;
1117 else:
1118 fp = f;
1119 closeFile = 0;
1121 # Seek to the end of the file where all ID3v1 tags are written.
1122 fp.seek(0, 2);
1123 strip_chars = string.whitespace + "\x00";
1124 if fp.tell() > 127:
1125 fp.seek(-128, 2);
1126 id3tag = fp.read(128);
1127 if id3tag[0:3] == "TAG":
1128 TRACE_MSG("Located ID3 v1 tag");
1129 # 1.0 is implied until a 1.1 feature is recognized.
1130 self.setVersion(ID3_V1_0);
1132 title = re.sub("\x00+$", "", id3tag[3:33].strip(strip_chars));
1133 TRACE_MSG("Tite: " + title);
1134 if title:
1135 self.setTitle(unicode(title, "latin1"));
1137 artist = re.sub("\x00+$", "", id3tag[33:63].strip(strip_chars));
1138 TRACE_MSG("Artist: " + artist);
1139 if artist:
1140 self.setArtist(unicode(artist, "latin1"));
1142 album = re.sub("\x00+$", "", id3tag[63:93].strip(strip_chars));
1143 TRACE_MSG("Album: " + album);
1144 if album:
1145 self.setAlbum(unicode(album, "latin1"));
1147 year = re.sub("\x00+$", "", id3tag[93:97].strip(strip_chars));
1148 TRACE_MSG("Year: " + year);
1149 try:
1150 if year and int(year):
1151 self.setDate(year);
1152 except ValueError:
1153 # Bogus year strings.
1154 pass;
1156 if re.sub("\x00+$", "", id3tag[97:127]):
1157 comment = id3tag[97:127];
1158 TRACE_MSG("Comment: " + comment);
1159 if comment[-2] == "\x00" and comment[-1] != "\x00":
1160 # Parse track number (added to ID3v1.1) if present.
1161 TRACE_MSG("Comment contains track number per v1.1 spec");
1162 track = ord(comment[-1]);
1163 self.setTrackNum((track, None));
1164 TRACE_MSG("Track: " + str(track));
1165 TRACE_MSG("Track Num found, setting version to v1.1s");
1166 self.setVersion(ID3_V1_1);
1167 comment = comment[:-2];
1168 else:
1169 track = None
1170 comment = re.sub("\x00+$", "", comment).rstrip();
1171 TRACE_MSG("Comment: " + comment);
1172 if comment:
1173 self.addComment(unicode(comment, 'latin1'),
1174 ID3_V1_COMMENT_DESC);
1176 genre = ord(id3tag[127:128])
1177 TRACE_MSG("Genre ID: " + str(genre));
1178 self.setGenre(genre);
1180 if closeFile:
1181 fp.close()
1182 return len(self.frames);
1184 def __saveV2Tag(self, version):
1185 assert(version & ID3_V2);
1186 TRACE_MSG("Rendering tag version: " + versionToString(version));
1188 self.setVersion(version);
1190 currPadding = 0;
1191 currTagSize = 0
1192 # We may be converting from 1.x to 2.x so we need to find any
1193 # current v2.x tag otherwise we're gonna hork the file.
1194 tmpTag = Tag();
1195 if tmpTag.link(self.linkedFile.name, ID3_V2):
1196 TRACE_MSG("Found current v2.x tag:");
1197 currTagSize = tmpTag.linkedFile.tagSize;
1198 TRACE_MSG("Current tag size: %d" % currTagSize);
1199 currPadding = tmpTag.linkedFile.tagPadding;
1200 TRACE_MSG("Current tag padding: %d" % currPadding);
1202 if self.do_tdtg:
1203 t = time.strftime("%Y-%m-%dT%H:%M:%S", time.gmtime());
1204 # Tag it!
1205 if self.header.minorVersion == 4:
1206 # TDTG for 2.4
1207 h = FrameHeader(self.header);
1208 h.id = "TDTG";
1209 dateFrame = DateFrame(h, date_str = self.strToUnicode(t),
1210 encoding = self.encoding);
1211 self.frames.removeFramesByID("TDTG");
1212 self.frames.addFrame(dateFrame);
1213 else:
1214 # TXXX (Tagging time) for older versions
1215 self.frames.removeFramesByID("TDTG");
1216 self.addUserTextFrame('Tagging time', t)
1218 # Render all frames first so the data size is known for the tag header.
1219 frameData = "";
1220 for f in self.frames:
1221 TRACE_MSG("Rendering frame: " + f.header.id);
1222 raw_frame = f.render();
1223 TRACE_MSG("Rendered %d bytes" % len(raw_frame));
1224 frameData += raw_frame;
1225 # Handle the overall tag header unsync bit. Frames themselves duplicate
1226 # this bit.
1227 if self.header.unsync:
1228 TRACE_MSG("Unsyncing all frames (sync-safe)");
1229 frameData = frames.unsyncData(frameData);
1231 rewriteFile = 0;
1232 paddingSize = 0;
1233 DEFAULT_PADDING = 1024
1234 def compute_padding():
1235 if currPadding <= DEFAULT_PADDING:
1236 return DEFAULT_PADDING
1237 else:
1238 return currPadding
1240 # Extended header
1241 extHeaderData = "";
1242 if self.header.extended:
1243 # This is sorta lame. We don't know the total framesize until
1244 # this is rendered, yet we can't render it witout knowing the
1245 # amount of padding for the crc. Force it.
1246 rewriteFile = 1;
1247 TRACE_MSG("Rendering extended header");
1248 paddingSize = compute_padding()
1249 extHeaderData += self.extendedHeader.render(self.header, frameData,
1250 paddingSize);
1252 new_size = 10 + len(extHeaderData) + len(frameData) + paddingSize
1253 if rewriteFile or new_size >= currTagSize:
1254 TRACE_MSG("File rewrite required");
1255 rewriteFile = 1;
1256 if paddingSize <= 0:
1257 paddingSize = compute_padding()
1258 elif paddingSize <= 0:
1259 paddingSize = currTagSize - (new_size - 10)
1260 TRACE_MSG("Adding %d bytes of padding" % paddingSize)
1261 frameData += ("\x00" * paddingSize);
1263 # Recompute with padding
1264 new_size = 10 + len(extHeaderData) + len(frameData)
1265 header_tag_size = new_size - 10
1267 # Render the tag header.
1268 TRACE_MSG("Rendering %s tag header with size %d" %
1269 (versionToString(self.getVersion()), header_tag_size))
1270 headerData = self.header.render(header_tag_size)
1272 # Assemble frame.
1273 tagData = headerData + extHeaderData + frameData;
1275 # Write the tag.
1276 if not rewriteFile:
1277 tagFile = file(self.linkedFile.name, "r+b");
1278 TRACE_MSG("Writing %d bytes of tag data" % len(tagData));
1279 tagFile.write(tagData);
1280 tagFile.close();
1281 else:
1282 # Open tmp file
1283 tmpName = tempfile.mktemp();
1284 tmpFile = file(tmpName, "w+b");
1285 TRACE_MSG("Writing %d bytes of tag data" % len(tagData));
1286 tmpFile.write(tagData);
1288 # Write audio data in chunks
1289 tagFile = file(self.linkedFile.name, "rb");
1290 if currTagSize != 0:
1291 seek_point = currTagSize + 10
1292 else:
1293 seek_point = 0
1294 TRACE_MSG("Seeking to beginning of audio data, byte %d (%x)" %
1295 (seek_point, seek_point))
1296 tagFile.seek(seek_point)
1297 self.__copyRemaining(tagFile, tmpFile);
1299 tagFile.close();
1300 tmpFile.close();
1302 # Move tmp to orig.
1303 shutil.copyfile(tmpName, self.linkedFile.name);
1304 os.unlink(tmpName);
1306 # Update our state.
1307 TRACE_MSG("Tag write complete. Updating state.");
1308 self.linkedFile.tagPadding = paddingSize;
1309 # XXX: getSize could cache sizes so to prevent rendering again.
1310 self.linkedFile.tagSize = self.frames.getSize();
1313 # Returns >= 0 to indicate the padding size of the read frame; -1 returned
1314 # when not tag was found.
1315 def __loadV2Tag(self, f):
1316 if isinstance(f, str) or isinstance(f, unicode):
1317 fp = file(f, "rb")
1318 closeFile = 1;
1319 else:
1320 fp = f;
1321 closeFile = 0;
1323 padding = -1;
1324 try:
1325 # Look for a tag and if found load it.
1326 if not self.header.parse(fp):
1327 return -1;
1329 # Read the extended header if present.
1330 if self.header.extended:
1331 self.extendedHeader.parse(fp, self.header);
1333 # Header is definitely there so at least one frame *must* follow.
1334 self.frames.setTagHeader(self.header);
1335 padding = self.frames.parse(fp, self.header, self.extendedHeader);
1336 TRACE_MSG("Tag contains %d bytes of padding." % padding);
1337 except FrameException, ex:
1338 fp.close();
1339 raise TagException(str(ex));
1340 except TagException:
1341 fp.close();
1342 raise;
1344 if closeFile:
1345 fp.close();
1346 return padding;
1348 def toInt(self, s):
1349 try:
1350 return int(s);
1351 except ValueError:
1352 return None;
1353 except TypeError:
1354 return None;
1356 def __padDateField(self, f):
1357 fStr = str(f);
1358 if len(fStr) == 2:
1359 pass;
1360 elif len(fStr) == 1:
1361 fStr = "0" + fStr;
1362 else:
1363 raise TagException("Invalid date field: " + fStr);
1364 return fStr;
1366 def __copyRemaining(self, src_fp, dest_fp):
1367 # Write audio data in chunks
1368 done = False
1369 amt = 1024 * 512
1370 while not done:
1371 data = src_fp.read(amt)
1372 if data:
1373 dest_fp.write(data)
1374 else:
1375 done = True
1376 del data
1378 # DEPRECATED
1379 # This method will return the first comment in the FrameSet
1380 # and not all of them. Multiple COMM frames are common and useful. Use
1381 # getComments which returns a list.
1382 def getComment(self):
1383 f = self.frames[COMMENT_FID];
1384 if f:
1385 return f[0].comment;
1386 else:
1387 return None;
1390 ################################################################################
1391 class GenreException(Exception):
1392 '''Problem looking up genre'''
1394 ################################################################################
1395 class Genre:
1396 id = None;
1397 name = None;
1399 def __init__(self, id = None, name = None):
1400 if id is not None:
1401 self.setId(id);
1402 elif name is not None:
1403 self.setName(name);
1405 def getId(self):
1406 return self.id;
1407 def getName(self):
1408 return self.name;
1410 # Sets the genre id. The objects name field is set to the corresponding
1411 # value obtained from eyeD3.genres.
1413 # Throws GenreException when name does not map to a valid ID3 v1.1. id.
1414 # This behavior can be disabled by passing 0 as the second argument.
1415 def setId(self, id):
1416 if not isinstance(id, int):
1417 raise TypeError("Invalid genre id: " + str(id));
1419 try:
1420 name = genres[id];
1421 except Exception, ex:
1422 if utils.strictID3():
1423 raise GenreException("Invalid genre id: " + str(id));
1425 if utils.strictID3() and not name:
1426 raise GenreException("Genre id maps to a null name: " + str(id));
1428 self.id = id;
1429 self.name = name;
1431 # Sets the genre name. The objects id field is set to the corresponding
1432 # value obtained from eyeD3.genres.
1434 # Throws GenreException when name does not map to a valid ID3 v1.1. name.
1435 # This behavior can be disabled by passing 0 as the second argument.
1436 def setName(self, name):
1437 if not isinstance(name, str):
1438 raise GenreException("Invalid genre name: " + str(name));
1440 try:
1441 id = genres[name];
1442 # Get titled case.
1443 name = genres[id];
1444 except:
1445 if utils.strictID3():
1446 raise GenreException("Invalid genre name: " + name);
1447 id = None;
1449 self.id = id;
1450 self.name = name;
1453 # Sets the genre id and name.
1455 # Throws GenreException when eyeD3.genres[id] != name (case insensitive).
1456 # This behavior can be disabled by passing 0 as the second argument.
1457 def set(self, id, name):
1458 if not isinstance(id, int):
1459 raise GenreException("Invalid genre id: " + id);
1460 if not isinstance(name, str):
1461 raise GenreException("Invalid genre name: " + str(name));
1463 if not utils.strictID3():
1464 self.id = id;
1465 self.name = name;
1466 else:
1467 try:
1468 if genres[name] != id:
1469 raise GenreException("eyeD3.genres[" + str(id) + "] " +\
1470 "does not match " + name);
1471 self.id = id;
1472 self.name = name;
1473 except:
1474 raise GenreException("eyeD3.genres[" + str(id) + "] " +\
1475 "does not match " + name);
1477 # Parses genre information from genreStr.
1478 # The following formats are supported:
1479 # 01, 2, 23, 125 - ID3 v1 style.
1480 # (01), (2), (129)Hardcore, (9)Metal - ID3 v2 style with and without
1481 # refinement.
1483 # Throws GenreException when an invalid string is passed.
1484 def parse(self, genreStr):
1485 genreStr =\
1486 str(genreStr.encode('utf-8')).strip(string.whitespace + '\x00');
1487 self.id = None;
1488 self.name = None;
1490 if not genreStr:
1491 return;
1493 # XXX: Utf-16 conversions leave a null byte at the end of the string.
1494 while genreStr[len(genreStr) - 1] == "\x00":
1495 genreStr = genreStr[:len(genreStr) - 1];
1496 if len(genreStr) == 0:
1497 break;
1499 # ID3 v1 style.
1500 # Match 03, 34, 129.
1501 regex = re.compile("[0-9][0-9]?[0-9]?$");
1502 if regex.match(genreStr):
1503 if len(genreStr) != 1 and genreStr[0] == '0':
1504 genreStr = genreStr[1:];
1506 self.setId(int(genreStr));
1507 return;
1509 # ID3 v2 style.
1510 # Match (03), (0)Blues, (15) Rap
1511 regex = re.compile("\(([0-9][0-9]?[0-9]?)\)(.*)$");
1512 m = regex.match(genreStr);
1513 if m:
1514 (id, name) = m.groups();
1515 if len(id) != 1 and id[0] == '0':
1516 id = id[1:];
1518 if id and name:
1519 self.set(int(id), name.strip());
1520 else:
1521 self.setId(int(id));
1522 return;
1524 # Non standard, but witnessed.
1525 # Match genre alone. e.g. Rap, Rock, blues, 'Rock|Punk|Pop-Punk', etc
1526 regex = re.compile("^[A-Z 0-9+/\-\|!&]+\00*$", re.IGNORECASE)
1527 if regex.match(genreStr):
1528 self.setName(genreStr);
1529 return;
1530 raise GenreException("Genre string cannot be parsed with '%s': %s" %\
1531 (regex.pattern, genreStr));
1533 def __str__(self):
1534 s = "";
1535 if self.id != None:
1536 s += "(" + str(self.id) + ")"
1537 if self.name:
1538 s += self.name;
1539 return s;
1541 ################################################################################
1542 class InvalidAudioFormatException(Exception):
1543 '''Problems with audio format'''
1545 ################################################################################
1546 class TagFile:
1547 fileName = str("");
1548 fileSize = int(0);
1549 tag = None;
1550 # Number of seconds required to play the audio file.
1551 play_time = int(0);
1553 def __init__(self, fileName):
1554 self.fileName = fileName;
1556 def getTag(self):
1557 return self.tag;
1559 def getSize(self):
1560 if not self.fileSize:
1561 self.fileSize = os.stat(self.fileName)[ST_SIZE];
1562 return self.fileSize;
1564 def rename(self, name, fsencoding):
1565 base = os.path.basename(self.fileName);
1566 base_ext = os.path.splitext(base)[1];
1567 dir = os.path.dirname(self.fileName);
1568 if not dir:
1569 dir = ".";
1570 new_name = dir + os.sep + name.encode(fsencoding) + base_ext;
1571 try:
1572 os.rename(self.fileName, new_name);
1573 self.fileName = new_name;
1574 except OSError, ex:
1575 raise TagException("Error renaming '%s' to '%s'" % (self.fileName,
1576 new_name));
1578 def getPlayTime(self):
1579 return self.play_time;
1581 def getPlayTimeString(self):
1582 from eyeD3.utils import format_track_time
1583 return format_track_time(self.getPlayTime())
1586 ################################################################################
1587 class Mp3AudioFile(TagFile):
1589 def __init__(self, fileName, tagVersion = ID3_ANY_VERSION):
1590 TagFile.__init__(self, fileName)
1592 self.tag = None
1593 self.header = None
1594 self.xingHeader = None
1595 self.lameTag = None
1597 if not isMp3File(fileName):
1598 raise InvalidAudioFormatException("File is not mp3");
1600 # Parse ID3 tag.
1601 f = file(self.fileName, "rb");
1602 self.tag = Tag();
1603 hasTag = self.tag.link(f, tagVersion);
1604 # Find the first mp3 frame.
1605 if self.tag.isV1():
1606 framePos = 0;
1607 elif not hasTag:
1608 framePos = 0;
1609 self.tag = None;
1610 else:
1611 framePos = self.tag.header.SIZE + self.tag.header.tagSize;
1613 TRACE_MSG("mp3 header search starting @ %x" % framePos)
1614 # Find an mp3 header
1615 header_pos, header, header_bytes = mp3.find_header(f, framePos)
1616 if header:
1617 try:
1618 self.header = mp3.Header(header)
1619 except mp3.Mp3Exception, ex:
1620 self.header = None
1621 raise InvalidAudioFormatException(str(ex));
1622 else:
1623 TRACE_MSG("mp3 header %x found at position: 0x%x" % (header,
1624 header_pos))
1625 else:
1626 raise InvalidAudioFormatException("Unable to find a valid mp3 frame")
1628 # Check for Xing/Info header information which will always be in the
1629 # first "null" frame.
1630 f.seek(header_pos)
1631 mp3_frame = f.read(self.header.frameLength)
1632 if re.compile('Xing|Info').search(mp3_frame):
1633 self.xingHeader = mp3.XingHeader();
1634 if not self.xingHeader.decode(mp3_frame):
1635 TRACE_MSG("Ignoring corrupt Xing header")
1636 self.xingHeader = None
1637 # Check for LAME Tag
1638 self.lameTag = mp3.LameTag(mp3_frame)
1640 # Compute track play time.
1641 tpf = mp3.computeTimePerFrame(self.header);
1642 if self.xingHeader and self.xingHeader.vbr:
1643 self.play_time = int(tpf * self.xingHeader.numFrames);
1644 else:
1645 length = self.getSize();
1646 if self.tag and self.tag.isV2():
1647 length -= self.tag.header.SIZE + self.tag.header.tagSize;
1648 # Handle the case where there is a v2 tag and a v1 tag.
1649 f.seek(-128, 2)
1650 if f.read(3) == "TAG":
1651 length -= 128;
1652 elif self.tag and self.tag.isV1():
1653 length -= 128;
1654 self.play_time = int((length / self.header.frameLength) * tpf);
1656 f.close();
1658 # Returns a tuple. The first value is a boolean which if true means the
1659 # bit rate returned in the second value is variable.
1660 def getBitRate(self):
1661 xHead = self.xingHeader;
1662 if xHead and xHead.vbr:
1663 tpf = eyeD3.mp3.computeTimePerFrame(self.header);
1664 # FIXME: if xHead.numFrames == 0 (Fuoco.mp3), ZeroDivisionError
1665 br = int((xHead.numBytes * 8) / (tpf * xHead.numFrames * 1000));
1666 vbr = 1;
1667 else:
1668 br = self.header.bitRate;
1669 vbr = 0;
1670 return (vbr, br);
1672 def getBitRateString(self):
1673 (vbr, bitRate) = self.getBitRate();
1674 brs = "%d kb/s" % bitRate;
1675 if vbr:
1676 brs = "~" + brs;
1677 return brs;
1678 def getSampleFreq(self):
1679 return self.header.sampleFreq;
1681 ################################################################################
1682 def isMp3File(fileName):
1683 (type, enc) = mimetypes.guess_type(fileName);
1684 return type == "audio/mpeg";
1686 ################################################################################
1687 class GenreMap(list):
1688 # None value are set in the ctor
1689 GENRE_MIN = 0;
1690 GENRE_MAX = None;
1691 ID3_GENRE_MIN = 0;
1692 ID3_GENRE_MAX = 79;
1693 WINAMP_GENRE_MIN = 80;
1694 WINAMP_GENRE_MAX = 147;
1695 EYED3_GENRE_MIN = None;
1696 EYED3_GENRE_MAX = None;
1698 # Accepts both int and string keys. Throws IndexError and TypeError.
1699 def __getitem__(self, key):
1700 if isinstance(key, int):
1701 if key >= 0 and key < len(self):
1702 v = list.__getitem__(self, key);
1703 if v:
1704 return v;
1705 else:
1706 return None;
1707 else:
1708 raise IndexError("genre index out of range");
1709 elif isinstance(key, str):
1710 if self.reverseDict.has_key(key.lower()):
1711 return self.reverseDict[key.lower()];
1712 else:
1713 raise IndexError(key + " genre not found");
1714 else:
1715 raise TypeError("genre key must be type int or string");
1717 def __init__(self):
1718 self.data = []
1719 self.reverseDict = {}
1720 # ID3 genres as defined by the v1.1 spec with WinAmp extensions.
1721 self.append('Blues');
1722 self.append('Classic Rock');
1723 self.append('Country');
1724 self.append('Dance');
1725 self.append('Disco');
1726 self.append('Funk');
1727 self.append('Grunge');
1728 self.append('Hip-Hop');
1729 self.append('Jazz');
1730 self.append('Metal');
1731 self.append('New Age');
1732 self.append('Oldies');
1733 self.append('Other');
1734 self.append('Pop');
1735 self.append('R&B');
1736 self.append('Rap');
1737 self.append('Reggae');
1738 self.append('Rock');
1739 self.append('Techno');
1740 self.append('Industrial');
1741 self.append('Alternative');
1742 self.append('Ska');
1743 self.append('Death Metal');
1744 self.append('Pranks');
1745 self.append('Soundtrack');
1746 self.append('Euro-Techno');
1747 self.append('Ambient');
1748 self.append('Trip-Hop');
1749 self.append('Vocal');
1750 self.append('Jazz+Funk');
1751 self.append('Fusion');
1752 self.append('Trance');
1753 self.append('Classical');
1754 self.append('Instrumental');
1755 self.append('Acid');
1756 self.append('House');
1757 self.append('Game');
1758 self.append('Sound Clip');
1759 self.append('Gospel');
1760 self.append('Noise');
1761 self.append('AlternRock');
1762 self.append('Bass');
1763 self.append('Soul');
1764 self.append('Punk');
1765 self.append('Space');
1766 self.append('Meditative');
1767 self.append('Instrumental Pop');
1768 self.append('Instrumental Rock');
1769 self.append('Ethnic');
1770 self.append('Gothic');
1771 self.append('Darkwave');
1772 self.append('Techno-Industrial');
1773 self.append('Electronic');
1774 self.append('Pop-Folk');
1775 self.append('Eurodance');
1776 self.append('Dream');
1777 self.append('Southern Rock');
1778 self.append('Comedy');
1779 self.append('Cult');
1780 self.append('Gangsta Rap');
1781 self.append('Top 40');
1782 self.append('Christian Rap');
1783 self.append('Pop / Funk');
1784 self.append('Jungle');
1785 self.append('Native American');
1786 self.append('Cabaret');
1787 self.append('New Wave');
1788 self.append('Psychedelic');
1789 self.append('Rave');
1790 self.append('Showtunes');
1791 self.append('Trailer');
1792 self.append('Lo-Fi');
1793 self.append('Tribal');
1794 self.append('Acid Punk');
1795 self.append('Acid Jazz');
1796 self.append('Polka');
1797 self.append('Retro');
1798 self.append('Musical');
1799 self.append('Rock & Roll');
1800 self.append('Hard Rock');
1801 self.append('Folk');
1802 self.append('Folk-Rock');
1803 self.append('National Folk');
1804 self.append('Swing');
1805 self.append('Fast Fusion');
1806 self.append('Bebob');
1807 self.append('Latin');
1808 self.append('Revival');
1809 self.append('Celtic');
1810 self.append('Bluegrass');
1811 self.append('Avantgarde');
1812 self.append('Gothic Rock');
1813 self.append('Progressive Rock');
1814 self.append('Psychedelic Rock');
1815 self.append('Symphonic Rock');
1816 self.append('Slow Rock');
1817 self.append('Big Band');
1818 self.append('Chorus');
1819 self.append('Easy Listening');
1820 self.append('Acoustic');
1821 self.append('Humour');
1822 self.append('Speech');
1823 self.append('Chanson');
1824 self.append('Opera');
1825 self.append('Chamber Music');
1826 self.append('Sonata');
1827 self.append('Symphony');
1828 self.append('Booty Bass');
1829 self.append('Primus');
1830 self.append('Porn Groove');
1831 self.append('Satire');
1832 self.append('Slow Jam');
1833 self.append('Club');
1834 self.append('Tango');
1835 self.append('Samba');
1836 self.append('Folklore');
1837 self.append('Ballad');
1838 self.append('Power Ballad');
1839 self.append('Rhythmic Soul');
1840 self.append('Freestyle');
1841 self.append('Duet');
1842 self.append('Punk Rock');
1843 self.append('Drum Solo');
1844 self.append('A Cappella');
1845 self.append('Euro-House');
1846 self.append('Dance Hall');
1847 self.append('Goa');
1848 self.append('Drum & Bass');
1849 self.append('Club-House');
1850 self.append('Hardcore');
1851 self.append('Terror');
1852 self.append('Indie');
1853 self.append('BritPop');
1854 self.append('Negerpunk');
1855 self.append('Polsk Punk');
1856 self.append('Beat');
1857 self.append('Christian Gangsta Rap');
1858 self.append('Heavy Metal');
1859 self.append('Black Metal');
1860 self.append('Crossover');
1861 self.append('Contemporary Christian');
1862 self.append('Christian Rock');
1863 self.append('Merengue');
1864 self.append('Salsa');
1865 self.append('Thrash Metal');
1866 self.append('Anime');
1867 self.append('JPop');
1868 self.append('Synthpop');
1869 # The follow genres I've encountered in the wild.
1870 self.append('Rock/Pop');
1871 self.EYED3_GENRE_MIN = len(self) - 1;
1872 # New genres go here
1874 self.EYED3_GENRE_MAX = len(self) - 1;
1875 self.GENRE_MAX = len(self) - 1;
1877 # Pad up to 255 with "Unknown"
1878 count = len(self);
1879 while count < 256:
1880 self.append("Unknown");
1881 count += 1;
1883 for index in range(len(self)):
1884 if self[index]:
1885 self.reverseDict[string.lower(self[index])] = index
1886 class LinkedFile:
1887 name = "";
1888 tagPadding = 0;
1889 tagSize = 0; # This includes the padding byte count.
1891 def __init__(self, fileName):
1892 if isinstance(fileName, str):
1893 try:
1894 self.name = unicode(fileName, sys.getfilesystemencoding());
1895 except:
1896 # Work around the local encoding not matching that of a mounted
1897 # filesystem
1898 self.name = fileName
1899 else:
1900 self.name = fileName;
1902 def tagToUserTune(tag):
1903 audio_file = None;
1904 if isinstance(tag, Mp3AudioFile):
1905 audio_file = tag;
1906 tag = audio_file.getTag();
1908 tune = u"<tune xmlns='http://jabber.org/protocol/tune'>\n";
1909 if tag.getArtist():
1910 tune += " <artist>" + tag.getArtist() + "</artist>\n";
1911 if tag.getTitle():
1912 tune += " <title>" + tag.getTitle() + "</title>\n";
1913 if tag.getAlbum():
1914 tune += " <source>" + tag.getAlbum() + "</source>\n";
1915 tune += " <track>" +\
1916 "file://" + unicode(os.path.abspath(tag.linkedFile.name)) +\
1917 "</track>\n";
1918 if audio_file:
1919 tune += " <length>" + unicode(audio_file.getPlayTime()) +\
1920 "</length>\n";
1921 tune += "</tune>\n";
1922 return tune;
1925 # Module level globals.
1927 genres = GenreMap();