root/eyeD3/frames.py

Revision 6ab8361a61197bb0a951e5f77d4d1c5c25a9702f, 67.2 kB (checked in by Jason Michalski <armooo@armooo.net>, 2 years ago)

pyTivo
- ID3 suport for music

  • Property mode set to 100644
Line 
1 ################################################################################
2 #
3 #  Copyright (C) 2002-2005  Travis Shirk <travis@pobox.com>
4 #  Copyright (C) 2001  Ryan Finne <ryan@finnie.org>
5 #
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.
10 #
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.
15 #
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
19 #
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 *;
25
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"];
33
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";
70
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";
85
86 DATE_FIDS          = ["TDRL", "TDOR", "TDRC", OBSOLETE_YEAR_FID,
87                       OBSOLETE_DATE_FID];
88
89 frameDesc = { "AENC": "Audio encryption",
90               "APIC": "Attached picture",
91               "ASPI": "Audio seek point index",
92
93               "COMM": "Comments",
94               "COMR": "Commercial frame",
95
96               "ENCR": "Encryption method registration",
97               "EQU2": "Equalisation (2)",
98               "ETCO": "Event timing codes",
99
100               "GEOB": "General encapsulated object",
101               "GRID": "Group identification registration",
102
103               "LINK": "Linked information",
104
105               "MCDI": "Music CD identifier",
106               "MLLT": "MPEG location lookup table",
107
108               "OWNE": "Ownership frame",
109
110               "PRIV": "Private frame",
111               "PCNT": "Play counter",
112               "POPM": "Popularimeter",
113               "POSS": "Position synchronisation frame",
114
115               "RBUF": "Recommended buffer size",
116               "RVA2": "Relative volume adjustment (2)",
117               "RVRB": "Reverb",
118
119               "SEEK": "Seek frame",
120               "SIGN": "Signature frame",
121               "SYLT": "Synchronised lyric/text",
122               "SYTC": "Synchronised tempo codes",
123
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",
170
171               "UFID": "Unique file identifier",
172               "USER": "Terms of use",
173               "USLT": "Unsynchronised lyric/text transcription",
174
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" };
184
185
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]
254 }
255
256
257 NULL_FRAME_FLAGS = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
258
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 + "$");
268
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
283 ]
284
285 LATIN1_ENCODING   = "\x00";
286 UTF_16_ENCODING   = "\x01";
287 UTF_16BE_ENCODING = "\x02";
288 UTF_8_ENCODING    = "\x03";
289
290 DEFAULT_ENCODING = LATIN1_ENCODING;
291 DEFAULT_ID3_MAJOR_VERSION = 2;
292 DEFAULT_ID3_MINOR_VERSION = 4;
293 DEFAULT_LANG = "eng";
294
295 def cleanNulls(s):
296    return "/".join([x for x in s.split('\x00') if x])
297
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";
312
313 ################################################################################
314 class FrameException(Exception):
315     '''Thrown by invalid frames'''
316     pass;
317
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;
339
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;   
350
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]);
358
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();
373
374    def setBitMask(self):
375       major = self.majorVersion;
376       minor = self.minorVersion;
377
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.");
409
410    def render(self, dataSize):
411       data = self.id;
412
413       if self.minorVersion == 3:
414          data += bin2bytes(dec2bin(dataSize, 32));
415       else:
416          data += bin2bytes(bin2synchsafe(dec2bin(dataSize, 32)));
417
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;
429
430       data += bin2bytes(self.flags);
431
432       return data;
433
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;
461      
462
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)
471      
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));
491  
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");
516
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;
529
530
531    def isFrameIdValid(self, id):
532       return re.compile(r"^[A-Z0-9][A-Z0-9][A-Z0-9][A-Z0-9]$").match(id);
533
534    def clearFlags(self):
535       flags = [0] * 16;
536
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;
543
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;
551
552 ################################################################################
553 class Frame:
554    header = None;
555    decompressedSize = 0;
556    groupId = 0;
557    encryptionMethod = 0;
558    dataLen = 0;
559    encoding = DEFAULT_ENCODING;
560
561    def __init__(self, frameHeader):
562        assert(isinstance(frameHeader, FrameHeader));
563        self.header = frameHeader;
564
565    def __str__(self):
566       desc = self.getFrameDesc();
567       return '<%s Frame (%s)>' % (desc, self.header.id);
568
569    def unsync(self, data):
570       if self.header.unsync:
571          data = unsyncData(data);
572       return data;
573
574    def deunsync(self, data):
575       data = deunsyncData(data);
576       return data;
577
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;
583
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;
589
590    def decrypt(self, data):
591       raise FrameException("Encryption not supported");
592
593    def encrypt(self, data):
594       raise FrameException("Encryption not supported");
595
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);
630
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;
638
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));
657
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);
665
666       data = formatFlagData + data;
667       return self.header.render(len(data)) + data;
668
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";
677
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";
684
685 ################################################################################
686 class TextFrame(Frame):
687    text = u"";
688
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;
701
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("I