Using FS mtime to reload non recursive cache.
[pyTivo.git] / eyeD3 / frames.py
blobe989be6ded205070af4bb44b38a8b126270a22d3
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;
23 from utils import *;
24 from binfuncs import *;
26 # Valid time stamp formats per ISO 8601 and used by time.strptime.
27 timeStampFormats = ["%Y",
28 "%Y-%m",
29 "%Y-%m-%d",
30 "%Y-%m-%dT%H",
31 "%Y-%m-%dT%H:%M",
32 "%Y-%m-%dT%H:%M:%S"];
34 ARTIST_FID = "TPE1";
35 BAND_FID = "TPE2";
36 CONDUCTOR_FID = "TPE3";
37 REMIXER_FID = "TPE4";
38 COMPOSER_FID = "TCOM";
39 ARTIST_FIDS = [ARTIST_FID, BAND_FID, CONDUCTOR_FID,
40 REMIXER_FID, COMPOSER_FID];
41 ALBUM_FID = "TALB";
42 TITLE_FID = "TIT2";
43 SUBTITLE_FID = "TIT3";
44 CONTENT_TITLE_FID = "TIT1";
45 TITLE_FIDS = [TITLE_FID, SUBTITLE_FID, CONTENT_TITLE_FID];
46 COMMENT_FID = "COMM";
47 GENRE_FID = "TCON";
48 TRACKNUM_FID = "TRCK";
49 DISCNUM_FID = "TPOS";
50 USERTEXT_FID = "TXXX";
51 CDID_FID = "MCDI";
52 IMAGE_FID = "APIC";
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,
64 URL_PUBLISHER_FID];
65 USERURL_FID = "WXXX";
66 PLAYCOUNT_FID = "PCNT";
67 UNIQUE_FILE_ID_FID = "UFID";
68 BPM_FID = "TBPM";
69 PUBLISHER_FID = "TPUB";
71 obsoleteFrames = {"EQUA": "Equalisation",
72 "IPLS": "Involved people list",
73 "RVAD": "Relative volume adjustment",
74 "TDAT": "Date",
75 "TORY": "Original release year",
76 "TRDA": "Recording dates",
77 "TYER": "Year"};
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,
87 OBSOLETE_DATE_FID];
89 frameDesc = { "AENC": "Audio encryption",
90 "APIC": "Attached picture",
91 "ASPI": "Audio seek point index",
93 "COMM": "Comments",
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)",
117 "RVRB": "Reverb",
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)",
126 "TCOM": "Composer",
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",
137 "TFLT": "File type",
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)",
144 "TLEN": "Length",
145 "TMCL": "Musician credits list",
146 "TMED": "Media type",
147 "TMOO": "Mood",
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",
159 "TPUB": "Publisher",
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",
181 "WPAY": "Payment",
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>
272 KNOWN_BAD_FRAMES = [
273 "\x00\x00MP",
274 "\x00MP3",
275 " MP3",
276 "MP3e",
277 "\x00MP",
278 " MP",
279 "MP3",
280 "COM ",
281 "TCP ", # iTunes
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";
295 def cleanNulls(s):
296 return "/".join([x for x in s.split('\x00') if x])
298 def id3EncodingToString(encoding):
299 if encoding == LATIN1_ENCODING:
300 return "latin_1";
301 elif encoding == UTF_8_ENCODING:
302 return "utf_8";
303 elif encoding == UTF_16_ENCODING:
304 return "utf_16";
305 elif encoding == UTF_16BE_ENCODING:
306 return "utf_16_be";
307 else:
308 if strictID3():
309 raise ValueError;
310 else:
311 return "latin_1";
313 ################################################################################
314 class FrameException(Exception):
315 '''Thrown by invalid frames'''
316 pass;
318 ################################################################################
319 class FrameHeader:
320 FRAME_HEADER_SIZE = 10;
321 # The tag header
322 majorVersion = DEFAULT_ID3_MAJOR_VERSION;
323 minorVersion = DEFAULT_ID3_MINOR_VERSION;
324 # The 4 character frame ID.
325 id = None;
326 # An array of 16 "bits"...
327 flags = NULL_FRAME_FLAGS;
328 # ...and the info they store.
329 tagAlter = 0;
330 fileAlter = 0;
331 readOnly = 0;
332 compressed = 0;
333 encrypted = 0;
334 grouped = 0;
335 unsync = 0;
336 dataLenIndicator = 0;
337 # The size of the data following this header.
338 dataSize = 0;
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.
342 TAG_ALTER = None;
343 FILE_ALTER = None;
344 READ_ONLY = None;
345 COMPRESSION = None;
346 ENCRYPTION = None;
347 GROUPING = None;
348 UNSYNC = None;
349 DATA_LEN = None;
351 # Constructor.
352 def __init__(self, tagHeader = None):
353 if tagHeader:
354 self.setVersion(tagHeader);
355 else:
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];
364 else:
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;
370 else:
371 self.FRAME_HEADER_SIZE = 10;
372 self.setBitMask();
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
382 pass;
383 elif (major == 2 and minor == 3):
384 self.TAG_ALTER = 0;
385 self.FILE_ALTER = 1;
386 self.READ_ONLY = 2;
387 self.COMPRESSION = 8;
388 self.ENCRYPTION = 9;
389 self.GROUPING = 10;
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.
393 self.UNSYNC = 14;
394 # And this is mapped to an used bit, so that 0 is returned.
395 self.DATA_LEN = 4;
396 elif (major == 2 and minor == 4) or \
397 (major == 1 and (minor == 0 or minor == 1)):
398 self.TAG_ALTER = 1;
399 self.FILE_ALTER = 2;
400 self.READ_ONLY = 3;
401 self.COMPRESSION = 12;
402 self.ENCRYPTION = 13;
403 self.GROUPING = 9;
404 self.UNSYNC = 14;
405 self.DATA_LEN = 15;
406 else:
407 raise ValueError("ID3 v" + str(major) + "." + str(minor) +\
408 " is not supported.");
410 def render(self, dataSize):
411 data = self.id;
413 if self.minorVersion == 3:
414 data += bin2bytes(dec2bin(dataSize, 32));
415 else:
416 data += bin2bytes(bin2synchsafe(dec2bin(dataSize, 32)));
418 self.setBitMask();
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);
432 return data;
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,
439 ord(frameId_22[0]),
440 ord(frameId_22[1]),
441 ord(frameId_22[2])));
442 self.id = frameId;
443 # dataSize corresponds to the size of the data segment after
444 # encryption, compression, and unsynchronization.
445 sz = f.read(3);
446 self.dataSize = bin2dec(bytes2bin(sz, 8));
447 TRACE_MSG("FrameHeader [data size]: %d (0x%X)" % (self.dataSize,
448 self.dataSize));
449 elif frameId == '\x00\x00\x00':
450 TRACE_MSG("FrameHeader: Null frame id found at byte " +\
451 str(f.tell()));
452 return 0;
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()));
457 return 0;
458 else:
459 raise FrameException("FrameHeader: Illegal Frame ID: " + frameId);
460 return 1;
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
465 # thrown.
466 def parse(self, f):
467 TRACE_MSG("FrameHeader [start byte]: %d (0x%X)" % (f.tell(),
468 f.tell()));
469 if self.minorVersion == 2:
470 return self.parse2_2(f)
472 frameId = f.read(4);
473 if self.isFrameIdValid(frameId):
474 TRACE_MSG("FrameHeader [id]: %s (0x%x%x%x%x)" % (frameId,
475 ord(frameId[0]),
476 ord(frameId[1]),
477 ord(frameId[2]),
478 ord(frameId[3])));
479 self.id = frameId;
480 # dataSize corresponds to the size of the data segment after
481 # encryption, compression, and unsynchronization.
482 sz = f.read(4);
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));
487 else:
488 self.dataSize = bin2dec(bytes2bin(sz, 7));
489 TRACE_MSG("FrameHeader [data size]: %d (0x%X)" % (self.dataSize,
490 self.dataSize));
492 # Frame flags.
493 flags = f.read(2);
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,
505 self.fileAlter,
506 self.readOnly,
507 self.compressed,
508 self.encrypted,
509 self.grouped,
510 self.unsync,
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 "
515 "length indicator");
517 elif frameId == '\x00\x00\x00\x00':
518 TRACE_MSG("FrameHeader: Null frame id found at byte " +\
519 str(f.tell()));
520 return 0;
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()));
525 return 0;
526 else:
527 raise FrameException("FrameHeader: Illegal Frame ID: " + frameId);
528 return 1;
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):
535 flags = [0] * 16;
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));
542 return data;
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)));
550 return data;
552 ################################################################################
553 class Frame:
554 header = None;
555 decompressedSize = 0;
556 groupId = 0;
557 encryptionMethod = 0;
558 dataLen = 0;
559 encoding = DEFAULT_ENCODING;
561 def __init__(self, frameHeader):
562 assert(isinstance(frameHeader, FrameHeader));
563 self.header = frameHeader;
565 def __str__(self):
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);
572 return data;
574 def deunsync(self, data):
575 data = deunsyncData(data);
576 return 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));
582 return 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));
588 return 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]));
603 data = data[4:];
604 TRACE_MSG("Decompressed Size: %d" % self.decompressedSize);
605 if self.header.encrypted:
606 self.encryptionMethod = bin2dec(bytes2bin(data[0]));
607 data = data[1:];
608 TRACE_MSG("Encryption Method: %d" % self.encryptionMethod);
609 if self.header.grouped:
610 self.groupId = bin2dec(bytes2bin(data[0]));
611 data = data[1:];
612 TRACE_MSG("Group ID: %d" % self.groupId);
613 else:
614 # 2.4: group(1), encrypted(1), dataLenIndicator(4,7)
615 if self.header.grouped:
616 self.groupId = bin2dec(bytes2bin(data[0]));
617 data = data[1:];
618 if self.header.encrypted:
619 self.encryptionMethod = bin2dec(bytes2bin(data[0]));
620 data = data[1:];
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));
625 data = data[4:];
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);
637 return data;
639 def assembleFrame (self, data):
640 formatFlagData = "";
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));
648 else:
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):
670 try:
671 return frameDesc[self.header.id];
672 except KeyError:
673 try:
674 return obsoleteFrames[self.header.id];
675 except KeyError:
676 return "UNKOWN FRAME";
678 def getTextDelim(self):
679 if self.encoding == UTF_16_ENCODING or \
680 self.encoding == UTF_16BE_ENCODING:
681 return "\x00\x00";
682 else:
683 return "\x00";
685 ################################################################################
686 class TextFrame(Frame):
687 text = u"";
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);
694 if data != None:
695 self._set(data, frameHeader);
696 return;
697 else:
698 assert(text != None and isinstance(text, unicode));
699 self.encoding = encoding;
700 self.text = text;
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));
712 try:
713 self.text = unicode(data[1:], id3EncodingToString(self.encoding));
714 if not strictID3():
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:];
720 if not strictID3():
721 self.text = cleanNulls(self.text)
722 else:
723 raise;
724 TRACE_MSG("TextFrame text: %s" % self.text);
726 def __unicode__(self):
727 return u'<%s (%s): %s>' % (self.getFrameDesc(), self.header.id,
728 self.text);
730 def render(self):
731 if self.header.minorVersion == 4 and self.header.id == "TSIZ":
732 TRACE_MSG("Dropping deprecated frame TSIZ");
733 return "";
734 data = self.encoding +\
735 self.text.encode(id3EncodingToString(self.encoding));
736 return self.assembleFrame(data);
738 ################################################################################
739 class DateFrame(TextFrame):
740 date = None;
741 date_str = u"";
743 def __init__(self, frameHeader, data = None, date_str = None,
744 encoding = DEFAULT_ENCODING):
745 if data != None:
746 TextFrame.__init__(self, frameHeader, data = data,
747 encoding = encoding);
748 self._set(data, frameHeader);
749 else:
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: " + \
759 self.header.id);
761 def setDate(self, d):
762 if not d:
763 self.date = None;
764 self.date_str = u"";
765 return;
767 for fmt in timeStampFormats:
768 try:
769 if isinstance(d, tuple):
770 self.date_str = unicode(time.strftime(fmt, d));
771 self.date = d;
772 else:
773 assert(isinstance(d, unicode));
774 # Witnessed oddball tags with NULL bytes (ozzy.tag from id3lib)
775 d = d.strip("\x00");
777 try:
778 self.date = time.strptime(d, fmt);
779 except TypeError, ex:
780 continue;
781 self.date_str = d;
782 break;
783 except ValueError:
784 self.date = None;
785 self.date_str = u"";
786 continue;
787 if strictID3() and not self.date:
788 raise FrameException("Invalid Date: " + str(d));
789 self.text = self.date_str;
791 def getDate(self):
792 return self.date_str;
794 def getYear(self):
795 if self.date:
796 return self.__padDateField(self.date[0], 4);
797 else:
798 return None;
800 def getMonth(self):
801 if self.date:
802 return self.__padDateField(self.date[1], 2);
803 else:
804 return None;
806 def getDay(self):
807 if self.date:
808 return self.__padDateField(self.date[2], 2);
809 else:
810 return None;
812 def getHour(self):
813 if self.date:
814 return self.__padDateField(self.date[3], 2);
815 else:
816 return None;
818 def getMinute(self):
819 if self.date:
820 return self.__padDateField(self.date[4], 2);
821 else:
822 return None;
824 def getSecond(self):
825 if self.date:
826 return self.__padDateField(self.date[5], 2);
827 else:
828 return None;
830 def __padDateField(self, f, sz):
831 fStr = str(f);
832 if len(fStr) == sz:
833 pass;
834 elif len(fStr) < sz:
835 fStr = ("0" * (sz - len(fStr))) + fStr;
836 else:
837 raise TagException("Invalid date field: " + fStr);
838 return fStr;
840 def render(self):
841 # Conversion crap
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);
859 return data;
862 ################################################################################
863 class UserTextFrame(TextFrame):
864 description = u"";
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):
870 if data != None:
871 TextFrame.__init__(self, frameHeader, data = data);
872 self._set(data, frameHeader);
873 else:
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):
883 assert(frameHeader);
884 if not USERTEXT_FRAME_RX.match(frameHeader.id):
885 raise FrameException("Invalid frame id for UserTextFrame: " +\
886 frameHeader.id);
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));
896 if not strictID3():
897 self.text = cleanNulls(self.text)
898 TRACE_MSG("UserTextFrame text: %s" % self.text);
900 def render(self):
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(),
909 self.header.id,
910 self.description, self.text);
912 ################################################################################
913 class URLFrame(Frame):
914 url = "";
916 # Data string format:
917 # url
918 def __init__(self, frameHeader, data = None, url = None):
919 Frame.__init__(self, frameHeader);
920 if data != None:
921 self._set(data, frameHeader);
922 else:
923 assert(url);
924 self.url = url;
926 # Data string format:
927 # url
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);
933 self.url = data;
934 if not strictID3():
935 self.url = cleanNulls(self.url)
937 def render(self):
938 data = str(self.url);
939 return self.assembleFrame(data);
941 def __str__(self):
942 return '<%s (%s): %s>' % (self.getFrameDesc(), self.header.id,
943 self.url);
945 ################################################################################
946 class UserURLFrame(URLFrame):
947 description = u"";
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);
954 if data:
955 self._set(data, frameHeader);
956 else:
957 assert(encoding);
958 assert(description and isinstance(description, unicode));
959 assert(url and isinstance(url, str));
960 self.encoding = encoding;
961 self.url = url;
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: " +\
970 frameHeader.id);
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);
979 self.url = u;
980 if not strictID3():
981 self.url = cleanNulls(self.url)
982 TRACE_MSG("UserURLFrame text: %s" % self.url);
984 def render(self):
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):
997 lang = "";
998 description = u"";
999 comment = u"";
1001 # Data string format:
1002 # encoding (one byte) + lang (three byte code) + description + "\x00" +
1003 # text
1004 def __init__(self, frameHeader, data = None, lang = "",
1005 description = u"", comment = u"", encoding = DEFAULT_ENCODING):
1006 Frame.__init__(self, frameHeader);
1007 if data != None:
1008 self._set(data, frameHeader);
1009 else:
1010 assert(isinstance(description, unicode));
1011 assert(isinstance(comment, unicode));
1012 assert(isinstance(lang, str));
1013 self.encoding = encoding;
1014 self.lang = lang;
1015 self.description = description;
1016 self.comment = comment;
1018 # Data string format:
1019 # encoding (one byte) + lang (three byte code) + description + "\x00" +
1020 # text
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: " +\
1025 frameHeader.id);
1027 data = self.disassembleFrame(data);
1028 self.encoding = data[0];
1029 TRACE_MSG("CommentFrame encoding: " + id3EncodingToString(self.encoding));
1030 try:
1031 self.lang = str(data[1:4]).strip("\x00");
1032 # Test ascii encoding
1033 temp_lang = unicode(self.lang, "ascii");
1034 if self.lang and \
1035 not re.compile("[A-Z][A-Z][A-Z]", re.IGNORECASE).match(self.lang):
1036 if strictID3():
1037 raise FrameException("[CommentFrame] Invalid language "\
1038 "code: %s" % self.lang);
1039 except UnicodeDecodeError, ex:
1040 if strictID3():
1041 raise FrameException("[CommentFrame] Invalid language code: "\
1042 "[%s] %s" % (ex.object, ex.reason));
1043 else:
1044 self.lang = "";
1045 try:
1046 (d, c) = splitUnicode(data[4:], self.encoding);
1047 self.description = unicode(d, id3EncodingToString(self.encoding));
1048 self.comment = unicode(c, id3EncodingToString(self.encoding));
1049 except ValueError:
1050 if strictID3():
1051 raise FrameException("Invalid comment; no description/comment");
1052 else:
1053 self.description = u"";
1054 self.comment = u"";
1055 if not strictID3():
1056 self.description = cleanNulls(self.description)
1057 self.comment = cleanNulls(self.comment)
1059 def render(self):
1060 lang = self.lang.encode("ascii");
1061 if len(lang) > 3:
1062 lang = lang[0:3];
1063 elif len(lang) < 3:
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
1078 # picture".
1079 class ImageFrame(Frame):
1080 mimeType = None;
1081 pictureType = None;
1082 description = u"";
1083 # Contains the image data when the mimetype is image type.
1084 # Otherwise it is None.
1085 imageData = None;
1086 # Contains a URL for the image when the mimetype is "-->" per the spec.
1087 # Otherwise it is None.
1088 imageURL = None;
1089 # Declared "picture types".
1090 OTHER = 0x00;
1091 ICON = 0x01; # 32x32 png only.
1092 OTHER_ICON = 0x02;
1093 FRONT_COVER = 0x03;
1094 BACK_COVER = 0x04;
1095 LEAFLET = 0x05;
1096 MEDIA = 0x06; # label side of cd, picture disc vinyl, etc.
1097 LEAD_ARTIST = 0x07;
1098 ARTIST = 0x08;
1099 CONDUCTOR = 0x09;
1100 BAND = 0x0A;
1101 COMPOSER = 0x0B;
1102 LYRICIST = 0x0C;
1103 RECORDING_LOCATION = 0x0D;
1104 DURING_RECORDING = 0x0E;
1105 DURING_PERFORMANCE = 0x0F;
1106 VIDEO = 0x10;
1107 BRIGHT_COLORED_FISH = 0x11; # There's always room for porno.
1108 ILLUSTRATION = 0x12;
1109 BAND_LOGO = 0x13;
1110 PUBLISHER_LOGO = 0x14;
1111 MIN_TYPE = OTHER;
1112 MAX_TYPE = PUBLISHER_LOGO;
1114 def __init__(self, frameHeader, data = None,
1115 description = u"",
1116 imageData = None, imageURL = None,
1117 pictureType = None, mimeType = None,
1118 encoding = DEFAULT_ENCODING):
1119 Frame.__init__(self, frameHeader);
1120 if data != None:
1121 self._set(data, frameHeader);
1122 else:
1123 assert(isinstance(description, unicode));
1124 self.description = description;
1125 self.encoding = encoding;
1126 assert(mimeType);
1127 self.mimeType = mimeType;
1128 assert(pictureType);
1129 self.pictureType = pictureType;
1130 if imageData:
1131 self.imageData = imageData;
1132 else:
1133 self.imageURL = imageURL;
1134 assert(self.imageData or self.imageURL);
1137 # Factory method
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.");
1142 # Load img
1143 fp = file(imgFile, "rb");
1144 imgData = fp.read();
1145 mt = mimetypes.guess_type(imgFile);
1146 if not mt[0]:
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">
1163 # Text encoding $xx
1164 # MIME type <text string> $00
1165 # Picture type $xx
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: " +\
1172 frameHeader.id);
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));
1181 # Mime type
1182 self.mimeType = "";
1183 if self.header.minorVersion != 2:
1184 ch = input.read(1);
1185 while ch != "\x00":
1186 self.mimeType += ch;
1187 ch = input.read(1);
1188 else:
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:
1200 if strictID3():
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.
1204 try:
1205 pt = int(chr(pt));
1206 except:
1207 pt = self.OTHER;
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();
1217 input.close();
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;
1228 else:
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.");
1239 if not name:
1240 name = self.getDefaultFileName();
1241 imageFile = os.path.join(path, name);
1243 f = file(imageFile, "wb");
1244 f.write(self.imageData);
1245 f.flush();
1246 f.close();
1247 def getDefaultFileName(self, suffix = ""):
1248 nameStr = self.picTypeToString(self.pictureType);
1249 if suffix:
1250 nameStr += suffix;
1251 nameStr = nameStr + "." + self.mimeType.split("/")[1];
1252 return nameStr;
1254 def render(self):
1255 data = self.encoding + self.mimeType + "\x00" +\
1256 bin2bytes(dec2bin(self.pictureType, 8)) +\
1257 self.description.encode(id3EncodingToString(self.encoding)) +\
1258 self.getTextDelim();
1259 if self.imageURL:
1260 data += self.imageURL.encode("ascii");
1261 else:
1262 data += self.imageData;
1263 return self.assembleFrame(data);
1265 def stringToPicType(s):
1266 if s == "OTHER":
1267 return ImageFrame.OTHER;
1268 elif s == "ICON":
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;
1278 elif s == "MEDIA":
1279 return ImageFrame.MEDIA;
1280 elif s == "LEAD_ARTIST":
1281 return ImageFrame.LEAD_ARTIST;
1282 elif s == "ARTIST":
1283 return ImageFrame.ARTIST;
1284 elif s == "CONDUCTOR":
1285 return ImageFrame.CONDUCTOR;
1286 elif s == "BAND":
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;
1298 elif s == "VIDEO":
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;
1308 else:
1309 raise FrameException("Invalid APIC picture type: %s" % s);
1310 stringToPicType = staticmethod(stringToPicType);
1312 def picTypeToString(t):
1313 if t == ImageFrame.OTHER:
1314 return "OTHER";
1315 elif t == ImageFrame.ICON:
1316 return "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:
1324 return "LEAFLET";
1325 elif t == ImageFrame.MEDIA:
1326 return "MEDIA";
1327 elif t == ImageFrame.LEAD_ARTIST:
1328 return "LEAD_ARTIST";
1329 elif t == ImageFrame.ARTIST:
1330 return "ARTIST";
1331 elif t == ImageFrame.CONDUCTOR:
1332 return "CONDUCTOR";
1333 elif t == ImageFrame.BAND:
1334 return "BAND";
1335 elif t == ImageFrame.COMPOSER:
1336 return "COMPOSER";
1337 elif t == ImageFrame.LYRICIST:
1338 return "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:
1346 return "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:
1352 return "BAND_LOGO";
1353 elif t == ImageFrame.PUBLISHER_LOGO:
1354 return "PUBLISHER_LOGO";
1355 else:
1356 raise FrameException("Invalid APIC picture type: %d" % t);
1357 picTypeToString = staticmethod(picTypeToString);
1359 class PlayCountFrame(Frame):
1360 count = None;
1362 def __init__(self, frameHeader, data = None, count = None):
1363 Frame.__init__(self, frameHeader);
1364 if data != None:
1365 self._set(data, frameHeader);
1366 else:
1367 assert(count != None and count >= 0);
1368 self.count = count;
1369 def _set(self, data, frameHeader):
1370 assert(frameHeader);
1371 assert(len(data) >= 4);
1372 self.count = long(bytes2dec(data));
1374 def render(self):
1375 data = dec2bytes(self.count, 32);
1376 return self.assembleFrame(data);
1378 class UniqueFileIDFrame(Frame):
1379 owner_id = "";
1380 id = "";
1382 def __init__(self, frameHeader, data = None, owner_id = None, id = None):
1383 Frame.__init__(self, frameHeader);
1384 if data != None:
1385 self._set(data, frameHeader);
1386 else:
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;
1390 self.id = id;
1392 def _set(self, data, frameHeader):
1393 assert(frameHeader);
1394 # Data format
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");
1404 def render(self):
1405 data = self.owner_id + "\x00" + self.id;
1406 return self.assembleFrame(data);
1408 ################################################################################
1409 class UnknownFrame(Frame):
1410 data = "";
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);
1419 self.data = data;
1421 def render(self):
1422 return self.assembleFrame(self.data)
1424 ################################################################################
1425 class MusicCDIdFrame(Frame):
1426 toc = "";
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);
1432 if 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: " +\
1439 frameHeader.id);
1440 data = self.disassembleFrame(data);
1441 self.toc = data;
1443 def render(self):
1444 data = self.toc;
1445 return self.assembleFrame(data);
1447 ################################################################################
1448 # A class for containing and managing ID3v2.Frame objects.
1449 class FrameSet(list):
1450 tagHeader = None;
1452 def __init__(self, tagHeader, l = None):
1453 self.tagHeader = tagHeader;
1454 if l:
1455 for f in l:
1456 if not isinstance(f, Frame):
1457 raise TypeError("Invalid type added to FrameSet: " +\
1458 f.__class__);
1459 self.append(f);
1461 # Setting a FrameSet instance like this 'fs = []' morphs the instance into
1462 # a list object.
1463 def clear(self):
1464 del self[0:];
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;
1471 paddingSize = 0;
1472 sizeLeft = tagHeader.tagSize;
1474 # Handle a tag-level unsync. Some frames may have their own unsync bit
1475 # set instead.
1476 tagData = f.read(sizeLeft);
1477 og_size = sizeLeft;
1478 if tagHeader.unsync:
1479 TRACE_MSG("Tag has unsync bit set");
1480 tagData = deunsyncData(tagData);
1481 sizeLeft = len(tagData);
1482 size_change = 0;
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);
1491 tagBuffer.seek(10);
1492 #tagBuffer.tell();
1494 while sizeLeft > 0:
1495 TRACE_MSG("sizeLeft: " + str(sizeLeft));
1496 if sizeLeft < (10 + 1):
1497 TRACE_MSG("FrameSet: Implied padding (sizeLeft < minFrameSize)");
1498 paddingSize = sizeLeft;
1499 break;
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;
1506 break;
1508 # Frame data.
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(),
1512 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);
1521 return paddingSize;
1523 # Returrns the size of the frame data.
1524 def getSize(self):
1525 sz = 0;
1526 for f in self:
1527 sz += len(f.render());
1528 return sz;
1530 def setTagHeader(self, tagHeader):
1531 self.tagHeader = tagHeader;
1532 for f in self:
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." %\
1559 fid);
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.
1573 # No multiples.
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
1592 # owner_id
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));
1601 self.append(frame);
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");
1614 if self[frameId]:
1615 curr = self[frameId][0];
1616 if encoding:
1617 curr.encoding = encoding;
1619 if isinstance(curr, DateFrame):
1620 curr.setDate(text);
1621 else:
1622 curr.text = text;
1623 else:
1624 h = FrameHeader(self.tagHeader);
1625 h.id = frameId;
1626 if not encoding:
1627 encoding = DEFAULT_ENCODING;
1628 if frameId in DATE_FIDS:
1629 self.addFrame(DateFrame(h, encoding = encoding, date_str = text));
1630 else:
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,
1636 encoding = None):
1637 assert(isinstance(comment, unicode) and isinstance(description, unicode));
1639 if self[COMMENT_FID]:
1640 found = 0;
1641 for f in self[COMMENT_FID]:
1642 if f.lang == lang and f.description == description:
1643 f.comment = comment;
1644 if encoding:
1645 f.encoding = encoding;
1646 found = 1;
1647 break;
1648 if not found:
1649 h = FrameHeader(self.tagHeader);
1650 h.id = COMMENT_FID;
1651 if not encoding:
1652 encoding = DEFAULT_ENCODING;
1653 self.addFrame(CommentFrame(h, encoding = encoding, lang = lang,
1654 description = description,
1655 comment = comment));
1656 else:
1657 if not encoding:
1658 encoding = DEFAULT_ENCODING;
1659 h = FrameHeader(self.tagHeader);
1660 h.id = COMMENT_FID;
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]:
1669 found = 0;
1670 for f in self[UNIQUE_FILE_ID_FID]:
1671 if f.owner_id == owner_id:
1672 f.id = id;
1673 found = 1;
1674 break;
1675 if not found:
1676 h = FrameHeader(self.tagHeader);
1677 h.id = UNIQUE_FILE_ID_FID;
1678 self.addFrame(UniqueFileIDFrame(h, owner_id = owner_id, id = id));
1679 else:
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]:
1691 found = 0;
1692 for f in self[USERTEXT_FID]:
1693 if f.description == description:
1694 f.text = txt;
1695 if encoding:
1696 f.encoding = encoding;
1697 found = 1;
1698 break;
1699 if not found:
1700 if not 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,
1706 text = txt));
1707 else:
1708 if not encoding:
1709 encoding = DEFAULT_ENCODING;
1710 h = FrameHeader(self.tagHeader);
1711 h.id = USERTEXT_FID;
1712 self.addFrame(UserTextFrame(h, encoding = encoding,
1713 description = description,
1714 text = txt));
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");
1724 i = 0;
1725 count = 0;
1726 while i < len(self):
1727 if self[i].header.id == fid:
1728 del self[i];
1729 count += 1;
1730 else:
1731 i += 1;
1732 return count;
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):
1738 raise\
1739 FrameException("removeFrameByIndex only operates on a frame index");
1740 try:
1741 del self.frames[key];
1742 return 1;
1743 except:
1744 return 0;
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);
1758 else:
1759 raise IndexError("FrameSet index out of range");
1760 elif isinstance(key, str):
1761 retList = list();
1762 for f in self:
1763 if f.header.id == key:
1764 retList.append(f);
1765 return retList;
1766 else:
1767 raise TypeError("FrameSet key must be type int or string");
1769 # Mmmmm! Cheesy!
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);
1776 d += "\x00";
1777 return (d, t);
1779 #######################################################################
1780 # Create and return the appropriate frame.
1781 # Exceptions: ....
1782 def createFrame(frameHeader, data):
1783 f = None;
1785 # Text Frames
1786 if TEXT_FRAME_RX.match(frameHeader.id):
1787 if USERTEXT_FRAME_RX.match(frameHeader.id):
1788 f = UserTextFrame(frameHeader, data = data);
1789 else:
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);
1795 else:
1796 f = TextFrame(frameHeader, data = data);
1797 # Comment Frames.
1798 elif COMMENT_FRAME_RX.match(frameHeader.id):
1799 f = CommentFrame(frameHeader, data = data);
1800 # URL Frames.
1801 elif URL_FRAME_RX.match(frameHeader.id):
1802 if USERURL_FRAME_RX.match(frameHeader.id):
1803 f = UserURLFrame(frameHeader, data = data);
1804 else:
1805 f = URLFrame(frameHeader, data = data);
1806 # CD Id frame.
1807 elif CDID_FRAME_RX.match(frameHeader.id):
1808 f = MusicCDIdFrame(frameHeader, data = data);
1809 # Attached picture
1810 elif IMAGE_FRAME_RX.match(frameHeader.id):
1811 f = ImageFrame(frameHeader, data = data);
1812 # Play count
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);
1819 if f == None:
1820 f = UnknownFrame(frameHeader, data);
1822 return f;
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]