1 ###############################################################################
2 # Copyright (C) 2002-2007 Travis Shirk <travis@pobox.com>
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 2 of the License, or
7 # (at your option) any later version.
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
18 ################################################################################
19 import re
, os
, string
, stat
, shutil
, tempfile
, binascii
;
26 from binfuncs
import *;
29 ID3_V1_COMMENT_DESC
= "ID3 v1 Comment";
31 ################################################################################
32 class TagException(Exception):
33 '''error reading tag'''
35 ################################################################################
51 # The size in the most recently parsed header.
59 self
.setVersion(None);
62 self
.experimental
= 0;
65 def setVersion(self
, v
):
68 self
.majorVersion
= None;
69 self
.minorVersion
= None;
70 self
.revVersion
= None;
73 if v
== ID3_CURRENT_VERSION
:
74 if self
.majorVersion
== None or self
.minorVersion
== None:
75 v
= ID3_DEFAULT_VERSION
;
78 elif v
== ID3_ANY_VERSION
:
79 v
= ID3_DEFAULT_VERSION
;
81 # Handle 3-element lists or tuples.
82 if isinstance(v
, tuple) or isinstance(v
, list):
83 self
.version
= eyeD3
.utils
.versionsToConstant(v
);
87 # Handle int constants.
88 elif isinstance(v
, int):
91 self
.revVersion
) = eyeD3
.utils
.constantToVersions(v
);
94 raise TypeError("Wrong type: %s" % str(type(v
)));
96 # Given a file handle this method attempts to identify and then parse
97 # a ID3 v2 header. If successful, the parsed values are stored in
98 # the instance variable. If the files does not contain an ID3v2 tag
99 # false is returned. A TagException is thrown if a tag is found, but is
100 # not valid or corrupt.
104 # The first three bytes of a v2 header is "ID3".
105 if f
.read(3) != "ID3":
107 TRACE_MSG("Located ID3 v2 tag");
109 # The next 2 bytes are the minor and revision versions.
112 minor
= ord(version
[0]);
113 rev
= ord(version
[1]);
114 TRACE_MSG("TagHeader [major]: " + str(major
));
115 TRACE_MSG("TagHeader [minor]: " + str(minor
));
116 TRACE_MSG("TagHeader [revis]: " + str(rev
));
117 if not (major
== 2 and (minor
>= 2 and minor
<= 4)):
118 raise TagException("ID3 v" + str(major
) + "." + str(minor
) +\
119 " is not supported.");
120 # Get all the version madness in sync.
121 self
.setVersion([major
, minor
, rev
]);
123 # The first 4 bits of the next byte are flags.
127 self
.footer
) = bytes2bin(f
.read(1))[0:4];
128 TRACE_MSG("TagHeader [flags]: unsync(%d) extended(%d) "\
129 "experimental(%d) footer(%d)" % (self
.unsync
, self
.extended
,
133 # The size of the extended header (optional), frames, and padding
134 # afer unsynchronization. This is a sync safe integer, so only the
135 # bottom 7 bits of each byte are used.
136 tagSizeStr
= f
.read(4);
137 TRACE_MSG("TagHeader [size string]: 0x%02x%02x%02x%02x" %\
138 (ord(tagSizeStr
[0]), ord(tagSizeStr
[1]),
139 ord(tagSizeStr
[2]), ord(tagSizeStr
[3])));
140 self
.tagSize
= bin2dec(bytes2bin(tagSizeStr
, 7));
141 TRACE_MSG("TagHeader [size]: %d (0x%x)" % (self
.tagSize
, self
.tagSize
));
145 def render(self
, tagLen
= None):
147 self
.tagSize
= tagLen
;
150 data
+= chr(self
.minorVersion
) + chr(self
.revVersion
);
151 # not not the values so we only get 1's and 0's.
152 data
+= bin2bytes([not not self
.unsync
,
153 not not self
.extended
,
154 not not self
.experimental
,
157 TRACE_MSG("Setting tag size to %d" % tagLen
);
158 szBytes
= bin2bytes(bin2synchsafe(dec2bin(tagLen
, 32)));
160 TRACE_MSG("TagHeader rendered %d bytes" % len(data
));
163 ################################################################################
164 class ExtendedTagHeader
:
171 return self
.flags
& 0x40;
173 return self
.flags
& 0x20;
174 def hasRestrictions(self
, minor_version
= None):
175 return self
.flags
& 0x10;
177 def setSizeRestrictions(self
, v
):
178 assert(v
>= 0 and v
<= 3);
179 self
.restrictions
= (v
<< 6) |
(self
.restrictions
& 0x3f);
180 def getSizeRestrictions(self
):
181 return self
.restrictions
>> 6;
182 def getSizeRestrictionsString(self
):
183 val
= self
.getSizeRestrictions();
185 return "No more than 128 frames and 1 MB total tag size.";
187 return "No more than 64 frames and 128 KB total tag size.";
189 return "No more than 32 frames and 40 KB total tag size.";
191 return "No more than 32 frames and 4 KB total tag size.";
193 def setTextEncodingRestrictions(self
, v
):
194 assert(v
== 0 or v
== 1);
195 self
.restrictions ^
= 0x20;
196 def getTextEncodingRestrictions(self
):
197 return self
.restrictions
& 0x20;
198 def getTextEncodingRestrictionsString(self
):
199 if self
.getTextEncodingRestrictions():
200 return "Strings are only encoded with ISO-8859-1 [ISO-8859-1] or "\
205 def setTextFieldSizeRestrictions(self
, v
):
206 assert(v
>= 0 and v
<= 3);
207 self
.restrictions
= (v
<< 3) |
(self
.restrictions
& 0xe7);
208 def getTextFieldSizeRestrictions(self
):
209 return (self
.restrictions
>> 3) & 0x03;
210 def getTextFieldSizeRestrictionsString(self
):
211 val
= self
.getTextFieldSizeRestrictions();
215 return "No string is longer than 1024 characters.";
217 return "No string is longer than 128 characters.";
219 return "No string is longer than 30 characters.";
221 def setImageEncodingRestrictions(self
, v
):
222 assert(v
== 0 or v
== 1);
223 self
.restrictions ^
= 0x04;
224 def getImageEncodingRestrictions(self
):
225 return self
.restrictions
& 0x04;
226 def getImageEncodingRestrictionsString(self
):
227 if self
.getImageEncodingRestrictions():
228 return "Images are encoded only with PNG [PNG] or JPEG [JFIF].";
232 def setImageSizeRestrictions(self
, v
):
233 assert(v
>= 0 and v
<= 3);
234 self
.restrictions
= v |
(self
.restrictions
& 0xfc);
235 def getImageSizeRestrictions(self
):
236 return self
.restrictions
& 0x03;
237 def getImageSizeRestrictionsString(self
):
238 val
= self
.getImageSizeRestrictions();
242 return "All images are 256x256 pixels or smaller.";
244 return "All images are 64x64 pixels or smaller.";
246 return "All images are exactly 64x64 pixels, unless required "\
249 def _syncsafeCRC(self
):
251 bites
+= chr((self
.crc
>> 28) & 0x7f);
252 bites
+= chr((self
.crc
>> 21) & 0x7f);
253 bites
+= chr((self
.crc
>> 14) & 0x7f);
254 bites
+= chr((self
.crc
>> 7) & 0x7f);
255 bites
+= chr((self
.crc
>> 0) & 0x7f);
259 def render(self
, header
, frameData
, padding
=0):
260 assert(header
.majorVersion
== 2);
264 if header
.minorVersion
== 4:
272 # XXX: Using the absolute value of the CRC. The spec is unclear
273 # about the type of this data.
274 self
.crc
= int(math
.fabs(binascii
.crc32(frameData
+\
275 ("\x00" * padding
))));
276 crc_data
= self
._syncsafeCRC
();
277 if len(crc_data
) < 5:
278 crc_data
= ("\x00" * (5 - len(crc_data
))) + crc_data
279 assert(len(crc_data
) == 5)
281 if self
.hasRestrictions():
283 assert(len(self
.restrictions
) == 1);
284 data
+= self
.restrictions
;
285 TRACE_MSG("Rendered extended header data (%d bytes)" % len(data
));
287 # Extended header size.
288 size
= bin2bytes(bin2synchsafe(dec2bin(len(data
) + 6, 32)))
289 assert(len(size
) == 4);
291 data
= size
+ "\x01" + bin2bytes(dec2bin(self
.flags
)) + data
;
292 TRACE_MSG("Rendered extended header of size %d" % len(data
));
295 size
= 6; # Note, the 4 size bytes are not included in the size
300 # XXX: Using the absolute value of the CRC. The spec is unclear
301 # about the type of this value.
302 self
.crc
= int(math
.fabs(binascii
.crc32(frameData
+\
303 ("\x00" * padding
))));
304 crc
= bin2bytes(dec2bin(self
.crc
));
305 assert(len(crc
) == 4);
307 flags
= bin2bytes(f
);
308 assert(len(flags
) == 2);
309 # Extended header size.
310 size
= bin2bytes(dec2bin(size
, 32))
311 assert(len(size
) == 4);
313 paddingSize
= bin2bytes(dec2bin(padding
, 32));
315 data
= size
+ flags
+ paddingSize
;
320 # Only call this when you *know* there is an extened header.
321 def parse(self
, fp
, header
):
322 assert(header
.majorVersion
== 2);
324 TRACE_MSG("Parsing extended header @ 0x%x" % fp
.tell());
325 # First 4 bytes is the size of the extended header.
327 if header
.minorVersion
== 4:
329 sz
= bin2dec(bytes2bin(data
, 7));
331 TRACE_MSG("Extended header size (includes the 4 size bytes): %d" % sz
);
332 data
= fp
.read(sz
- 4);
334 if ord(data
[0]) != 1 or (ord(data
[1]) & 0x8f):
335 # As of 2.4 the first byte is 1 and the second can only have
336 # bits 6, 5, and 4 set.
337 raise TagException("Invalid Extended Header");
340 self
.flags
= ord(data
[1]);
341 TRACE_MSG("Extended header flags: %x" % self
.flags
);
344 TRACE_MSG("Extended header has update bit set");
345 assert(ord(data
[offset
]) == 0);
348 TRACE_MSG("Extended header has CRC bit set");
349 assert(ord(data
[offset
]) == 5);
351 crcData
= data
[offset
:offset
+ 5];
353 self
.crc
= bin2dec(bytes2bin(crcData
, 7));
354 TRACE_MSG("Extended header CRC: %d" % self
.crc
);
356 if self
.hasRestrictions():
357 TRACE_MSG("Extended header has restrictions bit set");
358 assert(ord(data
[offset
]) == 5);
360 self
.restrictions
= ord(data
[offset
]);
363 # v2.3 is totally different... *sigh*
364 sz
= bin2dec(bytes2bin(data
));
365 TRACE_MSG("Extended header size (not including 4 size bytes): %d" % sz
)
366 self
.size
= sz
+ 4 # +4 to include size bytes
367 tmpFlags
= fp
.read(2);
368 # Read the padding size, but it'll be computed during the parse.
370 TRACE_MSG("Extended header says there is %d bytes of padding" %
371 bin2dec(bytes2bin(ps
)));
372 # Make this look like a v2.4 mask.
373 self
.flags
= ord(tmpFlags
[0]) >> 2;
375 TRACE_MSG("Extended header has CRC bit set");
376 crcData
= fp
.read(4);
377 self
.crc
= bin2dec(bytes2bin(crcData
));
378 TRACE_MSG("Extended header CRC: %d" % self
.crc
);
381 ################################################################################
382 # ID3 tag class. The class is capable of reading v1 and v2 tags. ID3 v1.x
383 # are converted to v2 frames.
385 # Latin1 is the default (0x00)
386 encoding
= DEFAULT_ENCODING
;
388 # ID3v1 tags do not contain a header. The only ID3v1 values stored
389 # in this header are the major/minor version.
390 header
= TagHeader();
392 # Optional in v2 tags.
393 extendedHeader
= ExtendedTagHeader();
395 # Contains the tag's frames. ID3v1 fields are read and converted
396 # the the corresponding v2 frame.
399 # Used internally for iterating over frames.
402 # If this value is None the tag is not linked to any particular file..
405 # add TDTG (or TXXX) - Tagging Time - when saved
408 # Constructor. An empty tag is created and the link method is used
409 # to read an mp3 file's v1.x or v2.x tag. You can optionally set a
410 # file name, but it will not be read, but may be written to.
411 def __init__(self
, fileName
= None):
413 self
.linkedFile
= LinkedFile(fileName
);
417 self
.header
= TagHeader();
418 self
.frames
= FrameSet(self
.header
);
419 self
.iterIndex
= None;
421 # Returns an read-only iterator for all frames.
426 self
.iterIndex
= None;
430 if self
.iterIndex
== None or self
.iterIndex
== len(self
.frames
):
432 frm
= self
.frames
[self
.iterIndex
];
436 # Returns true when an ID3 tag is read from f which may be a file name
437 # or an aleady opened file object. In the latter case, the file object
438 # is not closed when this method returns.
440 # By default, both ID3 v2 and v1 tags are parsed in that order.
441 # If a v2 tag is found then a v1 parse is not performed. This behavior
442 # can be refined by passing ID3_V1 or ID3_V2 as the second argument
443 # instead of the default ID3_ANY_VERSION.
445 # Converts all ID3v1 data into ID3v2 frames internally.
446 # May throw IOError, or TagException if parsing fails.
447 def link(self
, f
, v
= ID3_ANY_VERSION
):
448 self
.linkedFile
= None;
452 if isinstance(f
, file):
454 elif isinstance(f
, str) or isinstance(f
, unicode):
457 raise TagException("Invalid type passed to Tag.link: " +
460 if v
!= ID3_V1
and v
!= ID3_V2
and v
!= ID3_ANY_VERSION
:
461 raise TagException("Invalid version: " + hex(v
));
465 TRACE_MSG("Linking File: " + fileName
);
467 if self
.__loadV
1Tag
(f
):
470 padding
= self
.__loadV
2Tag
(f
);
473 elif v
== ID3_ANY_VERSION
:
474 padding
= self
.__loadV
2Tag
(f
);
479 if self
.__loadV
1Tag
(f
):
482 self
.linkedFile
= LinkedFile(fileName
);
484 # In the case of a v1.x tag this is zero.
485 self
.linkedFile
.tagSize
= self
.header
.tagSize
;
486 self
.linkedFile
.tagPadding
= padding
;
488 self
.linkedFile
.tagSize
= 0;
489 self
.linkedFile
.tagPadding
= 0;
492 # Write the current tag state to the linked file.
493 # The version of the ID3 file format that should be written can
494 # be passed as an argument; the default is ID3_CURRENT_VERSION.
495 def update(self
, version
= ID3_CURRENT_VERSION
, backup
= 0):
496 if not self
.linkedFile
:
497 raise TagException("The Tag is not linked to a file.");
500 shutil
.copyfile(self
.linkedFile
.name
, self
.linkedFile
.name
+ ".orig");
502 self
.setVersion(version
);
503 version
= self
.getVersion();
504 if version
== ID3_V2_2
:
505 raise TagException("Unable to write ID3 v2.2");
506 # If v1.0 is being requested explicitly then so be it, if not and there is
507 # a track number then bumping to v1.1 is /probably/ best.
508 if self
.header
.majorVersion
== 1 and self
.header
.minorVersion
== 0 and\
509 self
.getTrackNum()[0] != None and version
!= ID3_V1_0
:
511 self
.setVersion(version
);
513 # If there are no frames then simply remove the current tag.
514 if len(self
.frames
) == 0:
515 self
.remove(version
);
516 self
.header
= TagHeader();
517 self
.frames
.setTagHeader(self
.header
);
518 self
.linkedFile
.tagPadding
= 0;
519 self
.linkedFile
.tagSize
= 0;
523 self
.__saveV
1Tag
(version
);
525 elif version
& ID3_V2
:
526 self
.__saveV
2Tag
(version
);
529 raise TagException("Invalid version: %s" % hex(version
));
532 # Remove the tag. The version argument can selectively remove specific
533 # ID3 tag versions; the default is ID3_CURRENT_VERSION meaning the version
534 # of the current tag. A value of ID3_ANY_VERSION causes all tags to be
536 def remove(self
, version
= ID3_CURRENT_VERSION
):
537 if not self
.linkedFile
:
538 raise TagException("The Tag is not linked to a file; nothing to "\
541 if version
== ID3_CURRENT_VERSION
:
542 version
= self
.getVersion();
545 if version
& ID3_V1
or version
== ID3_ANY_VERSION
:
546 tagFile
= file(self
.linkedFile
.name
, "r+b");
547 tagFile
.seek(-128, 2);
548 if tagFile
.read(3) == "TAG":
549 TRACE_MSG("Removing ID3 v1.x Tag");
555 if ((version
& ID3_V2
) or (version
== ID3_ANY_VERSION
)) and\
557 tagFile
= file(self
.linkedFile
.name
, "r+b");
558 if tagFile
.read(3) == "ID3":
559 TRACE_MSG("Removing ID3 v2.x Tag");
560 tagSize
= self
.header
.tagSize
+ self
.header
.SIZE
;
561 tagFile
.seek(tagSize
);
564 tmpName
= tempfile
.mktemp();
565 tmpFile
= file(tmpName
, "w+b");
567 # Write audio data in chunks
568 self
.__copyRemaining
(tagFile
, tmpFile
);
575 shutil
.copyfile(tmpName
, self
.linkedFile
.name
);
582 # Get artist. There are a few frames that can contain this information,
583 # and they are subtley different.
584 # eyeD3.frames.ARTIST_FID - Lead performer(s)/Soloist(s)
585 # eyeD3.frames.BAND_FID - Band/orchestra/accompaniment
586 # eyeD3.frames.CONDUCTOR_FID - Conductor/performer refinement
587 # eyeD3.frames.REMIXER_FID - Interpreted, remixed, or otherwise modified by
589 # Any of these values can be passed as an argument to select the artist
590 # of interest. By default, the first one found (searched in the above order)
591 # is the value returned. Most tags only have the ARTIST_FID, btw.
593 # When no artist is found, an empty string is returned.
595 def getArtist(self
, artistID
= ARTIST_FIDS
):
596 if isinstance(artistID
, list):
599 frameIDs
= [artistID
];
602 f
= self
.frames
[fid
];
608 f
= self
.frames
[ALBUM_FID
];
614 # Get the track title. By default the main title is returned. Optionally,
616 # eyeD3.frames.TITLE_FID - The title; the default.
617 # eyeD3.frames.SUBTITLE_FID - The subtitle.
618 # eyeD3.frames.CONTENT_TITLE_FID - Conten group description???? Rare.
619 # An empty string is returned when no title exists.
620 def getTitle(self
, titleID
= TITLE_FID
):
621 f
= self
.frames
[titleID
];
627 def getDate(self
, fid
= None):
629 for fid
in DATE_FIDS
:
631 return self
.frames
[fid
];
633 return self
.frames
[fid
];
635 def getYear(self
, fid
= None):
636 dateFrame
= self
.getDate(fid
);
638 return dateFrame
[0].getYear();
642 # Throws GenreException when the tag contains an unrecognized genre format.
643 # Note this method returns a eyeD3.Genre object, not a raw string.
645 f
= self
.frames
[GENRE_FID
];
653 def _getNum(self
, fid
):
656 f
= self
.frames
[fid
];
658 n
= f
[0].text
.split('/')
660 tn
= self
.toInt(n
[0])
662 tn
= self
.toInt(n
[0])
663 tt
= self
.toInt(n
[1])
666 # Returns a tuple with the first value containing the track number and the
667 # second the total number of tracks. One or both of these values may be
668 # None depending on what is available in the tag.
669 def getTrackNum(self
):
670 return self
._getNum
(TRACKNUM_FID
)
672 # Like TrackNum, except for DiscNum--that is, position in a set. Most
673 # songs won't have this or it will be 1/1.
674 def getDiscNum(self
):
675 return self
._getNum
(DISCNUM_FID
)
677 # Since multiple comment frames are allowed this returns a list with 0
678 # or more elements. The elements are not the comment strings, they are
679 # eyeD3.frames.CommentFrame objects.
680 def getComments(self
):
681 return self
.frames
[COMMENT_FID
];
683 # Since multiple lyrics frames are allowed this returns a list with 0
684 # or more elements. The elements are not the lyrics strings, they are
685 # eyeD3.frames.LyricsFrame objects.
687 return self
.frames
[LYRICS_FID
];
689 # Returns a list (possibly empty) of eyeD3.frames.ImageFrame objects.
691 return self
.frames
[IMAGE_FID
];
693 # Returns a list (possibly empty) of eyeD3.frames.ObjectFrame objects.
694 def getObjects(self
):
695 return self
.frames
[OBJECT_FID
];
697 # Returns a list (possibly empty) of eyeD3.frames.URLFrame objects.
698 # Both URLFrame and UserURLFrame objects are returned. UserURLFrames
699 # add a description and encoding, and have a different frame ID.
703 urls
.extend(self
.frames
[fid
]);
704 urls
.extend(self
.frames
[USERURL_FID
]);
707 def getUserTextFrames(self
):
708 return self
.frames
[USERTEXT_FID
];
711 return self
.frames
[CDID_FID
];
713 def getVersion(self
):
714 return self
.header
.version
;
716 def getVersionStr(self
):
717 return versionToString(self
.header
.version
);
719 def strToUnicode(self
, s
):
721 if t
!= unicode and t
== str:
722 s
= unicode(s
, eyeD3
.LOCAL_ENCODING
);
723 elif t
!= unicode and t
!= str:
724 raise TagException("Wrong type passed to strToUnicode: %s" % str(t
));
727 # Set the artist name. Arguments equal to None or "" cause the frame to
728 # be removed. An optional second argument can be passed to select the
729 # actual artist frame that should be set. By default, the main artist frame
730 # (TPE1) is the value used.
731 def setArtist(self
, a
, id = ARTIST_FID
):
732 self
.setTextFrame(id, self
.strToUnicode(a
));
734 def setAlbum(self
, a
):
735 self
.setTextFrame(ALBUM_FID
, self
.strToUnicode(a
));
737 def setTitle(self
, t
, titleID
= TITLE_FID
):
738 self
.setTextFrame(titleID
, self
.strToUnicode(t
));
740 def setDate(self
, year
, month
= None, dayOfMonth
= None,
741 hour
= None, minute
= None, second
= None, fid
= None):
742 if not year
and not fid
:
743 dateFrames
= self
.getDate();
745 self
.frames
.removeFramesByID(dateFrames
[0].header
.id)
748 self
.frames
.removeFramesByID(fid
)
750 self
.frames
.removeFramesByID(frames
.OBSOLETE_YEAR_FID
)
752 dateStr
= self
.strToUnicode(str(year
));
753 if len(dateStr
) != 4:
754 raise TagException("Invalid Year field: " + dateStr
);
756 dateStr
+= "-" + self
.__padDateField
(month
);
758 dateStr
+= "-" + self
.__padDateField
(dayOfMonth
);
760 dateStr
+= "T" + self
.__padDateField
(hour
);
762 dateStr
+= ":" + self
.__padDateField
(minute
);
764 dateStr
+= ":" + self
.__padDateField
(second
);
768 dateFrame
= self
.frames
[fid
];
771 dateFrame
[0].setDate(self
.encoding
+ dateStr
);
773 header
= FrameHeader(self
.header
);
775 dateFrame
= DateFrame(header
, encoding
= self
.encoding
,
776 date_str
= self
.strToUnicode(dateStr
));
777 self
.frames
.addFrame(dateFrame
);
778 except FrameException
, ex
:
779 raise TagException(str(ex
));
781 # Three types are accepted for the genre parameter. A Genre object, an
782 # acceptable (see Genre.parse) genre string, or an integer genre id.
783 # Arguments equal to None or "" cause the frame to be removed.
784 def setGenre(self
, g
):
785 if g
== None or g
== "":
786 self
.frames
.removeFramesByID(GENRE_FID
);
789 if isinstance(g
, Genre
):
790 self
.frames
.setTextFrame(GENRE_FID
, self
.strToUnicode(str(g
)),
792 elif isinstance(g
, str):
795 self
.frames
.setTextFrame(GENRE_FID
, self
.strToUnicode(str(gObj
)),
797 elif isinstance(g
, int):
800 self
.frames
.setTextFrame(GENRE_FID
, self
.strToUnicode(str(gObj
)),
803 raise TagException("Invalid type passed to setGenre: %s" +
806 # Accepts a tuple with the first value containing the track number and the
807 # second the total number of tracks. One or both of these values may be
808 # None. If both values are None, the frame is removed.
809 def setTrackNum(self
, n
, zeropad
= True):
810 self
.setNum(TRACKNUM_FID
, n
, zeropad
)
812 def setDiscNum(self
, n
, zeropad
= True):
813 self
.setNum(DISCNUM_FID
, n
, zeropad
)
815 def setNum(self
, fid
, n
, zeropad
= True):
816 if n
[0] == None and n
[1] == None:
817 self
.frames
.removeFramesByID(fid
);
822 if zeropad
and n
[1] >= 0 and n
[1] <= 9:
823 totalStr
= "0" + str(n
[1]);
825 totalStr
= str(n
[1]);
833 # Pad with zeros according to how large the total count is.
835 if len(trackStr
) == 1:
836 trackStr
= "0" + trackStr
;
837 if len(trackStr
) < len(totalStr
):
838 trackStr
= ("0" * (len(totalStr
) - len(trackStr
))) + trackStr
;
841 if trackStr
and totalStr
:
842 s
= trackStr
+ "/" + totalStr
;
843 elif trackStr
and not totalStr
:
846 self
.frames
.setTextFrame(fid
, self
.strToUnicode(s
),
850 # Add a comment. This adds a comment unless one is already present with
851 # the same language and description in which case the current value is
852 # either changed (cmt != "") or removed (cmt equals "" or None).
853 def addComment(self
, cmt
, desc
= u
"", lang
= DEFAULT_LANG
):
855 # A little more then a call to removeFramesByID is involved since we
856 # need to look at more than the frame ID.
857 comments
= self
.frames
[COMMENT_FID
];
859 if c
.lang
== lang
and c
.description
== desc
:
860 self
.frames
.remove(c
);
863 self
.frames
.setCommentFrame(self
.strToUnicode(cmt
),
864 self
.strToUnicode(desc
),
865 lang
, self
.encoding
);
867 # Add lyrics. Semantics similar to addComment
868 def addLyrics(self
, lyr
, desc
= u
"", lang
= DEFAULT_LANG
):
870 # A little more than a call to removeFramesByID is involved since we
871 # need to look at more than the frame ID.
872 lyrics
= self
.frames
[LYRICS_FID
];
874 if l
.lang
== lang
and l
.description
== desc
:
875 self
.frames
.remove(l
);
878 self
.frames
.setLyricsFrame(self
.strToUnicode(lyr
),
879 self
.strToUnicode(desc
),
880 lang
, self
.encoding
);
882 # Semantics similar to addComment
883 def addUserTextFrame(self
, desc
, text
):
885 u_frames
= self
.frames
[USERTEXT_FID
];
887 if u
.description
== desc
:
888 self
.frames
.remove(u
);
891 self
.frames
.setUserTextFrame(self
.strToUnicode(text
),
892 self
.strToUnicode(desc
), self
.encoding
);
894 def removeUserTextFrame(self
, desc
):
895 self
.addUserTextFrame(desc
, "")
897 def removeComments(self
):
898 return self
.frames
.removeFramesByID(COMMENT_FID
);
900 def removeLyrics(self
):
901 return self
.frames
.removeFramesByID(LYRICS_FID
);
903 def removeImages(self
):
904 return self
.frames
.removeFramesByID(IMAGE_FID
)
906 def addImage(self
, type, image_file_path
, desc
= u
""):
908 image_frame
= ImageFrame
.create(type, image_file_path
, desc
);
909 self
.frames
.addFrame(image_frame
);
911 image_frames
= self
.frames
[IMAGE_FID
];
912 for i
in image_frames
:
913 if i
.pictureType
== type:
914 self
.frames
.remove(i
);
917 def addObject(self
, object_file_path
, mime
= "", desc
= u
"",
919 object_frames
= self
.frames
[OBJECT_FID
];
920 for i
in object_frames
:
921 if i
.description
== desc
:
922 self
.frames
.remove(i
);
924 object_frame
= ObjectFrame
.create(object_file_path
, mime
, desc
,
926 self
.frames
.addFrame(object_frame
);
928 def getPlayCount(self
):
929 if self
.frames
[PLAYCOUNT_FID
]:
930 pc
= self
.frames
[PLAYCOUNT_FID
][0];
931 assert(isinstance(pc
, PlayCountFrame
));
936 def setPlayCount(self
, count
):
938 if self
.frames
[PLAYCOUNT_FID
]:
939 pc
= self
.frames
[PLAYCOUNT_FID
][0];
940 assert(isinstance(pc
, PlayCountFrame
));
943 frameHeader
= FrameHeader(self
.header
);
944 frameHeader
.id = PLAYCOUNT_FID
;
945 pc
= PlayCountFrame(frameHeader
, count
= count
);
946 self
.frames
.addFrame(pc
);
948 def incrementPlayCount(self
, n
= 1):
949 pc
= self
.getPlayCount();
951 self
.setPlayCount(pc
+ n
);
953 self
.setPlayCount(n
);
955 def getUniqueFileIDs(self
):
956 return self
.frames
[UNIQUE_FILE_ID_FID
];
958 def addUniqueFileID(self
, owner_id
, id):
960 ufids
= self
.frames
[UNIQUE_FILE_ID_FID
];
962 if ufid
.owner_id
== owner_id
:
963 self
.frames
.remove(ufid
);
966 self
.frames
.setUniqueFileIDFrame(owner_id
, id);
969 bpm
= self
.frames
[BPM_FID
];
971 # Round floats since the spec says this is an integer
972 bpm
= int(float(bpm
[0].text
) + 0.5)
977 def setBPM(self
, bpm
):
978 self
.setTextFrame(BPM_FID
, self
.strToUnicode(str(bpm
)))
980 def getPublisher(self
):
981 pub
= self
.frames
[PUBLISHER_FID
];
983 return pub
[0].text
or None;
985 def setPublisher(self
, p
):
986 self
.setTextFrame(PUBLISHER_FID
, self
.strToUnicode(str(p
)));
988 # Test ID3 major version.
990 return self
.header
.majorVersion
== 1;
992 return self
.header
.majorVersion
== 2;
994 def setVersion(self
, v
):
998 v
= ID3_DEFAULT_VERSION
;
1000 if v
!= ID3_CURRENT_VERSION
:
1001 self
.header
.setVersion(v
);
1002 self
.frames
.setTagHeader(self
.header
);
1004 def setTextFrame(self
, fid
, txt
):
1006 self
.frames
.removeFramesByID(fid
);
1008 self
.frames
.setTextFrame(fid
, self
.strToUnicode(txt
), self
.encoding
);
1010 def setTextEncoding(self
, enc
):
1011 if enc
!= LATIN1_ENCODING
and enc
!= UTF_16_ENCODING
and\
1012 enc
!= UTF_16BE_ENCODING
and enc
!= UTF_8_ENCODING
:
1013 raise TagException("Invalid encoding");
1014 elif self
.getVersion() & ID3_V1
and enc
!= LATIN1_ENCODING
:
1015 raise TagException("ID3 v1.x supports ISO-8859 encoding only");
1016 elif self
.getVersion() <= ID3_V2_3
and enc
== UTF_8_ENCODING
:
1017 # This is unfortunate.
1018 raise TagException("UTF-8 is not supported by ID3 v2.3");
1020 self
.encoding
= enc
;
1021 for f
in self
.frames
:
1024 def tagToString(self
, pattern
):
1030 s
= self
._subst
(pattern
, "%A", self
.getArtist());
1031 s
= self
._subst
(s
, "%a", self
.getAlbum());
1032 s
= self
._subst
(s
, "%t", self
.getTitle());
1033 s
= self
._subst
(s
, "%n", self
._prettyTrack
(self
.getTrackNum()[0]));
1034 s
= self
._subst
(s
, "%N", self
._prettyTrack
(self
.getTrackNum()[1]));
1037 def _prettyTrack(self
, track
):
1040 track_str
= str(track
);
1041 if len(track_str
) == 1:
1042 track_str
= "0" + track_str
;
1045 def _subst(self
, name
, pattern
, repl
):
1046 regex
= re
.compile(pattern
);
1047 if regex
.search(name
) and repl
:
1048 # No '/' characters allowed
1049 (repl
, subs
) = re
.compile("/").subn("-", repl
);
1050 (name
, subs
) = regex
.subn(repl
, name
)
1053 def __saveV1Tag(self
, version
):
1054 assert(version
& ID3_V1
);
1058 tag
+= self
._fixToWidth
(self
.getTitle().encode("latin_1"), 30);
1059 tag
+= self
._fixToWidth
(self
.getArtist().encode("latin_1"), 30);
1060 tag
+= self
._fixToWidth
(self
.getAlbum().encode("latin_1"), 30);
1064 tag
+= self
._fixToWidth
(y
.encode("latin_1"), 4);
1067 for c
in self
.getComments():
1068 if c
.description
== ID3_V1_COMMENT_DESC
:
1070 # We prefer this one over "";
1072 elif c
.description
== "":
1074 # Keep searching in case we find the description eyeD3 uses.
1075 cmt
= self
._fixToWidth
(cmt
.encode("latin_1"), 30);
1076 if version
!= ID3_V1_0
:
1077 track
= self
.getTrackNum()[0];
1079 cmt
= cmt
[0:28] + "\x00" + chr(int(track
) & 0xff);
1082 if not self
.getGenre() or self
.getGenre().getId() is None:
1085 genre
= self
.getGenre().getId();
1086 tag
+= chr(genre
& 0xff);
1088 assert(len(tag
) == 128);
1090 tagFile
= file(self
.linkedFile
.name
, "r+b");
1091 # Write the tag over top an original or append it.
1093 tagFile
.seek(-128, 2);
1094 if tagFile
.read(3) == "TAG":
1095 tagFile
.seek(-128, 2);
1099 # File is smaller than 128 bytes.
1106 def _fixToWidth(self
, s
, n
):
1108 retval
= retval
[0:n
];
1109 retval
= retval
+ ("\x00" * (n
- len(retval
)));
1112 # Returns false when an ID3 v1 tag is not present, or contains no data.
1113 def __loadV1Tag(self
, f
):
1114 if isinstance(f
, str) or isinstance(f
, unicode):
1121 # Seek to the end of the file where all ID3v1 tags are written.
1123 strip_chars
= string
.whitespace
+ "\x00";
1126 id3tag
= fp
.read(128);
1127 if id3tag
[0:3] == "TAG":
1128 TRACE_MSG("Located ID3 v1 tag");
1129 # 1.0 is implied until a 1.1 feature is recognized.
1130 self
.setVersion(ID3_V1_0
);
1132 title
= re
.sub("\x00+$", "", id3tag
[3:33].strip(strip_chars
));
1133 TRACE_MSG("Tite: " + title
);
1135 self
.setTitle(unicode(title
, "latin1"));
1137 artist
= re
.sub("\x00+$", "", id3tag
[33:63].strip(strip_chars
));
1138 TRACE_MSG("Artist: " + artist
);
1140 self
.setArtist(unicode(artist
, "latin1"));
1142 album
= re
.sub("\x00+$", "", id3tag
[63:93].strip(strip_chars
));
1143 TRACE_MSG("Album: " + album
);
1145 self
.setAlbum(unicode(album
, "latin1"));
1147 year
= re
.sub("\x00+$", "", id3tag
[93:97].strip(strip_chars
));
1148 TRACE_MSG("Year: " + year
);
1150 if year
and int(year
):
1153 # Bogus year strings.
1156 if re
.sub("\x00+$", "", id3tag
[97:127]):
1157 comment
= id3tag
[97:127];
1158 TRACE_MSG("Comment: " + comment
);
1159 if comment
[-2] == "\x00" and comment
[-1] != "\x00":
1160 # Parse track number (added to ID3v1.1) if present.
1161 TRACE_MSG("Comment contains track number per v1.1 spec");
1162 track
= ord(comment
[-1]);
1163 self
.setTrackNum((track
, None));
1164 TRACE_MSG("Track: " + str(track
));
1165 TRACE_MSG("Track Num found, setting version to v1.1s");
1166 self
.setVersion(ID3_V1_1
);
1167 comment
= comment
[:-2];
1170 comment
= re
.sub("\x00+$", "", comment
).rstrip();
1171 TRACE_MSG("Comment: " + comment
);
1173 self
.addComment(unicode(comment
, 'latin1'),
1174 ID3_V1_COMMENT_DESC
);
1176 genre
= ord(id3tag
[127:128])
1177 TRACE_MSG("Genre ID: " + str(genre
));
1178 self
.setGenre(genre
);
1182 return len(self
.frames
);
1184 def __saveV2Tag(self
, version
):
1185 assert(version
& ID3_V2
);
1186 TRACE_MSG("Rendering tag version: " + versionToString(version
));
1188 self
.setVersion(version
);
1192 # We may be converting from 1.x to 2.x so we need to find any
1193 # current v2.x tag otherwise we're gonna hork the file.
1195 if tmpTag
.link(self
.linkedFile
.name
, ID3_V2
):
1196 TRACE_MSG("Found current v2.x tag:");
1197 currTagSize
= tmpTag
.linkedFile
.tagSize
;
1198 TRACE_MSG("Current tag size: %d" % currTagSize
);
1199 currPadding
= tmpTag
.linkedFile
.tagPadding
;
1200 TRACE_MSG("Current tag padding: %d" % currPadding
);
1203 t
= time
.strftime("%Y-%m-%dT%H:%M:%S", time
.gmtime());
1205 if self
.header
.minorVersion
== 4:
1207 h
= FrameHeader(self
.header
);
1209 dateFrame
= DateFrame(h
, date_str
= self
.strToUnicode(t
),
1210 encoding
= self
.encoding
);
1211 self
.frames
.removeFramesByID("TDTG");
1212 self
.frames
.addFrame(dateFrame
);
1214 # TXXX (Tagging time) for older versions
1215 self
.frames
.removeFramesByID("TDTG");
1216 self
.addUserTextFrame('Tagging time', t
)
1218 # Render all frames first so the data size is known for the tag header.
1220 for f
in self
.frames
:
1221 TRACE_MSG("Rendering frame: " + f
.header
.id);
1222 raw_frame
= f
.render();
1223 TRACE_MSG("Rendered %d bytes" % len(raw_frame
));
1224 frameData
+= raw_frame
;
1225 # Handle the overall tag header unsync bit. Frames themselves duplicate
1227 if self
.header
.unsync
:
1228 TRACE_MSG("Unsyncing all frames (sync-safe)");
1229 frameData
= frames
.unsyncData(frameData
);
1233 DEFAULT_PADDING
= 1024
1234 def compute_padding():
1235 if currPadding
<= DEFAULT_PADDING
:
1236 return DEFAULT_PADDING
1242 if self
.header
.extended
:
1243 # This is sorta lame. We don't know the total framesize until
1244 # this is rendered, yet we can't render it witout knowing the
1245 # amount of padding for the crc. Force it.
1247 TRACE_MSG("Rendering extended header");
1248 paddingSize
= compute_padding()
1249 extHeaderData
+= self
.extendedHeader
.render(self
.header
, frameData
,
1252 new_size
= 10 + len(extHeaderData
) + len(frameData
) + paddingSize
1253 if rewriteFile
or new_size
>= currTagSize
:
1254 TRACE_MSG("File rewrite required");
1256 if paddingSize
<= 0:
1257 paddingSize
= compute_padding()
1258 elif paddingSize
<= 0:
1259 paddingSize
= currTagSize
- (new_size
- 10)
1260 TRACE_MSG("Adding %d bytes of padding" % paddingSize
)
1261 frameData
+= ("\x00" * paddingSize
);
1263 # Recompute with padding
1264 new_size
= 10 + len(extHeaderData
) + len(frameData
)
1265 header_tag_size
= new_size
- 10
1267 # Render the tag header.
1268 TRACE_MSG("Rendering %s tag header with size %d" %
1269 (versionToString(self
.getVersion()), header_tag_size
))
1270 headerData
= self
.header
.render(header_tag_size
)
1273 tagData
= headerData
+ extHeaderData
+ frameData
;
1277 tagFile
= file(self
.linkedFile
.name
, "r+b");
1278 TRACE_MSG("Writing %d bytes of tag data" % len(tagData
));
1279 tagFile
.write(tagData
);
1283 tmpName
= tempfile
.mktemp();
1284 tmpFile
= file(tmpName
, "w+b");
1285 TRACE_MSG("Writing %d bytes of tag data" % len(tagData
));
1286 tmpFile
.write(tagData
);
1288 # Write audio data in chunks
1289 tagFile
= file(self
.linkedFile
.name
, "rb");
1290 if currTagSize
!= 0:
1291 seek_point
= currTagSize
+ 10
1294 TRACE_MSG("Seeking to beginning of audio data, byte %d (%x)" %
1295 (seek_point
, seek_point
))
1296 tagFile
.seek(seek_point
)
1297 self
.__copyRemaining
(tagFile
, tmpFile
);
1303 shutil
.copyfile(tmpName
, self
.linkedFile
.name
);
1307 TRACE_MSG("Tag write complete. Updating state.");
1308 self
.linkedFile
.tagPadding
= paddingSize
;
1309 # XXX: getSize could cache sizes so to prevent rendering again.
1310 self
.linkedFile
.tagSize
= self
.frames
.getSize();
1313 # Returns >= 0 to indicate the padding size of the read frame; -1 returned
1314 # when not tag was found.
1315 def __loadV2Tag(self
, f
):
1316 if isinstance(f
, str) or isinstance(f
, unicode):
1325 # Look for a tag and if found load it.
1326 if not self
.header
.parse(fp
):
1329 # Read the extended header if present.
1330 if self
.header
.extended
:
1331 self
.extendedHeader
.parse(fp
, self
.header
);
1333 # Header is definitely there so at least one frame *must* follow.
1334 self
.frames
.setTagHeader(self
.header
);
1335 padding
= self
.frames
.parse(fp
, self
.header
, self
.extendedHeader
);
1336 TRACE_MSG("Tag contains %d bytes of padding." % padding
);
1337 except FrameException
, ex
:
1339 raise TagException(str(ex
));
1340 except TagException
:
1356 def __padDateField(self
, f
):
1360 elif len(fStr
) == 1:
1363 raise TagException("Invalid date field: " + fStr
);
1366 def __copyRemaining(self
, src_fp
, dest_fp
):
1367 # Write audio data in chunks
1371 data
= src_fp
.read(amt
)
1379 # This method will return the first comment in the FrameSet
1380 # and not all of them. Multiple COMM frames are common and useful. Use
1381 # getComments which returns a list.
1382 def getComment(self
):
1383 f
= self
.frames
[COMMENT_FID
];
1385 return f
[0].comment
;
1390 ################################################################################
1391 class GenreException(Exception):
1392 '''Problem looking up genre'''
1394 ################################################################################
1399 def __init__(self
, id = None, name
= None):
1402 elif name
is not None:
1410 # Sets the genre id. The objects name field is set to the corresponding
1411 # value obtained from eyeD3.genres.
1413 # Throws GenreException when name does not map to a valid ID3 v1.1. id.
1414 # This behavior can be disabled by passing 0 as the second argument.
1415 def setId(self
, id):
1416 if not isinstance(id, int):
1417 raise TypeError("Invalid genre id: " + str(id));
1421 except Exception, ex
:
1422 if utils
.strictID3():
1423 raise GenreException("Invalid genre id: " + str(id));
1425 if utils
.strictID3() and not name
:
1426 raise GenreException("Genre id maps to a null name: " + str(id));
1431 # Sets the genre name. The objects id field is set to the corresponding
1432 # value obtained from eyeD3.genres.
1434 # Throws GenreException when name does not map to a valid ID3 v1.1. name.
1435 # This behavior can be disabled by passing 0 as the second argument.
1436 def setName(self
, name
):
1437 if not isinstance(name
, str):
1438 raise GenreException("Invalid genre name: " + str(name
));
1445 if utils
.strictID3():
1446 raise GenreException("Invalid genre name: " + name
);
1453 # Sets the genre id and name.
1455 # Throws GenreException when eyeD3.genres[id] != name (case insensitive).
1456 # This behavior can be disabled by passing 0 as the second argument.
1457 def set(self
, id, name
):
1458 if not isinstance(id, int):
1459 raise GenreException("Invalid genre id: " + id);
1460 if not isinstance(name
, str):
1461 raise GenreException("Invalid genre name: " + str(name
));
1463 if not utils
.strictID3():
1468 if genres
[name
] != id:
1469 raise GenreException("eyeD3.genres[" + str(id) + "] " +\
1470 "does not match " + name
);
1474 raise GenreException("eyeD3.genres[" + str(id) + "] " +\
1475 "does not match " + name
);
1477 # Parses genre information from genreStr.
1478 # The following formats are supported:
1479 # 01, 2, 23, 125 - ID3 v1 style.
1480 # (01), (2), (129)Hardcore, (9)Metal - ID3 v2 style with and without
1483 # Throws GenreException when an invalid string is passed.
1484 def parse(self
, genreStr
):
1486 str(genreStr
.encode('utf-8')).strip(string
.whitespace
+ '\x00');
1493 # XXX: Utf-16 conversions leave a null byte at the end of the string.
1494 while genreStr
[len(genreStr
) - 1] == "\x00":
1495 genreStr
= genreStr
[:len(genreStr
) - 1];
1496 if len(genreStr
) == 0:
1500 # Match 03, 34, 129.
1501 regex
= re
.compile("[0-9][0-9]?[0-9]?$");
1502 if regex
.match(genreStr
):
1503 if len(genreStr
) != 1 and genreStr
[0] == '0':
1504 genreStr
= genreStr
[1:];
1506 self
.setId(int(genreStr
));
1510 # Match (03), (0)Blues, (15) Rap
1511 regex
= re
.compile("\(([0-9][0-9]?[0-9]?)\)(.*)$");
1512 m
= regex
.match(genreStr
);
1514 (id, name
) = m
.groups();
1515 if len(id) != 1 and id[0] == '0':
1519 self
.set(int(id), name
.strip());
1521 self
.setId(int(id));
1524 # Non standard, but witnessed.
1525 # Match genre alone. e.g. Rap, Rock, blues, 'Rock|Punk|Pop-Punk', etc
1526 regex
= re
.compile("^[A-Z 0-9+/\-\|!&]+\00*$", re
.IGNORECASE
)
1527 if regex
.match(genreStr
):
1528 self
.setName(genreStr
);
1530 raise GenreException("Genre string cannot be parsed with '%s': %s" %\
1531 (regex
.pattern
, genreStr
));
1536 s
+= "(" + str(self
.id) + ")"
1541 ################################################################################
1542 class InvalidAudioFormatException(Exception):
1543 '''Problems with audio format'''
1545 ################################################################################
1550 # Number of seconds required to play the audio file.
1553 def __init__(self
, fileName
):
1554 self
.fileName
= fileName
;
1560 if not self
.fileSize
:
1561 self
.fileSize
= os
.stat(self
.fileName
)[ST_SIZE
];
1562 return self
.fileSize
;
1564 def rename(self
, name
, fsencoding
):
1565 base
= os
.path
.basename(self
.fileName
);
1566 base_ext
= os
.path
.splitext(base
)[1];
1567 dir = os
.path
.dirname(self
.fileName
);
1570 new_name
= dir + os
.sep
+ name
.encode(fsencoding
) + base_ext
;
1572 os
.rename(self
.fileName
, new_name
);
1573 self
.fileName
= new_name
;
1575 raise TagException("Error renaming '%s' to '%s'" % (self
.fileName
,
1578 def getPlayTime(self
):
1579 return self
.play_time
;
1581 def getPlayTimeString(self
):
1582 from eyeD3
.utils
import format_track_time
1583 return format_track_time(self
.getPlayTime())
1586 ################################################################################
1587 class Mp3AudioFile(TagFile
):
1589 def __init__(self
, fileName
, tagVersion
= ID3_ANY_VERSION
):
1590 TagFile
.__init
__(self
, fileName
)
1594 self
.xingHeader
= None
1597 if not isMp3File(fileName
):
1598 raise InvalidAudioFormatException("File is not mp3");
1601 f
= file(self
.fileName
, "rb");
1603 hasTag
= self
.tag
.link(f
, tagVersion
);
1604 # Find the first mp3 frame.
1611 framePos
= self
.tag
.header
.SIZE
+ self
.tag
.header
.tagSize
;
1613 TRACE_MSG("mp3 header search starting @ %x" % framePos
)
1614 # Find an mp3 header
1615 header_pos
, header
, header_bytes
= mp3
.find_header(f
, framePos
)
1618 self
.header
= mp3
.Header(header
)
1619 except mp3
.Mp3Exception
, ex
:
1621 raise InvalidAudioFormatException(str(ex
));
1623 TRACE_MSG("mp3 header %x found at position: 0x%x" % (header
,
1626 raise InvalidAudioFormatException("Unable to find a valid mp3 frame")
1628 # Check for Xing/Info header information which will always be in the
1629 # first "null" frame.
1631 mp3_frame
= f
.read(self
.header
.frameLength
)
1632 if re
.compile('Xing|Info').search(mp3_frame
):
1633 self
.xingHeader
= mp3
.XingHeader();
1634 if not self
.xingHeader
.decode(mp3_frame
):
1635 TRACE_MSG("Ignoring corrupt Xing header")
1636 self
.xingHeader
= None
1637 # Check for LAME Tag
1638 self
.lameTag
= mp3
.LameTag(mp3_frame
)
1640 # Compute track play time.
1641 tpf
= mp3
.computeTimePerFrame(self
.header
);
1642 if self
.xingHeader
and self
.xingHeader
.vbr
:
1643 self
.play_time
= int(tpf
* self
.xingHeader
.numFrames
);
1645 length
= self
.getSize();
1646 if self
.tag
and self
.tag
.isV2():
1647 length
-= self
.tag
.header
.SIZE
+ self
.tag
.header
.tagSize
;
1648 # Handle the case where there is a v2 tag and a v1 tag.
1650 if f
.read(3) == "TAG":
1652 elif self
.tag
and self
.tag
.isV1():
1654 self
.play_time
= int((length
/ self
.header
.frameLength
) * tpf
);
1658 # Returns a tuple. The first value is a boolean which if true means the
1659 # bit rate returned in the second value is variable.
1660 def getBitRate(self
):
1661 xHead
= self
.xingHeader
;
1662 if xHead
and xHead
.vbr
:
1663 tpf
= eyeD3
.mp3
.computeTimePerFrame(self
.header
);
1664 # FIXME: if xHead.numFrames == 0 (Fuoco.mp3), ZeroDivisionError
1665 br
= int((xHead
.numBytes
* 8) / (tpf
* xHead
.numFrames
* 1000));
1668 br
= self
.header
.bitRate
;
1672 def getBitRateString(self
):
1673 (vbr
, bitRate
) = self
.getBitRate();
1674 brs
= "%d kb/s" % bitRate
;
1678 def getSampleFreq(self
):
1679 return self
.header
.sampleFreq
;
1681 ################################################################################
1682 def isMp3File(fileName
):
1683 (type, enc
) = mimetypes
.guess_type(fileName
);
1684 return type == "audio/mpeg";
1686 ################################################################################
1687 class GenreMap(list):
1688 # None value are set in the ctor
1693 WINAMP_GENRE_MIN
= 80;
1694 WINAMP_GENRE_MAX
= 147;
1695 EYED3_GENRE_MIN
= None;
1696 EYED3_GENRE_MAX
= None;
1698 # Accepts both int and string keys. Throws IndexError and TypeError.
1699 def __getitem__(self
, key
):
1700 if isinstance(key
, int):
1701 if key
>= 0 and key
< len(self
):
1702 v
= list.__getitem
__(self
, key
);
1708 raise IndexError("genre index out of range");
1709 elif isinstance(key
, str):
1710 if self
.reverseDict
.has_key(key
.lower()):
1711 return self
.reverseDict
[key
.lower()];
1713 raise IndexError(key
+ " genre not found");
1715 raise TypeError("genre key must be type int or string");
1719 self
.reverseDict
= {}
1720 # ID3 genres as defined by the v1.1 spec with WinAmp extensions.
1721 self
.append('Blues');
1722 self
.append('Classic Rock');
1723 self
.append('Country');
1724 self
.append('Dance');
1725 self
.append('Disco');
1726 self
.append('Funk');
1727 self
.append('Grunge');
1728 self
.append('Hip-Hop');
1729 self
.append('Jazz');
1730 self
.append('Metal');
1731 self
.append('New Age');
1732 self
.append('Oldies');
1733 self
.append('Other');
1737 self
.append('Reggae');
1738 self
.append('Rock');
1739 self
.append('Techno');
1740 self
.append('Industrial');
1741 self
.append('Alternative');
1743 self
.append('Death Metal');
1744 self
.append('Pranks');
1745 self
.append('Soundtrack');
1746 self
.append('Euro-Techno');
1747 self
.append('Ambient');
1748 self
.append('Trip-Hop');
1749 self
.append('Vocal');
1750 self
.append('Jazz+Funk');
1751 self
.append('Fusion');
1752 self
.append('Trance');
1753 self
.append('Classical');
1754 self
.append('Instrumental');
1755 self
.append('Acid');
1756 self
.append('House');
1757 self
.append('Game');
1758 self
.append('Sound Clip');
1759 self
.append('Gospel');
1760 self
.append('Noise');
1761 self
.append('AlternRock');
1762 self
.append('Bass');
1763 self
.append('Soul');
1764 self
.append('Punk');
1765 self
.append('Space');
1766 self
.append('Meditative');
1767 self
.append('Instrumental Pop');
1768 self
.append('Instrumental Rock');
1769 self
.append('Ethnic');
1770 self
.append('Gothic');
1771 self
.append('Darkwave');
1772 self
.append('Techno-Industrial');
1773 self
.append('Electronic');
1774 self
.append('Pop-Folk');
1775 self
.append('Eurodance');
1776 self
.append('Dream');
1777 self
.append('Southern Rock');
1778 self
.append('Comedy');
1779 self
.append('Cult');
1780 self
.append('Gangsta Rap');
1781 self
.append('Top 40');
1782 self
.append('Christian Rap');
1783 self
.append('Pop / Funk');
1784 self
.append('Jungle');
1785 self
.append('Native American');
1786 self
.append('Cabaret');
1787 self
.append('New Wave');
1788 self
.append('Psychedelic');
1789 self
.append('Rave');
1790 self
.append('Showtunes');
1791 self
.append('Trailer');
1792 self
.append('Lo-Fi');
1793 self
.append('Tribal');
1794 self
.append('Acid Punk');
1795 self
.append('Acid Jazz');
1796 self
.append('Polka');
1797 self
.append('Retro');
1798 self
.append('Musical');
1799 self
.append('Rock & Roll');
1800 self
.append('Hard Rock');
1801 self
.append('Folk');
1802 self
.append('Folk-Rock');
1803 self
.append('National Folk');
1804 self
.append('Swing');
1805 self
.append('Fast Fusion');
1806 self
.append('Bebob');
1807 self
.append('Latin');
1808 self
.append('Revival');
1809 self
.append('Celtic');
1810 self
.append('Bluegrass');
1811 self
.append('Avantgarde');
1812 self
.append('Gothic Rock');
1813 self
.append('Progressive Rock');
1814 self
.append('Psychedelic Rock');
1815 self
.append('Symphonic Rock');
1816 self
.append('Slow Rock');
1817 self
.append('Big Band');
1818 self
.append('Chorus');
1819 self
.append('Easy Listening');
1820 self
.append('Acoustic');
1821 self
.append('Humour');
1822 self
.append('Speech');
1823 self
.append('Chanson');
1824 self
.append('Opera');
1825 self
.append('Chamber Music');
1826 self
.append('Sonata');
1827 self
.append('Symphony');
1828 self
.append('Booty Bass');
1829 self
.append('Primus');
1830 self
.append('Porn Groove');
1831 self
.append('Satire');
1832 self
.append('Slow Jam');
1833 self
.append('Club');
1834 self
.append('Tango');
1835 self
.append('Samba');
1836 self
.append('Folklore');
1837 self
.append('Ballad');
1838 self
.append('Power Ballad');
1839 self
.append('Rhythmic Soul');
1840 self
.append('Freestyle');
1841 self
.append('Duet');
1842 self
.append('Punk Rock');
1843 self
.append('Drum Solo');
1844 self
.append('A Cappella');
1845 self
.append('Euro-House');
1846 self
.append('Dance Hall');
1848 self
.append('Drum & Bass');
1849 self
.append('Club-House');
1850 self
.append('Hardcore');
1851 self
.append('Terror');
1852 self
.append('Indie');
1853 self
.append('BritPop');
1854 self
.append('Negerpunk');
1855 self
.append('Polsk Punk');
1856 self
.append('Beat');
1857 self
.append('Christian Gangsta Rap');
1858 self
.append('Heavy Metal');
1859 self
.append('Black Metal');
1860 self
.append('Crossover');
1861 self
.append('Contemporary Christian');
1862 self
.append('Christian Rock');
1863 self
.append('Merengue');
1864 self
.append('Salsa');
1865 self
.append('Thrash Metal');
1866 self
.append('Anime');
1867 self
.append('JPop');
1868 self
.append('Synthpop');
1869 # The follow genres I've encountered in the wild.
1870 self
.append('Rock/Pop');
1871 self
.EYED3_GENRE_MIN
= len(self
) - 1;
1872 # New genres go here
1874 self
.EYED3_GENRE_MAX
= len(self
) - 1;
1875 self
.GENRE_MAX
= len(self
) - 1;
1877 # Pad up to 255 with "Unknown"
1880 self
.append("Unknown");
1883 for index
in range(len(self
)):
1885 self
.reverseDict
[string
.lower(self
[index
])] = index
1889 tagSize
= 0; # This includes the padding byte count.
1891 def __init__(self
, fileName
):
1892 if isinstance(fileName
, str):
1894 self
.name
= unicode(fileName
, sys
.getfilesystemencoding());
1896 # Work around the local encoding not matching that of a mounted
1898 self
.name
= fileName
1900 self
.name
= fileName
;
1902 def tagToUserTune(tag
):
1904 if isinstance(tag
, Mp3AudioFile
):
1906 tag
= audio_file
.getTag();
1908 tune
= u
"<tune xmlns='http://jabber.org/protocol/tune'>\n";
1910 tune
+= " <artist>" + tag
.getArtist() + "</artist>\n";
1912 tune
+= " <title>" + tag
.getTitle() + "</title>\n";
1914 tune
+= " <source>" + tag
.getAlbum() + "</source>\n";
1915 tune
+= " <track>" +\
1916 "file://" + unicode(os
.path
.abspath(tag
.linkedFile
.name
)) +\
1919 tune
+= " <length>" + unicode(audio_file
.getPlayTime()) +\
1921 tune
+= "</tune>\n";
1925 # Module level globals.
1927 genres
= GenreMap();