not needed with 20Mi
[pyTivo/wgw.git] / eyeD3 / frames.py
blobe5155722b4ffe796f8f0be75472983acbfee6aef
1 ################################################################################
2 # Copyright (C) 2002-2007 Travis Shirk <travis@pobox.com>
3 # Copyright (C) 2005 Michael Urman
4 # - Sync-safe encoding/decoding algorithms
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 LYRICS_FID = "USLT";
48 GENRE_FID = "TCON";
49 TRACKNUM_FID = "TRCK";
50 DISCNUM_FID = "TPOS";
51 USERTEXT_FID = "TXXX";
52 CDID_FID = "MCDI";
53 IMAGE_FID = "APIC";
54 OBJECT_FID = "GEOB";
55 URL_COMMERCIAL_FID = "WCOM";
56 URL_COPYRIGHT_FID = "WCOP";
57 URL_AUDIOFILE_FID = "WOAF";
58 URL_ARTIST_FID = "WOAR";
59 URL_AUDIOSRC_FID = "WOAS";
60 URL_INET_RADIO_FID = "WORS";
61 URL_PAYMENT_FID = "WPAY";
62 URL_PUBLISHER_FID = "WPUB";
63 URL_FIDS = [URL_COMMERCIAL_FID, URL_COPYRIGHT_FID,
64 URL_AUDIOFILE_FID, URL_ARTIST_FID, URL_AUDIOSRC_FID,
65 URL_INET_RADIO_FID, URL_PAYMENT_FID,
66 URL_PUBLISHER_FID];
67 USERURL_FID = "WXXX";
68 PLAYCOUNT_FID = "PCNT";
69 UNIQUE_FILE_ID_FID = "UFID";
70 BPM_FID = "TBPM";
71 PUBLISHER_FID = "TPUB";
73 obsoleteFrames = {"EQUA": "Equalisation",
74 "IPLS": "Involved people list",
75 "RVAD": "Relative volume adjustment",
76 "TDAT": "Date",
77 "TORY": "Original release year",
78 "TRDA": "Recording dates",
79 "TYER": "Year"};
80 # Both of these are "coerced" into a v2.4 TDRC frame when read, and
81 # recreated when saving v2.3.
82 OBSOLETE_DATE_FID = "TDAT";
83 OBSOLETE_YEAR_FID = "TYER";
84 OBSOLETE_TIME_FID = "TIME";
85 OBSOLETE_ORIG_RELEASE_FID = "TORY";
86 OBSOLETE_RECORDING_DATE_FID = "TRDA";
88 DATE_FIDS = ["TDRL", "TDOR", "TDRC", OBSOLETE_YEAR_FID,
89 OBSOLETE_DATE_FID];
91 frameDesc = { "AENC": "Audio encryption",
92 "APIC": "Attached picture",
93 "ASPI": "Audio seek point index",
95 "COMM": "Comments",
96 "COMR": "Commercial frame",
98 "ENCR": "Encryption method registration",
99 "EQU2": "Equalisation (2)",
100 "ETCO": "Event timing codes",
102 "GEOB": "General encapsulated object",
103 "GRID": "Group identification registration",
105 "LINK": "Linked information",
107 "MCDI": "Music CD identifier",
108 "MLLT": "MPEG location lookup table",
110 "OWNE": "Ownership frame",
112 "PRIV": "Private frame",
113 "PCNT": "Play counter",
114 "POPM": "Popularimeter",
115 "POSS": "Position synchronisation frame",
117 "RBUF": "Recommended buffer size",
118 "RVA2": "Relative volume adjustment (2)",
119 "RVRB": "Reverb",
121 "SEEK": "Seek frame",
122 "SIGN": "Signature frame",
123 "SYLT": "Synchronised lyric/text",
124 "SYTC": "Synchronised tempo codes",
126 "TALB": "Album/Movie/Show title",
127 "TBPM": "BPM (beats per minute)",
128 "TCOM": "Composer",
129 "TCON": "Content type",
130 "TCOP": "Copyright message",
131 "TDEN": "Encoding time",
132 "TDLY": "Playlist delay",
133 "TDOR": "Original release time",
134 "TDRC": "Recording time",
135 "TDRL": "Release time",
136 "TDTG": "Tagging time",
137 "TENC": "Encoded by",
138 "TEXT": "Lyricist/Text writer",
139 "TFLT": "File type",
140 "TIPL": "Involved people list",
141 "TIT1": "Content group description",
142 "TIT2": "Title/songname/content description",
143 "TIT3": "Subtitle/Description refinement",
144 "TKEY": "Initial key",
145 "TLAN": "Language(s)",
146 "TLEN": "Length",
147 "TMCL": "Musician credits list",
148 "TMED": "Media type",
149 "TMOO": "Mood",
150 "TOAL": "Original album/movie/show title",
151 "TOFN": "Original filename",
152 "TOLY": "Original lyricist(s)/text writer(s)",
153 "TOPE": "Original artist(s)/performer(s)",
154 "TOWN": "File owner/licensee",
155 "TPE1": "Lead performer(s)/Soloist(s)",
156 "TPE2": "Band/orchestra/accompaniment",
157 "TPE3": "Conductor/performer refinement",
158 "TPE4": "Interpreted, remixed, or otherwise modified by",
159 "TPOS": "Part of a set",
160 "TPRO": "Produced notice",
161 "TPUB": "Publisher",
162 "TRCK": "Track number/Position in set",
163 "TRSN": "Internet radio station name",
164 "TRSO": "Internet radio station owner",
165 "TSOA": "Album sort order",
166 "TSOP": "Performer sort order",
167 "TSOT": "Title sort order",
168 "TSRC": "ISRC (international standard recording code)",
169 "TSSE": "Software/Hardware and settings used for encoding",
170 "TSST": "Set subtitle",
171 "TXXX": "User defined text information frame",
173 "UFID": "Unique file identifier",
174 "USER": "Terms of use",
175 "USLT": "Unsynchronised lyric/text transcription",
177 "WCOM": "Commercial information",
178 "WCOP": "Copyright/Legal information",
179 "WOAF": "Official audio file webpage",
180 "WOAR": "Official artist/performer webpage",
181 "WOAS": "Official audio source webpage",
182 "WORS": "Official Internet radio station homepage",
183 "WPAY": "Payment",
184 "WPUB": "Publishers official webpage",
185 "WXXX": "User defined URL link frame" };
188 # mapping of 2.2 frames to 2.3/2.4
189 TAGS2_2_TO_TAGS_2_3_AND_4 = {
190 "TT1" : "TIT1", # CONTENTGROUP content group description
191 "TT2" : "TIT2", # TITLE title/songname/content description
192 "TT3" : "TIT3", # SUBTITLE subtitle/description refinement
193 "TP1" : "TPE1", # ARTIST lead performer(s)/soloist(s)
194 "TP2" : "TPE2", # BAND band/orchestra/accompaniment
195 "TP3" : "TPE3", # CONDUCTOR conductor/performer refinement
196 "TP4" : "TPE4", # MIXARTIST interpreted, remixed, modified by
197 "TCM" : "TCOM", # COMPOSER composer
198 "TXT" : "TEXT", # LYRICIST lyricist/text writer
199 "TLA" : "TLAN", # LANGUAGE language(s)
200 "TCO" : "TCON", # CONTENTTYPE content type
201 "TAL" : "TALB", # ALBUM album/movie/show title
202 "TRK" : "TRCK", # TRACKNUM track number/position in set
203 "TPA" : "TPOS", # PARTINSET part of set
204 "TRC" : "TSRC", # ISRC international standard recording code
205 "TDA" : "TDAT", # DATE date
206 "TYE" : "TYER", # YEAR year
207 "TIM" : "TIME", # TIME time
208 "TRD" : "TRDA", # RECORDINGDATES recording dates
209 "TOR" : "TORY", # ORIGYEAR original release year
210 "TBP" : "TBPM", # BPM beats per minute
211 "TMT" : "TMED", # MEDIATYPE media type
212 "TFT" : "TFLT", # FILETYPE file type
213 "TCR" : "TCOP", # COPYRIGHT copyright message
214 "TPB" : "TPUB", # PUBLISHER publisher
215 "TEN" : "TENC", # ENCODEDBY encoded by
216 "TSS" : "TSSE", # ENCODERSETTINGS software/hardware + settings for encoding
217 "TLE" : "TLEN", # SONGLEN length (ms)
218 "TSI" : "TSIZ", # SIZE size (bytes)
219 "TDY" : "TDLY", # PLAYLISTDELAY playlist delay
220 "TKE" : "TKEY", # INITIALKEY initial key
221 "TOT" : "TOAL", # ORIGALBUM original album/movie/show title
222 "TOF" : "TOFN", # ORIGFILENAME original filename
223 "TOA" : "TOPE", # ORIGARTIST original artist(s)/performer(s)
224 "TOL" : "TOLY", # ORIGLYRICIST original lyricist(s)/text writer(s)
225 "TXX" : "TXXX", # USERTEXT user defined text information frame
226 "WAF" : "WOAF", # WWWAUDIOFILE official audio file webpage
227 "WAR" : "WOAR", # WWWARTIST official artist/performer webpage
228 "WAS" : "WOAS", # WWWAUDIOSOURCE official audion source webpage
229 "WCM" : "WCOM", # WWWCOMMERCIALINFO commercial information
230 "WCP" : "WCOP", # WWWCOPYRIGHT copyright/legal information
231 "WPB" : "WPUB", # WWWPUBLISHER publishers official webpage
232 "WXX" : "WXXX", # WWWUSER user defined URL link frame
233 "IPL" : "IPLS", # INVOLVEDPEOPLE involved people list
234 "ULT" : "USLT", # UNSYNCEDLYRICS unsynchronised lyrics/text transcription
235 "COM" : "COMM", # COMMENT comments
236 "UFI" : "UFID", # UNIQUEFILEID unique file identifier
237 "MCI" : "MCDI", # CDID music CD identifier
238 "ETC" : "ETCO", # EVENTTIMING event timing codes
239 "MLL" : "MLLT", # MPEGLOOKUP MPEG location lookup table
240 "STC" : "SYTC", # SYNCEDTEMPO synchronised tempo codes
241 "SLT" : "SYLT", # SYNCEDLYRICS synchronised lyrics/text
242 "RVA" : "RVAD", # VOLUMEADJ relative volume adjustment
243 "EQU" : "EQUA", # EQUALIZATION equalization
244 "REV" : "RVRB", # REVERB reverb
245 "PIC" : "APIC", # PICTURE attached picture
246 "GEO" : "GEOB", # GENERALOBJECT general encapsulated object
247 "CNT" : "PCNT", # PLAYCOUNTER play counter
248 "POP" : "POPM", # POPULARIMETER popularimeter
249 "BUF" : "RBUF", # BUFFERSIZE recommended buffer size
250 "CRA" : "AENC", # AUDIOCRYPTO audio encryption
251 "LNK" : "LINK", # LINKEDINFO linked information
252 # Extension workarounds i.e., ignore them
253 "TCP" : "TCP ", # iTunes "extension" for compilation marking
254 "CM1" : "CM1 " # Seems to be some script kiddie tagging the tag.
255 # For example, [rH] join #rH on efnet [rH]
259 NULL_FRAME_FLAGS = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
261 TEXT_FRAME_RX = re.compile("^T[A-Z0-9][A-Z0-9][A-Z0-9]$");
262 USERTEXT_FRAME_RX = re.compile("^" + USERTEXT_FID + "$");
263 URL_FRAME_RX = re.compile("^W[A-Z0-9][A-Z0-9][A-Z0-9]$");
264 USERURL_FRAME_RX = re.compile("^" + USERURL_FID + "$");
265 COMMENT_FRAME_RX = re.compile("^" + COMMENT_FID + "$");
266 LYRICS_FRAME_RX = re.compile("^" + LYRICS_FID + "$");
267 CDID_FRAME_RX = re.compile("^" + CDID_FID + "$");
268 IMAGE_FRAME_RX = re.compile("^" + IMAGE_FID + "$");
269 OBJECT_FRAME_RX = re.compile("^" + OBJECT_FID + "$");
270 PLAYCOUNT_FRAME_RX = re.compile("^" + PLAYCOUNT_FID + "$");
271 UNIQUE_FILE_ID_FRAME_RX = re.compile("^" + UNIQUE_FILE_ID_FID + "$");
273 # MP3ext causes illegal frames to be inserted, which must be ignored.
274 # Copied from http://shell.lab49.com/~vivake/python/MP3Info.py
275 # Henning Kiel <henning.kiel@rwth-aachen.de>
276 KNOWN_BAD_FRAMES = [
277 "\x00\x00MP",
278 "\x00MP3",
279 " MP3",
280 "MP3e",
281 "\x00MP",
282 " MP",
283 "MP3",
284 "COM ",
285 "TCP ", # iTunes
286 "CM1 " # Script kiddie
289 LATIN1_ENCODING = "\x00";
290 UTF_16_ENCODING = "\x01";
291 UTF_16BE_ENCODING = "\x02";
292 UTF_8_ENCODING = "\x03";
294 DEFAULT_ENCODING = LATIN1_ENCODING;
295 DEFAULT_ID3_MAJOR_VERSION = 2;
296 DEFAULT_ID3_MINOR_VERSION = 4;
297 DEFAULT_LANG = "eng";
299 def cleanNulls(s):
300 return "/".join([x for x in s.split('\x00') if x])
302 def id3EncodingToString(encoding):
303 if encoding == LATIN1_ENCODING:
304 return "latin_1";
305 elif encoding == UTF_8_ENCODING:
306 return "utf_8";
307 elif encoding == UTF_16_ENCODING:
308 return "utf_16";
309 elif encoding == UTF_16BE_ENCODING:
310 return "utf_16_be";
311 else:
312 if strictID3():
313 raise ValueError;
314 else:
315 return "latin_1";
317 ################################################################################
318 class FrameException(Exception):
319 '''Thrown by invalid frames'''
320 pass;
322 ################################################################################
323 class FrameHeader:
324 FRAME_HEADER_SIZE = 10;
325 # The tag header
326 majorVersion = DEFAULT_ID3_MAJOR_VERSION;
327 minorVersion = DEFAULT_ID3_MINOR_VERSION;
328 # The 4 character frame ID.
329 id = None;
330 # An array of 16 "bits"...
331 flags = NULL_FRAME_FLAGS;
332 # ...and the info they store.
333 tagAlter = 0;
334 fileAlter = 0;
335 readOnly = 0;
336 compressed = 0;
337 encrypted = 0;
338 grouped = 0;
339 unsync = 0;
340 dataLenIndicator = 0;
341 # The size of the data following this header.
342 dataSize = 0;
344 # 2.4 not only added flag bits, but also reordered the previously defined
345 # flags. So these are mapped once we know the version.
346 TAG_ALTER = None;
347 FILE_ALTER = None;
348 READ_ONLY = None;
349 COMPRESSION = None;
350 ENCRYPTION = None;
351 GROUPING = None;
352 UNSYNC = None;
353 DATA_LEN = None;
355 # Constructor.
356 def __init__(self, tagHeader = None):
357 if tagHeader:
358 self.setVersion(tagHeader);
359 else:
360 self.setVersion([DEFAULT_ID3_MAJOR_VERSION,
361 DEFAULT_ID3_MINOR_VERSION]);
363 def setVersion(self, tagHeader):
364 # A slight hack to make the default ctor work.
365 if isinstance(tagHeader, list):
366 self.majorVersion = tagHeader[0];
367 self.minorVersion = tagHeader[1];
368 else:
369 self.majorVersion = tagHeader.majorVersion;
370 self.minorVersion = tagHeader.minorVersion;
371 # Correctly set size of header
372 if self.minorVersion == 2:
373 self.FRAME_HEADER_SIZE = 6;
374 else:
375 self.FRAME_HEADER_SIZE = 10;
376 self.setBitMask();
378 def setBitMask(self):
379 major = self.majorVersion;
380 minor = self.minorVersion;
382 # 1.x tags are converted to 2.4 frames internally. These frames are
383 # created with frame flags \x00.
384 if (major == 2 and minor == 2):
385 # no flags for 2.2 frames
386 pass;
387 elif (major == 2 and minor == 3):
388 self.TAG_ALTER = 0;
389 self.FILE_ALTER = 1;
390 self.READ_ONLY = 2;
391 self.COMPRESSION = 8;
392 self.ENCRYPTION = 9;
393 self.GROUPING = 10;
394 # This is not really in 2.3 frame header flags, but there is
395 # a "global" unsync bit in the tag header and that is written here
396 # so access to the tag header is not required.
397 self.UNSYNC = 14;
398 # And this is mapped to an used bit, so that 0 is returned.
399 self.DATA_LEN = 4;
400 elif (major == 2 and minor == 4) or \
401 (major == 1 and (minor == 0 or minor == 1)):
402 self.TAG_ALTER = 1;
403 self.FILE_ALTER = 2;
404 self.READ_ONLY = 3;
405 self.COMPRESSION = 12;
406 self.ENCRYPTION = 13;
407 self.GROUPING = 9;
408 self.UNSYNC = 14;
409 self.DATA_LEN = 15;
410 else:
411 raise ValueError("ID3 v" + str(major) + "." + str(minor) +\
412 " is not supported.");
414 def render(self, dataSize):
415 data = self.id;
417 if self.minorVersion == 3:
418 data += bin2bytes(dec2bin(dataSize, 32));
419 else:
420 data += bin2bytes(bin2synchsafe(dec2bin(dataSize, 32)));
422 self.setBitMask();
423 self.flags = NULL_FRAME_FLAGS;
424 self.flags[self.TAG_ALTER] = self.tagAlter;
425 self.flags[self.FILE_ALTER] = self.fileAlter;
426 self.flags[self.READ_ONLY] = self.readOnly;
427 self.flags[self.COMPRESSION] = self.compressed;
428 self.flags[self.COMPRESSION] = self.compressed;
429 self.flags[self.ENCRYPTION] = self.encrypted;
430 self.flags[self.GROUPING] = self.grouped;
431 self.flags[self.UNSYNC] = self.unsync;
432 self.flags[self.DATA_LEN] = self.dataLenIndicator;
434 data += bin2bytes(self.flags);
436 return data;
438 def parse2_2(self, f):
439 frameId_22 = f.read(3);
440 frameId = map2_2FrameId(frameId_22);
441 if self.isFrameIdValid(frameId):
442 TRACE_MSG("FrameHeader [id]: %s (0x%x%x%x)" % (frameId_22,
443 ord(frameId_22[0]),
444 ord(frameId_22[1]),
445 ord(frameId_22[2])));
446 self.id = frameId;
447 # dataSize corresponds to the size of the data segment after
448 # encryption, compression, and unsynchronization.
449 sz = f.read(3);
450 self.dataSize = bin2dec(bytes2bin(sz, 8));
451 TRACE_MSG("FrameHeader [data size]: %d (0x%X)" % (self.dataSize,
452 self.dataSize));
453 return True
454 elif frameId == '\x00\x00\x00':
455 TRACE_MSG("FrameHeader: Null frame id found at byte " +\
456 str(f.tell()));
457 elif not strictID3() and frameId in KNOWN_BAD_FRAMES:
458 TRACE_MSG("FrameHeader: Illegal but known frame found; "\
459 "Happily ignoring" + str(f.tell()));
460 elif strictID3():
461 raise FrameException("FrameHeader: Illegal Frame ID: " + frameId);
463 return False
466 # Returns 1 on success and 0 when a null tag (marking the beginning of
467 # padding). In the case of an invalid frame header, a FrameException is
468 # thrown.
469 def parse(self, f):
470 TRACE_MSG("FrameHeader [start byte]: %d (0x%X)" % (f.tell(),
471 f.tell()));
472 if self.minorVersion == 2:
473 return self.parse2_2(f)
475 frameId = f.read(4);
476 if self.isFrameIdValid(frameId):
477 TRACE_MSG("FrameHeader [id]: %s (0x%x%x%x%x)" % (frameId,
478 ord(frameId[0]),
479 ord(frameId[1]),
480 ord(frameId[2]),
481 ord(frameId[3])));
482 self.id = frameId;
483 # dataSize corresponds to the size of the data segment after
484 # encryption, compression, and unsynchronization.
485 sz = f.read(4);
486 # In ID3 v2.4 this value became a synch-safe integer, meaning only
487 # the low 7 bits are used per byte.
488 if self.minorVersion == 3:
489 self.dataSize = bin2dec(bytes2bin(sz, 8));
490 else:
491 self.dataSize = bin2dec(bytes2bin(sz, 7));
492 TRACE_MSG("FrameHeader [data size]: %d (0x%X)" % (self.dataSize,
493 self.dataSize));
495 # Frame flags.
496 flags = f.read(2);
497 self.flags = bytes2bin(flags);
498 self.tagAlter = self.flags[self.TAG_ALTER];
499 self.fileAlter = self.flags[self.FILE_ALTER];
500 self.readOnly = self.flags[self.READ_ONLY];
501 self.compressed = self.flags[self.COMPRESSION];
502 self.encrypted = self.flags[self.ENCRYPTION];
503 self.grouped = self.flags[self.GROUPING];
504 self.unsync = self.flags[self.UNSYNC];
505 self.dataLenIndicator = self.flags[self.DATA_LEN];
506 TRACE_MSG("FrameHeader [flags]: ta(%d) fa(%d) ro(%d) co(%d) "\
507 "en(%d) gr(%d) un(%d) dl(%d)" % (self.tagAlter,
508 self.fileAlter,
509 self.readOnly,
510 self.compressed,
511 self.encrypted,
512 self.grouped,
513 self.unsync,
514 self.dataLenIndicator));
515 if self.minorVersion >= 4 and self.compressed and \
516 not self.dataLenIndicator:
517 raise FrameException("Invalid frame; compressed with no data "
518 "length indicator");
520 return True
521 elif frameId == '\x00\x00\x00\x00':
522 TRACE_MSG("FrameHeader: Null frame id found at byte " +\
523 str(f.tell()));
524 elif not strictID3() and frameId in KNOWN_BAD_FRAMES:
525 TRACE_MSG("FrameHeader: Illegal but known "\
526 "(possibly created by the shitty mp3ext) frame found; "\
527 "Happily ignoring!" + str(f.tell()));
528 elif strictID3():
529 raise FrameException("FrameHeader: Illegal Frame ID: " + frameId);
530 return False
533 def isFrameIdValid(self, id):
534 return re.compile(r"^[A-Z0-9][A-Z0-9][A-Z0-9][A-Z0-9]$").match(id);
536 def clearFlags(self):
537 flags = [0] * 16;
539 ################################################################################
540 def unsyncData(data):
541 output = []
542 safe = True
543 for val in data:
544 if safe:
545 output.append(val)
546 if val == '\xff':
547 safe = False
548 elif val == '\x00' or val >= '\xe0':
549 output.append('\x00')
550 output.append(val)
551 safe = (val != '\xff')
552 else:
553 output.append(val)
554 safe = True
555 if not safe:
556 output.append('\x00')
557 return ''.join(output)
559 def deunsyncData(data):
560 output = []
561 safe = True
562 for val in data:
563 if safe:
564 output.append(val)
565 safe = (val != '\xff')
566 else:
567 if val != '\x00':
568 output.append(val)
569 safe = True
570 return ''.join(output)
573 ################################################################################
574 class Frame:
576 def __init__(self, frameHeader, unsync_default):
577 self.header = None
578 self.decompressedSize = 0
579 self.groupId = 0
580 self.encryptionMethod = 0
581 self.dataLen = 0
582 self.encoding = DEFAULT_ENCODING
583 self.header = frameHeader
584 self.unsync_default = unsync_default
586 def __str__(self):
587 desc = self.getFrameDesc();
588 return '<%s Frame (%s)>' % (desc, self.header.id);
590 def unsync(self, data):
591 data = unsyncData(data)
592 return data
594 def deunsync(self, data):
595 data = deunsyncData(data)
596 return data
598 def decompress(self, data):
599 TRACE_MSG("before decompression: %d bytes" % len(data));
600 data = zlib.decompress(data, 15, self.decompressedSize);
601 TRACE_MSG("after decompression: %d bytes" % len(data));
602 return data;
604 def compress(self, data):
605 TRACE_MSG("before compression: %d bytes" % len(data));
606 data = zlib.compress(data);
607 TRACE_MSG("after compression: %d bytes" % len(data));
608 return data;
610 def decrypt(self, data):
611 raise FrameException("Encryption not supported");
613 def encrypt(self, data):
614 raise FrameException("Encryption not supported");
616 def disassembleFrame(self, data):
617 # Format flags in the frame header may add extra data to the
618 # beginning of this data.
619 if self.header.minorVersion <= 3:
620 # 2.3: compression(4), encryption(1), group(1)
621 if self.header.compressed:
622 self.decompressedSize = bin2dec(bytes2bin(data[:4]));
623 data = data[4:];
624 TRACE_MSG("Decompressed Size: %d" % self.decompressedSize);
625 if self.header.encrypted:
626 self.encryptionMethod = bin2dec(bytes2bin(data[0]));
627 data = data[1:];
628 TRACE_MSG("Encryption Method: %d" % self.encryptionMethod);
629 if self.header.grouped:
630 self.groupId = bin2dec(bytes2bin(data[0]));
631 data = data[1:];
632 TRACE_MSG("Group ID: %d" % self.groupId);
633 else:
634 # 2.4: group(1), encrypted(1), dataLenIndicator(4,7)
635 if self.header.grouped:
636 self.groupId = bin2dec(bytes2bin(data[0]));
637 data = data[1:];
638 if self.header.encrypted:
639 self.encryptionMethod = bin2dec(bytes2bin(data[0]));
640 data = data[1:];
641 TRACE_MSG("Encryption Method: %d" % self.encryptionMethod);
642 TRACE_MSG("Group ID: %d" % self.groupId);
643 if self.header.dataLenIndicator:
644 self.dataLen = bin2dec(bytes2bin(data[:4], 7));
645 data = data[4:];
646 TRACE_MSG("Data Length: %d" % self.dataLen);
647 if self.header.compressed:
648 self.decompressedSize = self.dataLen;
649 TRACE_MSG("Decompressed Size: %d" % self.decompressedSize);
651 if self.header.unsync or self.unsync_default:
652 data = self.deunsync(data)
653 if self.header.encrypted:
654 data = self.decrypt(data);
655 if self.header.compressed:
656 data = self.decompress(data);
657 return data;
659 def assembleFrame (self, data):
660 formatFlagData = "";
661 if self.header.minorVersion == 3:
662 if self.header.compressed:
663 formatFlagData += bin2bytes(dec2bin(len(data), 32));
664 if self.header.encrypted:
665 formatFlagData += bin2bytes(dec2bin(self.encryptionMethod, 8));
666 if self.header.grouped:
667 formatFlagData += bin2bytes(dec2bin(self.groupId, 8));
668 else:
669 if self.header.grouped:
670 formatFlagData += bin2bytes(dec2bin(self.groupId, 8));
671 if self.header.encrypted:
672 formatFlagData += bin2bytes(dec2bin(self.encryptionMethod, 8));
673 if self.header.compressed or self.header.dataLenIndicator:
674 # Just in case, not sure about this?
675 self.header.dataLenIndicator = 1;
676 formatFlagData += bin2bytes(dec2bin(len(data), 32));
678 if self.header.compressed:
679 data = self.compress(data);
680 if self.header.encrypted:
681 data = self.encrypt(data);
682 if self.header.unsync or self.unsync_default:
683 data = self.unsync(data)
685 data = formatFlagData + data;
686 return self.header.render(len(data)) + data;
688 def getFrameDesc(self):
689 try:
690 return frameDesc[self.header.id];
691 except KeyError:
692 try:
693 return obsoleteFrames[self.header.id];
694 except KeyError:
695 return "UNKOWN FRAME";
697 def getTextDelim(self):
698 if self.encoding == UTF_16_ENCODING or \
699 self.encoding == UTF_16BE_ENCODING:
700 return "\x00\x00";
701 else:
702 return "\x00";
704 ################################################################################
705 class TextFrame(Frame):
706 text = u"";
708 # Data string format:
709 # encoding (one byte) + text
710 def __init__(self, frameHeader, data=None, text=u"",
711 encoding=DEFAULT_ENCODING, unsync_default=False):
712 Frame.__init__(self, frameHeader, unsync_default)
713 if data != None:
714 self._set(data, frameHeader);
715 return;
716 else:
717 assert(text != None and isinstance(text, unicode));
718 self.encoding = encoding;
719 self.text = text;
721 # Data string format:
722 # encoding (one byte) + text;
723 def _set(self, data, frameHeader):
724 fid = frameHeader.id;
725 if not TEXT_FRAME_RX.match(fid) or USERTEXT_FRAME_RX.match(fid):
726 raise FrameException("Invalid frame id for TextFrame: " + fid);
728 data = self.disassembleFrame(data);
729 self.encoding = data[0];
730 TRACE_MSG("TextFrame encoding: %s" % id3EncodingToString(self.encoding));
731 try:
732 self.text = unicode(data[1:], id3EncodingToString(self.encoding));
733 if not strictID3():
734 self.text = cleanNulls(self.text)
735 except TypeError, excArg:
736 # if data is already unicode, just copy it
737 if excArg.args == ("decoding Unicode is not supported",):
738 self.text = data[1:];
739 if not strictID3():
740 self.text = cleanNulls(self.text)
741 else:
742 raise;
744 TRACE_MSG("TextFrame text: %s" % self.text);
746 def __unicode__(self):
747 return u'<%s (%s): %s>' % (self.getFrameDesc(), self.header.id,
748 self.text);
750 def render(self):
751 if self.header.minorVersion == 4 and self.header.id == "TSIZ":
752 TRACE_MSG("Dropping deprecated frame TSIZ")
753 return ""
754 data = self.encoding +\
755 self.text.encode(id3EncodingToString(self.encoding));
756 return self.assembleFrame(data);
758 ################################################################################
759 class DateFrame(TextFrame):
760 date = None;
761 date_str = u"";
763 def __init__(self, frameHeader, data=None, date_str=None,
764 encoding=DEFAULT_ENCODING, unsync_default=False):
765 if data != None:
766 TextFrame.__init__(self, frameHeader, data=data,
767 encoding=encoding, unsync_default=unsync_default)
768 self._set(data, frameHeader)
769 else:
770 assert(date_str and isinstance(date_str, unicode))
771 TextFrame.__init__(self, frameHeader, text=date_str,
772 encoding=encoding, unsync_default=unsync_default)
773 self.setDate(self.text)
775 def _set(self, data, frameHeader):
776 TextFrame._set(self, data, frameHeader);
777 if self.header.id[:2] != "TD" and self.header.minorVersion >= 4:
778 raise FrameException("Invalid frame id for DateFrame: " + \
779 self.header.id);
781 def setDate(self, d):
782 if not d:
783 self.date = None;
784 self.date_str = u"";
785 return;
787 for fmt in timeStampFormats:
788 try:
789 if isinstance(d, tuple):
790 self.date_str = unicode(time.strftime(fmt, d));
791 self.date = d;
792 else:
793 assert(isinstance(d, unicode));
794 # Witnessed oddball tags with NULL bytes (ozzy.tag from id3lib)
795 d = d.strip("\x00");
797 try:
798 self.date = time.strptime(d, fmt);
799 except TypeError, ex:
800 continue;
801 self.date_str = d;
802 break;
803 except ValueError:
804 self.date = None;
805 self.date_str = u"";
806 continue;
807 if strictID3() and not self.date:
808 raise FrameException("Invalid Date: " + str(d));
809 self.text = self.date_str;
811 def getDate(self):
812 return self.date_str;
814 def getYear(self):
815 if self.date:
816 return self.__padDateField(self.date[0], 4);
817 else:
818 return None;
820 def getMonth(self):
821 if self.date:
822 return self.__padDateField(self.date[1], 2);
823 else:
824 return None;
826 def getDay(self):
827 if self.date:
828 return self.__padDateField(self.date[2], 2);
829 else:
830 return None;
832 def getHour(self):
833 if self.date:
834 return self.__padDateField(self.date[3], 2);
835 else:
836 return None;
838 def getMinute(self):
839 if self.date:
840 return self.__padDateField(self.date[4], 2);
841 else:
842 return None;
844 def getSecond(self):
845 if self.date:
846 return self.__padDateField(self.date[5], 2);
847 else:
848 return None;
850 def __padDateField(self, f, sz):
851 fStr = str(f);
852 if len(fStr) == sz:
853 pass;
854 elif len(fStr) < sz:
855 fStr = ("0" * (sz - len(fStr))) + fStr;
856 else:
857 raise TagException("Invalid date field: " + fStr);
858 return fStr;
860 def render(self):
861 # Conversion crap
862 if self.header.minorVersion == 4 and\
863 (self.header.id == OBSOLETE_DATE_FID or\
864 self.header.id == OBSOLETE_YEAR_FID or\
865 self.header.id == OBSOLETE_TIME_FID or\
866 self.header.id == OBSOLETE_RECORDING_DATE_FID):
867 self.header.id = "TDRC";
868 elif self.header.minorVersion == 4 and\
869 self.header.id == OBSOLETE_ORIG_RELEASE_FID:
870 self.header.id = "TDOR";
871 elif self.header.minorVersion == 3 and self.header.id == "TDOR":
872 self.header.id = OBSOLETE_ORIG_RELEASE_FID;
873 elif self.header.minorVersion == 3 and self.header.id == "TDEN":
874 TRACE_MSG('Converting TDEN to TXXX(Encoding time) frame')
875 self.header.id = "TXXX";
876 self.description = "Encoding time";
877 data = self.encoding +\
878 self.description.encode(id3EncodingToString(self.encoding)) +\
879 self.getTextDelim() +\
880 self.date_str.encode(id3EncodingToString(self.encoding));
881 return self.assembleFrame(data)
883 elif self.header.minorVersion == 3 and self.header.id[:2] == "TD":
884 if self.header.id not in ['TDEN', 'TDLY', 'TDTG']:
885 self.header.id = OBSOLETE_YEAR_FID;
887 data = self.encoding +\
888 self.date_str.encode(id3EncodingToString(self.encoding));
889 data = self.assembleFrame(data);
890 return data;
893 ################################################################################
894 class UserTextFrame(TextFrame):
895 description = u"";
897 # Data string format:
898 # encoding (one byte) + description + "\x00" + text
899 def __init__(self, frameHeader, data=None, description=u"", text=u"",
900 encoding=DEFAULT_ENCODING, unsync_default=False):
901 if data != None:
902 TextFrame.__init__(self, frameHeader, data=data,
903 unsync_default=unsync_default)
904 self._set(data, frameHeader)
905 else:
906 assert(isinstance(description, unicode) and\
907 isinstance(text, unicode))
908 TextFrame.__init__(self, frameHeader, text=text, encoding=encoding,
909 unsync_default=unsync_default)
910 self.description = description
912 # Data string format:
913 # encoding (one byte) + description + "\x00" + text;
914 def _set(self, data, frameHeader = None):
915 assert(frameHeader);
916 if not USERTEXT_FRAME_RX.match(frameHeader.id):
917 raise FrameException("Invalid frame id for UserTextFrame: " +\
918 frameHeader.id);
920 data = self.disassembleFrame(data);
921 self.encoding = data[0];
922 TRACE_MSG("UserTextFrame encoding: %s" %\
923 id3EncodingToString(self.encoding));
924 (d, t) = splitUnicode(data[1:], self.encoding);
925 self.description = unicode(d, id3EncodingToString(self.encoding));
926 TRACE_MSG("UserTextFrame description: %s" % self.description);
927 self.text = unicode(t, id3EncodingToString(self.encoding));
928 if not strictID3():
929 self.text = cleanNulls(self.text)
930 TRACE_MSG("UserTextFrame text: %s" % self.text);
932 def render(self):
933 if self.header.minorVersion == 4:
934 if self.description.lower() == 'tagging time':
935 TRACE_MSG("Converting TXXX(%s) to TDTG frame)" % self.description)
936 return ""
937 if self.description.lower() == 'encoding time':
938 TRACE_MSG("Converting TXXX(%s) to TDEN frame" % self.description)
939 self.header.id = 'TDEN'
940 data = self.encoding +\
941 self.text.encode(id3EncodingToString(self.encoding))
942 return self.assembleFrame(data);
943 data = self.encoding +\
944 self.description.encode(id3EncodingToString(self.encoding)) +\
945 self.getTextDelim() +\
946 self.text.encode(id3EncodingToString(self.encoding));
947 return self.assembleFrame(data);
949 def __unicode__(self):
950 return u'<%s (%s): {Desc: %s} %s>' % (self.getFrameDesc(),
951 self.header.id,
952 self.description, self.text);
954 ################################################################################
955 class URLFrame(Frame):
956 url = "";
958 # Data string format:
959 # url
960 def __init__(self, frameHeader, data=None, url=None, unsync_default=False):
961 Frame.__init__(self, frameHeader, unsync_default)
962 if data != None:
963 self._set(data, frameHeader)
964 else:
965 assert(url)
966 self.url = url
968 # Data string format:
969 # url
970 def _set(self, data, frameHeader):
971 fid = frameHeader.id;
972 if not URL_FRAME_RX.match(fid) or USERURL_FRAME_RX.match(fid):
973 raise FrameException("Invalid frame id for URLFrame: " + fid);
974 data = self.disassembleFrame(data);
975 self.url = data;
976 if not strictID3():
977 self.url = cleanNulls(self.url)
979 def render(self):
980 data = str(self.url);
981 return self.assembleFrame(data);
983 def __str__(self):
984 return '<%s (%s): %s>' % (self.getFrameDesc(), self.header.id,
985 self.url);
987 ################################################################################
988 class UserURLFrame(URLFrame):
989 description = u"";
991 # Data string format:
992 # encoding (one byte) + description + "\x00" + url
993 def __init__(self, frameHeader, data=None, url="", description=u"",
994 encoding=DEFAULT_ENCODING, unsync_default=False):
995 Frame.__init__(self, frameHeader, unsync_default)
996 if data:
997 self._set(data, frameHeader);
998 else:
999 assert(encoding);
1000 assert(description and isinstance(description, unicode));
1001 assert(url and isinstance(url, str));
1002 self.encoding = encoding;
1003 self.url = url;
1004 self.description = description;
1006 # Data string format:
1007 # encoding (one byte) + description + "\x00" + url;
1008 def _set(self, data, frameHeader):
1009 assert(data and frameHeader);
1010 if not USERURL_FRAME_RX.match(frameHeader.id):
1011 raise FrameException("Invalid frame id for UserURLFrame: " +\
1012 frameHeader.id);
1014 data = self.disassembleFrame(data);
1015 self.encoding = data[0];
1016 TRACE_MSG("UserURLFrame encoding: %s" %\
1017 id3EncodingToString(self.encoding));
1018 try:
1019 (d, u) = splitUnicode(data[1:], self.encoding);
1020 except ValueError, ex:
1021 if strictID3():
1022 raise FrameException("Invalid WXXX frame, no null byte")
1023 d = data[1:]
1024 u = ""
1025 self.description = unicode(d, id3EncodingToString(self.encoding));
1026 TRACE_MSG("UserURLFrame description: %s" % self.description);
1027 self.url = u;
1028 if not strictID3():
1029 self.url = cleanNulls(self.url)
1030 TRACE_MSG("UserURLFrame text: %s" % self.url);
1032 def render(self):
1033 data = self.encoding +\
1034 self.description.encode(id3EncodingToString(self.encoding)) +\
1035 self.getTextDelim() + self.url;
1036 return self.assembleFrame(data);
1038 def __unicode__(self):
1039 return u'<%s (%s): %s [Encoding: %s] [Desc: %s]>' %\
1040 (self.getFrameDesc(), self.header.id,
1041 self.url, self.encoding, self.description)
1043 ################################################################################
1044 class CommentFrame(Frame):
1045 lang = "";
1046 description = u"";
1047 comment = u"";
1049 # Data string format:
1050 # encoding (one byte) + lang (three byte code) + description + "\x00" +
1051 # text
1052 def __init__(self, frameHeader, data=None, lang="",
1053 description=u"", comment=u"", encoding=DEFAULT_ENCODING,
1054 unsync_default=False):
1055 Frame.__init__(self, frameHeader, unsync_default)
1056 if data != None:
1057 self._set(data, frameHeader)
1058 else:
1059 assert(isinstance(description, unicode))
1060 assert(isinstance(comment, unicode))
1061 assert(isinstance(lang, str))
1062 self.encoding = encoding
1063 self.lang = lang
1064 self.description = description
1065 self.comment = comment
1067 # Data string format:
1068 # encoding (one byte) + lang (three byte code) + description + "\x00" +
1069 # text
1070 def _set(self, data, frameHeader = None):
1071 assert(frameHeader);
1072 if not COMMENT_FRAME_RX.match(frameHeader.id):
1073 raise FrameException("Invalid frame id for CommentFrame: " +\
1074 frameHeader.id);
1076 data = self.disassembleFrame(data);
1077 self.encoding = data[0];
1078 TRACE_MSG("CommentFrame encoding: " + id3EncodingToString(self.encoding));
1079 try:
1080 self.lang = str(data[1:4]).strip("\x00");
1081 # Test ascii encoding
1082 temp_lang = unicode(self.lang, "ascii");
1083 if self.lang and \
1084 not re.compile("[A-Z][A-Z][A-Z]", re.IGNORECASE).match(self.lang):
1085 if strictID3():
1086 raise FrameException("[CommentFrame] Invalid language "\
1087 "code: %s" % self.lang);
1088 except UnicodeDecodeError, ex:
1089 if strictID3():
1090 raise FrameException("[CommentFrame] Invalid language code: "\
1091 "[%s] %s" % (ex.object, ex.reason));
1092 else:
1093 self.lang = "";
1094 try:
1095 (d, c) = splitUnicode(data[4:], self.encoding);
1096 self.description = unicode(d, id3EncodingToString(self.encoding));
1097 self.comment = unicode(c, id3EncodingToString(self.encoding));
1098 except ValueError:
1099 if strictID3():
1100 raise FrameException("Invalid comment; no description/comment");
1101 else:
1102 self.description = u"";
1103 self.comment = u"";
1104 if not strictID3():
1105 self.description = cleanNulls(self.description)
1106 self.comment = cleanNulls(self.comment)
1108 def render(self):
1109 lang = self.lang.encode("ascii");
1110 if len(lang) > 3:
1111 lang = lang[0:3];
1112 elif len(lang) < 3:
1113 lang = lang + ('\x00' * (3 - len(lang)));
1114 data = self.encoding + lang +\
1115 self.description.encode(id3EncodingToString(self.encoding)) +\
1116 self.getTextDelim() +\
1117 self.comment.encode(id3EncodingToString(self.encoding));
1118 return self.assembleFrame(data);
1120 def __unicode__(self):
1121 return u"<%s (%s): %s [Lang: %s] [Desc: %s]>" %\
1122 (self.getFrameDesc(), self.header.id, self.comment,
1123 self.lang, self.description);
1125 ################################################################################
1126 class LyricsFrame(Frame):
1127 lang = "";
1128 description = u"";
1129 lyrics = u"";
1131 # Data string format:
1132 # encoding (one byte) + lang (three byte code) + description + "\x00" +
1133 # text
1134 def __init__(self, frameHeader, data=None, lang="",
1135 description=u"", lyrics=u"", encoding=DEFAULT_ENCODING,
1136 unsync_default=False):
1137 Frame.__init__(self, frameHeader, unsync_default)
1138 if data != None:
1139 self._set(data, frameHeader)
1140 else:
1141 assert(isinstance(description, unicode))
1142 assert(isinstance(lyrics, unicode))
1143 assert(isinstance(lang, str))
1144 self.encoding = encoding
1145 self.lang = lang
1146 self.description = description
1147 self.lyrics = lyrics
1149 # Data string format:
1150 # encoding (one byte) + lang (three byte code) + description + "\x00" +
1151 # text
1152 def _set(self, data, frameHeader = None):
1153 assert(frameHeader);
1154 if not LYRICS_FRAME_RX.match(frameHeader.id):
1155 raise FrameException("Invalid frame id for LyricsFrame: " +\
1156 frameHeader.id);
1158 data = self.disassembleFrame(data);
1159 self.encoding = data[0];
1160 TRACE_MSG("LyricsFrame encoding: " + id3EncodingToString(self.encoding));
1161 try:
1162 self.lang = str(data[1:4]).strip("\x00");
1163 # Test ascii encoding
1164 temp_lang = unicode(self.lang, "ascii");
1165 if self.lang and \
1166 not re.compile("[A-Z][A-Z][A-Z]", re.IGNORECASE).match(self.lang):
1167 if strictID3():
1168 raise FrameException("[LyricsFrame] Invalid language "\
1169 "code: %s" % self.lang);
1170 except UnicodeDecodeError, ex:
1171 if strictID3():
1172 raise FrameException("[LyricsFrame] Invalid language code: "\
1173 "[%s] %s" % (ex.object, ex.reason));
1174 else:
1175 self.lang = "";
1176 try:
1177 (d, c) = splitUnicode(data[4:], self.encoding);
1178 self.description = unicode(d, id3EncodingToString(self.encoding));
1179 self.lyrics = unicode(c, id3EncodingToString(self.encoding));
1180 except ValueError:
1181 if strictID3():
1182 raise FrameException("Invalid lyrics; no description/lyrics");
1183 else:
1184 self.description = u"";
1185 self.lyrics = u"";
1186 if not strictID3():
1187 self.description = cleanNulls(self.description)
1188 self.lyrics = cleanNulls(self.lyrics)
1190 def render(self):
1191 lang = self.lang.encode("ascii");
1192 if len(lang) > 3:
1193 lang = lang[0:3];
1194 elif len(lang) < 3:
1195 lang = lang + ('\x00' * (3 - len(lang)));
1196 data = self.encoding + lang +\
1197 self.description.encode(id3EncodingToString(self.encoding)) +\
1198 self.getTextDelim() +\
1199 self.lyrics.encode(id3EncodingToString(self.encoding));
1200 return self.assembleFrame(data);
1202 def __unicode__(self):
1203 return u"<%s (%s): %s [Lang: %s] [Desc: %s]>" %\
1204 (self.getFrameDesc(), self.header.id, self.lyrics,
1205 self.lang, self.description);
1207 ################################################################################
1208 # This class refers to the APIC frame, otherwise known as an "attached
1209 # picture".
1210 class ImageFrame(Frame):
1211 mimeType = None;
1212 pictureType = None;
1213 description = u"";
1214 # Contains the image data when the mimetype is image type.
1215 # Otherwise it is None.
1216 imageData = None;
1217 # Contains a URL for the image when the mimetype is "-->" per the spec.
1218 # Otherwise it is None.
1219 imageURL = None;
1220 # Declared "picture types".
1221 OTHER = 0x00
1222 ICON = 0x01 # 32x32 png only.
1223 OTHER_ICON = 0x02
1224 FRONT_COVER = 0x03
1225 BACK_COVER = 0x04
1226 LEAFLET = 0x05
1227 MEDIA = 0x06 # label side of cd, picture disc vinyl, etc.
1228 LEAD_ARTIST = 0x07
1229 ARTIST = 0x08
1230 CONDUCTOR = 0x09
1231 BAND = 0x0A
1232 COMPOSER = 0x0B
1233 LYRICIST = 0x0C
1234 RECORDING_LOCATION = 0x0D
1235 DURING_RECORDING = 0x0E
1236 DURING_PERFORMANCE = 0x0F
1237 VIDEO = 0x10
1238 BRIGHT_COLORED_FISH = 0x11 # There's always room for porno.
1239 ILLUSTRATION = 0x12
1240 BAND_LOGO = 0x13
1241 PUBLISHER_LOGO = 0x14
1242 MIN_TYPE = OTHER
1243 MAX_TYPE = PUBLISHER_LOGO
1245 def __init__(self, frameHeader, data=None,
1246 description=u"",
1247 imageData=None, imageURL=None,
1248 pictureType=None, mimeType=None,
1249 encoding=DEFAULT_ENCODING, unsync_default=False):
1250 Frame.__init__(self, frameHeader, unsync_default)
1251 if data != None:
1252 self._set(data, frameHeader);
1253 else:
1254 assert(isinstance(description, unicode));
1255 self.description = description;
1256 self.encoding = encoding;
1257 assert(mimeType);
1258 self.mimeType = mimeType;
1259 assert(pictureType != None);
1260 self.pictureType = pictureType;
1261 if imageData:
1262 self.imageData = imageData;
1263 else:
1264 self.imageURL = imageURL;
1265 assert(self.imageData or self.imageURL);
1268 # Factory method
1269 def create(type, imgFile, desc = u"", encoding = DEFAULT_ENCODING):
1270 if not isinstance(desc, unicode) or \
1271 not isinstance(type, int):
1272 raise FrameException("Wrong description and/or image-type type.");
1273 # Load img
1274 fp = file(imgFile, "rb");
1275 imgData = fp.read();
1276 mt = mimetypes.guess_type(imgFile);
1277 if not mt[0]:
1278 raise FrameException("Unable to guess mime-type for %s" % (imgFile));
1280 frameData = DEFAULT_ENCODING;
1281 frameData += mt[0] + "\x00";
1282 frameData += bin2bytes(dec2bin(type, 8));
1283 frameData += desc.encode(id3EncodingToString(encoding)) + "\x00";
1284 frameData += imgData;
1286 frameHeader = FrameHeader();
1287 frameHeader.id = IMAGE_FID;
1288 return ImageFrame(frameHeader, data = frameData);
1289 # Make create a static method. Odd....
1290 create = staticmethod(create);
1292 # Data string format:
1293 # <Header for 'Attached picture', ID: "APIC">
1294 # Text encoding $xx
1295 # MIME type <text string> $00
1296 # Picture type $xx
1297 # Description <text string according to encoding> $00 (00)
1298 # Picture data <binary data>
1299 def _set(self, data, frameHeader = None):
1300 assert(frameHeader);
1301 if not IMAGE_FRAME_RX.match(frameHeader.id):
1302 raise FrameException("Invalid frame id for ImageFrame: " +\
1303 frameHeader.id);
1305 data = self.disassembleFrame(data);
1307 input = StringIO(data);
1308 TRACE_MSG("APIC frame data size: " + str(len(data)));
1309 self.encoding = input.read(1);
1310 TRACE_MSG("APIC encoding: " + id3EncodingToString(self.encoding));
1312 # Mime type
1313 self.mimeType = "";
1314 if self.header.minorVersion != 2:
1315 ch = input.read(1)
1316 while ch and ch != "\x00":
1317 self.mimeType += ch
1318 ch = input.read(1)
1319 else:
1320 # v2.2 (OBSOLETE) special case
1321 self.mimeType = input.read(3);
1322 TRACE_MSG("APIC mime type: " + self.mimeType);
1323 if strictID3() and not self.mimeType:
1324 raise FrameException("APIC frame does not contain a mime type");
1325 if self.mimeType.find("/") == -1:
1326 self.mimeType = "image/" + self.mimeType;
1328 pt = ord(input.read(1));
1329 TRACE_MSG("Initial APIC picture type: " + str(pt));
1330 if pt < self.MIN_TYPE or pt > self.MAX_TYPE:
1331 if strictID3():
1332 raise FrameException("Invalid APIC picture type: %d" % (pt));
1333 # Rather than force this to UNKNOWN, let's assume that they put a
1334 # character literal instead of it's byte value.
1335 try:
1336 pt = int(chr(pt));
1337 except:
1338 pt = self.OTHER;
1339 if pt < self.MIN_TYPE or pt > self.MAX_TYPE:
1340 self.pictureType = self.OTHER;
1341 self.pictureType = pt;
1342 TRACE_MSG("APIC picture type: " + str(self.pictureType));
1344 self.desciption = u"";
1346 # Remaining data is a NULL separated description and image data
1347 buffer = input.read();
1348 input.close();
1350 (desc, img) = splitUnicode(buffer, self.encoding);
1351 TRACE_MSG("description len: %d" % len(desc));
1352 TRACE_MSG("description len: %d" % len(img));
1353 self.description = unicode(desc, id3EncodingToString(self.encoding));
1354 TRACE_MSG("APIC description: " + self.description);
1356 if self.mimeType.find("-->") != -1:
1357 self.imageData = None;
1358 self.imageURL = img;
1359 else:
1360 self.imageData = img;
1361 self.imageURL = None;
1362 TRACE_MSG("APIC image data: " + str(len(self.imageData)) + " bytes");
1363 if strictID3() and not self.imageData and not self.imageURL:
1364 raise FrameException("APIC frame does not contain any image data");
1367 def writeFile(self, path = "./", name = None):
1368 if not self.imageData:
1369 raise IOError("Fetching remote image files is not implemented.");
1370 if not name:
1371 name = self.getDefaultFileName();
1372 imageFile = os.path.join(path, name);
1374 f = file(imageFile, "wb");
1375 f.write(self.imageData);
1376 f.flush();
1377 f.close();
1378 def getDefaultFileName(self, suffix = ""):
1379 nameStr = self.picTypeToString(self.pictureType);
1380 if suffix:
1381 nameStr += suffix;
1382 nameStr = nameStr + "." + self.mimeType.split("/")[1];
1383 return nameStr;
1385 def render(self):
1386 data = self.encoding + self.mimeType + "\x00" +\
1387 bin2bytes(dec2bin(self.pictureType, 8)) +\
1388 self.description.encode(id3EncodingToString(self.encoding)) +\
1389 self.getTextDelim();
1390 if self.imageURL:
1391 data += self.imageURL.encode("ascii");
1392 else:
1393 data += self.imageData;
1394 return self.assembleFrame(data);
1396 def stringToPicType(s):
1397 if s == "OTHER":
1398 return ImageFrame.OTHER;
1399 elif s == "ICON":
1400 return ImageFrame.ICON;
1401 elif s == "OTHER_ICON":
1402 return ImageFrame.OTHER_ICON;
1403 elif s == "FRONT_COVER":
1404 return ImageFrame.FRONT_COVER
1405 elif s == "BACK_COVER":
1406 return ImageFrame.BACK_COVER;
1407 elif s == "LEAFLET":
1408 return ImageFrame.LEAFLET;
1409 elif s == "MEDIA":
1410 return ImageFrame.MEDIA;
1411 elif s == "LEAD_ARTIST":
1412 return ImageFrame.LEAD_ARTIST;
1413 elif s == "ARTIST":
1414 return ImageFrame.ARTIST;
1415 elif s == "CONDUCTOR":
1416 return ImageFrame.CONDUCTOR;
1417 elif s == "BAND":
1418 return ImageFrame.BAND;
1419 elif s == "COMPOSER":
1420 return ImageFrame.COMPOSER;
1421 elif s == "LYRICIST":
1422 return ImageFrame.LYRICIST;
1423 elif s == "RECORDING_LOCATION":
1424 return ImageFrame.RECORDING_LOCATION;
1425 elif s == "DURING_RECORDING":
1426 return ImageFrame.DURING_RECORDING;
1427 elif s == "DURING_PERFORMANCE":
1428 return ImageFrame.DURING_PERFORMANCE;
1429 elif s == "VIDEO":
1430 return ImageFrame.VIDEO;
1431 elif s == "BRIGHT_COLORED_FISH":
1432 return ImageFrame.BRIGHT_COLORED_FISH;
1433 elif s == "ILLUSTRATION":
1434 return ImageFrame.ILLUSTRATION;
1435 elif s == "BAND_LOGO":
1436 return ImageFrame.BAND_LOGO;
1437 elif s == "PUBLISHER_LOGO":
1438 return ImageFrame.PUBLISHER_LOGO;
1439 else:
1440 raise FrameException("Invalid APIC picture type: %s" % s);
1441 stringToPicType = staticmethod(stringToPicType);
1443 def picTypeToString(t):
1444 if t == ImageFrame.OTHER:
1445 return "OTHER";
1446 elif t == ImageFrame.ICON:
1447 return "ICON";
1448 elif t == ImageFrame.OTHER_ICON:
1449 return "OTHER_ICON";
1450 elif t == ImageFrame.FRONT_COVER:
1451 return "FRONT_COVER";
1452 elif t == ImageFrame.BACK_COVER:
1453 return "BACK_COVER";
1454 elif t == ImageFrame.LEAFLET:
1455 return "LEAFLET";
1456 elif t == ImageFrame.MEDIA:
1457 return "MEDIA";
1458 elif t == ImageFrame.LEAD_ARTIST:
1459 return "LEAD_ARTIST";
1460 elif t == ImageFrame.ARTIST:
1461 return "ARTIST";
1462 elif t == ImageFrame.CONDUCTOR:
1463 return "CONDUCTOR";
1464 elif t == ImageFrame.BAND:
1465 return "BAND";
1466 elif t == ImageFrame.COMPOSER:
1467 return "COMPOSER";
1468 elif t == ImageFrame.LYRICIST:
1469 return "LYRICIST";
1470 elif t == ImageFrame.RECORDING_LOCATION:
1471 return "RECORDING_LOCATION";
1472 elif t == ImageFrame.DURING_RECORDING:
1473 return "DURING_RECORDING";
1474 elif t == ImageFrame.DURING_PERFORMANCE:
1475 return "DURING_PERFORMANCE";
1476 elif t == ImageFrame.VIDEO:
1477 return "VIDEO";
1478 elif t == ImageFrame.BRIGHT_COLORED_FISH:
1479 return "BRIGHT_COLORED_FISH";
1480 elif t == ImageFrame.ILLUSTRATION:
1481 return "ILLUSTRATION";
1482 elif t == ImageFrame.BAND_LOGO:
1483 return "BAND_LOGO";
1484 elif t == ImageFrame.PUBLISHER_LOGO:
1485 return "PUBLISHER_LOGO";
1486 else:
1487 raise FrameException("Invalid APIC picture type: %d" % t);
1488 picTypeToString = staticmethod(picTypeToString);
1490 ################################################################################
1491 # This class refers to the GEOB frame
1492 class ObjectFrame(Frame):
1493 mimeType = None
1494 description = u""
1495 filename = u""
1496 objectData = None
1498 def __init__(self, frameHeader, data=None,
1499 desc=u"", filename=u"",
1500 objectData=None, mimeType=None,
1501 encoding=DEFAULT_ENCODING, unsync_default=False):
1502 Frame.__init__(self, frameHeader, unsync_default)
1503 if data != None:
1504 self._set(data, frameHeader);
1505 else:
1506 assert(isinstance(desc, unicode));
1507 self.description = desc;
1508 assert(isinstance(filename, unicode));
1509 self.filename = filename;
1510 self.encoding = encoding;
1511 assert(mimeType);
1512 self.mimeType = mimeType;
1513 assert(objectData);
1514 self.objectData = objectData;
1516 # Factory method
1517 def create(objFile, mime = u"", desc = u"", filename = None,
1518 encoding = DEFAULT_ENCODING):
1519 if filename == None:
1520 filename = unicode(os.path.basename(objFile));
1521 if not isinstance(desc, unicode) or \
1522 (not isinstance(filename, unicode) and filename != ""):
1523 raise FrameException("Wrong description and/or filename type.");
1524 # Load file
1525 fp = file(objFile, "rb");
1526 objData = fp.read();
1527 if mime:
1528 print("Using specified mime type %s" % mime);
1529 else:
1530 mt = mimetypes.guess_type(objFile);
1531 if not mt[0]:
1532 raise FrameException("Unable to guess mime-type for %s" %
1533 objFile);
1534 mime = mt[0];
1535 print("Guessing mime type %s" % mime);
1537 frameData = DEFAULT_ENCODING;
1538 frameData += mime + "\x00";
1539 frameData += filename.encode(id3EncodingToString(encoding)) + "\x00";
1540 frameData += desc.encode(id3EncodingToString(encoding)) + "\x00";
1541 frameData += objData;
1543 frameHeader = FrameHeader();
1544 frameHeader.id = OBJECT_FID;
1545 return ObjectFrame(frameHeader, data = frameData);
1546 # Make create a static method. Odd....
1547 create = staticmethod(create);
1549 # Data string format:
1550 # <Header for 'General encapsulated object', ID: "GEOB">
1551 # Text encoding $xx
1552 # MIME type <text string> $00
1553 # Filename <text string according to encoding> $00 (00)
1554 # Content description <text string according to encoding> $00 (00)
1555 # Encapsulated object <binary data>
1556 def _set(self, data, frameHeader = None):
1557 assert(frameHeader);
1558 if not OBJECT_FRAME_RX.match(frameHeader.id):
1559 raise FrameException("Invalid frame id for ObjectFrame: " +\
1560 frameHeader.id);
1562 data = self.disassembleFrame(data);
1564 input = StringIO(data);
1565 TRACE_MSG("GEOB frame data size: " + str(len(data)));
1566 self.encoding = input.read(1);
1567 TRACE_MSG("GEOB encoding: " + id3EncodingToString(self.encoding));
1569 # Mime type
1570 self.mimeType = "";
1571 if self.header.minorVersion != 2:
1572 ch = input.read(1);
1573 while ch != "\x00":
1574 self.mimeType += ch;
1575 ch = input.read(1);
1576 else:
1577 # v2.2 (OBSOLETE) special case
1578 self.mimeType = input.read(3);
1579 TRACE_MSG("GEOB mime type: " + self.mimeType);
1580 if strictID3() and not self.mimeType:
1581 raise FrameException("GEOB frame does not contain a mime type");
1582 if strictID3() and self.mimeType.find("/") == -1:
1583 raise FrameException("GEOB frame does not contain a valid mime type");
1585 self.filename = u"";
1586 self.description = u"";
1588 # Remaining data is a NULL separated filename, description and object data
1589 buffer = input.read();
1590 input.close();
1592 (filename, buffer) = splitUnicode(buffer, self.encoding);
1593 (desc, obj) = splitUnicode(buffer, self.encoding);
1594 TRACE_MSG("filename len: %d" % len(filename));
1595 TRACE_MSG("description len: %d" % len(desc));
1596 TRACE_MSG("data len: %d" % len(obj));
1597 self.filename = unicode(filename, id3EncodingToString(self.encoding));
1598 self.description = unicode(desc, id3EncodingToString(self.encoding));
1599 TRACE_MSG("GEOB filename: " + self.filename);
1600 TRACE_MSG("GEOB description: " + self.description);
1602 self.objectData = obj;
1603 TRACE_MSG("GEOB data: " + str(len(self.objectData)) + " bytes");
1604 if strictID3() and not self.objectData:
1605 raise FrameException("GEOB frame does not contain any data");
1608 def writeFile(self, path = "./", name = None):
1609 if not self.objectData:
1610 raise IOError("Fetching remote object files is not implemented.");
1611 if not name:
1612 name = self.getDefaultFileName();
1613 objectFile = os.path.join(path, name);
1615 f = file(objectFile, "wb");
1616 f.write(self.objectData);
1617 f.flush();
1618 f.close();
1619 def getDefaultFileName(self, suffix = ""):
1620 nameStr = self.filename;
1621 if suffix:
1622 nameStr += suffix;
1623 nameStr = nameStr + "." + self.mimeType.split("/")[1];
1624 return nameStr;
1626 def render(self):
1627 data = self.encoding + self.mimeType + "\x00" +\
1628 self.filename.encode(id3EncodingToString(self.encoding)) +\
1629 self.getTextDelim() +\
1630 self.description.encode(id3EncodingToString(self.encoding)) +\
1631 self.getTextDelim() +\
1632 self.objectData;
1633 return self.assembleFrame(data);
1635 class PlayCountFrame(Frame):
1636 count = None;
1638 def __init__(self, frameHeader, data=None, count=None,
1639 unsync_default=False):
1640 Frame.__init__(self, frameHeader, unsync_default)
1641 if data != None:
1642 self._set(data, frameHeader);
1643 else:
1644 assert(count != None and count >= 0);
1645 self.count = count;
1647 def _set(self, data, frameHeader):
1648 assert(frameHeader);
1649 assert(len(data) >= 4);
1650 self.count = long(bytes2dec(data));
1652 def render(self):
1653 data = dec2bytes(self.count, 32);
1654 return self.assembleFrame(data);
1656 class UniqueFileIDFrame(Frame):
1657 owner_id = "";
1658 id = "";
1660 def __init__(self, frameHeader, data=None, owner_id=None, id=None,
1661 unsync_default=False):
1662 Frame.__init__(self, frameHeader, unsync_default)
1663 if data != None:
1664 self._set(data, frameHeader);
1665 else:
1666 assert(owner_id != None and len(owner_id) > 0);
1667 assert(id != None and len(id) > 0 and len(id) <= 64);
1668 self.owner_id = owner_id;
1669 self.id = id;
1671 def _set(self, data, frameHeader):
1672 assert(frameHeader);
1673 # Data format
1674 # Owner identifier <text string> $00
1675 # Identifier up to 64 bytes binary data>
1676 (self.owner_id, self.id) = data.split("\x00", 1);
1677 TRACE_MSG("UFID owner_id: " + self.owner_id);
1678 TRACE_MSG("UFID id: " + self.id);
1679 if strictID3() and (len(self.owner_id) == 0 or
1680 len(self.id) == 0 or len(self.id) > 64):
1681 raise FrameException("Invalid UFID frame");
1683 def render(self):
1684 data = self.owner_id + "\x00" + self.id;
1685 return self.assembleFrame(data);
1687 ################################################################################
1688 class UnknownFrame(Frame):
1689 data = "";
1691 def __init__(self, frameHeader, data, unsync_default=False):
1692 assert(frameHeader and data)
1693 Frame.__init__(self, frameHeader, unsync_default)
1694 self._set(data, frameHeader)
1696 def _set(self, data, frameHeader):
1697 self.data = self.disassembleFrame(data);
1699 def render(self):
1700 return self.assembleFrame(self.data)
1702 ################################################################################
1703 class MusicCDIdFrame(Frame):
1704 toc = "";
1706 def __init__(self, frameHeader, data=None, unsync_default=False):
1707 Frame.__init__(self, frameHeader, unsync_default)
1708 # XXX: Flesh this class out and add a toc arg
1709 assert(data != None);
1710 if data != None:
1711 self._set(data, frameHeader);
1713 # TODO: Parse the TOC and comment the format.
1714 def _set(self, data, frameHeader):
1715 if not CDID_FRAME_RX.match(frameHeader.id):
1716 raise FrameException("Invalid frame id for MusicCDIdFrame: " +\
1717 frameHeader.id);
1718 data = self.disassembleFrame(data);
1719 self.toc = data;
1721 def render(self):
1722 data = self.toc;
1723 return self.assembleFrame(data);
1725 ################################################################################
1726 # A class for containing and managing ID3v2.Frame objects.
1727 class FrameSet(list):
1728 tagHeader = None;
1730 def __init__(self, tagHeader, l = None):
1731 self.tagHeader = tagHeader;
1732 if l:
1733 for f in l:
1734 if not isinstance(f, Frame):
1735 raise TypeError("Invalid type added to FrameSet: " +\
1736 f.__class__);
1737 self.append(f);
1739 # Setting a FrameSet instance like this 'fs = []' morphs the instance into
1740 # a list object.
1741 def clear(self):
1742 del self[0:];
1744 # Read frames starting from the current read position of the file object.
1745 # Returns the amount of padding which occurs after the tag, but before the
1746 # audio content. A return valule of 0 DOES NOT imply an error.
1747 def parse(self, f, tagHeader, extendedHeader):
1748 self.tagHeader = tagHeader;
1749 self.extendedHeader = extendedHeader
1750 paddingSize = 0;
1751 sizeLeft = tagHeader.tagSize - extendedHeader.size
1752 start_size = sizeLeft
1753 consumed_size = 0
1755 # Handle a tag-level unsync. Some frames may have their own unsync bit
1756 # set instead.
1757 tagData = f.read(sizeLeft)
1759 # If the tag is 2.3 and the tag header unsync bit is set then all the
1760 # frame data is deunsync'd at once, otherwise it will happen on a per
1761 # frame basis.
1762 from eyeD3 import ID3_V2_3
1763 if tagHeader.unsync and tagHeader.version <= ID3_V2_3:
1764 TRACE_MSG("De-unsynching %d bytes at once (<= 2.3 tag)" %
1765 len(tagData))
1766 og_size = len(tagData)
1767 tagData = deunsyncData(tagData)
1768 sizeLeft = len(tagData)
1769 TRACE_MSG("De-unsynch'd %d bytes at once (<= 2.3 tag) to %d bytes" %
1770 (og_size, sizeLeft))
1772 # Adding bytes to simulate the tag header(s) in the buffer. This keeps
1773 # f.tell() values matching the file offsets.
1774 prepadding = '\x00' * 10 # Tag header
1775 prepadding += '\x00' * extendedHeader.size
1776 tagBuffer = StringIO(prepadding + tagData);
1777 tagBuffer.seek(len(prepadding));
1779 while sizeLeft > 0:
1780 TRACE_MSG("sizeLeft: " + str(sizeLeft));
1781 if sizeLeft < (10 + 1): # The size of the smallest frame.
1782 TRACE_MSG("FrameSet: Implied padding (sizeLeft < minFrameSize)");
1783 paddingSize = sizeLeft
1784 break;
1786 TRACE_MSG("+++++++++++++++++++++++++++++++++++++++++++++++++");
1787 TRACE_MSG("FrameSet: Reading Frame #" + str(len(self) + 1));
1788 frameHeader = FrameHeader(tagHeader);
1789 if not frameHeader.parse(tagBuffer):
1790 TRACE_MSG("No frame found, implied padding of %d bytes" % sizeLeft)
1791 paddingSize = sizeLeft
1792 break;
1794 # Frame data.
1795 if frameHeader.dataSize:
1796 TRACE_MSG("FrameSet: Reading %d (0x%X) bytes of data from byte "
1797 "pos %d (0x%X)" % (frameHeader.dataSize,
1798 frameHeader.dataSize,
1799 tagBuffer.tell(),
1800 tagBuffer.tell()));
1801 data = tagBuffer.read(frameHeader.dataSize);
1803 TRACE_MSG("FrameSet: %d bytes of data read" % len(data));
1805 consumed_size += (frameHeader.FRAME_HEADER_SIZE +
1806 frameHeader.dataSize)
1807 self.addFrame(createFrame(frameHeader, data, tagHeader))
1809 # Each frame contains dataSize + headerSize bytes.
1810 sizeLeft -= (frameHeader.FRAME_HEADER_SIZE + frameHeader.dataSize);
1812 return paddingSize
1814 # Returrns the size of the frame data.
1815 def getSize(self):
1816 sz = 0;
1817 for f in self:
1818 sz += len(f.render());
1819 return sz;
1821 def setTagHeader(self, tagHeader):
1822 self.tagHeader = tagHeader;
1823 for f in self:
1824 f.header.setVersion(tagHeader);
1826 # This methods adds the frame if it is addable per the ID3 spec.
1827 def addFrame(self, frame):
1828 fid = frame.header.id;
1830 # Text frame restrictions.
1831 # No multiples except for TXXX which must have unique descriptions.
1832 if strictID3() and TEXT_FRAME_RX.match(fid) and self[fid]:
1833 if not USERTEXT_FRAME_RX.match(fid):
1834 raise FrameException("Multiple %s frames not allowed." % fid);
1835 userTextFrames = self[fid];
1836 for frm in userTextFrames:
1837 if frm.description == frame.description:
1838 raise FrameException("Multiple %s frames with the same\
1839 description not allowed." % fid);
1841 # Comment frame restrictions.
1842 # Multiples must have a unique description/language combination.
1843 if strictID3() and COMMENT_FRAME_RX.match(fid) and self[fid]:
1844 commentFrames = self[fid];
1845 for frm in commentFrames:
1846 if frm.description == frame.description and\
1847 frm.lang == frame.lang:
1848 raise FrameException("Multiple %s frames with the same\
1849 language and description not allowed." %\
1850 fid);
1852 # Lyrics frame restrictions.
1853 # Multiples must have a unique description/language combination.
1854 if strictID3() and LYRICS_FRAME_RX.match(fid) and self[fid]:
1855 lyricsFrames = self[fid];
1856 for frm in lyricsFrames:
1857 if frm.description == frame.description and\
1858 frm.lang == frame.lang:
1859 raise FrameException("Multiple %s frames with the same\
1860 language and description not allowed." %\
1861 fid);
1863 # URL frame restrictions.
1864 # No multiples except for TXXX which must have unique descriptions.
1865 if strictID3() and URL_FRAME_RX.match(fid) and self[fid]:
1866 if not USERURL_FRAME_RX.match(fid):
1867 raise FrameException("Multiple %s frames not allowed." % fid);
1868 userUrlFrames = self[fid];
1869 for frm in userUrlFrames:
1870 if frm.description == frame.description:
1871 raise FrameException("Multiple %s frames with the same\
1872 description not allowed." % fid);
1874 # Music CD ID restrictions.
1875 # No multiples.
1876 if strictID3() and CDID_FRAME_RX.match(fid) and self[fid]:
1877 raise FrameException("Multiple %s frames not allowed." % fid);
1879 # Image (attached picture) frame restrictions.
1880 # Multiples must have a unique content desciptor. I'm assuming that
1881 # the spec means the picture type.....
1882 if IMAGE_FRAME_RX.match(fid) and self[fid] and strictID3():
1883 imageFrames = self[fid];
1884 for frm in imageFrames:
1885 if frm.pictureType == frame.pictureType:
1886 raise FrameException("Multiple %s frames with the same "\
1887 "content descriptor not allowed." % fid);
1889 # Object (GEOB) frame restrictions.
1890 # Multiples must have a unique content desciptor.
1891 if OBJECT_FRAME_RX.match(fid) and self[fid] and strictID3():
1892 objectFrames = self[fid];
1893 for frm in objectFrames:
1894 if frm.description == frame.description:
1895 raise FrameException("Multiple %s frames with the same "\
1896 "content descriptor not allowed." % fid);
1898 # Play count frame (PCNT). There may be only one
1899 if PLAYCOUNT_FRAME_RX.match(fid) and self[fid]:
1900 raise FrameException("Multiple %s frames not allowed." % fid);
1902 # Unique File identifier frame. There may be only one with the same
1903 # owner_id
1904 if UNIQUE_FILE_ID_FRAME_RX.match(fid) and self[fid]:
1905 ufid_frames = self[fid];
1906 for frm in ufid_frames:
1907 if frm.owner_id == frame.owner_id:
1908 raise FrameException("Multiple %s frames not allowed with "\
1909 "the same owner ID (%s)" %\
1910 (fid, frame.owner_id));
1912 self.append(frame);
1914 # Set a text frame value. Text frame IDs must be unique. If a frame with
1915 # the same Id is already in the list it's value is changed, otherwise
1916 # the frame is added.
1917 def setTextFrame(self, frameId, text, encoding = None):
1918 assert(type(text) == unicode);
1920 if not TEXT_FRAME_RX.match(frameId):
1921 raise FrameException("Invalid Frame ID: " + frameId);
1922 if USERTEXT_FRAME_RX.match(frameId):
1923 raise FrameException("Wrong method, use setUserTextFrame");
1925 if self[frameId]:
1926 curr = self[frameId][0];
1927 if encoding:
1928 curr.encoding = encoding;
1930 if isinstance(curr, DateFrame):
1931 curr.setDate(text);
1932 else:
1933 curr.text = text;
1934 else:
1935 h = FrameHeader(self.tagHeader);
1936 h.id = frameId;
1937 if not encoding:
1938 encoding = DEFAULT_ENCODING;
1939 if frameId in DATE_FIDS:
1940 self.addFrame(DateFrame(h, encoding = encoding, date_str = text));
1941 else:
1942 self.addFrame(TextFrame(h, encoding = encoding, text = text));
1944 # If a user text frame with the same description exists then
1945 # the frame text is replaced, otherwise the frame is added.
1946 def setCommentFrame(self, comment, description, lang = DEFAULT_LANG,
1947 encoding = None):
1948 assert(isinstance(comment, unicode) and isinstance(description, unicode));
1950 if self[COMMENT_FID]:
1951 found = 0;
1952 for f in self[COMMENT_FID]:
1953 if f.lang == lang and f.description == description:
1954 f.comment = comment;
1955 if encoding:
1956 f.encoding = encoding;
1957 found = 1;
1958 break;
1959 if not found:
1960 h = FrameHeader(self.tagHeader);
1961 h.id = COMMENT_FID;
1962 if not encoding:
1963 encoding = DEFAULT_ENCODING;
1964 self.addFrame(CommentFrame(h, encoding = encoding, lang = lang,
1965 description = description,
1966 comment = comment));
1967 else:
1968 if not encoding:
1969 encoding = DEFAULT_ENCODING;
1970 h = FrameHeader(self.tagHeader);
1971 h.id = COMMENT_FID;
1972 self.addFrame(CommentFrame(h, encoding = encoding, lang = lang,
1973 description = description,
1974 comment = comment));
1976 # If a user text frame with the same description exists then
1977 # the frame text is replaced, otherwise the frame is added.
1978 def setLyricsFrame(self, lyrics, description, lang = DEFAULT_LANG,
1979 encoding = None):
1980 assert(isinstance(lyrics, unicode) and isinstance(description, unicode));
1982 if self[LYRICS_FID]:
1983 found = 0;
1984 for f in self[LYRICS_FID]:
1985 if f.lang == lang and f.description == description:
1986 f.lyrics = lyrics;
1987 if encoding:
1988 f.encoding = encoding;
1989 found = 1;
1990 break;
1991 if not found:
1992 h = FrameHeader(self.tagHeader);
1993 h.id = LYRICS_FID;
1994 if not encoding:
1995 encoding = DEFAULT_ENCODING;
1996 self.addFrame(LyricsFrame(h, encoding = encoding, lang = lang,
1997 description = description,
1998 lyrics = lyrics));
1999 else:
2000 if not encoding:
2001 encoding = DEFAULT_ENCODING;
2002 h = FrameHeader(self.tagHeader);
2003 h.id = LYRICS_FID;
2004 self.addFrame(LyricsFrame(h, encoding = encoding, lang = lang,
2005 description = description,
2006 lyrics = lyrics));
2008 def setUniqueFileIDFrame(self, owner_id, id):
2009 assert(isinstance(owner_id, str) and isinstance(id, str));
2011 if self[UNIQUE_FILE_ID_FID]:
2012 found = 0;
2013 for f in self[UNIQUE_FILE_ID_FID]:
2014 if f.owner_id == owner_id:
2015 f.id = id;
2016 found = 1;
2017 break;
2018 if not found:
2019 h = FrameHeader(self.tagHeader);
2020 h.id = UNIQUE_FILE_ID_FID;
2021 self.addFrame(UniqueFileIDFrame(h, owner_id = owner_id, id = id));
2022 else:
2023 h = FrameHeader(self.tagHeader);
2024 h.id = UNIQUE_FILE_ID_FID;
2025 self.addFrame(UniqueFileIDFrame(h, owner_id = owner_id, id = id));
2027 # If a comment frame with the same language and description exists then
2028 # the comment text is replaced, otherwise the frame is added.
2029 def setUserTextFrame(self, txt, description, encoding = None):
2030 assert(isinstance(txt, unicode));
2031 assert(isinstance(description, unicode));
2033 if self[USERTEXT_FID]:
2034 found = 0;
2035 for f in self[USERTEXT_FID]:
2036 if f.description == description:
2037 f.text = txt;
2038 if encoding:
2039 f.encoding = encoding;
2040 found = 1;
2041 break;
2042 if not found:
2043 if not encoding:
2044 encoding = DEFAULT_ENCODING;
2045 h = FrameHeader(self.tagHeader);
2046 h.id = USERTEXT_FID;
2047 self.addFrame(UserTextFrame(h, encoding = encoding,
2048 description = description,
2049 text = txt));
2050 else:
2051 if not encoding:
2052 encoding = DEFAULT_ENCODING;
2053 h = FrameHeader(self.tagHeader);
2054 h.id = USERTEXT_FID;
2055 self.addFrame(UserTextFrame(h, encoding = encoding,
2056 description = description,
2057 text = txt));
2059 # This method removes all frames with the matching frame ID.
2060 # The number of frames removed is returned.
2061 # Note that calling this method with a key like "COMM" may remove more
2062 # frames then you really want.
2063 def removeFramesByID(self, fid):
2064 if not isinstance(fid, str):
2065 raise FrameException("removeFramesByID only operates on frame IDs");
2067 i = 0;
2068 count = 0;
2069 while i < len(self):
2070 if self[i].header.id == fid:
2071 del self[i];
2072 count += 1;
2073 else:
2074 i += 1;
2075 return count;
2077 # Removes the frame at index. True is returned if the element was
2078 # removed, and false otherwise.
2079 def removeFrameByIndex(self, index):
2080 if not isinstance(index, int):
2081 raise\
2082 FrameException("removeFrameByIndex only operates on a frame index");
2083 try:
2084 del self.frames[key];
2085 return 1;
2086 except:
2087 return 0;
2089 # Accepts both int (indexed access) and string keys (a valid frame Id).
2090 # A list of frames (commonly with only one element) is returned when the
2091 # FrameSet is accessed using frame IDs since some frames can appear
2092 # multiple times in a tag. To sum it all up htis method returns
2093 # string or None when indexed using an integer, and a 0 to N length
2094 # list of strings when indexed with a frame ID.
2096 # Throws IndexError and TypeError.
2097 def __getitem__(self, key):
2098 if isinstance(key, int):
2099 if key >= 0 and key < len(self):
2100 return list.__getitem__(self, key);
2101 else:
2102 raise IndexError("FrameSet index out of range");
2103 elif isinstance(key, str):
2104 retList = list();
2105 for f in self:
2106 if f.header.id == key:
2107 retList.append(f);
2108 return retList;
2109 else:
2110 raise TypeError("FrameSet key must be type int or string");
2112 def splitUnicode(data, encoding):
2113 if encoding == LATIN1_ENCODING or encoding == UTF_8_ENCODING:
2114 return data.split("\x00", 1);
2115 elif encoding == UTF_16_ENCODING or encoding == UTF_16BE_ENCODING:
2116 # Two null bytes split, but since each utf16 char is also two
2117 # bytes we need to ensure we found a proper boundary.
2118 (d, t) = data.split("\x00\x00", 1);
2119 if (len(d) % 2) != 0:
2120 (d, t) = data.split("\x00\x00\x00", 1);
2121 d += "\x00";
2122 return (d, t);
2124 #######################################################################
2125 # Create and return the appropriate frame.
2126 # Exceptions: ....
2127 def createFrame(frameHeader, data, tagHeader):
2128 f = None
2130 # Text Frames
2131 if TEXT_FRAME_RX.match(frameHeader.id):
2132 if USERTEXT_FRAME_RX.match(frameHeader.id):
2133 f = UserTextFrame(frameHeader, data=data,
2134 unsync_default=tagHeader.unsync)
2135 else:
2136 if frameHeader.id[:2] == "TD" or\
2137 frameHeader.id == OBSOLETE_DATE_FID or\
2138 frameHeader.id == OBSOLETE_YEAR_FID or \
2139 frameHeader.id == OBSOLETE_ORIG_RELEASE_FID:
2140 f = DateFrame(frameHeader, data=data,
2141 unsync_default=tagHeader.unsync)
2142 else:
2143 f = TextFrame(frameHeader, data=data,
2144 unsync_default=tagHeader.unsync)
2145 # Comment Frames.
2146 elif COMMENT_FRAME_RX.match(frameHeader.id):
2147 f = CommentFrame(frameHeader, data=data, unsync_default=tagHeader.unsync)
2148 # Lyrics Frames.
2149 elif LYRICS_FRAME_RX.match(frameHeader.id):
2150 f = LyricsFrame(frameHeader, data=data, unsync_default=tagHeader.unsync)
2151 # URL Frames.
2152 elif URL_FRAME_RX.match(frameHeader.id):
2153 if USERURL_FRAME_RX.match(frameHeader.id):
2154 f = UserURLFrame(frameHeader, data=data,
2155 unsync_default=tagHeader.unsync)
2156 else:
2157 f = URLFrame(frameHeader, data=data, unsync_default=tagHeader.unsync)
2158 # CD Id frame.
2159 elif CDID_FRAME_RX.match(frameHeader.id):
2160 f = MusicCDIdFrame(frameHeader, data=data, unsync_default=tagHeader.unsync)
2161 # Attached picture
2162 elif IMAGE_FRAME_RX.match(frameHeader.id):
2163 f = ImageFrame(frameHeader, data=data, unsync_default=tagHeader.unsync)
2164 # Encapsulated object
2165 elif OBJECT_FRAME_RX.match(frameHeader.id):
2166 f = ObjectFrame(frameHeader, data=data, unsync_default=tagHeader.unsync)
2167 # Play count
2168 elif PLAYCOUNT_FRAME_RX.match(frameHeader.id):
2169 f = PlayCountFrame(frameHeader, data=data, unsync_default=tagHeader.unsync)
2170 # Unique file identifier
2171 elif UNIQUE_FILE_ID_FRAME_RX.match(frameHeader.id):
2172 f = UniqueFileIDFrame(frameHeader, data=data,
2173 unsync_default=tagHeader.unsync)
2175 if f == None:
2176 f = UnknownFrame(frameHeader, data=data, unsync_default=tagHeader.unsync)
2178 return f
2181 def map2_2FrameId(originalId):
2182 if not TAGS2_2_TO_TAGS_2_3_AND_4.has_key(originalId):
2183 return originalId
2184 return TAGS2_2_TO_TAGS_2_3_AND_4[originalId]