1 ################################################################################
3 # Copyright (C) 2002-2005 Travis Shirk <travis@pobox.com>
4 # Copyright (C) 2001 Ryan Finne <ryan@finnie.org>
6 # This program is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 2 of the License, or
9 # (at your option) any later version.
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
20 ################################################################################
21 import sys
, os
, os
.path
, re
, zlib
, StringIO
, time
, mimetypes
;
22 from StringIO
import StringIO
;
24 from binfuncs
import *;
26 # Valid time stamp formats per ISO 8601 and used by time.strptime.
27 timeStampFormats
= ["%Y",
36 CONDUCTOR_FID
= "TPE3";
38 COMPOSER_FID
= "TCOM";
39 ARTIST_FIDS
= [ARTIST_FID
, BAND_FID
, CONDUCTOR_FID
,
40 REMIXER_FID
, COMPOSER_FID
];
43 SUBTITLE_FID
= "TIT3";
44 CONTENT_TITLE_FID
= "TIT1";
45 TITLE_FIDS
= [TITLE_FID
, SUBTITLE_FID
, CONTENT_TITLE_FID
];
48 TRACKNUM_FID
= "TRCK";
50 USERTEXT_FID
= "TXXX";
53 URL_COMMERCIAL_FID
= "WCOM";
54 URL_COPYRIGHT_FID
= "WCOP";
55 URL_AUDIOFILE_FID
= "WOAF";
56 URL_ARTIST_FID
= "WOAR";
57 URL_AUDIOSRC_FID
= "WOAS";
58 URL_INET_RADIO_FID
= "WORS";
59 URL_PAYMENT_FID
= "WPAY";
60 URL_PUBLISHER_FID
= "WPUB";
61 URL_FIDS
= [URL_COMMERCIAL_FID
, URL_COPYRIGHT_FID
,
62 URL_AUDIOFILE_FID
, URL_ARTIST_FID
, URL_AUDIOSRC_FID
,
63 URL_INET_RADIO_FID
, URL_PAYMENT_FID
,
66 PLAYCOUNT_FID
= "PCNT";
67 UNIQUE_FILE_ID_FID
= "UFID";
69 PUBLISHER_FID
= "TPUB";
71 obsoleteFrames
= {"EQUA": "Equalisation",
72 "IPLS": "Involved people list",
73 "RVAD": "Relative volume adjustment",
75 "TORY": "Original release year",
76 "TRDA": "Recording dates",
78 # Both of these are "coerced" into a v2.4 TDRC frame when read, and
79 # recreated when saving v2.3.
80 OBSOLETE_DATE_FID
= "TDAT";
81 OBSOLETE_YEAR_FID
= "TYER";
82 OBSOLETE_TIME_FID
= "TIME";
83 OBSOLETE_ORIG_RELEASE_FID
= "TORY";
84 OBSOLETE_RECORDING_DATE_FID
= "TRDA";
86 DATE_FIDS
= ["TDRL", "TDOR", "TDRC", OBSOLETE_YEAR_FID
,
89 frameDesc
= { "AENC": "Audio encryption",
90 "APIC": "Attached picture",
91 "ASPI": "Audio seek point index",
94 "COMR": "Commercial frame",
96 "ENCR": "Encryption method registration",
97 "EQU2": "Equalisation (2)",
98 "ETCO": "Event timing codes",
100 "GEOB": "General encapsulated object",
101 "GRID": "Group identification registration",
103 "LINK": "Linked information",
105 "MCDI": "Music CD identifier",
106 "MLLT": "MPEG location lookup table",
108 "OWNE": "Ownership frame",
110 "PRIV": "Private frame",
111 "PCNT": "Play counter",
112 "POPM": "Popularimeter",
113 "POSS": "Position synchronisation frame",
115 "RBUF": "Recommended buffer size",
116 "RVA2": "Relative volume adjustment (2)",
119 "SEEK": "Seek frame",
120 "SIGN": "Signature frame",
121 "SYLT": "Synchronised lyric/text",
122 "SYTC": "Synchronised tempo codes",
124 "TALB": "Album/Movie/Show title",
125 "TBPM": "BPM (beats per minute)",
127 "TCON": "Content type",
128 "TCOP": "Copyright message",
129 "TDEN": "Encoding time",
130 "TDLY": "Playlist delay",
131 "TDOR": "Original release time",
132 "TDRC": "Recording time",
133 "TDRL": "Release time",
134 "TDTG": "Tagging time",
135 "TENC": "Encoded by",
136 "TEXT": "Lyricist/Text writer",
138 "TIPL": "Involved people list",
139 "TIT1": "Content group description",
140 "TIT2": "Title/songname/content description",
141 "TIT3": "Subtitle/Description refinement",
142 "TKEY": "Initial key",
143 "TLAN": "Language(s)",
145 "TMCL": "Musician credits list",
146 "TMED": "Media type",
148 "TOAL": "Original album/movie/show title",
149 "TOFN": "Original filename",
150 "TOLY": "Original lyricist(s)/text writer(s)",
151 "TOPE": "Original artist(s)/performer(s)",
152 "TOWN": "File owner/licensee",
153 "TPE1": "Lead performer(s)/Soloist(s)",
154 "TPE2": "Band/orchestra/accompaniment",
155 "TPE3": "Conductor/performer refinement",
156 "TPE4": "Interpreted, remixed, or otherwise modified by",
157 "TPOS": "Part of a set",
158 "TPRO": "Produced notice",
160 "TRCK": "Track number/Position in set",
161 "TRSN": "Internet radio station name",
162 "TRSO": "Internet radio station owner",
163 "TSOA": "Album sort order",
164 "TSOP": "Performer sort order",
165 "TSOT": "Title sort order",
166 "TSRC": "ISRC (international standard recording code)",
167 "TSSE": "Software/Hardware and settings used for encoding",
168 "TSST": "Set subtitle",
169 "TXXX": "User defined text information frame",
171 "UFID": "Unique file identifier",
172 "USER": "Terms of use",
173 "USLT": "Unsynchronised lyric/text transcription",
175 "WCOM": "Commercial information",
176 "WCOP": "Copyright/Legal information",
177 "WOAF": "Official audio file webpage",
178 "WOAR": "Official artist/performer webpage",
179 "WOAS": "Official audio source webpage",
180 "WORS": "Official Internet radio station homepage",
182 "WPUB": "Publishers official webpage",
183 "WXXX": "User defined URL link frame" };
186 # mapping of 2.2 frames to 2.3/2.4
187 TAGS2_2_TO_TAGS_2_3_AND_4
= {
188 "TT1" : "TIT1", # CONTENTGROUP content group description
189 "TT2" : "TIT2", # TITLE title/songname/content description
190 "TT3" : "TIT3", # SUBTITLE subtitle/description refinement
191 "TP1" : "TPE1", # ARTIST lead performer(s)/soloist(s)
192 "TP2" : "TPE2", # BAND band/orchestra/accompaniment
193 "TP3" : "TPE3", # CONDUCTOR conductor/performer refinement
194 "TP4" : "TPE4", # MIXARTIST interpreted, remixed, modified by
195 "TCM" : "TCOM", # COMPOSER composer
196 "TXT" : "TEXT", # LYRICIST lyricist/text writer
197 "TLA" : "TLAN", # LANGUAGE language(s)
198 "TCO" : "TCON", # CONTENTTYPE content type
199 "TAL" : "TALB", # ALBUM album/movie/show title
200 "TRK" : "TRCK", # TRACKNUM track number/position in set
201 "TPA" : "TPOS", # PARTINSET part of set
202 "TRC" : "TSRC", # ISRC international standard recording code
203 "TDA" : "TDAT", # DATE date
204 "TYE" : "TYER", # YEAR year
205 "TIM" : "TIME", # TIME time
206 "TRD" : "TRDA", # RECORDINGDATES recording dates
207 "TOR" : "TORY", # ORIGYEAR original release year
208 "TBP" : "TBPM", # BPM beats per minute
209 "TMT" : "TMED", # MEDIATYPE media type
210 "TFT" : "TFLT", # FILETYPE file type
211 "TCR" : "TCOP", # COPYRIGHT copyright message
212 "TPB" : "TPUB", # PUBLISHER publisher
213 "TEN" : "TENC", # ENCODEDBY encoded by
214 "TSS" : "TSSE", # ENCODERSETTINGS software/hardware + settings for encoding
215 "TLE" : "TLEN", # SONGLEN length (ms)
216 "TSI" : "TSIZ", # SIZE size (bytes)
217 "TDY" : "TDLY", # PLAYLISTDELAY playlist delay
218 "TKE" : "TKEY", # INITIALKEY initial key
219 "TOT" : "TOAL", # ORIGALBUM original album/movie/show title
220 "TOF" : "TOFN", # ORIGFILENAME original filename
221 "TOA" : "TOPE", # ORIGARTIST original artist(s)/performer(s)
222 "TOL" : "TOLY", # ORIGLYRICIST original lyricist(s)/text writer(s)
223 "TXX" : "TXXX", # USERTEXT user defined text information frame
224 "WAF" : "WOAF", # WWWAUDIOFILE official audio file webpage
225 "WAR" : "WOAR", # WWWARTIST official artist/performer webpage
226 "WAS" : "WOAS", # WWWAUDIOSOURCE official audion source webpage
227 "WCM" : "WCOM", # WWWCOMMERCIALINFO commercial information
228 "WCP" : "WCOP", # WWWCOPYRIGHT copyright/legal information
229 "WPB" : "WPUB", # WWWPUBLISHER publishers official webpage
230 "WXX" : "WXXX", # WWWUSER user defined URL link frame
231 "IPL" : "IPLS", # INVOLVEDPEOPLE involved people list
232 "ULT" : "USLT", # UNSYNCEDLYRICS unsynchronised lyrics/text transcription
233 "COM" : "COMM", # COMMENT comments
234 "UFI" : "UFID", # UNIQUEFILEID unique file identifier
235 "MCI" : "MCDI", # CDID music CD identifier
236 "ETC" : "ETCO", # EVENTTIMING event timing codes
237 "MLL" : "MLLT", # MPEGLOOKUP MPEG location lookup table
238 "STC" : "SYTC", # SYNCEDTEMPO synchronised tempo codes
239 "SLT" : "SYLT", # SYNCEDLYRICS synchronised lyrics/text
240 "RVA" : "RVAD", # VOLUMEADJ relative volume adjustment
241 "EQU" : "EQUA", # EQUALIZATION equalization
242 "REV" : "RVRB", # REVERB reverb
243 "PIC" : "APIC", # PICTURE attached picture
244 "GEO" : "GEOB", # GENERALOBJECT general encapsulated object
245 "CNT" : "PCNT", # PLAYCOUNTER play counter
246 "POP" : "POPM", # POPULARIMETER popularimeter
247 "BUF" : "RBUF", # BUFFERSIZE recommended buffer size
248 "CRA" : "AENC", # AUDIOCRYPTO audio encryption
249 "LNK" : "LINK", # LINKEDINFO linked information
250 # Extension workarounds i.e., ignore them
251 "TCP" : "TCP ", # iTunes "extension" for compilation marking
252 "CM1" : "CM1 " # Seems to be some script kiddie tagging the tag.
253 # For example, [rH] join #rH on efnet [rH]
257 NULL_FRAME_FLAGS
= [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
259 TEXT_FRAME_RX
= re
.compile("^T[A-Z0-9][A-Z0-9][A-Z0-9]$");
260 USERTEXT_FRAME_RX
= re
.compile("^" + USERTEXT_FID
+ "$");
261 URL_FRAME_RX
= re
.compile("^W[A-Z0-9][A-Z0-9][A-Z0-9]$");
262 USERURL_FRAME_RX
= re
.compile("^" + USERURL_FID
+ "$");
263 COMMENT_FRAME_RX
= re
.compile("^" + COMMENT_FID
+ "$");
264 CDID_FRAME_RX
= re
.compile("^" + CDID_FID
+ "$");
265 IMAGE_FRAME_RX
= re
.compile("^" + IMAGE_FID
+ "$");
266 PLAYCOUNT_FRAME_RX
= re
.compile("^" + PLAYCOUNT_FID
+ "$");
267 UNIQUE_FILE_ID_FRAME_RX
= re
.compile("^" + UNIQUE_FILE_ID_FID
+ "$");
269 # MP3ext causes illegal frames to be inserted, which must be ignored.
270 # Copied from http://shell.lab49.com/~vivake/python/MP3Info.py
271 # Henning Kiel <henning.kiel@rwth-aachen.de>
282 "CM1 " # Script kiddie
285 LATIN1_ENCODING
= "\x00";
286 UTF_16_ENCODING
= "\x01";
287 UTF_16BE_ENCODING
= "\x02";
288 UTF_8_ENCODING
= "\x03";
290 DEFAULT_ENCODING
= LATIN1_ENCODING
;
291 DEFAULT_ID3_MAJOR_VERSION
= 2;
292 DEFAULT_ID3_MINOR_VERSION
= 4;
293 DEFAULT_LANG
= "eng";
296 return "/".join([x
for x
in s
.split('\x00') if x
])
298 def id3EncodingToString(encoding
):
299 if encoding
== LATIN1_ENCODING
:
301 elif encoding
== UTF_8_ENCODING
:
303 elif encoding
== UTF_16_ENCODING
:
305 elif encoding
== UTF_16BE_ENCODING
:
313 ################################################################################
314 class FrameException(Exception):
315 '''Thrown by invalid frames'''
318 ################################################################################
320 FRAME_HEADER_SIZE
= 10;
322 majorVersion
= DEFAULT_ID3_MAJOR_VERSION
;
323 minorVersion
= DEFAULT_ID3_MINOR_VERSION
;
324 # The 4 character frame ID.
326 # An array of 16 "bits"...
327 flags
= NULL_FRAME_FLAGS
;
328 # ...and the info they store.
336 dataLenIndicator
= 0;
337 # The size of the data following this header.
340 # 2.4 not only added flag bits, but also reordered the previously defined
341 # flags. So these are mapped once we know the version.
352 def __init__(self
, tagHeader
= None):
354 self
.setVersion(tagHeader
);
356 self
.setVersion([DEFAULT_ID3_MAJOR_VERSION
,
357 DEFAULT_ID3_MINOR_VERSION
]);
359 def setVersion(self
, tagHeader
):
360 # A slight hack to make the default ctor work.
361 if isinstance(tagHeader
, list):
362 self
.majorVersion
= tagHeader
[0];
363 self
.minorVersion
= tagHeader
[1];
365 self
.majorVersion
= tagHeader
.majorVersion
;
366 self
.minorVersion
= tagHeader
.minorVersion
;
367 # Correctly set size of header
368 if self
.minorVersion
== 2:
369 self
.FRAME_HEADER_SIZE
= 6;
371 self
.FRAME_HEADER_SIZE
= 10;
374 def setBitMask(self
):
375 major
= self
.majorVersion
;
376 minor
= self
.minorVersion
;
378 # 1.x tags are converted to 2.4 frames internally. These frames are
379 # created with frame flags \x00.
380 if (major
== 2 and minor
== 2):
381 # no flags for 2.2 frames
383 elif (major
== 2 and minor
== 3):
387 self
.COMPRESSION
= 8;
390 # This is not really in 2.3 frame header flags, but there is
391 # a "global" unsync bit in the tag header and that is written here
392 # so access to the tag header is not required.
394 # And this is mapped to an used bit, so that 0 is returned.
396 elif (major
== 2 and minor
== 4) or \
397 (major
== 1 and (minor
== 0 or minor
== 1)):
401 self
.COMPRESSION
= 12;
402 self
.ENCRYPTION
= 13;
407 raise ValueError("ID3 v" + str(major
) + "." + str(minor
) +\
408 " is not supported.");
410 def render(self
, dataSize
):
413 if self
.minorVersion
== 3:
414 data
+= bin2bytes(dec2bin(dataSize
, 32));
416 data
+= bin2bytes(bin2synchsafe(dec2bin(dataSize
, 32)));
419 self
.flags
= NULL_FRAME_FLAGS
;
420 self
.flags
[self
.TAG_ALTER
] = self
.tagAlter
;
421 self
.flags
[self
.FILE_ALTER
] = self
.fileAlter
;
422 self
.flags
[self
.READ_ONLY
] = self
.readOnly
;
423 self
.flags
[self
.COMPRESSION
] = self
.compressed
;
424 self
.flags
[self
.COMPRESSION
] = self
.compressed
;
425 self
.flags
[self
.ENCRYPTION
] = self
.encrypted
;
426 self
.flags
[self
.GROUPING
] = self
.grouped
;
427 self
.flags
[self
.UNSYNC
] = self
.unsync
;
428 self
.flags
[self
.DATA_LEN
] = self
.dataLenIndicator
;
430 data
+= bin2bytes(self
.flags
);
434 def parse2_2(self
, f
):
435 frameId_22
= f
.read(3);
436 frameId
= map2_2FrameId(frameId_22
);
437 if self
.isFrameIdValid(frameId
):
438 TRACE_MSG("FrameHeader [id]: %s (0x%x%x%x)" % (frameId_22
,
441 ord(frameId_22
[2])));
443 # dataSize corresponds to the size of the data segment after
444 # encryption, compression, and unsynchronization.
446 self
.dataSize
= bin2dec(bytes2bin(sz
, 8));
447 TRACE_MSG("FrameHeader [data size]: %d (0x%X)" % (self
.dataSize
,
449 elif frameId
== '\x00\x00\x00':
450 TRACE_MSG("FrameHeader: Null frame id found at byte " +\
453 elif not strictID3() and frameId
in KNOWN_BAD_FRAMES
:
454 TRACE_MSG("FrameHeader: Illegal but known "\
455 "(possibly created by the shitty mp3ext) frame found; "\
456 "Happily ignoring!" + str(f
.tell()));
459 raise FrameException("FrameHeader: Illegal Frame ID: " + frameId
);
463 # Returns 1 on success and 0 when a null tag (marking the beginning of
464 # padding). In the case of an invalid frame header, a FrameException is
467 TRACE_MSG("FrameHeader [start byte]: %d (0x%X)" % (f
.tell(),
469 if self
.minorVersion
== 2:
470 return self
.parse2_2(f
)
473 if self
.isFrameIdValid(frameId
):
474 TRACE_MSG("FrameHeader [id]: %s (0x%x%x%x%x)" % (frameId
,
480 # dataSize corresponds to the size of the data segment after
481 # encryption, compression, and unsynchronization.
483 # In ID3 v2.4 this value became a synch-safe integer, meaning only
484 # the low 7 bits are used per byte.
485 if self
.minorVersion
== 3:
486 self
.dataSize
= bin2dec(bytes2bin(sz
, 8));
488 self
.dataSize
= bin2dec(bytes2bin(sz
, 7));
489 TRACE_MSG("FrameHeader [data size]: %d (0x%X)" % (self
.dataSize
,
494 self
.flags
= bytes2bin(flags
);
495 self
.tagAlter
= self
.flags
[self
.TAG_ALTER
];
496 self
.fileAlter
= self
.flags
[self
.FILE_ALTER
];
497 self
.readOnly
= self
.flags
[self
.READ_ONLY
];
498 self
.compressed
= self
.flags
[self
.COMPRESSION
];
499 self
.encrypted
= self
.flags
[self
.ENCRYPTION
];
500 self
.grouped
= self
.flags
[self
.GROUPING
];
501 self
.unsync
= self
.flags
[self
.UNSYNC
];
502 self
.dataLenIndicator
= self
.flags
[self
.DATA_LEN
];
503 TRACE_MSG("FrameHeader [flags]: ta(%d) fa(%d) ro(%d) co(%d) "\
504 "en(%d) gr(%d) un(%d) dl(%d)" % (self
.tagAlter
,
511 self
.dataLenIndicator
));
512 if self
.minorVersion
>= 4 and self
.compressed
and \
513 not self
.dataLenIndicator
:
514 raise FrameException("Invalid frame; compressed with no data "
517 elif frameId
== '\x00\x00\x00\x00':
518 TRACE_MSG("FrameHeader: Null frame id found at byte " +\
521 elif not strictID3() and frameId
in KNOWN_BAD_FRAMES
:
522 TRACE_MSG("FrameHeader: Illegal but known "\
523 "(possibly created by the shitty mp3ext) frame found; "\
524 "Happily ignoring!" + str(f
.tell()));
527 raise FrameException("FrameHeader: Illegal Frame ID: " + frameId
);
531 def isFrameIdValid(self
, id):
532 return re
.compile(r
"^[A-Z0-9][A-Z0-9][A-Z0-9][A-Z0-9]$").match(id);
534 def clearFlags(self
):
537 ################################################################################
538 def unsyncData(data
):
539 (data
, s0
) = re
.compile("\xff\x00").subn("\xff\x00\x00", data
);
540 (data
, s1
) = re
.compile("\xff(?=[\xe0-\xff])").subn("\xff\x00", data
);
541 TRACE_MSG("Unsynchronizing data: (%d,%d)" % (s0
, s1
));
544 def deunsyncData(data
):
545 TRACE_MSG("Frame: [size before deunsync]: " + str(len(data
)));
546 data
= re
.compile("\xff\x00([\xe0-\xff])").sub("\xff\\1", data
);
547 TRACE_MSG("Frame: [size after stage #1 deunsync]: " + str(len(data
)));
548 data
= re
.compile("\xff\x00\x00").sub("\xff\x00", data
);
549 TRACE_MSG("Frame: [size after deunsync: " + str(len(data
)));
552 ################################################################################
555 decompressedSize
= 0;
557 encryptionMethod
= 0;
559 encoding
= DEFAULT_ENCODING
;
561 def __init__(self
, frameHeader
):
562 assert(isinstance(frameHeader
, FrameHeader
));
563 self
.header
= frameHeader
;
566 desc
= self
.getFrameDesc();
567 return '<%s Frame (%s)>' % (desc
, self
.header
.id);
569 def unsync(self
, data
):
570 if self
.header
.unsync
:
571 data
= unsyncData(data
);
574 def deunsync(self
, data
):
575 data
= deunsyncData(data
);
578 def decompress(self
, data
):
579 TRACE_MSG("before decompression: %d bytes" % len(data
));
580 data
= zlib
.decompress(data
, 15, self
.decompressedSize
);
581 TRACE_MSG("after decompression: %d bytes" % len(data
));
584 def compress(self
, data
):
585 TRACE_MSG("before compression: %d bytes" % len(data
));
586 data
= zlib
.compress(data
);
587 TRACE_MSG("after compression: %d bytes" % len(data
));
590 def decrypt(self
, data
):
591 raise FrameException("Encryption not supported");
593 def encrypt(self
, data
):
594 raise FrameException("Encryption not supported");
596 def disassembleFrame(self
, data
):
597 # Format flags in the frame header may add extra data to the
598 # beginning of this data.
599 if self
.header
.minorVersion
== 3:
600 # 2.3: compression(4), encryption(1), group(1)
601 if self
.header
.compressed
:
602 self
.decompressedSize
= bin2dec(bytes2bin(data
[:4]));
604 TRACE_MSG("Decompressed Size: %d" % self
.decompressedSize
);
605 if self
.header
.encrypted
:
606 self
.encryptionMethod
= bin2dec(bytes2bin(data
[0]));
608 TRACE_MSG("Encryption Method: %d" % self
.encryptionMethod
);
609 if self
.header
.grouped
:
610 self
.groupId
= bin2dec(bytes2bin(data
[0]));
612 TRACE_MSG("Group ID: %d" % self
.groupId
);
614 # 2.4: group(1), encrypted(1), dataLenIndicator(4,7)
615 if self
.header
.grouped
:
616 self
.groupId
= bin2dec(bytes2bin(data
[0]));
618 if self
.header
.encrypted
:
619 self
.encryptionMethod
= bin2dec(bytes2bin(data
[0]));
621 TRACE_MSG("Encryption Method: %d" % self
.encryptionMethod
);
622 TRACE_MSG("Group ID: %d" % self
.groupId
);
623 if self
.header
.dataLenIndicator
:
624 self
.dataLen
= bin2dec(bytes2bin(data
[:4], 7));
626 TRACE_MSG("Data Length: %d" % self
.dataLen
);
627 if self
.header
.compressed
:
628 self
.decompressedSize
= self
.dataLen
;
629 TRACE_MSG("Decompressed Size: %d" % self
.decompressedSize
);
631 if self
.header
.unsync
:
632 data
= self
.deunsync(data
);
633 if self
.header
.encrypted
:
634 data
= self
.decrypt(data
);
635 if self
.header
.compressed
:
636 data
= self
.decompress(data
);
639 def assembleFrame (self
, data
):
641 if self
.header
.minorVersion
== 3:
642 if self
.header
.compressed
:
643 formatFlagData
+= bin2bytes(dec2bin(len(data
), 32));
644 if self
.header
.encrypted
:
645 formatFlagData
+= bin2bytes(dec2bin(self
.encryptionMethod
, 8));
646 if self
.header
.grouped
:
647 formatFlagData
+= bin2bytes(dec2bin(self
.groupId
, 8));
649 if self
.header
.grouped
:
650 formatFlagData
+= bin2bytes(dec2bin(self
.groupId
, 8));
651 if self
.header
.encrypted
:
652 formatFlagData
+= bin2bytes(dec2bin(self
.encryptionMethod
, 8));
653 if self
.header
.compressed
or self
.header
.dataLenIndicator
:
654 # Just in case, not sure about this?
655 self
.header
.dataLenIndicator
= 1;
656 formatFlagData
+= bin2bytes(dec2bin(len(data
), 32));
658 if self
.header
.compressed
:
659 data
= self
.compress(data
);
660 if self
.header
.encrypted
:
661 data
= self
.encrypt(data
);
662 if self
.header
.unsync
:
663 TRACE_MSG("Creating sync-safe frame");
664 data
= self
.unsync(data
);
666 data
= formatFlagData
+ data
;
667 return self
.header
.render(len(data
)) + data
;
669 def getFrameDesc(self
):
671 return frameDesc
[self
.header
.id];
674 return obsoleteFrames
[self
.header
.id];
676 return "UNKOWN FRAME";
678 def getTextDelim(self
):
679 if self
.encoding
== UTF_16_ENCODING
or \
680 self
.encoding
== UTF_16BE_ENCODING
:
685 ################################################################################
686 class TextFrame(Frame
):
689 # Data string format:
690 # encoding (one byte) + text;
691 def __init__(self
, frameHeader
, data
= None, text
= u
"",
692 encoding
= DEFAULT_ENCODING
):
693 Frame
.__init
__(self
, frameHeader
);
695 self
._set
(data
, frameHeader
);
698 assert(text
!= None and isinstance(text
, unicode));
699 self
.encoding
= encoding
;
702 # Data string format:
703 # encoding (one byte) + text;
704 def _set(self
, data
, frameHeader
):
705 fid
= frameHeader
.id;
706 if not TEXT_FRAME_RX
.match(fid
) or USERTEXT_FRAME_RX
.match(fid
):
707 raise FrameException("Invalid frame id for TextFrame: " + fid
);
709 data
= self
.disassembleFrame(data
);
710 self
.encoding
= data
[0];
711 TRACE_MSG("TextFrame encoding: %s" % id3EncodingToString(self
.encoding
));
713 self
.text
= unicode(data
[1:], id3EncodingToString(self
.encoding
));
715 self
.text
= cleanNulls(self
.text
)
716 except TypeError, excArg
:
717 # if data is already unicode, just copy it
718 if excArg
.args
== ("decoding Unicode is not supported",):
719 self
.text
= data
[1:];
721 self
.text
= cleanNulls(self
.text
)
724 TRACE_MSG("TextFrame text: %s" % self
.text
);
726 def __unicode__(self
):
727 return u
'<%s (%s): %s>' % (self
.getFrameDesc(), self
.header
.id,
731 if self
.header
.minorVersion
== 4 and self
.header
.id == "TSIZ":
732 TRACE_MSG("Dropping deprecated frame TSIZ");
734 data
= self
.encoding
+\
735 self
.text
.encode(id3EncodingToString(self
.encoding
));
736 return self
.assembleFrame(data
);
738 ################################################################################
739 class DateFrame(TextFrame
):
743 def __init__(self
, frameHeader
, data
= None, date_str
= None,
744 encoding
= DEFAULT_ENCODING
):
746 TextFrame
.__init
__(self
, frameHeader
, data
= data
,
747 encoding
= encoding
);
748 self
._set
(data
, frameHeader
);
750 assert(date_str
and isinstance(date_str
, unicode));
751 TextFrame
.__init
__(self
, frameHeader
, text
= date_str
,
752 encoding
= encoding
);
753 self
.setDate(self
.text
);
755 def _set(self
, data
, frameHeader
):
756 TextFrame
._set
(self
, data
, frameHeader
);
757 if self
.header
.id[:2] != "TD" and self
.header
.minorVersion
>= 4:
758 raise FrameException("Invalid frame id for DateFrame: " + \
761 def setDate(self
, d
):
767 for fmt
in timeStampFormats
:
769 if isinstance(d
, tuple):
770 self
.date_str
= unicode(time
.strftime(fmt
, d
));
773 assert(isinstance(d
, unicode));
774 # Witnessed oddball tags with NULL bytes (ozzy.tag from id3lib)
778 self
.date
= time
.strptime(d
, fmt
);
779 except TypeError, ex
:
787 if strictID3() and not self
.date
:
788 raise FrameException("Invalid Date: " + str(d
));
789 self
.text
= self
.date_str
;
792 return self
.date_str
;
796 return self
.__padDateField
(self
.date
[0], 4);
802 return self
.__padDateField
(self
.date
[1], 2);
808 return self
.__padDateField
(self
.date
[2], 2);
814 return self
.__padDateField
(self
.date
[3], 2);
820 return self
.__padDateField
(self
.date
[4], 2);
826 return self
.__padDateField
(self
.date
[5], 2);
830 def __padDateField(self
, f
, sz
):
835 fStr
= ("0" * (sz
- len(fStr
))) + fStr
;
837 raise TagException("Invalid date field: " + fStr
);
842 if self
.header
.minorVersion
== 4 and\
843 (self
.header
.id == OBSOLETE_DATE_FID
or\
844 self
.header
.id == OBSOLETE_YEAR_FID
or\
845 self
.header
.id == OBSOLETE_TIME_FID
or\
846 self
.header
.id == OBSOLETE_RECORDING_DATE_FID
):
847 self
.header
.id = "TDRC";
848 elif self
.header
.minorVersion
== 4 and\
849 self
.header
.id == OBSOLETE_ORIG_RELEASE_FID
:
850 self
.header
.id = "TDOR";
851 elif self
.header
.minorVersion
== 3 and self
.header
.id == "TDOR":
852 self
.header
.id = OBSOLETE_ORIG_RELEASE_FID
;
853 elif self
.header
.minorVersion
== 3 and self
.header
.id[:2] == "TD":
854 self
.header
.id = OBSOLETE_YEAR_FID
;
856 data
= self
.encoding
+\
857 self
.date_str
.encode(id3EncodingToString(self
.encoding
));
858 data
= self
.assembleFrame(data
);
862 ################################################################################
863 class UserTextFrame(TextFrame
):
866 # Data string format:
867 # encoding (one byte) + description + "\x00" + text;
868 def __init__(self
, frameHeader
, data
= None, description
= u
"", text
= u
"",
869 encoding
= DEFAULT_ENCODING
):
871 TextFrame
.__init
__(self
, frameHeader
, data
= data
);
872 self
._set
(data
, frameHeader
);
874 assert(isinstance(description
, unicode) and\
875 isinstance(text
, unicode))
876 TextFrame
.__init
__(self
, frameHeader
, text
= text
,
877 encoding
= encoding
);
878 self
.description
= description
;
880 # Data string format:
881 # encoding (one byte) + description + "\x00" + text;
882 def _set(self
, data
, frameHeader
= None):
884 if not USERTEXT_FRAME_RX
.match(frameHeader
.id):
885 raise FrameException("Invalid frame id for UserTextFrame: " +\
888 data
= self
.disassembleFrame(data
);
889 self
.encoding
= data
[0];
890 TRACE_MSG("UserTextFrame encoding: %s" %\
891 id3EncodingToString(self
.encoding
));
892 (d
, t
) = splitUnicode(data
[1:], self
.encoding
);
893 self
.description
= unicode(d
, id3EncodingToString(self
.encoding
));
894 TRACE_MSG("UserTextFrame description: %s" % self
.description
);
895 self
.text
= unicode(t
, id3EncodingToString(self
.encoding
));
897 self
.text
= cleanNulls(self
.text
)
898 TRACE_MSG("UserTextFrame text: %s" % self
.text
);
901 data
= self
.encoding
+\
902 self
.description
.encode(id3EncodingToString(self
.encoding
)) +\
903 self
.getTextDelim() +\
904 self
.text
.encode(id3EncodingToString(self
.encoding
));
905 return self
.assembleFrame(data
);
907 def __unicode__(self
):
908 return u
'<%s (%s): {Desc: %s} %s>' % (self
.getFrameDesc(),
910 self
.description
, self
.text
);
912 ################################################################################
913 class URLFrame(Frame
):
916 # Data string format:
918 def __init__(self
, frameHeader
, data
= None, url
= None):
919 Frame
.__init
__(self
, frameHeader
);
921 self
._set
(data
, frameHeader
);
926 # Data string format:
928 def _set(self
, data
, frameHeader
):
929 fid
= frameHeader
.id;
930 if not URL_FRAME_RX
.match(fid
) or USERURL_FRAME_RX
.match(fid
):
931 raise FrameException("Invalid frame id for URLFrame: " + fid
);
932 data
= self
.disassembleFrame(data
);
935 self
.url
= cleanNulls(self
.url
)
938 data
= str(self
.url
);
939 return self
.assembleFrame(data
);
942 return '<%s (%s): %s>' % (self
.getFrameDesc(), self
.header
.id,
945 ################################################################################
946 class UserURLFrame(URLFrame
):
949 # Data string format:
950 # encoding (one byte) + description + "\x00" + url;
951 def __init__(self
, frameHeader
, data
= None, url
= "", description
= u
"",
952 encoding
= DEFAULT_ENCODING
):
953 Frame
.__init
__(self
, frameHeader
);
955 self
._set
(data
, frameHeader
);
958 assert(description
and isinstance(description
, unicode));
959 assert(url
and isinstance(url
, str));
960 self
.encoding
= encoding
;
962 self
.description
= description
;
964 # Data string format:
965 # encoding (one byte) + description + "\x00" + url;
966 def _set(self
, data
, frameHeader
):
967 assert(data
and frameHeader
);
968 if not USERURL_FRAME_RX
.match(frameHeader
.id):
969 raise FrameException("Invalid frame id for UserURLFrame: " +\
972 data
= self
.disassembleFrame(data
);
973 self
.encoding
= data
[0];
974 TRACE_MSG("UserURLFrame encoding: %s" %\
975 id3EncodingToString(self
.encoding
));
976 (d
, u
) = splitUnicode(data
[1:], self
.encoding
);
977 self
.description
= unicode(d
, id3EncodingToString(self
.encoding
));
978 TRACE_MSG("UserURLFrame description: %s" % self
.description
);
981 self
.url
= cleanNulls(self
.url
)
982 TRACE_MSG("UserURLFrame text: %s" % self
.url
);
985 data
= self
.encoding
+\
986 self
.description
.encode(id3EncodingToString(self
.encoding
)) +\
987 self
.getTextDelim() + self
.url
;
988 return self
.assembleFrame(data
);
990 def __unicode__(self
):
991 return u
'<%s (%s): %s [Encoding: %s] [Desc: %s]>' %\
992 (self
.getFrameDesc(), self
.header
.id,
993 self
.url
, self
.encoding
, self
.description
)
995 ################################################################################
996 class CommentFrame(Frame
):
1001 # Data string format:
1002 # encoding (one byte) + lang (three byte code) + description + "\x00" +
1004 def __init__(self
, frameHeader
, data
= None, lang
= "",
1005 description
= u
"", comment
= u
"", encoding
= DEFAULT_ENCODING
):
1006 Frame
.__init
__(self
, frameHeader
);
1008 self
._set
(data
, frameHeader
);
1010 assert(isinstance(description
, unicode));
1011 assert(isinstance(comment
, unicode));
1012 assert(isinstance(lang
, str));
1013 self
.encoding
= encoding
;
1015 self
.description
= description
;
1016 self
.comment
= comment
;
1018 # Data string format:
1019 # encoding (one byte) + lang (three byte code) + description + "\x00" +
1021 def _set(self
, data
, frameHeader
= None):
1022 assert(frameHeader
);
1023 if not COMMENT_FRAME_RX
.match(frameHeader
.id):
1024 raise FrameException("Invalid frame id for CommentFrame: " +\
1027 data
= self
.disassembleFrame(data
);
1028 self
.encoding
= data
[0];
1029 TRACE_MSG("CommentFrame encoding: " + id3EncodingToString(self
.encoding
));
1031 self
.lang
= str(data
[1:4]).strip("\x00");
1032 # Test ascii encoding
1033 temp_lang
= unicode(self
.lang
, "ascii");
1035 not re
.compile("[A-Z][A-Z][A-Z]", re
.IGNORECASE
).match(self
.lang
):
1037 raise FrameException("[CommentFrame] Invalid language "\
1038 "code: %s" % self
.lang
);
1039 except UnicodeDecodeError, ex
:
1041 raise FrameException("[CommentFrame] Invalid language code: "\
1042 "[%s] %s" % (ex
.object, ex
.reason
));
1046 (d
, c
) = splitUnicode(data
[4:], self
.encoding
);
1047 self
.description
= unicode(d
, id3EncodingToString(self
.encoding
));
1048 self
.comment
= unicode(c
, id3EncodingToString(self
.encoding
));
1051 raise FrameException("Invalid comment; no description/comment");
1053 self
.description
= u
"";
1056 self
.description
= cleanNulls(self
.description
)
1057 self
.comment
= cleanNulls(self
.comment
)
1060 lang
= self
.lang
.encode("ascii");
1064 lang
= lang
+ ('\x00' * (3 - len(lang
)));
1065 data
= self
.encoding
+ lang
+\
1066 self
.description
.encode(id3EncodingToString(self
.encoding
)) +\
1067 self
.getTextDelim() +\
1068 self
.comment
.encode(id3EncodingToString(self
.encoding
));
1069 return self
.assembleFrame(data
);
1071 def __unicode__(self
):
1072 return u
"<%s (%s): %s [Lang: %s] [Desc: %s]>" %\
1073 (self
.getFrameDesc(), self
.header
.id, self
.comment
,
1074 self
.lang
, self
.description
);
1076 ################################################################################
1077 # This class refers to the APIC frame, otherwise known as an "attached
1079 class ImageFrame(Frame
):
1083 # Contains the image data when the mimetype is image type.
1084 # Otherwise it is None.
1086 # Contains a URL for the image when the mimetype is "-->" per the spec.
1087 # Otherwise it is None.
1089 # Declared "picture types".
1091 ICON
= 0x01; # 32x32 png only.
1096 MEDIA
= 0x06; # label side of cd, picture disc vinyl, etc.
1103 RECORDING_LOCATION
= 0x0D;
1104 DURING_RECORDING
= 0x0E;
1105 DURING_PERFORMANCE
= 0x0F;
1107 BRIGHT_COLORED_FISH
= 0x11; # There's always room for porno.
1108 ILLUSTRATION
= 0x12;
1110 PUBLISHER_LOGO
= 0x14;
1112 MAX_TYPE
= PUBLISHER_LOGO
;
1114 def __init__(self
, frameHeader
, data
= None,
1116 imageData
= None, imageURL
= None,
1117 pictureType
= None, mimeType
= None,
1118 encoding
= DEFAULT_ENCODING
):
1119 Frame
.__init
__(self
, frameHeader
);
1121 self
._set
(data
, frameHeader
);
1123 assert(isinstance(description
, unicode));
1124 self
.description
= description
;
1125 self
.encoding
= encoding
;
1127 self
.mimeType
= mimeType
;
1128 assert(pictureType
);
1129 self
.pictureType
= pictureType
;
1131 self
.imageData
= imageData
;
1133 self
.imageURL
= imageURL
;
1134 assert(self
.imageData
or self
.imageURL
);
1138 def create(type, imgFile
, desc
= u
"", encoding
= DEFAULT_ENCODING
):
1139 if not isinstance(desc
, unicode) or \
1140 not isinstance(type, int):
1141 raise FrameException("Wrong description and/or image-type type.");
1143 fp
= file(imgFile
, "rb");
1144 imgData
= fp
.read();
1145 mt
= mimetypes
.guess_type(imgFile
);
1147 raise FrameException("Unable to guess mime-type for %s" % (imgFile
));
1149 frameData
= DEFAULT_ENCODING
;
1150 frameData
+= mt
[0] + "\x00";
1151 frameData
+= bin2bytes(dec2bin(type, 8));
1152 frameData
+= desc
.encode(id3EncodingToString(encoding
)) + "\x00";
1153 frameData
+= imgData
;
1155 frameHeader
= FrameHeader();
1156 frameHeader
.id = IMAGE_FID
;
1157 return ImageFrame(frameHeader
, data
= frameData
);
1158 # Make create a static method. Odd....
1159 create
= staticmethod(create
);
1161 # Data string format:
1162 # <Header for 'Attached picture', ID: "APIC">
1164 # MIME type <text string> $00
1166 # Description <text string according to encoding> $00 (00)
1167 # Picture data <binary data>
1168 def _set(self
, data
, frameHeader
= None):
1169 assert(frameHeader
);
1170 if not IMAGE_FRAME_RX
.match(frameHeader
.id):
1171 raise FrameException("Invalid frame id for ImageFrame: " +\
1174 data
= self
.disassembleFrame(data
);
1176 input = StringIO(data
);
1177 TRACE_MSG("APIC frame data size: " + str(len(data
)));
1178 self
.encoding
= input.read(1);
1179 TRACE_MSG("APIC encoding: " + id3EncodingToString(self
.encoding
));
1183 if self
.header
.minorVersion
!= 2:
1186 self
.mimeType
+= ch
;
1189 # v2.2 (OBSOLETE) special case
1190 self
.mimeType
= input.read(3);
1191 TRACE_MSG("APIC mime type: " + self
.mimeType
);
1192 if strictID3() and not self
.mimeType
:
1193 raise FrameException("APIC frame does not contain a mime type");
1194 if self
.mimeType
.find("/") == -1:
1195 self
.mimeType
= "image/" + self
.mimeType
;
1197 pt
= ord(input.read(1));
1198 TRACE_MSG("Initial APIC picture type: " + str(pt
));
1199 if pt
< self
.MIN_TYPE
or pt
> self
.MAX_TYPE
:
1201 raise FrameException("Invalid APIC picture type: %d" % (pt
));
1202 # Rather than force this to UNKNOWN, let's assume that they put a
1203 # character literal instead of it's byte value.
1208 if pt
< self
.MIN_TYPE
or pt
> self
.MAX_TYPE
:
1209 self
.pictureType
= self
.OTHER
;
1210 self
.pictureType
= pt
;
1211 TRACE_MSG("APIC picture type: " + str(self
.pictureType
));
1213 self
.desciption
= u
"";
1215 # Remaining data is a NULL separated description and image data
1216 buffer = input.read();
1219 (desc
, img
) = splitUnicode(buffer, self
.encoding
);
1220 TRACE_MSG("description len: %d" % len(desc
));
1221 TRACE_MSG("description len: %d" % len(img
));
1222 self
.description
= unicode(desc
, id3EncodingToString(self
.encoding
));
1223 TRACE_MSG("APIC description: " + self
.description
);
1225 if self
.mimeType
.find("-->") != -1:
1226 self
.imageData
= None;
1227 self
.imageURL
= img
;
1229 self
.imageData
= img
;
1230 self
.imageURL
= None;
1231 TRACE_MSG("APIC image data: " + str(len(self
.imageData
)) + " bytes");
1232 if strictID3() and not self
.imageData
and not self
.imageURL
:
1233 raise FrameException("APIC frame does not contain any image data");
1236 def writeFile(self
, path
= "./", name
= None):
1237 if not self
.imageData
:
1238 raise IOError("Fetching remote image files is not implemented.");
1240 name
= self
.getDefaultFileName();
1241 imageFile
= os
.path
.join(path
, name
);
1243 f
= file(imageFile
, "wb");
1244 f
.write(self
.imageData
);
1247 def getDefaultFileName(self
, suffix
= ""):
1248 nameStr
= self
.picTypeToString(self
.pictureType
);
1251 nameStr
= nameStr
+ "." + self
.mimeType
.split("/")[1];
1255 data
= self
.encoding
+ self
.mimeType
+ "\x00" +\
1256 bin2bytes(dec2bin(self
.pictureType
, 8)) +\
1257 self
.description
.encode(id3EncodingToString(self
.encoding
)) +\
1258 self
.getTextDelim();
1260 data
+= self
.imageURL
.encode("ascii");
1262 data
+= self
.imageData
;
1263 return self
.assembleFrame(data
);
1265 def stringToPicType(s
):
1267 return ImageFrame
.OTHER
;
1269 return ImageFrame
.ICON
;
1270 elif s
== "OTHER_ICON":
1271 return ImageFrame
.OTHER_ICON
;
1272 elif s
== "FRONT_COVER":
1273 return ImageFrame
.FRONT_COVER
1274 elif s
== "BACK_COVER":
1275 return ImageFrame
.BACK_COVER
;
1276 elif s
== "LEAFLET":
1277 return ImageFrame
.LEAFLET
;
1279 return ImageFrame
.MEDIA
;
1280 elif s
== "LEAD_ARTIST":
1281 return ImageFrame
.LEAD_ARTIST
;
1283 return ImageFrame
.ARTIST
;
1284 elif s
== "CONDUCTOR":
1285 return ImageFrame
.CONDUCTOR
;
1287 return ImageFrame
.BAND
;
1288 elif s
== "COMPOSER":
1289 return ImageFrame
.COMPOSER
;
1290 elif s
== "LYRICIST":
1291 return ImageFrame
.LYRICIST
;
1292 elif s
== "RECORDING_LOCATION":
1293 return ImageFrame
.RECORDING_LOCATION
;
1294 elif s
== "DURING_RECORDING":
1295 return ImageFrame
.DURING_RECORDING
;
1296 elif s
== "DURING_PERFORMANCE":
1297 return ImageFrame
.DURING_PERFORMANCE
;
1299 return ImageFrame
.VIDEO
;
1300 elif s
== "BRIGHT_COLORED_FISH":
1301 return ImageFrame
.BRIGHT_COLORED_FISH
;
1302 elif s
== "ILLUSTRATION":
1303 return ImageFrame
.ILLUSTRATION
;
1304 elif s
== "BAND_LOGO":
1305 return ImageFrame
.BAND_LOGO
;
1306 elif s
== "PUBLISHER_LOGO":
1307 return ImageFrame
.PUBLISHER_LOGO
;
1309 raise FrameException("Invalid APIC picture type: %s" % s
);
1310 stringToPicType
= staticmethod(stringToPicType
);
1312 def picTypeToString(t
):
1313 if t
== ImageFrame
.OTHER
:
1315 elif t
== ImageFrame
.ICON
:
1317 elif t
== ImageFrame
.OTHER_ICON
:
1318 return "OTHER_ICON";
1319 elif t
== ImageFrame
.FRONT_COVER
:
1320 return "FRONT_COVER";
1321 elif t
== ImageFrame
.BACK_COVER
:
1322 return "BACK_COVER";
1323 elif t
== ImageFrame
.LEAFLET
:
1325 elif t
== ImageFrame
.MEDIA
:
1327 elif t
== ImageFrame
.LEAD_ARTIST
:
1328 return "LEAD_ARTIST";
1329 elif t
== ImageFrame
.ARTIST
:
1331 elif t
== ImageFrame
.CONDUCTOR
:
1333 elif t
== ImageFrame
.BAND
:
1335 elif t
== ImageFrame
.COMPOSER
:
1337 elif t
== ImageFrame
.LYRICIST
:
1339 elif t
== ImageFrame
.RECORDING_LOCATION
:
1340 return "RECORDING_LOCATION";
1341 elif t
== ImageFrame
.DURING_RECORDING
:
1342 return "DURING_RECORDING";
1343 elif t
== ImageFrame
.DURING_PERFORMANCE
:
1344 return "DURING_PERFORMANCE";
1345 elif t
== ImageFrame
.VIDEO
:
1347 elif t
== ImageFrame
.BRIGHT_COLORED_FISH
:
1348 return "BRIGHT_COLORED_FISH";
1349 elif t
== ImageFrame
.ILLUSTRATION
:
1350 return "ILLUSTRATION";
1351 elif t
== ImageFrame
.BAND_LOGO
:
1353 elif t
== ImageFrame
.PUBLISHER_LOGO
:
1354 return "PUBLISHER_LOGO";
1356 raise FrameException("Invalid APIC picture type: %d" % t
);
1357 picTypeToString
= staticmethod(picTypeToString
);
1359 class PlayCountFrame(Frame
):
1362 def __init__(self
, frameHeader
, data
= None, count
= None):
1363 Frame
.__init
__(self
, frameHeader
);
1365 self
._set
(data
, frameHeader
);
1367 assert(count
!= None and count
>= 0);
1369 def _set(self
, data
, frameHeader
):
1370 assert(frameHeader
);
1371 assert(len(data
) >= 4);
1372 self
.count
= long(bytes2dec(data
));
1375 data
= dec2bytes(self
.count
, 32);
1376 return self
.assembleFrame(data
);
1378 class UniqueFileIDFrame(Frame
):
1382 def __init__(self
, frameHeader
, data
= None, owner_id
= None, id = None):
1383 Frame
.__init
__(self
, frameHeader
);
1385 self
._set
(data
, frameHeader
);
1387 assert(owner_id
!= None and len(owner_id
) > 0);
1388 assert(id != None and len(id) > 0 and len(id) <= 64);
1389 self
.owner_id
= owner_id
;
1392 def _set(self
, data
, frameHeader
):
1393 assert(frameHeader
);
1395 # Owner identifier <text string> $00
1396 # Identifier up to 64 bytes binary data>
1397 (self
.owner_id
, self
.id) = data
.split("\x00", 1);
1398 TRACE_MSG("UFID owner_id: " + self
.owner_id
);
1399 TRACE_MSG("UFID id: " + self
.id);
1400 if strictID3() and (len(self
.owner_id
) == 0 or
1401 len(self
.id) == 0 or len(self
.id) > 64):
1402 raise FrameException("Invalid UFID frame");
1405 data
= self
.owner_id
+ "\x00" + self
.id;
1406 return self
.assembleFrame(data
);
1408 ################################################################################
1409 class UnknownFrame(Frame
):
1412 def __init__(self
, frameHeader
, data
):
1413 Frame
.__init
__(self
, frameHeader
);
1414 assert(frameHeader
and data
);
1415 self
._set
(data
, frameHeader
);
1417 def _set(self
, data
, frameHeader
):
1418 data
= self
.disassembleFrame(data
);
1422 return self
.assembleFrame(self
.data
)
1424 ################################################################################
1425 class MusicCDIdFrame(Frame
):
1428 def __init__(self
, frameHeader
, data
= None):
1429 Frame
.__init
__(self
, frameHeader
);
1430 # XXX: Flesh this class out and add a toc arg
1431 assert(data
!= None);
1433 self
._set
(data
, frameHeader
);
1435 # TODO: Parse the TOC and comment the format.
1436 def _set(self
, data
, frameHeader
):
1437 if not CDID_FRAME_RX
.match(frameHeader
.id):
1438 raise FrameException("Invalid frame id for MusicCDIdFrame: " +\
1440 data
= self
.disassembleFrame(data
);
1445 return self
.assembleFrame(data
);
1447 ################################################################################
1448 # A class for containing and managing ID3v2.Frame objects.
1449 class FrameSet(list):
1452 def __init__(self
, tagHeader
, l
= None):
1453 self
.tagHeader
= tagHeader
;
1456 if not isinstance(f
, Frame
):
1457 raise TypeError("Invalid type added to FrameSet: " +\
1461 # Setting a FrameSet instance like this 'fs = []' morphs the instance into
1466 # Read frames starting from the current read position of the file object.
1467 # Returns the amount of padding which occurs after the tag, but before the
1468 # audio content. A return valule of 0 DOES NOT imply an error.
1469 def parse(self
, f
, tagHeader
):
1470 self
.tagHeader
= tagHeader
;
1472 sizeLeft
= tagHeader
.tagSize
;
1474 # Handle a tag-level unsync. Some frames may have their own unsync bit
1476 tagData
= f
.read(sizeLeft
);
1478 if tagHeader
.unsync
:
1479 TRACE_MSG("Tag has unsync bit set");
1480 tagData
= deunsyncData(tagData
);
1481 sizeLeft
= len(tagData
);
1483 if og_size
!= sizeLeft
:
1484 TRACE_MSG("De-unsyncing changed size: %d" % (og_size
- sizeLeft
));
1485 # Deunsyncing changed the tag size we are working with.
1486 size_change
= og_size
- sizeLeft
;
1488 # Adding 10 to simulate the tag header in the buffer. This keeps
1489 # f.tell() values matching the file itself.
1490 tagBuffer
= StringIO((10 * '\x00') + tagData
);
1495 TRACE_MSG("sizeLeft: " + str(sizeLeft
));
1496 if sizeLeft
< (10 + 1):
1497 TRACE_MSG("FrameSet: Implied padding (sizeLeft < minFrameSize)");
1498 paddingSize
= sizeLeft
;
1501 TRACE_MSG("+++++++++++++++++++++++++++++++++++++++++++++++++");
1502 TRACE_MSG("FrameSet: Reading Frame #" + str(len(self
) + 1));
1503 frameHeader
= FrameHeader(tagHeader
);
1504 if not frameHeader
.parse(tagBuffer
):
1505 paddingSize
= sizeLeft
;
1509 TRACE_MSG("FrameSet: Reading %d (0x%X) bytes of data from byte "\
1510 "pos %d (0x%X)" % (frameHeader
.dataSize
,
1511 frameHeader
.dataSize
, tagBuffer
.tell(),
1513 data
= tagBuffer
.read(frameHeader
.dataSize
);
1514 TRACE_MSG("FrameSet: %d bytes of data read" % len(data
));
1516 self
.addFrame(createFrame(frameHeader
, data
));
1518 # Each frame contains dataSize + headerSize bytes.
1519 sizeLeft
-= (frameHeader
.FRAME_HEADER_SIZE
+ frameHeader
.dataSize
);
1523 # Returrns the size of the frame data.
1527 sz
+= len(f
.render());
1530 def setTagHeader(self
, tagHeader
):
1531 self
.tagHeader
= tagHeader
;
1533 f
.header
.setVersion(tagHeader
);
1535 # This methods adds the frame if it is addable per the ID3 spec.
1536 def addFrame(self
, frame
):
1537 fid
= frame
.header
.id;
1539 # Text frame restrictions.
1540 # No multiples except for TXXX which must have unique descriptions.
1541 if strictID3() and TEXT_FRAME_RX
.match(fid
) and self
[fid
]:
1542 if not USERTEXT_FRAME_RX
.match(fid
):
1543 raise FrameException("Multiple %s frames not allowed." % fid
);
1544 userTextFrames
= self
[fid
];
1545 for frm
in userTextFrames
:
1546 if frm
.description
== frame
.description
:
1547 raise FrameException("Multiple %s frames with the same\
1548 description not allowed." % fid
);
1550 # Comment frame restrictions.
1551 # Multiples must have a unique description/language combination.
1552 if strictID3() and COMMENT_FRAME_RX
.match(fid
) and self
[fid
]:
1553 commentFrames
= self
[fid
];
1554 for frm
in commentFrames
:
1555 if frm
.description
== frame
.description
and\
1556 frm
.lang
== frame
.lang
:
1557 raise FrameException("Multiple %s frames with the same\
1558 language and description not allowed." %\
1561 # URL frame restrictions.
1562 # No multiples except for TXXX which must have unique descriptions.
1563 if strictID3() and URL_FRAME_RX
.match(fid
) and self
[fid
]:
1564 if not USERURL_FRAME_RX
.match(fid
):
1565 raise FrameException("Multiple %s frames not allowed." % fid
);
1566 userUrlFrames
= self
[fid
];
1567 for frm
in userUrlFrames
:
1568 if frm
.description
== frame
.description
:
1569 raise FrameException("Multiple %s frames with the same\
1570 description not allowed." % fid
);
1572 # Music CD ID restrictions.
1574 if strictID3() and CDID_FRAME_RX
.match(fid
) and self
[fid
]:
1575 raise FrameException("Multiple %s frames not allowed." % fid
);
1577 # Image (attached picture) frame restrictions.
1578 # Multiples must have a unique content desciptor. I'm assuming that
1579 # the spec means the picture type.....
1580 if IMAGE_FRAME_RX
.match(fid
) and self
[fid
] and strictID3():
1581 imageFrames
= self
[fid
];
1582 for frm
in imageFrames
:
1583 if frm
.pictureType
== frame
.pictureType
:
1584 raise FrameException("Multiple %s frames with the same "\
1585 "content descriptor not allowed." % fid
);
1587 # Play count frame (PCNT). There may be only one
1588 if PLAYCOUNT_FRAME_RX
.match(fid
) and self
[fid
]:
1589 raise FrameException("Multiple %s frames not allowed." % fid
);
1591 # Unique File identifier frame. There may be only one with the same
1593 if UNIQUE_FILE_ID_FRAME_RX
.match(fid
) and self
[fid
]:
1594 ufid_frames
= self
[fid
];
1595 for frm
in ufid_frames
:
1596 if frm
.owner_id
== frame
.owner_id
:
1597 raise FrameException("Multiple %s frames not allowed with "\
1598 "the same owner ID (%s)" %\
1599 (fid
, frame
.owner_id
));
1603 # Set a text frame value. Text frame IDs must be unique. If a frame with
1604 # the same Id is already in the list it's value is changed, otherwise
1605 # the frame is added.
1606 def setTextFrame(self
, frameId
, text
, encoding
= None):
1607 assert(type(text
) == unicode);
1609 if not TEXT_FRAME_RX
.match(frameId
):
1610 raise FrameException("Invalid Frame ID: " + frameId
);
1611 if USERTEXT_FRAME_RX
.match(frameId
):
1612 raise FrameException("Wrong method, use setUserTextFrame");
1615 curr
= self
[frameId
][0];
1617 curr
.encoding
= encoding
;
1619 if isinstance(curr
, DateFrame
):
1624 h
= FrameHeader(self
.tagHeader
);
1627 encoding
= DEFAULT_ENCODING
;
1628 if frameId
in DATE_FIDS
:
1629 self
.addFrame(DateFrame(h
, encoding
= encoding
, date_str
= text
));
1631 self
.addFrame(TextFrame(h
, encoding
= encoding
, text
= text
));
1633 # If a user text frame with the same description exists then
1634 # the frame text is replaced, otherwise the frame is added.
1635 def setCommentFrame(self
, comment
, description
, lang
= DEFAULT_LANG
,
1637 assert(isinstance(comment
, unicode) and isinstance(description
, unicode));
1639 if self
[COMMENT_FID
]:
1641 for f
in self
[COMMENT_FID
]:
1642 if f
.lang
== lang
and f
.description
== description
:
1643 f
.comment
= comment
;
1645 f
.encoding
= encoding
;
1649 h
= FrameHeader(self
.tagHeader
);
1652 encoding
= DEFAULT_ENCODING
;
1653 self
.addFrame(CommentFrame(h
, encoding
= encoding
, lang
= lang
,
1654 description
= description
,
1655 comment
= comment
));
1658 encoding
= DEFAULT_ENCODING
;
1659 h
= FrameHeader(self
.tagHeader
);
1661 self
.addFrame(CommentFrame(h
, encoding
= encoding
, lang
= lang
,
1662 description
= description
,
1663 comment
= comment
));
1665 def setUniqueFileIDFrame(self
, owner_id
, id):
1666 assert(isinstance(owner_id
, str) and isinstance(id, str));
1668 if self
[UNIQUE_FILE_ID_FID
]:
1670 for f
in self
[UNIQUE_FILE_ID_FID
]:
1671 if f
.owner_id
== owner_id
:
1676 h
= FrameHeader(self
.tagHeader
);
1677 h
.id = UNIQUE_FILE_ID_FID
;
1678 self
.addFrame(UniqueFileIDFrame(h
, owner_id
= owner_id
, id = id));
1680 h
= FrameHeader(self
.tagHeader
);
1681 h
.id = UNIQUE_FILE_ID_FID
;
1682 self
.addFrame(UniqueFileIDFrame(h
, owner_id
= owner_id
, id = id));
1684 # If a comment frame with the same language and description exists then
1685 # the comment text is replaced, otherwise the frame is added.
1686 def setUserTextFrame(self
, txt
, description
, encoding
= None):
1687 assert(isinstance(txt
, unicode));
1688 assert(isinstance(description
, unicode));
1690 if self
[USERTEXT_FID
]:
1692 for f
in self
[USERTEXT_FID
]:
1693 if f
.description
== description
:
1696 f
.encoding
= encoding
;
1701 encoding
= DEFAULT_ENCODING
;
1702 h
= FrameHeader(self
.tagHeader
);
1703 h
.id = USERTEXT_FID
;
1704 self
.addFrame(UserTextFrame(h
, encoding
= encoding
,
1705 description
= description
,
1709 encoding
= DEFAULT_ENCODING
;
1710 h
= FrameHeader(self
.tagHeader
);
1711 h
.id = USERTEXT_FID
;
1712 self
.addFrame(UserTextFrame(h
, encoding
= encoding
,
1713 description
= description
,
1716 # This method removes all frames with the matching frame ID.
1717 # The number of frames removed is returned.
1718 # Note that calling this method with a key like "COMM" may remove more
1719 # frames then you really want.
1720 def removeFramesByID(self
, fid
):
1721 if not isinstance(fid
, str):
1722 raise FrameException("removeFramesByID only operates on frame IDs");
1726 while i
< len(self
):
1727 if self
[i
].header
.id == fid
:
1734 # Removes the frame at index. True is returned if the element was
1735 # removed, and false otherwise.
1736 def removeFrameByIndex(self
, index
):
1737 if not isinstance(index
, int):
1739 FrameException("removeFrameByIndex only operates on a frame index");
1741 del self
.frames
[key
];
1746 # Accepts both int (indexed access) and string keys (a valid frame Id).
1747 # A list of frames (commonly with only one element) is returned when the
1748 # FrameSet is accessed using frame IDs since some frames can appear
1749 # multiple times in a tag. To sum it all up htis method returns
1750 # string or None when indexed using an integer, and a 0 to N length
1751 # list of strings when indexed with a frame ID.
1753 # Throws IndexError and TypeError.
1754 def __getitem__(self
, key
):
1755 if isinstance(key
, int):
1756 if key
>= 0 and key
< len(self
):
1757 return list.__getitem
__(self
, key
);
1759 raise IndexError("FrameSet index out of range");
1760 elif isinstance(key
, str):
1763 if f
.header
.id == key
:
1767 raise TypeError("FrameSet key must be type int or string");
1770 def splitUnicode(data
, encoding
):
1771 if encoding
== LATIN1_ENCODING
or encoding
== UTF_8_ENCODING
or\
1772 encoding
== UTF_16BE_ENCODING
:
1773 return data
.split("\x00", 1);
1774 elif encoding
== UTF_16_ENCODING
:
1775 (d
, t
) = data
.split("\x00\x00\x00", 1);
1779 #######################################################################
1780 # Create and return the appropriate frame.
1782 def createFrame(frameHeader
, data
):
1786 if TEXT_FRAME_RX
.match(frameHeader
.id):
1787 if USERTEXT_FRAME_RX
.match(frameHeader
.id):
1788 f
= UserTextFrame(frameHeader
, data
= data
);
1790 if frameHeader
.id[:2] == "TD" or\
1791 frameHeader
.id == OBSOLETE_DATE_FID
or\
1792 frameHeader
.id == OBSOLETE_YEAR_FID
or \
1793 frameHeader
.id == OBSOLETE_ORIG_RELEASE_FID
:
1794 f
= DateFrame(frameHeader
, data
= data
);
1796 f
= TextFrame(frameHeader
, data
= data
);
1798 elif COMMENT_FRAME_RX
.match(frameHeader
.id):
1799 f
= CommentFrame(frameHeader
, data
= data
);
1801 elif URL_FRAME_RX
.match(frameHeader
.id):
1802 if USERURL_FRAME_RX
.match(frameHeader
.id):
1803 f
= UserURLFrame(frameHeader
, data
= data
);
1805 f
= URLFrame(frameHeader
, data
= data
);
1807 elif CDID_FRAME_RX
.match(frameHeader
.id):
1808 f
= MusicCDIdFrame(frameHeader
, data
= data
);
1810 elif IMAGE_FRAME_RX
.match(frameHeader
.id):
1811 f
= ImageFrame(frameHeader
, data
= data
);
1813 elif PLAYCOUNT_FRAME_RX
.match(frameHeader
.id):
1814 f
= PlayCountFrame(frameHeader
, data
= data
);
1815 # Unique file identifier
1816 elif UNIQUE_FILE_ID_FRAME_RX
.match(frameHeader
.id):
1817 f
= UniqueFileIDFrame(frameHeader
, data
= data
);
1820 f
= UnknownFrame(frameHeader
, data
);
1825 def map2_2FrameId(originalId
):
1827 if not TAGS2_2_TO_TAGS_2_3_AND_4
.has_key(originalId
): return originalId
1828 return TAGS2_2_TO_TAGS_2_3_AND_4
[originalId
]