pyTivo
[pyTivo/krkeegan.git] / eyeD3 / tag.py
blob4cee7ce967189c04b1dfc975986b0c230ad3f83d
1 ###############################################################################
3 # Copyright (C) 2002-2005 Travis Shirk <travis@pobox.com>
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.
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.
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
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;
30 ID3_V1_COMMENT_DESC = "ID3 v1 Comment";
32 ################################################################################
33 class TagException(Exception):
34 '''error reading tag'''
36 ################################################################################
37 class TagHeader:
38 SIZE = 10;
40 version = None;
41 majorVersion = None;
42 minorVersion = None;
43 revVersion = None;
45 # Flag bits
46 unsync = 0;
47 extended = 0;
48 experimental = 0;
49 # v2.4 addition
50 footer = 0;
52 # The size in the most recently parsed header.
53 tagSize = 0;
55 # Constructor
56 def __init__(self):
57 self.clear();
59 def clear(self):
60 self.setVersion(None);
61 self.unsync = 0;
62 self.extended = 0;
63 self.experimental = 0;
64 self.tagSize = 0;
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;
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;
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)));
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();
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");
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]);
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));
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));
144 return 1;
146 def render(self, tagLen = None):
147 if tagLen != None:
148 self.tagSize = tagLen;
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;
164 ################################################################################
165 class ExtendedTagHeader:
166 size = 0;
167 flags = 0;
168 crc = 0;
169 restrictions = 0;
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;
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.";
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";
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.";
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";
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.";
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;
260 def render(self, header, frameData, padding = 0):
261 assert(header.majorVersion == 2);
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));
288 # Extended header size.
289 size = bin2bytes(bin2synchsafe(dec2bin(len(data) + 6, 32)))
290 assert(len(size) == 4);
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));
316 data = size + flags + paddingSize;
317 if crc:
318 data += crc;
319 return data;
321 # Only call this when you *know* there is an extened header.
322 def parse(self, fp, header):
323 assert(header.majorVersion == 2);
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);
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");
340 offset = 2;
341 self.flags = ord(data[1]);
342 TRACE_MSG("Extended header flags: %x" % self.flags);
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);
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;
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();
393 # Optional in v2 tags.
394 extendedHeader = ExtendedTagHeader();
396 # Contains the tag's frames. ID3v1 fields are read and converted
397 # the the corresponding v2 frame.
398 frames = None;
400 # Used internally for iterating over frames.
401 iterIndex = None;
403 # If this value is None the tag is not linked to any particular file..
404 linkedFile = None;
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();
414 def clear(self):
415 self.header = TagHeader();
416 self.frames = FrameSet(self.header);
417 self.iterIndex = None;
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;
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;
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.
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.
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();
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)));
458 if v != ID3_V1 and v != ID3_V2 and v != ID3_ANY_VERSION:
459 raise TagException("Invalid version: " + hex(v));
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;
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;
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.");
497 if backup:
498 shutil.copyfile(self.linkedFile.name, self.linkedFile.name + ".orig");
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);
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;
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;
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.");
539 if version == ID3_CURRENT_VERSION:
540 version = self.getVersion();
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();
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;
567 return retval;
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
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.
582 # When no artist is found, an empty string is returned.
584 def getArtist(self, artistID = ARTIST_FIDS):
585 if isinstance(artistID, list):
586 frameIDs = artistID;
587 else:
588 frameIDs = [artistID];
590 for fid in frameIDs:
591 f = self.frames[fid];
592 if f:
593 return f[0].text;
594 return u"";
596 def getAlbum(self):
597 f = self.frames[ALBUM_FID];
598 if f:
599 return f[0].text;
600 else:
601 return u"";
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"";
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];
624 def getYear(self, fid = None):
625 dateFrame = self.getDate(fid);
626 if dateFrame:
627 return dateFrame[0].getYear();
628 else:
629 return None;
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;
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)
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)
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)
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];
672 # Returns a list (possibly empty) of eyeD3.frames.ImageFrame objects.
673 def getImages(self):
674 return self.frames[IMAGE_FID];
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;
686 def getUserTextFrames(self):
687 return self.frames[USERTEXT_FID];
689 def getCDID(self):
690 return self.frames[CDID_FID];
692 def getVersion(self):
693 return self.header.version;
695 def getVersionStr(self):
696 return versionToString(self.header.version);
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;
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));
713 def setAlbum(self, a):
714 self.setTextFrame(ALBUM_FID, self.strToUnicode(a));
716 def setTitle(self, t, titleID = TITLE_FID):
717 self.setTextFrame(titleID, self.strToUnicode(t));
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);
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);
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));
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;
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)));
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)
789 def setDiscNum(self, n):
790 self.setNum(DISCNUM_FID, n)
792 def setNum(self, fid, n):
793 if n[0] == None and n[1] == None:
794 self.frames.removeFramesByID(fid);
795 return;
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;
806 t = n[0];
807 if t == None:
808 t = 0;
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;
817 s = "";
818 if trackStr and totalStr:
819 s = trackStr + "/" + totalStr;
820 elif trackStr and not totalStr:
821 s = trackStr;
823 self.frames.setTextFrame(fid, self.strToUnicode(s),
824 self.encoding);
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):
831 if not cmt:
832 # A little more then a call to removeFramesByID is involved since we
833 # need to look at more than the frame ID.
834 comments = self.frames[COMMENT_FID];
835 for c in comments:
836 if c.lang == lang and c.description == desc:
837 self.frames.remove(c);
838 break;
839 else:
840 self.frames.setCommentFrame(self.strToUnicode(cmt),
841 self.strToUnicode(desc),
842 lang, self.encoding);
844 # Semantics similar to addComment
845 def addUserTextFrame(self, desc, text):
846 if not text:
847 u_frames = self.frames[USERTEXT_FID];
848 for u in u_frames:
849 if u.description == desc:
850 self.frames.remove(u);
851 break;
852 else:
853 self.frames.setUserTextFrame(self.strToUnicode(text),
854 self.strToUnicode(desc), self.encoding);
856 def removeComments(self):
857 return self.frames.removeFramesByID(COMMENT_FID);
859 def addImage(self, type, image_file_path, desc = u""):
860 if image_file_path:
861 image_frame = ImageFrame.create(type, image_file_path, desc);
862 self.frames.addFrame(image_frame);
863 else:
864 image_frames = self.frames[IMAGE_FID];
865 for i in image_frames:
866 if i.pictureType == type:
867 self.frames.remove(i);
868 break;
870 def getPlayCount(self):
871 if self.frames[PLAYCOUNT_FID]:
872 pc = self.frames[PLAYCOUNT_FID][0];
873 assert(isinstance(pc, PlayCountFrame));
874 return pc.count;
875 else:
876 return None;
878 def setPlayCount(self, count):
879 assert(count >= 0);
880 if self.frames[PLAYCOUNT_FID]:
881 pc = self.frames[PLAYCOUNT_FID][0];
882 assert(isinstance(pc, PlayCountFrame));
883 pc.count = count;
884 else:
885 frameHeader = FrameHeader(self.header);
886 frameHeader.id = PLAYCOUNT_FID;
887 pc = PlayCountFrame(frameHeader, count = count);
888 self.frames.addFrame(pc);
890 def incrementPlayCount(self, n = 1):
891 pc = self.getPlayCount();
892 if pc != None:
893 self.setPlayCount(pc + n);
894 else:
895 self.setPlayCount(n);
897 def getUniqueFileIDs(self):
898 return self.frames[UNIQUE_FILE_ID_FID];
900 def addUniqueFileID(self, owner_id, id):
901 if not id:
902 ufids = self.frames[UNIQUE_FILE_ID_FID];
903 for ufid in ufids:
904 if ufid.owner_id == owner_id:
905 self.frames.remove(ufid);
906 break;
907 else:
908 self.frames.setUniqueFileIDFrame(owner_id, id);
910 def getBPM(self):
911 bpm = self.frames[BPM_FID];
912 if bpm:
913 return int(bpm[0].text);
914 else:
915 return None;
917 def setBPM(self, bpm):
918 self.setTextFrame(BPM_FID, self.strToUnicode(str(bpm)));
920 def getPublisher(self):
921 pub = self.frames[PUBLISHER_FID];
922 if pub:
923 return pub[0].text or None;
925 def setPublisher(self, p):
926 self.setTextFrame(PUBLISHER_FID, self.strToUnicode(str(p)));
928 # Test ID3 major version.
929 def isV1(self):
930 return self.header.majorVersion == 1;
931 def isV2(self):
932 return self.header.majorVersion == 2;
934 def setVersion(self, v):
935 if v == ID3_V1:
936 v = ID3_V1_1;
937 elif v == ID3_V2:
938 v = ID3_DEFAULT_VERSION;
940 if v != ID3_CURRENT_VERSION:
941 self.header.setVersion(v);
942 self.frames.setTagHeader(self.header);
944 def setTextFrame(self, fid, txt):
945 if not txt:
946 self.frames.removeFramesByID(fid);
947 else:
948 self.frames.setTextFrame(fid, self.strToUnicode(txt), self.encoding);
950 def setTextEncoding(self, enc):
951 if enc != LATIN1_ENCODING and enc != UTF_16_ENCODING and\
952 enc != UTF_16BE_ENCODING and enc != UTF_8_ENCODING:
953 raise TagException("Invalid encoding");
954 elif self.getVersion() & ID3_V1 and enc != LATIN1_ENCODING:
955 raise TagException("ID3 v1.x supports ISO-8859 encoding only");
956 elif self.getVersion() <= ID3_V2_3 and enc == UTF_8_ENCODING:
957 # This is unfortunate.
958 raise TagException("UTF-8 is not supported by ID3 v2.3");
960 self.encoding = enc;
961 for f in self.frames:
962 f.encoding = enc;
964 def tagToString(self, pattern):
965 # %A - artist
966 # %a - album
967 # %t - title
968 # %n - track number
969 # %N - track total
970 s = self._subst(pattern, "%A", self.getArtist());
971 s = self._subst(s, "%a", self.getAlbum());
972 s = self._subst(s, "%t", self.getTitle());
973 s = self._subst(s, "%n", self._prettyTrack(self.getTrackNum()[0]));
974 s = self._subst(s, "%N", self._prettyTrack(self.getTrackNum()[1]));
975 return s;
977 def _prettyTrack(self, track):
978 if not track:
979 return None;
980 track_str = str(track);
981 if len(track_str) == 1:
982 track_str = "0" + track_str;
983 return track_str;
985 def _subst(self, name, pattern, repl):
986 regex = re.compile(pattern);
987 if regex.search(name) and repl:
988 # No '/' characters allowed
989 (repl, subs) = re.compile("/").subn("-", repl);
990 (name, subs) = regex.subn(repl, name)
991 return name;
993 def __saveV1Tag(self, version):
994 assert(version & ID3_V1);
996 # Build tag buffer.
997 tag = "TAG";
998 tag += self._fixToWidth(self.getTitle().encode("latin_1"), 30);
999 tag += self._fixToWidth(self.getArtist().encode("latin_1"), 30);
1000 tag += self._fixToWidth(self.getAlbum().encode("latin_1"), 30);
1001 y = self.getYear();
1002 if y is None:
1003 y = "";
1004 tag += self._fixToWidth(y.encode("latin_1"), 4);
1006 cmt = "";
1007 for c in self.getComments():
1008 if c.description == ID3_V1_COMMENT_DESC:
1009 cmt = c.comment;
1010 # We prefer this one over "";
1011 break;
1012 elif c.description == "":
1013 cmt = c.comment;
1014 # Keep searching in case we find the description eyeD3 uses.
1015 cmt = self._fixToWidth(cmt, 30);
1016 if version != ID3_V1_0:
1017 track = self.getTrackNum()[0];
1018 if track != None:
1019 cmt = cmt[0:28] + "\x00" + chr(int(track) & 0xff);
1020 tag += cmt;
1022 if not self.getGenre():
1023 genre = 0;
1024 else:
1025 genre = self.getGenre().getId();
1026 tag += chr(genre & 0xff);
1028 assert(len(tag) == 128);
1030 tagFile = file(self.linkedFile.name, "r+b");
1031 # Write the tag over top an original or append it.
1032 try:
1033 tagFile.seek(-128, 2);
1034 if tagFile.read(3) == "TAG":
1035 tagFile.seek(-128, 2);
1036 else:
1037 tagFile.seek(0, 2);
1038 except IOError:
1039 # File is smaller than 128 bytes.
1040 tagFile.seek(0, 2);
1042 tagFile.write(tag);
1043 tagFile.flush();
1044 tagFile.close();
1046 def _fixToWidth(self, s, n):
1047 retval = str(s);
1048 retval = retval[0:n];
1049 retval = retval + ("\x00" * (n - len(retval)));
1050 return retval;
1052 # Returns false when an ID3 v1 tag is not present, or contains no data.
1053 def __loadV1Tag(self, f):
1054 if isinstance(f, str) or isinstance(f, unicode):
1055 fp = file(f, "rb")
1056 closeFile = 1;
1057 else:
1058 fp = f;
1059 closeFile = 0;
1061 # Seek to the end of the file where all ID3v1 tags are written.
1062 fp.seek(0, 2);
1063 strip_chars = string.whitespace + "\x00";
1064 if fp.tell() > 127:
1065 fp.seek(-128, 2);
1066 id3tag = fp.read(128);
1067 if id3tag[0:3] == "TAG":
1068 TRACE_MSG("Located ID3 v1 tag");
1069 # 1.0 is implied until a 1.1 feature is recognized.
1070 self.setVersion(ID3_V1_0);
1072 title = re.sub("\x00+$", "", id3tag[3:33].strip(strip_chars));
1073 TRACE_MSG("Tite: " + title);
1074 if title:
1075 self.setTitle(unicode(title, "latin1"));
1077 artist = re.sub("\x00+$", "", id3tag[33:63].strip(strip_chars));
1078 TRACE_MSG("Artist: " + artist);
1079 if artist:
1080 self.setArtist(unicode(artist, "latin1"));
1082 album = re.sub("\x00+$", "", id3tag[63:93].strip(strip_chars));
1083 TRACE_MSG("Album: " + album);
1084 if album:
1085 self.setAlbum(unicode(album, "latin1"));
1087 year = re.sub("\x00+$", "", id3tag[93:97].strip(strip_chars));
1088 TRACE_MSG("Year: " + year);
1089 try:
1090 if year and int(year):
1091 self.setDate(year);
1092 except ValueError:
1093 # Bogus year strings.
1094 pass;
1096 if re.sub("\x00+$", "", id3tag[97:127]):
1097 comment = id3tag[97:127];
1098 TRACE_MSG("Comment: " + comment);
1099 if comment[-2] == "\x00" and comment[-1] != "\x00":
1100 # Parse track number (added to ID3v1.1) if present.
1101 TRACE_MSG("Comment contains track number per v1.1 spec");
1102 track = ord(comment[-1]);
1103 self.setTrackNum((track, None));
1104 TRACE_MSG("Track: " + str(track));
1105 TRACE_MSG("Track Num found, setting version to v1.1s");
1106 self.setVersion(ID3_V1_1);
1107 comment = comment[:-2];
1108 else:
1109 track = None
1110 comment = re.sub("\x00+$", "", comment).rstrip();
1111 TRACE_MSG("Comment: " + comment);
1112 if comment:
1113 self.addComment(unicode(comment, 'latin1'),
1114 ID3_V1_COMMENT_DESC);
1116 genre = ord(id3tag[127:128])
1117 TRACE_MSG("Genre ID: " + str(genre));
1118 self.setGenre(genre);
1120 if closeFile:
1121 fp.close()
1122 return len(self.frames);
1124 def __saveV2Tag(self, version):
1125 assert(version & ID3_V2);
1126 TRACE_MSG("Rendering tag version: " + versionToString(version));
1128 self.setVersion(version);
1130 currPadding = 0;
1131 currTagSize = 0
1132 if currTagSize == 0:
1133 # We may be converting from 1.x to 2.x so we need to find any
1134 # current v2.x tag otherwise we're gonna hork the file.
1135 tmpTag = Tag();
1136 if tmpTag.link(self.linkedFile.name, ID3_V2):
1137 TRACE_MSG("Found current v2.x tag:");
1138 currTagSize = tmpTag.linkedFile.tagSize;
1139 TRACE_MSG("Current tag size: %d" % currTagSize);
1140 currPadding = tmpTag.linkedFile.tagPadding;
1141 TRACE_MSG("Current tag padding: %d" % currPadding);
1143 # Tag it!
1144 if self.header.minorVersion == 4:
1145 h = FrameHeader(self.header);
1146 h.id = "TDTG";
1147 t = time.strftime("%Y-%m-%dT%H:%M:%S");
1148 dateFrame = DateFrame(h, date_str = self.strToUnicode(t),
1149 encoding = self.encoding);
1150 self.frames.removeFramesByID("TDTG");
1151 self.frames.addFrame(dateFrame);
1153 # Render all frames first so the data size is known for the tag header.
1154 frameData = "";
1155 for f in self.frames:
1156 TRACE_MSG("Rendering frame: " + f.header.id);
1157 raw_frame = f.render();
1158 TRACE_MSG("Rendered %d bytes" % len(raw_frame));
1159 frameData += raw_frame;
1160 # Handle the overall tag header unsync bit. Frames themselves duplicate
1161 # this bit.
1162 if self.header.unsync:
1163 TRACE_MSG("Unsyncing all frames (sync-safe)");
1164 frameData = frames.unsyncData(frameData);
1166 TRACE_MSG("Rendered tag size: " + str(len(frameData)));
1168 rewriteFile = 0;
1169 paddingSize = 0;
1170 headerData = "";
1171 extHeaderData = "";
1173 # Extended header
1174 if self.header.extended:
1175 # This is sorta lame. We don't know the total framesize until
1176 # this is rendered, yet we can't render it witout knowing the
1177 # amount of padding. Force it.
1178 rewriteFile = 1;
1179 paddingSize = 2048;
1180 TRACE_MSG("Rendering extended header");
1181 extHeaderData += self.extendedHeader.render(self.header, frameData,
1182 paddingSize);
1184 if rewriteFile or (10 + len(headerData) + len(extHeaderData) +\
1185 len(frameData)) >= currTagSize:
1186 TRACE_MSG("File rewrite required");
1187 rewriteFile = 1;
1188 paddingSize = 2048;
1189 else:
1190 paddingSize = currTagSize - (len(headerData) + len(extHeaderData) +\
1191 len(frameData));
1192 frameData += ("\x00" * paddingSize);
1194 # Render the tag header.
1195 TRACE_MSG("Rendering %s tag header with size %d" %\
1196 (versionToString(self.getVersion()), len(frameData)));
1197 headerData = self.header.render(len(frameData));
1199 # Assemble frame.
1200 tagData = headerData + extHeaderData + frameData;
1202 # Write the tag.
1203 if not rewriteFile:
1204 tagFile = file(self.linkedFile.name, "r+b");
1205 TRACE_MSG("Writing %d bytes of tag data" % len(tagData));
1206 tagFile.write(tagData);
1207 tagFile.close();
1208 else:
1209 # Open original
1210 tagFile = file(self.linkedFile.name, "rb");
1211 # Read all audio data
1212 tagFile.seek(currTagSize);
1213 audioData = tagFile.read();
1214 tagFile.close();
1216 # Open tmp file
1217 tmpName = tempfile.mktemp();
1218 tmpFile = file(tmpName, "w+b");
1219 TRACE_MSG("Writing %d bytes of tag data" % len(tagData));
1220 tmpFile.write(tagData);
1221 tmpFile.write(audioData);
1222 tmpFile.close();
1224 # Move tmp to orig.
1225 shutil.copyfile(tmpName, self.linkedFile.name);
1226 os.unlink(tmpName);
1228 # Update our state.
1229 TRACE_MSG("Tag write complete. Updating state.");
1230 self.linkedFile.tagPadding = paddingSize;
1231 # XXX: getSize could cache sizes so to prevent rendering again.
1232 self.linkedFile.tagSize = self.frames.getSize();
1235 # Returns >= 0 to indicate the padding size of the read frame; -1 returned
1236 # when not tag was found.
1237 def __loadV2Tag(self, f):
1238 if isinstance(f, str) or isinstance(f, unicode):
1239 fp = file(f, "rb")
1240 closeFile = 1;
1241 else:
1242 fp = f;
1243 closeFile = 0;
1245 padding = -1;
1246 try:
1247 # Look for a tag and if found load it.
1248 if not self.header.parse(fp):
1249 return -1;
1251 # Read the extended header if present.
1252 if self.header.extended:
1253 self.extendedHeader.parse(fp, self.header);
1255 # Header is definitely there so at least one frame *must* follow.
1256 self.frames.setTagHeader(self.header);
1257 padding = self.frames.parse(fp, self.header);
1258 TRACE_MSG("Tag contains %d bytes of padding." % padding);
1259 except FrameException, ex:
1260 if utils.strictID3():
1261 fp.close();
1262 raise TagException(str(ex));
1263 except TagException:
1264 fp.close();
1265 raise;
1267 if closeFile:
1268 fp.close();
1269 return padding;
1271 def toInt(self, s):
1272 try:
1273 return int(s);
1274 except ValueError:
1275 return None;
1276 except TypeError:
1277 return None;
1279 def __padDateField(self, f):
1280 fStr = str(f);
1281 if len(fStr) == 2:
1282 pass;
1283 elif len(fStr) == 1:
1284 fStr = "0" + fStr;
1285 else:
1286 raise TagException("Invalid date field: " + fStr);
1287 return fStr;
1289 # DEPRECATED
1290 # This method will return the first comment in the FrameSet
1291 # and not all of them. Multiple COMM frames are common and useful. Use
1292 # getComments which returns a list.
1293 def getComment(self):
1294 f = self.frames[COMMENT_FID];
1295 if f:
1296 return f[0].comment;
1297 else:
1298 return None;
1301 ################################################################################
1302 class GenreException(Exception):
1303 '''Problem looking up genre'''
1305 ################################################################################
1306 class Genre:
1307 id = None;
1308 name = None;
1310 def __init__(self, id = None, name = None):
1311 if id is not None:
1312 self.setId(id);
1313 elif name is not None:
1314 self.setName(name);
1316 def getId(self):
1317 return self.id;
1318 def getName(self):
1319 return self.name;
1321 # Sets the genre id. The objects name field is set to the corresponding
1322 # value obtained from eyeD3.genres.
1324 # Throws GenreException when name does not map to a valid ID3 v1.1. id.
1325 # This behavior can be disabled by passing 0 as the second argument.
1326 def setId(self, id):
1327 if not isinstance(id, int):
1328 raise TypeError("Invalid genre id: " + str(id));
1330 try:
1331 name = genres[id];
1332 except Exception, ex:
1333 if utils.strictID3():
1334 raise GenreException("Invalid genre id: " + str(id));
1336 if utils.strictID3() and not name:
1337 raise GenreException("Genre id maps to a null name: " + str(id));
1339 self.id = id;
1340 self.name = name;
1342 # Sets the genre name. The objects id field is set to the corresponding
1343 # value obtained from eyeD3.genres.
1345 # Throws GenreException when name does not map to a valid ID3 v1.1. name.
1346 # This behavior can be disabled by passing 0 as the second argument.
1347 def setName(self, name):
1348 if not isinstance(name, str):
1349 raise GenreException("Invalid genre name: " + str(name));
1351 try:
1352 id = genres[name];
1353 # Get titled case.
1354 name = genres[id];
1355 except:
1356 if utils.strictID3():
1357 raise GenreException("Invalid genre name: " + name);
1358 id = None;
1360 self.id = id;
1361 self.name = name;
1364 # Sets the genre id and name.
1366 # Throws GenreException when eyeD3.genres[id] != name (case insensitive).
1367 # This behavior can be disabled by passing 0 as the second argument.
1368 def set(self, id, name):
1369 if not isinstance(id, int):
1370 raise GenreException("Invalid genre id: " + id);
1371 if not isinstance(name, str):
1372 raise GenreException("Invalid genre name: " + str(name));
1374 if not utils.strictID3():
1375 self.id = id;
1376 self.name = name;
1377 else:
1378 try:
1379 if genres[name] != id:
1380 raise GenreException("eyeD3.genres[" + str(id) + "] " +\
1381 "does not match " + name);
1382 self.id = id;
1383 self.name = name;
1384 except:
1385 raise GenreException("eyeD3.genres[" + str(id) + "] " +\
1386 "does not match " + name);
1388 # Parses genre information from genreStr.
1389 # The following formats are supported:
1390 # 01, 2, 23, 125 - ID3 v1 style.
1391 # (01), (2), (129)Hardcore, (9)Metal - ID3 v2 style with and without
1392 # refinement.
1394 # Throws GenreException when an invalid string is passed.
1395 def parse(self, genreStr):
1396 genreStr =\
1397 str(genreStr.encode('utf-8')).strip(string.whitespace + '\x00');
1398 self.id = None;
1399 self.name = None;
1401 if not genreStr:
1402 return;
1404 # XXX: Utf-16 conversions leave a null byte at the end of the string.
1405 while genreStr[len(genreStr) - 1] == "\x00":
1406 genreStr = genreStr[:len(genreStr) - 1];
1407 if len(genreStr) == 0:
1408 break;
1410 # ID3 v1 style.
1411 # Match 03, 34, 129.
1412 regex = re.compile("[0-9][0-9]?[0-9]?$");
1413 if regex.match(genreStr):
1414 if len(genreStr) != 1 and genreStr[0] == '0':
1415 genreStr = genreStr[1:];
1417 self.setId(int(genreStr));
1418 return;
1420 # ID3 v2 style.
1421 # Match (03), (0)Blues, (15) Rap
1422 regex = re.compile("\(([0-9][0-9]?[0-9]?)\)(.*)$");
1423 m = regex.match(genreStr);
1424 if m:
1425 (id, name) = m.groups();
1426 if len(id) != 1 and id[0] == '0':
1427 id = id[1:];
1429 if id and name:
1430 self.set(int(id), name.strip());
1431 else:
1432 self.setId(int(id));
1433 return;
1435 # Non standard, but witnessed.
1436 # Match genreName alone. e.g. Rap, Rock, blues.
1437 regex = re.compile("^[A-Z 0-9+/\-&]+\00*$", re.IGNORECASE);
1438 if regex.match(genreStr):
1439 self.setName(genreStr);
1440 return;
1441 raise GenreException("Genre string cannot be parsed with '%s': %s" %\
1442 (regex.pattern, genreStr));
1444 def __str__(self):
1445 s = "";
1446 if self.id != None:
1447 s += "(" + str(self.id) + ")"
1448 if self.name:
1449 s += self.name;
1450 return s;
1452 ################################################################################
1453 class InvalidAudioFormatException(Exception):
1454 '''Problems with audio format'''
1456 ################################################################################
1457 class TagFile:
1458 fileName = str("");
1459 fileSize = int(0);
1460 tag = None;
1461 # Number of seconds required to play the audio file.
1462 play_time = int(0);
1464 def __init__(self, fileName):
1465 self.fileName = fileName;
1467 def getTag(self):
1468 return self.tag;
1470 def getSize(self):
1471 if not self.fileSize:
1472 self.fileSize = os.stat(self.fileName)[ST_SIZE];
1473 return self.fileSize;
1475 def rename(self, name, fsencoding):
1476 base = os.path.basename(self.fileName);
1477 base_ext = os.path.splitext(base)[1];
1478 dir = os.path.dirname(self.fileName);
1479 if not dir:
1480 dir = ".";
1481 new_name = dir + os.sep + name.encode(fsencoding) + base_ext;
1482 try:
1483 os.rename(self.fileName, new_name);
1484 self.fileName = new_name;
1485 except OSError, ex:
1486 raise TagException("Error renaming '%s' to '%s'" % (self.fileName,
1487 new_name));
1489 def getPlayTime(self):
1490 return self.play_time;
1492 def getPlayTimeString(self):
1493 total = self.getPlayTime();
1494 h = total / 3600;
1495 m = (total % 3600) / 60;
1496 s = (total % 3600) % 60;
1497 if h:
1498 timeStr = "%d:%.2d:%.2d" % (h, m, s);
1499 else:
1500 timeStr = "%d:%.2d" % (m, s);
1501 return timeStr;
1504 ################################################################################
1505 class Mp3AudioFile(TagFile):
1506 header = mp3.Header();
1507 xingHeader = None;
1508 invalidFileExc = InvalidAudioFormatException("File is not mp3");
1510 def __init__(self, fileName, tagVersion = ID3_ANY_VERSION):
1511 TagFile.__init__(self, fileName);
1513 if not isMp3File(fileName):
1514 raise self.invalidFileExc;
1516 # Parse ID3 tag.
1517 f = file(fileName, "rb");
1518 tag = Tag();
1519 hasTag = tag.link(f, tagVersion);
1520 # Find the first mp3 frame.
1521 if tag.isV1():
1522 framePos = 0;
1523 elif not hasTag:
1524 framePos = 0;
1525 tag = None;
1526 else:
1527 # XXX: Note that v2.4 allows for appended tags; account for that.
1528 framePos = tag.header.SIZE + tag.header.tagSize;
1530 f.seek(framePos);
1531 bString = f.read(4);
1532 if len(bString) < 4:
1533 raise InvalidAudioFormatException("Unable to find a valid mp3 "\
1534 "frame");
1535 frameHead = bin2dec(bytes2bin(bString));
1536 header = mp3.Header();
1537 it_count = 0;
1538 # Keep reading until we find a valid mp3 frame header.
1539 while not header.isValid(frameHead):
1540 # Originally, the search was one byte at a time. Occasionally a tag
1541 # was heavily, and incorrectly, padded or a corrupt mp3 would cause
1542 # a byte by byte search to take a long time. This algorithm speeds
1543 # up this particular case.
1544 if it_count > 9:
1545 f.seek(f.tell() + 128);
1546 bString = f.read(4);
1547 if len(bString) < 4:
1548 raise InvalidAudioFormatException("Unable to find a valid mp3 "\
1549 "frame");
1550 frameHead = bin2dec(bytes2bin(bString));
1551 it_count = 0;
1552 continue;
1554 frameHead <<= 8;
1555 bString = f.read(1);
1556 if len(bString) != 1:
1557 raise InvalidAudioFormatException("Unable to find a valid mp3 "\
1558 "frame");
1559 frameHead |= ord(bString[0]);
1560 it_count += 1;
1561 TRACE_MSG("mp3 header %x found at position: %d (0x%x)" % \
1562 (frameHead, f.tell() - 4, f.tell() - 4));
1564 # Decode the header.
1565 try:
1566 header.decode(frameHead);
1567 # Check for Xing header inforamtion which will always be in the
1568 # first "null" frame.
1569 f.seek(-4, 1);
1570 mp3Frame = f.read(header.frameLength);
1571 if mp3Frame.find("Xing") != -1:
1572 xingHeader = mp3.XingHeader();
1573 if not xingHeader.decode(mp3Frame):
1574 raise InvalidAudioFormatException("Corrupt Xing header");
1575 else:
1576 xingHeader = None;
1577 except mp3.Mp3Exception, ex:
1578 raise InvalidAudioFormatException(str(ex));
1580 # Compute track play time.
1581 tpf = mp3.computeTimePerFrame(header);
1582 if xingHeader:
1583 self.play_time = int(tpf * xingHeader.numFrames);
1584 else:
1585 length = self.getSize();
1586 if tag and tag.isV2():
1587 length -= tag.header.SIZE + tag.header.tagSize;
1588 # Handle the case where there is a v2 tag and a v1 tag.
1589 f.seek(-128, 2)
1590 if f.read(3) == "TAG":
1591 length -= 128;
1592 elif tag and tag.isV1():
1593 length -= 128;
1594 self.play_time = int((length / header.frameLength) * tpf);
1596 self.header = header;
1597 self.xingHeader = xingHeader;
1598 self.tag = tag;
1599 f.close();
1601 # Returns a tuple. The first value is a boolean which if true means the
1602 # bit rate returned in the second value is variable.
1603 def getBitRate(self):
1604 xHead = self.xingHeader;
1605 if xHead:
1606 tpf = eyeD3.mp3.computeTimePerFrame(self.header);
1607 br = int((xHead.numBytes * 8) / (tpf * xHead.numFrames * 1000));
1608 vbr = 1;
1609 else:
1610 br = self.header.bitRate;
1611 vbr = 0;
1612 return (vbr, br);
1614 def getBitRateString(self):
1615 (vbr, bitRate) = self.getBitRate();
1616 brs = "%d kb/s" % bitRate;
1617 if vbr:
1618 brs = "~" + brs;
1619 return brs;
1620 def getSampleFreq(self):
1621 return self.header.sampleFreq;
1623 ################################################################################
1624 def isMp3File(fileName):
1625 (type, enc) = mimetypes.guess_type(fileName);
1626 return type == "audio/mpeg";
1628 ################################################################################
1629 class GenreMap(list):
1630 # None value are set in the ctor
1631 GENRE_MIN = 0;
1632 GENRE_MAX = None;
1633 ID3_GENRE_MIN = 0;
1634 ID3_GENRE_MAX = 79;
1635 WINAMP_GENRE_MIN = 80;
1636 WINAMP_GENRE_MAX = 147;
1637 EYED3_GENRE_MIN = None;
1638 EYED3_GENRE_MAX = None;
1640 # Accepts both int and string keys. Throws IndexError and TypeError.
1641 def __getitem__(self, key):
1642 if isinstance(key, int):
1643 if key >= 0 and key < len(self):
1644 v = list.__getitem__(self, key);
1645 if v:
1646 return v;
1647 else:
1648 return None;
1649 else:
1650 raise IndexError("genre index out of range");
1651 elif isinstance(key, str):
1652 if self.reverseDict.has_key(key.lower()):
1653 return self.reverseDict[key.lower()];
1654 else:
1655 raise IndexError(key + " genre not found");
1656 else:
1657 raise TypeError("genre key must be type int or string");
1659 def __init__(self):
1660 self.data = []
1661 self.reverseDict = {}
1662 # ID3 genres as defined by the v1.1 spec with WinAmp extensions.
1663 self.append('Blues');
1664 self.append('Classic Rock');
1665 self.append('Country');
1666 self.append('Dance');
1667 self.append('Disco');
1668 self.append('Funk');
1669 self.append('Grunge');
1670 self.append('Hip-Hop');
1671 self.append('Jazz');
1672 self.append('Metal');
1673 self.append('New Age');
1674 self.append('Oldies');
1675 self.append('Other');
1676 self.append('Pop');
1677 self.append('R&B');
1678 self.append('Rap');
1679 self.append('Reggae');
1680 self.append('Rock');
1681 self.append('Techno');
1682 self.append('Industrial');
1683 self.append('Alternative');
1684 self.append('Ska');
1685 self.append('Death Metal');
1686 self.append('Pranks');
1687 self.append('Soundtrack');
1688 self.append('Euro-Techno');
1689 self.append('Ambient');
1690 self.append('Trip-Hop');
1691 self.append('Vocal');
1692 self.append('Jazz+Funk');
1693 self.append('Fusion');
1694 self.append('Trance');
1695 self.append('Classical');
1696 self.append('Instrumental');
1697 self.append('Acid');
1698 self.append('House');
1699 self.append('Game');
1700 self.append('Sound Clip');
1701 self.append('Gospel');
1702 self.append('Noise');
1703 self.append('AlternRock');
1704 self.append('Bass');
1705 self.append('Soul');
1706 self.append('Punk');
1707 self.append('Space');
1708 self.append('Meditative');
1709 self.append('Instrumental Pop');
1710 self.append('Instrumental Rock');
1711 self.append('Ethnic');
1712 self.append('Gothic');
1713 self.append('Darkwave');
1714 self.append('Techno-Industrial');
1715 self.append('Electronic');
1716 self.append('Pop-Folk');
1717 self.append('Eurodance');
1718 self.append('Dream');
1719 self.append('Southern Rock');
1720 self.append('Comedy');
1721 self.append('Cult');
1722 self.append('Gangsta Rap');
1723 self.append('Top 40');
1724 self.append('Christian Rap');
1725 self.append('Pop / Funk');
1726 self.append('Jungle');
1727 self.append('Native American');
1728 self.append('Cabaret');
1729 self.append('New Wave');
1730 self.append('Psychedelic');
1731 self.append('Rave');
1732 self.append('Showtunes');
1733 self.append('Trailer');
1734 self.append('Lo-Fi');
1735 self.append('Tribal');
1736 self.append('Acid Punk');
1737 self.append('Acid Jazz');
1738 self.append('Polka');
1739 self.append('Retro');
1740 self.append('Musical');
1741 self.append('Rock & Roll');
1742 self.append('Hard Rock');
1743 self.append('Folk');
1744 self.append('Folk-Rock');
1745 self.append('National Folk');
1746 self.append('Swing');
1747 self.append('Fast Fusion');
1748 self.append('Bebob');
1749 self.append('Latin');
1750 self.append('Revival');
1751 self.append('Celtic');
1752 self.append('Bluegrass');
1753 self.append('Avantgarde');
1754 self.append('Gothic Rock');
1755 self.append('Progressive Rock');
1756 self.append('Psychedelic Rock');
1757 self.append('Symphonic Rock');
1758 self.append('Slow Rock');
1759 self.append('Big Band');
1760 self.append('Chorus');
1761 self.append('Easy Listening');
1762 self.append('Acoustic');
1763 self.append('Humour');
1764 self.append('Speech');
1765 self.append('Chanson');
1766 self.append('Opera');
1767 self.append('Chamber Music');
1768 self.append('Sonata');
1769 self.append('Symphony');
1770 self.append('Booty Bass');
1771 self.append('Primus');
1772 self.append('Porn Groove');
1773 self.append('Satire');
1774 self.append('Slow Jam');
1775 self.append('Club');
1776 self.append('Tango');
1777 self.append('Samba');
1778 self.append('Folklore');
1779 self.append('Ballad');
1780 self.append('Power Ballad');
1781 self.append('Rhythmic Soul');
1782 self.append('Freestyle');
1783 self.append('Duet');
1784 self.append('Punk Rock');
1785 self.append('Drum Solo');
1786 self.append('A Cappella');
1787 self.append('Euro-House');
1788 self.append('Dance Hall');
1789 self.append('Goa');
1790 self.append('Drum & Bass');
1791 self.append('Club-House');
1792 self.append('Hardcore');
1793 self.append('Terror');
1794 self.append('Indie');
1795 self.append('BritPop');
1796 self.append('Negerpunk');
1797 self.append('Polsk Punk');
1798 self.append('Beat');
1799 self.append('Christian Gangsta Rap');
1800 self.append('Heavy Metal');
1801 self.append('Black Metal');
1802 self.append('Crossover');
1803 self.append('Contemporary Christian');
1804 self.append('Christian Rock');
1805 self.append('Merengue');
1806 self.append('Salsa');
1807 self.append('Thrash Metal');
1808 self.append('Anime');
1809 self.append('JPop');
1810 self.append('Synthpop');
1811 # The follow genres I've encountered in the wild.
1812 self.append('Rock/Pop');
1813 self.EYED3_GENRE_MIN = len(self) - 1;
1814 # New genres go here
1816 self.EYED3_GENRE_MAX = len(self) - 1;
1817 self.GENRE_MAX = len(self) - 1;
1819 # Pad up to 255 with "Unknown"
1820 count = len(self);
1821 while count < 256:
1822 self.append("Unknown");
1823 count += 1;
1825 for index in range(len(self)):
1826 if self[index]:
1827 self.reverseDict[string.lower(self[index])] = index
1828 class LinkedFile:
1829 name = "";
1830 tagPadding = 0;
1831 tagSize = 0; # This includes the padding byte count.
1833 def __init__(self, fileName):
1834 if isinstance(fileName, str):
1835 self.name = unicode(fileName, sys.getfilesystemencoding());
1836 else:
1837 self.name = fileName;
1839 def tagToUserTune(tag):
1840 audio_file = None;
1841 if isinstance(tag, Mp3AudioFile):
1842 audio_file = tag;
1843 tag = audio_file.getTag();
1845 tune = u"<tune xmlns='http://jabber.org/protocol/tune'>\n";
1846 if tag.getArtist():
1847 tune += " <artist>" + tag.getArtist() + "</artist>\n";
1848 if tag.getTitle():
1849 tune += " <title>" + tag.getTitle() + "</title>\n";
1850 if tag.getAlbum():
1851 tune += " <source>" + tag.getAlbum() + "</source>\n";
1852 tune += " <track>" +\
1853 "file://" + unicode(os.path.abspath(tag.linkedFile.name)) +\
1854 "</track>\n";
1855 if audio_file:
1856 tune += " <length>" + unicode(audio_file.getPlayTime()) +\
1857 "</length>\n";
1858 tune += "</tune>\n";
1859 return tune;
1862 # Module level globals.
1864 genres = GenreMap();