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
;
27 from binfuncs
import *;
30 ID3_V1_COMMENT_DESC
= "ID3 v1 Comment";
32 ################################################################################
33 class TagException(Exception):
34 '''error reading tag'''
36 ################################################################################
52 # The size in the most recently parsed header.
60 self
.setVersion(None);
63 self
.experimental
= 0;
66 def setVersion(self
, v
):
69 self
.majorVersion
= None;
70 self
.minorVersion
= None;
71 self
.revVersion
= None;
74 if v
== ID3_CURRENT_VERSION
:
75 if self
.majorVersion
== None or self
.minorVersion
== None:
76 v
= ID3_DEFAULT_VERSION
;
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
);
88 # Handle int constants.
89 elif isinstance(v
, int):
92 self
.revVersion
) = eyeD3
.utils
.constantToVersions(v
);
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.
105 # The first three bytes of a v2 header is "ID3".
106 if f
.read(3) != "ID3":
108 TRACE_MSG("Located ID3 v2 tag");
110 # The next 2 bytes are the minor and revision versions.
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.
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
,
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
));
146 def render(self
, tagLen
= None):
148 self
.tagSize
= tagLen
;
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
,
158 TRACE_MSG("Setting tag size to %d" % tagLen
);
159 szBytes
= bin2bytes(bin2synchsafe(dec2bin(tagLen
, 32)));
161 TRACE_MSG("TagHeader Rendered");
164 ################################################################################
165 class ExtendedTagHeader
:
172 return self
.flags
& 0x40;
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();
186 return "No more than 128 frames and 1 MB total tag size.";
188 return "No more than 64 frames and 128 KB total tag size.";
190 return "No more than 32 frames and 40 KB total tag size.";
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 "\
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();
216 return "No string is longer than 1024 characters.";
218 return "No string is longer than 128 characters.";
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].";
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();
243 return "All images are 256x256 pixels or smaller.";
245 return "All images are 64x64 pixels or smaller.";
247 return "All images are exactly 64x64 pixels, unless required "\
250 def _syncsafeCRC(self
):
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);
260 def render(self
, header
, frameData
, padding
= 0):
261 assert(header
.majorVersion
== 2);
265 if header
.minorVersion
== 4:
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)
282 if self
.hasRestrictions():
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
));
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);
308 flags
= bin2bytes(f
);
309 assert(len(flags
) == 2);
310 # Extended header size.
311 size
= bin2bytes(dec2bin(size
, 32))
312 assert(len(size
) == 4);
314 paddingSize
= bin2bytes(dec2bin(padding
, 32));
316 data
= size
+ flags
+ paddingSize
;
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.
328 if header
.minorVersion
== 4:
330 sz
= bin2dec(bytes2bin(data
, 7));
331 TRACE_MSG("Extended header size (not including 4 byte size): %d" %\
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");
341 self
.flags
= ord(data
[1]);
342 TRACE_MSG("Extended header flags: %x" % self
.flags
);
345 TRACE_MSG("Extended header has update bit set");
346 assert(ord(data
[offset
]) == 0);
349 TRACE_MSG("Extended header has CRC bit set");
350 assert(ord(data
[offset
]) == 5);
352 crcData
= data
[offset
:offset
+ 5];
354 self
.crc
= bin2dec(bytes2bin(crcData
, 7));
355 TRACE_MSG("Extended header CRC: %d" % self
.crc
);
357 if self
.hasRestrictions():
358 TRACE_MSG("Extended header has restrictions bit set");
359 assert(ord(data
[offset
]) == 5);
361 self
.restrictions
= ord(data
[offset
]);
364 # v2.3 is totally different... *sigh*
365 sz
= bin2dec(bytes2bin(data
));
366 TRACE_MSG("Extended header size (not including 4 bytes size): %d" %\
368 tmpFlags
= fp
.read(2);
369 # Read the padding size, but it'll be computed during the parse.
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;
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.
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.
400 # Used internally for iterating over frames.
403 # If this value is None the tag is not linked to any particular file..
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):
411 self
.linkedFile
= LinkedFile(fileName
);
415 self
.header
= TagHeader();
416 self
.frames
= FrameSet(self
.header
);
417 self
.iterIndex
= None;
419 # Returns an read-only iterator for all frames.
424 self
.iterIndex
= None;
428 if self
.iterIndex
== None or self
.iterIndex
== len(self
.frames
):
430 frm
= self
.frames
[self
.iterIndex
];
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;
450 if isinstance(f
, file):
452 elif isinstance(f
, str) or isinstance(f
, unicode):
455 raise TagException("Invalid type passed to Tag.link: " +
458 if v
!= ID3_V1
and v
!= ID3_V2
and v
!= ID3_ANY_VERSION
:
459 raise TagException("Invalid version: " + hex(v
));
463 TRACE_MSG("Linking File: " + fileName
);
465 if self
.__loadV
1Tag
(f
):
468 padding
= self
.__loadV
2Tag
(f
);
471 elif v
== ID3_ANY_VERSION
:
472 padding
= self
.__loadV
2Tag
(f
);
477 if self
.__loadV
1Tag
(f
):
480 self
.linkedFile
= LinkedFile(fileName
);
482 # In the case of a v1.x tag this is zero.
483 self
.linkedFile
.tagSize
= self
.header
.tagSize
;
484 self
.linkedFile
.tagPadding
= padding
;
486 self
.linkedFile
.tagSize
= 0;
487 self
.linkedFile
.tagPadding
= 0;
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.");
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
:
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;
521 self
.__saveV
1Tag
(version
);
523 elif version
& ID3_V2
:
524 self
.__saveV
2Tag
(version
);
527 raise TagException("Invalid version: %s" % hex(version
));
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
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 "\
539 if version
== ID3_CURRENT_VERSION
:
540 version
= self
.getVersion();
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");
553 if ((version
& ID3_V2
) or (version
== ID3_ANY_VERSION
)) and\
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();
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):
588 frameIDs
= [artistID
];
591 f
= self
.frames
[fid
];
597 f
= self
.frames
[ALBUM_FID
];
603 # Get the track title. By default the main title is returned. Optionally,
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
];
616 def getDate(self
, fid
= None):
618 for fid
in DATE_FIDS
:
620 return self
.frames
[fid
];
622 return self
.frames
[fid
];
624 def getYear(self
, fid
= None):
625 dateFrame
= self
.getDate(fid
);
627 return dateFrame
[0].getYear();
631 # Throws GenreException when the tag contains an unrecognized genre format.
632 # Note this method returns a eyeD3.Genre object, not a raw string.
634 f
= self
.frames
[GENRE_FID
];
642 def _getNum(self
, fid
):
645 f
= self
.frames
[fid
];
647 n
= f
[0].text
.split('/')
649 tn
= self
.toInt(n
[0])
651 tn
= self
.toInt(n
[0])
652 tt
= self
.toInt(n
[1])
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.
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.
682 urls
.extend(self
.frames
[fid
]);
683 urls
.extend(self
.frames
[USERURL_FID
]);
686 def getUserTextFrames(self
):
687 return self
.frames
[USERTEXT_FID
];
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
):
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
));
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();
724 self
.frames
.removeFramesByID(dateFrames
[0].header
.id);
727 self
.frames
.removeFramesByID(fid
);
729 dateStr
= self
.strToUnicode(str(year
));
730 if len(dateStr
) != 4:
731 raise TagException("Invalid Year field: " + dateStr
);
733 dateStr
+= "-" + self
.__padDateField
(month
);
735 dateStr
+= "-" + self
.__padDateField
(dayOfMonth
);
737 dateStr
+= "T" + self
.__padDateField
(hour
);
739 dateStr
+= ":" + self
.__padDateField
(minute
);
741 dateStr
+= ":" + self
.__padDateField
(second
);
745 dateFrame
= self
.frames
[fid
];
748 dateFrame
[0].setDate(self
.encoding
+ dateStr
);
750 header
= FrameHeader(self
.header
);
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
);
766 if isinstance(g
, Genre
):
767 self
.frames
.setTextFrame(GENRE_FID
, self
.strToUnicode(str(g
)),
769 elif isinstance(g
, str):
772 self
.frames
.setTextFrame(GENRE_FID
, self
.strToUnicode(str(gObj
)),
774 elif isinstance(g
, int):
777 self
.frames
.setTextFrame(GENRE_FID
, self
.strToUnicode(str(gObj
)),
780 raise TagException("Invalid type passed to setGenre: %s" +
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
);
800 if n
[1] >= 0 and n
[1] <= 9:
801 totalStr
= "0" + str(n
[1]);
803 totalStr
= str(n
[1]);
804 zPadding
= len(totalStr
) - 1;
810 # Pad with zeros according to how large the total count is.
812 if len(trackStr
) == 1:
813 trackStr
= "0" + trackStr
;
814 if len(trackStr
) < len(totalStr
):
815 trackStr
= ("0" * (len(totalStr
) - len(trackStr
))) + trackStr
;
818 if trackStr
and totalStr
:
819 s
= trackStr
+ "/" + totalStr
;
820 elif trackStr
and not totalStr
:
823 self
.frames
.setTextFrame(fid
, self
.strToUnicode(s
),
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
):
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
];
836 if c
.lang
== lang
and c
.description
== desc
:
837 self
.frames
.remove(c
);
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
):
847 u_frames
= self
.frames
[USERTEXT_FID
];
849 if u
.description
== desc
:
850 self
.frames
.remove(u
);
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
""):
861 image_frame
= ImageFrame
.create(type, image_file_path
, desc
);
862 self
.frames
.addFrame(image_frame
);
864 image_frames
= self
.frames
[IMAGE_FID
];
865 for i
in image_frames
:
866 if i
.pictureType
== type:
867 self
.frames
.remove(i
);
870 def getPlayCount(self
):
871 if self
.frames
[PLAYCOUNT_FID
]:
872 pc
= self
.frames
[PLAYCOUNT_FID
][0];
873 assert(isinstance(pc
, PlayCountFrame
));
878 def setPlayCount(self
, count
):
880 if self
.frames
[PLAYCOUNT_FID
]:
881 pc
= self
.frames
[PLAYCOUNT_FID
][0];
882 assert(isinstance(pc
, PlayCountFrame
));
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();
893 self
.setPlayCount(pc
+ n
);
895 self
.setPlayCount(n
);
897 def getUniqueFileIDs(self
):
898 return self
.frames
[UNIQUE_FILE_ID_FID
];
900 def addUniqueFileID(self
, owner_id
, id):
902 ufids
= self
.frames
[UNIQUE_FILE_ID_FID
];
904 if ufid
.owner_id
== owner_id
:
905 self
.frames
.remove(ufid
);
908 self
.frames
.setUniqueFileIDFrame(owner_id
, id);
911 bpm
= self
.frames
[BPM_FID
];
913 return int(bpm
[0].text
);
917 def setBPM(self
, bpm
):
918 self
.setTextFrame(BPM_FID
, self
.strToUnicode(str(bpm
)));
920 def getPublisher(self
):
921 pub
= self
.frames
[PUBLISHER_FID
];
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.
930 return self
.header
.majorVersion
== 1;
932 return self
.header
.majorVersion
== 2;
934 def setVersion(self
, v
):
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
):
946 self
.frames
.removeFramesByID(fid
);
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");
961 for f
in self
.frames
:
964 def tagToString(self
, pattern
):
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]));
977 def _prettyTrack(self
, track
):
980 track_str
= str(track
);
981 if len(track_str
) == 1:
982 track_str
= "0" + 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
)
993 def __saveV1Tag(self
, version
):
994 assert(version
& ID3_V1
);
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);
1004 tag
+= self
._fixToWidth
(y
.encode("latin_1"), 4);
1007 for c
in self
.getComments():
1008 if c
.description
== ID3_V1_COMMENT_DESC
:
1010 # We prefer this one over "";
1012 elif c
.description
== "":
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];
1019 cmt
= cmt
[0:28] + "\x00" + chr(int(track
) & 0xff);
1022 if not self
.getGenre():
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.
1033 tagFile
.seek(-128, 2);
1034 if tagFile
.read(3) == "TAG":
1035 tagFile
.seek(-128, 2);
1039 # File is smaller than 128 bytes.
1046 def _fixToWidth(self
, s
, n
):
1048 retval
= retval
[0:n
];
1049 retval
= retval
+ ("\x00" * (n
- len(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):
1061 # Seek to the end of the file where all ID3v1 tags are written.
1063 strip_chars
= string
.whitespace
+ "\x00";
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
);
1075 self
.setTitle(unicode(title
, "latin1"));
1077 artist
= re
.sub("\x00+$", "", id3tag
[33:63].strip(strip_chars
));
1078 TRACE_MSG("Artist: " + artist
);
1080 self
.setArtist(unicode(artist
, "latin1"));
1082 album
= re
.sub("\x00+$", "", id3tag
[63:93].strip(strip_chars
));
1083 TRACE_MSG("Album: " + album
);
1085 self
.setAlbum(unicode(album
, "latin1"));
1087 year
= re
.sub("\x00+$", "", id3tag
[93:97].strip(strip_chars
));
1088 TRACE_MSG("Year: " + year
);
1090 if year
and int(year
):
1093 # Bogus year strings.
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];
1110 comment
= re
.sub("\x00+$", "", comment
).rstrip();
1111 TRACE_MSG("Comment: " + 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
);
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
);
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.
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
);
1144 if self
.header
.minorVersion
== 4:
1145 h
= FrameHeader(self
.header
);
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.
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
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
)));
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.
1180 TRACE_MSG("Rendering extended header");
1181 extHeaderData
+= self
.extendedHeader
.render(self
.header
, frameData
,
1184 if rewriteFile
or (10 + len(headerData
) + len(extHeaderData
) +\
1185 len(frameData
)) >= currTagSize
:
1186 TRACE_MSG("File rewrite required");
1190 paddingSize
= currTagSize
- (len(headerData
) + len(extHeaderData
) +\
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
));
1200 tagData
= headerData
+ extHeaderData
+ frameData
;
1204 tagFile
= file(self
.linkedFile
.name
, "r+b");
1205 TRACE_MSG("Writing %d bytes of tag data" % len(tagData
));
1206 tagFile
.write(tagData
);
1210 tagFile
= file(self
.linkedFile
.name
, "rb");
1211 # Read all audio data
1212 tagFile
.seek(currTagSize
);
1213 audioData
= tagFile
.read();
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
);
1225 shutil
.copyfile(tmpName
, self
.linkedFile
.name
);
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):
1247 # Look for a tag and if found load it.
1248 if not self
.header
.parse(fp
):
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():
1262 raise TagException(str(ex
));
1263 except TagException
:
1279 def __padDateField(self
, f
):
1283 elif len(fStr
) == 1:
1286 raise TagException("Invalid date field: " + fStr
);
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
];
1296 return f
[0].comment
;
1301 ################################################################################
1302 class GenreException(Exception):
1303 '''Problem looking up genre'''
1305 ################################################################################
1310 def __init__(self
, id = None, name
= None):
1313 elif name
is not None:
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));
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));
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
));
1356 if utils
.strictID3():
1357 raise GenreException("Invalid genre 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():
1379 if genres
[name
] != id:
1380 raise GenreException("eyeD3.genres[" + str(id) + "] " +\
1381 "does not match " + name
);
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
1394 # Throws GenreException when an invalid string is passed.
1395 def parse(self
, genreStr
):
1397 str(genreStr
.encode('utf-8')).strip(string
.whitespace
+ '\x00');
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:
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
));
1421 # Match (03), (0)Blues, (15) Rap
1422 regex
= re
.compile("\(([0-9][0-9]?[0-9]?)\)(.*)$");
1423 m
= regex
.match(genreStr
);
1425 (id, name
) = m
.groups();
1426 if len(id) != 1 and id[0] == '0':
1430 self
.set(int(id), name
.strip());
1432 self
.setId(int(id));
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
);
1441 raise GenreException("Genre string cannot be parsed with '%s': %s" %\
1442 (regex
.pattern
, genreStr
));
1447 s
+= "(" + str(self
.id) + ")"
1452 ################################################################################
1453 class InvalidAudioFormatException(Exception):
1454 '''Problems with audio format'''
1456 ################################################################################
1461 # Number of seconds required to play the audio file.
1464 def __init__(self
, fileName
):
1465 self
.fileName
= fileName
;
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
);
1481 new_name
= dir + os
.sep
+ name
.encode(fsencoding
) + base_ext
;
1483 os
.rename(self
.fileName
, new_name
);
1484 self
.fileName
= new_name
;
1486 raise TagException("Error renaming '%s' to '%s'" % (self
.fileName
,
1489 def getPlayTime(self
):
1490 return self
.play_time
;
1492 def getPlayTimeString(self
):
1493 total
= self
.getPlayTime();
1495 m
= (total
% 3600) / 60;
1496 s
= (total
% 3600) % 60;
1498 timeStr
= "%d:%.2d:%.2d" % (h
, m
, s
);
1500 timeStr
= "%d:%.2d" % (m
, s
);
1504 ################################################################################
1505 class Mp3AudioFile(TagFile
):
1506 header
= mp3
.Header();
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
;
1517 f
= file(fileName
, "rb");
1519 hasTag
= tag
.link(f
, tagVersion
);
1520 # Find the first mp3 frame.
1527 # XXX: Note that v2.4 allows for appended tags; account for that.
1528 framePos
= tag
.header
.SIZE
+ tag
.header
.tagSize
;
1531 bString
= f
.read(4);
1532 if len(bString
) < 4:
1533 raise InvalidAudioFormatException("Unable to find a valid mp3 "\
1535 frameHead
= bin2dec(bytes2bin(bString
));
1536 header
= mp3
.Header();
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.
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 "\
1550 frameHead
= bin2dec(bytes2bin(bString
));
1555 bString
= f
.read(1);
1556 if len(bString
) != 1:
1557 raise InvalidAudioFormatException("Unable to find a valid mp3 "\
1559 frameHead |
= ord(bString
[0]);
1561 TRACE_MSG("mp3 header %x found at position: %d (0x%x)" % \
1562 (frameHead
, f
.tell() - 4, f
.tell() - 4));
1564 # Decode the header.
1566 header
.decode(frameHead
);
1567 # Check for Xing header inforamtion which will always be in the
1568 # first "null" frame.
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");
1577 except mp3
.Mp3Exception
, ex
:
1578 raise InvalidAudioFormatException(str(ex
));
1580 # Compute track play time.
1581 tpf
= mp3
.computeTimePerFrame(header
);
1583 self
.play_time
= int(tpf
* xingHeader
.numFrames
);
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.
1590 if f
.read(3) == "TAG":
1592 elif tag
and tag
.isV1():
1594 self
.play_time
= int((length
/ header
.frameLength
) * tpf
);
1596 self
.header
= header
;
1597 self
.xingHeader
= xingHeader
;
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
;
1606 tpf
= eyeD3
.mp3
.computeTimePerFrame(self
.header
);
1607 br
= int((xHead
.numBytes
* 8) / (tpf
* xHead
.numFrames
* 1000));
1610 br
= self
.header
.bitRate
;
1614 def getBitRateString(self
):
1615 (vbr
, bitRate
) = self
.getBitRate();
1616 brs
= "%d kb/s" % bitRate
;
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
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
);
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()];
1655 raise IndexError(key
+ " genre not found");
1657 raise TypeError("genre key must be type int or string");
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');
1679 self
.append('Reggae');
1680 self
.append('Rock');
1681 self
.append('Techno');
1682 self
.append('Industrial');
1683 self
.append('Alternative');
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');
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"
1822 self
.append("Unknown");
1825 for index
in range(len(self
)):
1827 self
.reverseDict
[string
.lower(self
[index
])] = index
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());
1837 self
.name
= fileName
;
1839 def tagToUserTune(tag
):
1841 if isinstance(tag
, Mp3AudioFile
):
1843 tag
= audio_file
.getTag();
1845 tune
= u
"<tune xmlns='http://jabber.org/protocol/tune'>\n";
1847 tune
+= " <artist>" + tag
.getArtist() + "</artist>\n";
1849 tune
+= " <title>" + tag
.getTitle() + "</title>\n";
1851 tune
+= " <source>" + tag
.getAlbum() + "</source>\n";
1852 tune
+= " <track>" +\
1853 "file://" + unicode(os
.path
.abspath(tag
.linkedFile
.name
)) +\
1856 tune
+= " <length>" + unicode(audio_file
.getPlayTime()) +\
1858 tune
+= "</tune>\n";
1862 # Module level globals.
1864 genres
= GenreMap();