Back to index

eyed3  0.6.18
frames.py
Go to the documentation of this file.
00001 ################################################################################
00002 #  Copyright (C) 2002-2007  Travis Shirk <travis@pobox.com>
00003 #  Copyright (C) 2005  Michael Urman
00004 #    - Sync-safe encoding/decoding algorithms
00005 #
00006 #  This program is free software; you can redistribute it and/or modify
00007 #  it under the terms of the GNU General Public License as published by
00008 #  the Free Software Foundation; either version 2 of the License, or
00009 #  (at your option) any later version.
00010 #
00011 #  This program is distributed in the hope that it will be useful,
00012 #  but WITHOUT ANY WARRANTY; without even the implied warranty of
00013 #  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
00014 #  GNU General Public License for more details.
00015 #
00016 #  You should have received a copy of the GNU General Public License
00017 #  along with this program; if not, write to the Free Software
00018 #  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
00019 #
00020 ################################################################################
00021 import sys, os, os.path, re, zlib, StringIO, time
00022 from StringIO import StringIO;
00023 from utils import *;
00024 from binfuncs import *;
00025 
00026 # Valid time stamp formats per ISO 8601 and used by time.strptime.
00027 timeStampFormats = ["%Y",
00028                     "%Y-%m",
00029                     "%Y-%m-%d",
00030                     "%Y-%m-%dT%H",
00031                     "%Y-%m-%dT%H:%M",
00032                     "%Y-%m-%dT%H:%M:%S"]
00033 
00034 ARTIST_FID         = "TPE1"
00035 BAND_FID           = "TPE2"
00036 CONDUCTOR_FID      = "TPE3"
00037 REMIXER_FID        = "TPE4"
00038 COMPOSER_FID       = "TCOM"
00039 ARTIST_FIDS        = [ARTIST_FID, BAND_FID, CONDUCTOR_FID,
00040                       REMIXER_FID, COMPOSER_FID]
00041 ALBUM_FID          = "TALB"
00042 TITLE_FID          = "TIT2"
00043 SUBTITLE_FID       = "TIT3"
00044 CONTENT_TITLE_FID  = "TIT1"
00045 TITLE_FIDS         = [TITLE_FID, SUBTITLE_FID, CONTENT_TITLE_FID]
00046 COMMENT_FID        = "COMM"
00047 LYRICS_FID         = "USLT"
00048 GENRE_FID          = "TCON"
00049 TRACKNUM_FID       = "TRCK"
00050 DISCNUM_FID        = "TPOS"
00051 USERTEXT_FID       = "TXXX"
00052 CDID_FID           = "MCDI"
00053 IMAGE_FID          = "APIC"
00054 OBJECT_FID         = "GEOB"
00055 URL_COMMERCIAL_FID = "WCOM"
00056 URL_COPYRIGHT_FID  = "WCOP"
00057 URL_AUDIOFILE_FID  = "WOAF"
00058 URL_ARTIST_FID     = "WOAR"
00059 URL_AUDIOSRC_FID   = "WOAS"
00060 URL_INET_RADIO_FID = "WORS"
00061 URL_PAYMENT_FID    = "WPAY"
00062 URL_PUBLISHER_FID  = "WPUB"
00063 URL_FIDS           = [URL_COMMERCIAL_FID, URL_COPYRIGHT_FID,
00064                       URL_AUDIOFILE_FID, URL_ARTIST_FID, URL_AUDIOSRC_FID,
00065                       URL_INET_RADIO_FID, URL_PAYMENT_FID,
00066                       URL_PUBLISHER_FID]
00067 USERURL_FID        = "WXXX"
00068 PLAYCOUNT_FID      = "PCNT"
00069 UNIQUE_FILE_ID_FID = "UFID"
00070 BPM_FID            = "TBPM"
00071 PUBLISHER_FID      = "TPUB"
00072 
00073 obsoleteFrames = {"EQUA": "Equalisation",
00074                   "IPLS": "Involved people list",
00075                   "RVAD": "Relative volume adjustment",
00076                   "TDAT": "Date",
00077                   "TORY": "Original release year",
00078                   "TRDA": "Recording dates",
00079                   "TYER": "Year"}
00080 # Both of these are "coerced" into a v2.4 TDRC frame when read, and 
00081 # recreated when saving v2.3.
00082 OBSOLETE_DATE_FID            = "TDAT"
00083 OBSOLETE_YEAR_FID            = "TYER"
00084 OBSOLETE_TIME_FID            = "TIME"
00085 OBSOLETE_ORIG_RELEASE_FID    = "TORY"
00086 OBSOLETE_RECORDING_DATE_FID  = "TRDA"
00087 
00088 DATE_FIDS          = ["TDRL", "TDOR", "TDRC", OBSOLETE_YEAR_FID,
00089                       OBSOLETE_DATE_FID]
00090 
00091 frameDesc = { "AENC": "Audio encryption",
00092               "APIC": "Attached picture",
00093               "ASPI": "Audio seek point index",
00094 
00095               "COMM": "Comments",
00096               "COMR": "Commercial frame",
00097 
00098               "ENCR": "Encryption method registration",
00099               "EQU2": "Equalisation (2)",
00100               "ETCO": "Event timing codes",
00101 
00102               "GEOB": "General encapsulated object",
00103               "GRID": "Group identification registration",
00104 
00105               "LINK": "Linked information",
00106 
00107               "MCDI": "Music CD identifier",
00108               "MLLT": "MPEG location lookup table",
00109 
00110               "OWNE": "Ownership frame",
00111 
00112               "PRIV": "Private frame",
00113               "PCNT": "Play counter",
00114               "POPM": "Popularimeter",
00115               "POSS": "Position synchronisation frame",
00116 
00117               "RBUF": "Recommended buffer size",
00118               "RVA2": "Relative volume adjustment (2)",
00119               "RVRB": "Reverb",
00120 
00121               "SEEK": "Seek frame",
00122               "SIGN": "Signature frame",
00123               "SYLT": "Synchronised lyric/text",
00124               "SYTC": "Synchronised tempo codes",
00125 
00126               "TALB": "Album/Movie/Show title",
00127               "TBPM": "BPM (beats per minute)",
00128               "TCOM": "Composer",
00129               "TCON": "Content type",
00130               "TCOP": "Copyright message",
00131               "TDEN": "Encoding time",
00132               "TDLY": "Playlist delay",
00133               "TDOR": "Original release time",
00134               "TDRC": "Recording time",
00135               "TDRL": "Release time",
00136               "TDTG": "Tagging time",
00137               "TENC": "Encoded by",
00138               "TEXT": "Lyricist/Text writer",
00139               "TFLT": "File type",
00140               "TIPL": "Involved people list",
00141               "TIT1": "Content group description",
00142               "TIT2": "Title/songname/content description",
00143               "TIT3": "Subtitle/Description refinement",
00144               "TKEY": "Initial key",
00145               "TLAN": "Language(s)",
00146               "TLEN": "Length",
00147               "TMCL": "Musician credits list",
00148               "TMED": "Media type",
00149               "TMOO": "Mood",
00150               "TOAL": "Original album/movie/show title",
00151               "TOFN": "Original filename",
00152               "TOLY": "Original lyricist(s)/text writer(s)",
00153               "TOPE": "Original artist(s)/performer(s)",
00154               "TOWN": "File owner/licensee",
00155               "TPE1": "Lead performer(s)/Soloist(s)",
00156               "TPE2": "Band/orchestra/accompaniment",
00157               "TPE3": "Conductor/performer refinement",
00158               "TPE4": "Interpreted, remixed, or otherwise modified by",
00159               "TPOS": "Part of a set",
00160               "TPRO": "Produced notice",
00161               "TPUB": "Publisher",
00162               "TRCK": "Track number/Position in set",
00163               "TRSN": "Internet radio station name",
00164               "TRSO": "Internet radio station owner",
00165               "TSOA": "Album sort order",
00166               "TSOP": "Performer sort order",
00167               "TSOT": "Title sort order",
00168               "TSRC": "ISRC (international standard recording code)",
00169               "TSSE": "Software/Hardware and settings used for encoding",
00170               "TSST": "Set subtitle",
00171               "TXXX": "User defined text information frame",
00172 
00173               "UFID": "Unique file identifier",
00174               "USER": "Terms of use",
00175               "USLT": "Unsynchronised lyric/text transcription",
00176 
00177               "WCOM": "Commercial information",
00178               "WCOP": "Copyright/Legal information",
00179               "WOAF": "Official audio file webpage",
00180               "WOAR": "Official artist/performer webpage",
00181               "WOAS": "Official audio source webpage",
00182               "WORS": "Official Internet radio station homepage",
00183               "WPAY": "Payment",
00184               "WPUB": "Publishers official webpage",
00185               "WXXX": "User defined URL link frame" }
00186 
00187 
00188 # mapping of 2.2 frames to 2.3/2.4
00189 TAGS2_2_TO_TAGS_2_3_AND_4 = {
00190     "TT1" : "TIT1", # CONTENTGROUP content group description
00191     "TT2" : "TIT2", # TITLE title/songname/content description
00192     "TT3" : "TIT3", # SUBTITLE subtitle/description refinement
00193     "TP1" : "TPE1", # ARTIST lead performer(s)/soloist(s)
00194     "TP2" : "TPE2", # BAND band/orchestra/accompaniment
00195     "TP3" : "TPE3", # CONDUCTOR conductor/performer refinement
00196     "TP4" : "TPE4", # MIXARTIST interpreted, remixed, modified by
00197     "TCM" : "TCOM", # COMPOSER composer
00198     "TXT" : "TEXT", # LYRICIST lyricist/text writer
00199     "TLA" : "TLAN", # LANGUAGE language(s)
00200     "TCO" : "TCON", # CONTENTTYPE content type
00201     "TAL" : "TALB", # ALBUM album/movie/show title
00202     "TRK" : "TRCK", # TRACKNUM track number/position in set
00203     "TPA" : "TPOS", # PARTINSET part of set
00204     "TRC" : "TSRC", # ISRC international standard recording code
00205     "TDA" : "TDAT", # DATE date
00206     "TYE" : "TYER", # YEAR year
00207     "TIM" : "TIME", # TIME time
00208     "TRD" : "TRDA", # RECORDINGDATES recording dates
00209     "TOR" : "TORY", # ORIGYEAR original release year
00210     "TBP" : "TBPM", # BPM beats per minute
00211     "TMT" : "TMED", # MEDIATYPE media type
00212     "TFT" : "TFLT", # FILETYPE file type
00213     "TCR" : "TCOP", # COPYRIGHT copyright message
00214     "TPB" : "TPUB", # PUBLISHER publisher
00215     "TEN" : "TENC", # ENCODEDBY encoded by
00216     "TSS" : "TSSE", # ENCODERSETTINGS software/hardware + settings for encoding
00217     "TLE" : "TLEN", # SONGLEN length (ms)
00218     "TSI" : "TSIZ", # SIZE size (bytes)
00219     "TDY" : "TDLY", # PLAYLISTDELAY playlist delay
00220     "TKE" : "TKEY", # INITIALKEY initial key
00221     "TOT" : "TOAL", # ORIGALBUM original album/movie/show title
00222     "TOF" : "TOFN", # ORIGFILENAME original filename
00223     "TOA" : "TOPE", # ORIGARTIST original artist(s)/performer(s)
00224     "TOL" : "TOLY", # ORIGLYRICIST original lyricist(s)/text writer(s)
00225     "TXX" : "TXXX", # USERTEXT user defined text information frame
00226     "WAF" : "WOAF", # WWWAUDIOFILE official audio file webpage
00227     "WAR" : "WOAR", # WWWARTIST official artist/performer webpage
00228     "WAS" : "WOAS", # WWWAUDIOSOURCE official audion source webpage
00229     "WCM" : "WCOM", # WWWCOMMERCIALINFO commercial information
00230     "WCP" : "WCOP", # WWWCOPYRIGHT copyright/legal information
00231     "WPB" : "WPUB", # WWWPUBLISHER publishers official webpage
00232     "WXX" : "WXXX", # WWWUSER user defined URL link frame
00233     "IPL" : "IPLS", # INVOLVEDPEOPLE involved people list
00234     "ULT" : "USLT", # UNSYNCEDLYRICS unsynchronised lyrics/text transcription
00235     "COM" : "COMM", # COMMENT comments
00236     "UFI" : "UFID", # UNIQUEFILEID unique file identifier
00237     "MCI" : "MCDI", # CDID music CD identifier
00238     "ETC" : "ETCO", # EVENTTIMING event timing codes
00239     "MLL" : "MLLT", # MPEGLOOKUP MPEG location lookup table
00240     "STC" : "SYTC", # SYNCEDTEMPO synchronised tempo codes
00241     "SLT" : "SYLT", # SYNCEDLYRICS synchronised lyrics/text
00242     "RVA" : "RVAD", # VOLUMEADJ relative volume adjustment
00243     "EQU" : "EQUA", # EQUALIZATION equalization
00244     "REV" : "RVRB", # REVERB reverb
00245     "PIC" : "APIC", # PICTURE attached picture
00246     "GEO" : "GEOB", # GENERALOBJECT general encapsulated object
00247     "CNT" : "PCNT", # PLAYCOUNTER play counter
00248     "POP" : "POPM", # POPULARIMETER popularimeter
00249     "BUF" : "RBUF", # BUFFERSIZE recommended buffer size
00250     "CRA" : "AENC", # AUDIOCRYPTO audio encryption
00251     "LNK" : "LINK", # LINKEDINFO linked information
00252     # Extension workarounds i.e., ignore them
00253     "TCP" : "TCP ", # iTunes "extension" for compilation marking
00254     "CM1" : "CM1 "  # Seems to be some script kiddie tagging the tag.
00255                     # For example, [rH] join #rH on efnet [rH]
00256 }
00257 
00258 
00259 NULL_FRAME_FLAGS = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
00260 
00261 TEXT_FRAME_RX = re.compile("^T[A-Z0-9][A-Z0-9][A-Z0-9]$");
00262 USERTEXT_FRAME_RX = re.compile("^" + USERTEXT_FID + "$");
00263 URL_FRAME_RX = re.compile("^W[A-Z0-9][A-Z0-9][A-Z0-9]$");
00264 USERURL_FRAME_RX = re.compile("^" + USERURL_FID + "$");
00265 COMMENT_FRAME_RX = re.compile("^" + COMMENT_FID + "$");
00266 LYRICS_FRAME_RX = re.compile("^" + LYRICS_FID + "$");
00267 CDID_FRAME_RX = re.compile("^" + CDID_FID + "$");
00268 IMAGE_FRAME_RX = re.compile("^" + IMAGE_FID + "$");
00269 OBJECT_FRAME_RX = re.compile("^" + OBJECT_FID + "$");
00270 PLAYCOUNT_FRAME_RX = re.compile("^" + PLAYCOUNT_FID + "$");
00271 UNIQUE_FILE_ID_FRAME_RX = re.compile("^" + UNIQUE_FILE_ID_FID + "$");
00272 
00273 # MP3ext causes illegal frames to be inserted, which must be ignored.
00274 # Copied from http://shell.lab49.com/~vivake/python/MP3Info.py
00275 # Henning Kiel <henning.kiel@rwth-aachen.de>
00276 KNOWN_BAD_FRAMES = [
00277     "\x00\x00MP",
00278     "\x00MP3",
00279     " MP3",
00280     "MP3e",
00281     "\x00MP",
00282     " MP",
00283     "MP3",
00284     "COM ",
00285     "TCP ", # iTunes
00286     "CM1 "  # Script kiddie
00287 ]
00288 
00289 LATIN1_ENCODING   = "\x00";
00290 UTF_16_ENCODING   = "\x01";
00291 UTF_16BE_ENCODING = "\x02";
00292 UTF_8_ENCODING    = "\x03";
00293 
00294 DEFAULT_ENCODING = LATIN1_ENCODING;
00295 DEFAULT_ID3_MAJOR_VERSION = 2;
00296 DEFAULT_ID3_MINOR_VERSION = 4;
00297 DEFAULT_LANG = "eng";
00298 
00299 def cleanNulls(s):
00300     return "/".join([x for x in s.split('\x00') if x])
00301 
00302 def id3EncodingToString(encoding):
00303     if encoding == LATIN1_ENCODING:
00304         return "latin_1";
00305     elif encoding == UTF_8_ENCODING:
00306         return "utf_8";
00307     elif encoding == UTF_16_ENCODING:
00308         return "utf_16";
00309     elif encoding == UTF_16BE_ENCODING:
00310         return "utf_16_be";
00311     else:
00312         if strictID3():
00313             raise ValueError;
00314         else:
00315             return "latin_1";
00316 
00317 ################################################################################
00318 class FrameException(Exception):
00319     '''Thrown by invalid frames'''
00320     pass;
00321 
00322 ################################################################################
00323 class FrameHeader:
00324    FRAME_HEADER_SIZE = 10;
00325    # The tag header
00326    majorVersion = DEFAULT_ID3_MAJOR_VERSION;
00327    minorVersion = DEFAULT_ID3_MINOR_VERSION;
00328    # The 4 character frame ID.
00329    id = None;
00330    # An array of 16 "bits"...
00331    flags = NULL_FRAME_FLAGS;
00332    # ...and the info they store.
00333    tagAlter = 0;
00334    fileAlter = 0;
00335    readOnly = 0;
00336    compressed = 0;
00337    encrypted = 0;
00338    grouped = 0;
00339    unsync = 0;
00340    dataLenIndicator = 0;
00341    # The size of the data following this header.
00342    dataSize = 0;
00343 
00344    # 2.4 not only added flag bits, but also reordered the previously defined
00345    # flags.  So these are mapped once we know the version.
00346    TAG_ALTER   = None;
00347    FILE_ALTER  = None;
00348    READ_ONLY   = None;
00349    COMPRESSION = None;
00350    ENCRYPTION  = None;
00351    GROUPING    = None;
00352    UNSYNC      = None;
00353    DATA_LEN    = None;
00354 
00355    # Constructor.
00356    def __init__(self, tagHeader = None):
00357       if tagHeader:
00358          self.setVersion(tagHeader);
00359       else:
00360          self.setVersion([DEFAULT_ID3_MAJOR_VERSION,
00361                           DEFAULT_ID3_MINOR_VERSION]);
00362 
00363    def setVersion(self, tagHeader):
00364       # A slight hack to make the default ctor work.
00365       if isinstance(tagHeader, list):
00366          self.majorVersion = tagHeader[0];
00367          self.minorVersion = tagHeader[1];
00368       else:
00369          self.majorVersion = tagHeader.majorVersion;
00370          self.minorVersion = tagHeader.minorVersion;
00371       # Correctly set size of header
00372       if self.minorVersion == 2:
00373          self.FRAME_HEADER_SIZE = 6;
00374       else:
00375          self.FRAME_HEADER_SIZE = 10;
00376       self.setBitMask();
00377 
00378    def setBitMask(self):
00379       major = self.majorVersion;
00380       minor = self.minorVersion;
00381 
00382       # 1.x tags are converted to 2.4 frames internally.  These frames are
00383       # created with frame flags \x00.
00384       if (major == 2 and minor == 2):
00385           # no flags for 2.2 frames
00386           pass;
00387       elif (major == 2 and minor == 3):
00388          self.TAG_ALTER   = 0;
00389          self.FILE_ALTER  = 1;
00390          self.READ_ONLY   = 2;
00391          self.COMPRESSION = 8;
00392          self.ENCRYPTION  = 9;
00393          self.GROUPING    = 10;
00394          # This is not really in 2.3 frame header flags, but there is
00395          # a "global" unsync bit in the tag header and that is written here
00396          # so access to the tag header is not required.
00397          self.UNSYNC      = 14;
00398          # And this is mapped to an used bit, so that 0 is returned.
00399          self.DATA_LEN    = 4;
00400       elif (major == 2 and minor == 4) or \
00401            (major == 1 and (minor == 0 or minor == 1)):
00402          self.TAG_ALTER   = 1;
00403          self.FILE_ALTER  = 2;
00404          self.READ_ONLY   = 3;
00405          self.COMPRESSION = 12;
00406          self.ENCRYPTION  = 13;
00407          self.GROUPING    = 9;
00408          self.UNSYNC      = 14;
00409          self.DATA_LEN    = 15;
00410       else:
00411          raise ValueError("ID3 v" + str(major) + "." + str(minor) +\
00412                           " is not supported.");
00413 
00414    def render(self, dataSize):
00415       data = self.id;
00416 
00417       if self.minorVersion == 3:
00418          data += bin2bytes(dec2bin(dataSize, 32));
00419       else:
00420          data += bin2bytes(bin2synchsafe(dec2bin(dataSize, 32)));
00421 
00422       self.setBitMask();
00423       self.flags = NULL_FRAME_FLAGS;
00424       self.flags[self.TAG_ALTER] = self.tagAlter;
00425       self.flags[self.FILE_ALTER] = self.fileAlter;
00426       self.flags[self.READ_ONLY] = self.readOnly;
00427       self.flags[self.COMPRESSION] = self.compressed;
00428       self.flags[self.COMPRESSION] = self.compressed;
00429       self.flags[self.ENCRYPTION] = self.encrypted;
00430       self.flags[self.GROUPING] = self.grouped;
00431       self.flags[self.UNSYNC] = self.unsync;
00432       self.flags[self.DATA_LEN] = self.dataLenIndicator;
00433 
00434       data += bin2bytes(self.flags);
00435 
00436       return data;
00437 
00438    def parse2_2(self, f):
00439       frameId_22 = f.read(3);
00440       frameId = map2_2FrameId(frameId_22);
00441       if self.isFrameIdValid(frameId):
00442          TRACE_MSG("FrameHeader [id]: %s (0x%x%x%x)" % (frameId_22,
00443                                                         ord(frameId_22[0]),
00444                                                         ord(frameId_22[1]),
00445                                                         ord(frameId_22[2])));
00446          self.id = frameId;
00447          # dataSize corresponds to the size of the data segment after
00448          # encryption, compression, and unsynchronization.
00449          sz = f.read(3);
00450          self.dataSize = bin2dec(bytes2bin(sz, 8));
00451          TRACE_MSG("FrameHeader [data size]: %d (0x%X)" % (self.dataSize,
00452                                                            self.dataSize));
00453          return True
00454       elif frameId == '\x00\x00\x00':
00455          TRACE_MSG("FrameHeader: Null frame id found at byte " +\
00456                    str(f.tell()));
00457       elif not strictID3() and frameId in KNOWN_BAD_FRAMES:
00458          TRACE_MSG("FrameHeader: Illegal but known frame found; "\
00459                    "Happily ignoring" + str(f.tell()));
00460       elif strictID3():
00461          raise FrameException("FrameHeader: Illegal Frame ID: " + frameId);
00462 
00463       return False
00464 
00465 
00466    # Returns 1 on success and 0 when a null tag (marking the beginning of
00467    # padding).  In the case of an invalid frame header, a FrameException is 
00468    # thrown.
00469    def parse(self, f):
00470       TRACE_MSG("FrameHeader [start byte]: %d (0x%X)" % (f.tell(),
00471                                                          f.tell()));
00472       if self.minorVersion == 2:
00473           return self.parse2_2(f)
00474 
00475       frameId = f.read(4);
00476       if self.isFrameIdValid(frameId):
00477          TRACE_MSG("FrameHeader [id]: %s (0x%x%x%x%x)" % (frameId,
00478                                                           ord(frameId[0]),
00479                                                           ord(frameId[1]),
00480                                                           ord(frameId[2]),
00481                                                           ord(frameId[3])));
00482          self.id = frameId;
00483          # dataSize corresponds to the size of the data segment after
00484          # encryption, compression, and unsynchronization.
00485          sz = f.read(4);
00486          # In ID3 v2.4 this value became a synch-safe integer, meaning only
00487          # the low 7 bits are used per byte.
00488          if self.minorVersion == 3:
00489             self.dataSize = bin2dec(bytes2bin(sz, 8));
00490          else:
00491             self.dataSize = bin2dec(bytes2bin(sz, 7));
00492          TRACE_MSG("FrameHeader [data size]: %d (0x%X)" % (self.dataSize,
00493                                                            self.dataSize));
00494 
00495          # Frame flags.
00496          flags = f.read(2);
00497          self.flags = bytes2bin(flags);
00498          self.tagAlter = self.flags[self.TAG_ALTER];
00499          self.fileAlter = self.flags[self.FILE_ALTER];
00500          self.readOnly = self.flags[self.READ_ONLY];
00501          self.compressed = self.flags[self.COMPRESSION];
00502          self.encrypted = self.flags[self.ENCRYPTION];
00503          self.grouped = self.flags[self.GROUPING];
00504          self.unsync = self.flags[self.UNSYNC];
00505          self.dataLenIndicator = self.flags[self.DATA_LEN];
00506          TRACE_MSG("FrameHeader [flags]: ta(%d) fa(%d) ro(%d) co(%d) "\
00507                    "en(%d) gr(%d) un(%d) dl(%d)" % (self.tagAlter,
00508                                                     self.fileAlter,
00509                                                     self.readOnly,
00510                                                     self.compressed,
00511                                                     self.encrypted,
00512                                                     self.grouped,
00513                                                     self.unsync,
00514                                                     self.dataLenIndicator));
00515          if self.minorVersion >= 4 and self.compressed and \
00516             not self.dataLenIndicator:
00517             raise FrameException("Invalid frame; compressed with no data "
00518                                  "length indicator");
00519 
00520          return True
00521       elif frameId == '\x00\x00\x00\x00':
00522          TRACE_MSG("FrameHeader: Null frame id found at byte " +\
00523                    str(f.tell()));
00524       elif not strictID3() and frameId in KNOWN_BAD_FRAMES:
00525          TRACE_MSG("FrameHeader: Illegal but known "\
00526                    "(possibly created by the shitty mp3ext) frame found; "\
00527                    "Happily ignoring!" + str(f.tell()));
00528       elif strictID3():
00529          raise FrameException("FrameHeader: Illegal Frame ID: " + frameId);
00530       return False
00531 
00532 
00533    def isFrameIdValid(self, id):
00534       return re.compile(r"^[A-Z0-9][A-Z0-9][A-Z0-9][A-Z0-9]$").match(id);
00535 
00536    def clearFlags(self):
00537       flags = [0] * 16;
00538 
00539 ################################################################################
00540 def unsyncData(data):
00541     output = []
00542     safe = True
00543     for val in data:
00544         if safe:
00545             output.append(val)
00546             if val == '\xff':
00547                 safe = False
00548         elif val == '\x00' or val >= '\xe0':
00549             output.append('\x00')
00550             output.append(val)
00551             safe = (val != '\xff')
00552         else:
00553             output.append(val)
00554             safe = True
00555     if not safe:
00556         output.append('\x00')
00557     return ''.join(output)
00558 
00559 def deunsyncData(data):
00560     output = []
00561     safe = True
00562     for val in data:
00563         if safe:
00564             output.append(val)
00565             safe = (val != '\xff')
00566         else:
00567             if val != '\x00':
00568                 output.append(val)
00569             safe = True
00570     return ''.join(output)
00571 
00572 
00573 ################################################################################
00574 class Frame:
00575 
00576    def __init__(self, frameHeader, unsync_default):
00577        self.header = None
00578        self.decompressedSize = 0
00579        self.groupId = 0
00580        self.encryptionMethod = 0
00581        self.dataLen = 0
00582        self.encoding = DEFAULT_ENCODING
00583        self.header = frameHeader
00584        self.unsync_default = unsync_default
00585 
00586    def __str__(self):
00587       desc = self.getFrameDesc();
00588       return '<%s Frame (%s)>' % (desc, self.header.id);
00589 
00590    def unsync(self, data):
00591        data = unsyncData(data)
00592        return data
00593 
00594    def deunsync(self, data):
00595        data = deunsyncData(data)
00596        return data
00597 
00598    def decompress(self, data):
00599       TRACE_MSG("before decompression: %d bytes" % len(data));
00600       data = zlib.decompress(data, 15, self.decompressedSize);
00601       TRACE_MSG("after decompression: %d bytes" % len(data));
00602       return data;
00603 
00604    def compress(self, data):
00605       TRACE_MSG("before compression: %d bytes" % len(data));
00606       data = zlib.compress(data);
00607       TRACE_MSG("after compression: %d bytes" % len(data));
00608       return data;
00609 
00610    def decrypt(self, data):
00611       raise FrameException("Encryption not supported");
00612 
00613    def encrypt(self, data):
00614       raise FrameException("Encryption not supported");
00615 
00616    def disassembleFrame(self, data):
00617       # Format flags in the frame header may add extra data to the
00618       # beginning of this data.
00619       if self.header.minorVersion <= 3:
00620          # 2.3:  compression(4), encryption(1), group(1) 
00621          if self.header.compressed:
00622             self.decompressedSize = bin2dec(bytes2bin(data[:4]));
00623             data = data[4:];
00624             TRACE_MSG("Decompressed Size: %d" % self.decompressedSize);
00625          if self.header.encrypted:
00626             self.encryptionMethod = bin2dec(bytes2bin(data[0]));
00627             data = data[1:];
00628             TRACE_MSG("Encryption Method: %d" % self.encryptionMethod);
00629          if self.header.grouped:
00630             self.groupId = bin2dec(bytes2bin(data[0]));
00631             data = data[1:];
00632             TRACE_MSG("Group ID: %d" % self.groupId);
00633       else:
00634          # 2.4:  group(1), encrypted(1), dataLenIndicator(4,7)
00635          if self.header.grouped:
00636             self.groupId = bin2dec(bytes2bin(data[0]));
00637             data = data[1:];
00638          if self.header.encrypted:
00639             self.encryptionMethod = bin2dec(bytes2bin(data[0]));
00640             data = data[1:];
00641             TRACE_MSG("Encryption Method: %d" % self.encryptionMethod);
00642             TRACE_MSG("Group ID: %d" % self.groupId);
00643          if self.header.dataLenIndicator:
00644             self.dataLen = bin2dec(bytes2bin(data[:4], 7));
00645             data = data[4:];
00646             TRACE_MSG("Data Length: %d" % self.dataLen);
00647             if self.header.compressed:
00648                self.decompressedSize = self.dataLen;
00649                TRACE_MSG("Decompressed Size: %d" % self.decompressedSize);
00650 
00651       if self.header.unsync or self.unsync_default:
00652          data = self.deunsync(data)
00653       if self.header.encrypted:
00654          data = self.decrypt(data);
00655       if self.header.compressed:
00656          data = self.decompress(data);
00657       return data;
00658 
00659    def assembleFrame (self, data):
00660       formatFlagData = "";
00661       if self.header.minorVersion == 3:
00662          if self.header.compressed:
00663             formatFlagData += bin2bytes(dec2bin(len(data), 32));
00664          if self.header.encrypted:
00665             formatFlagData += bin2bytes(dec2bin(self.encryptionMethod, 8));
00666          if self.header.grouped:
00667             formatFlagData += bin2bytes(dec2bin(self.groupId, 8));
00668       else:
00669          if self.header.grouped:
00670             formatFlagData += bin2bytes(dec2bin(self.groupId, 8));
00671          if self.header.encrypted:
00672             formatFlagData += bin2bytes(dec2bin(self.encryptionMethod, 8));
00673          if self.header.compressed or self.header.dataLenIndicator:
00674             # Just in case, not sure about this?
00675             self.header.dataLenIndicator = 1;
00676             formatFlagData += bin2bytes(dec2bin(len(data), 32));
00677 
00678       if self.header.compressed:
00679           data = self.compress(data);
00680       if self.header.encrypted:
00681           data = self.encrypt(data);
00682       if self.header.unsync or self.unsync_default:
00683           data = self.unsync(data)
00684 
00685       data = formatFlagData + data;
00686       return self.header.render(len(data)) + data;
00687 
00688    def getFrameDesc(self):
00689       try:
00690          return frameDesc[self.header.id];
00691       except KeyError:
00692          try:
00693             return obsoleteFrames[self.header.id];
00694          except KeyError:
00695             return "UNKOWN FRAME";
00696 
00697    def getTextDelim(self):
00698        if self.encoding == UTF_16_ENCODING or \
00699           self.encoding == UTF_16BE_ENCODING:
00700            return "\x00\x00";
00701        else:
00702            return "\x00";
00703 
00704 ################################################################################
00705 class TextFrame(Frame):
00706    text = u"";
00707 
00708    # Data string format:
00709    # encoding (one byte) + text
00710    def __init__(self, frameHeader, data=None, text=u"",
00711                 encoding=DEFAULT_ENCODING, unsync_default=False):
00712       Frame.__init__(self, frameHeader, unsync_default)
00713       if data != None:
00714           self._set(data, frameHeader);
00715           return;
00716       else:
00717           assert(text != None and isinstance(text, unicode));
00718           self.encoding = encoding;
00719           self.text = text;
00720 
00721    # Data string format:
00722    # encoding (one byte) + text;
00723    def _set(self, data, frameHeader):
00724       fid = frameHeader.id;
00725       if not TEXT_FRAME_RX.match(fid) or USERTEXT_FRAME_RX.match(fid):
00726          raise FrameException("Invalid frame id for TextFrame: " + fid);
00727 
00728       data = self.disassembleFrame(data);
00729       self.encoding = data[0];
00730       TRACE_MSG("TextFrame encoding: %s" % id3EncodingToString(self.encoding));
00731       try:
00732           data = data[1:]
00733           self.text = encodeUnicode(data, id3EncodingToString(self.encoding))
00734           if not strictID3():
00735               self.text = cleanNulls(self.text)
00736       except TypeError, excArg:
00737           # if data is already unicode, just copy it
00738           if excArg.args == ("decoding Unicode is not supported",):
00739               self.text = data
00740               if not strictID3():
00741                  self.text = cleanNulls(self.text)
00742           else:
00743               raise;
00744 
00745       TRACE_MSG("TextFrame text: %s" % self.text);
00746 
00747    def __unicode__(self):
00748       return u'<%s (%s): %s>' % (self.getFrameDesc(), self.header.id,
00749                                  self.text);
00750 
00751    def render(self):
00752        if self.header.minorVersion == 4 and self.header.id == "TSIZ":
00753            TRACE_MSG("Dropping deprecated frame TSIZ")
00754            return ""
00755        data = self.encoding +\
00756               self.text.encode(id3EncodingToString(self.encoding));
00757        return self.assembleFrame(data);
00758 
00759 ################################################################################
00760 class DateFrame(TextFrame):
00761    date = None;
00762    date_str = u"";
00763 
00764    def __init__(self, frameHeader, data=None, date_str=None,
00765                 encoding=DEFAULT_ENCODING, unsync_default=False):
00766       if data != None:
00767           TextFrame.__init__(self, frameHeader, data=data,
00768                              encoding=encoding, unsync_default=unsync_default)
00769           self._set(data, frameHeader)
00770       else:
00771           assert(date_str and isinstance(date_str, unicode))
00772           TextFrame.__init__(self, frameHeader, text=date_str,
00773                              encoding=encoding, unsync_default=unsync_default)
00774       self.setDate(self.text)
00775 
00776    def _set(self, data, frameHeader):
00777       TextFrame._set(self, data, frameHeader)
00778       if self.header.id[:2] != "TD" and self.header.minorVersion >= 4:
00779          raise FrameException("Invalid frame id for DateFrame: " + \
00780                               self.header.id)
00781 
00782    def setDate(self, d):
00783       if not d:
00784          self.date = None
00785          self.date_str = u""
00786          return
00787 
00788       for fmt in timeStampFormats:
00789          try:
00790             if isinstance(d, tuple):
00791                self.date_str = unicode(time.strftime(fmt, d))
00792                self.date = d
00793             else:
00794                assert(isinstance(d, unicode))
00795                # Witnessed oddball tags with NULL bytes (ozzy.tag from id3lib)
00796                d = d.strip("\x00")
00797 
00798                try:
00799                   self.date = time.strptime(d, fmt)
00800                except TypeError, ex:
00801                   continue
00802                self.date_str = d
00803             break
00804          except ValueError:
00805             self.date = None
00806             self.date_str = u""
00807             continue
00808       if strictID3() and not self.date:
00809          raise FrameException("Invalid Date: " + str(d))
00810       self.text = self.date_str
00811 
00812    def getDate(self):
00813       return self.date_str
00814 
00815    def getYear(self):
00816       if self.date:
00817          return self.__padDateField(self.date[0], 4)
00818       else:
00819          return None
00820 
00821    def getMonth(self):
00822       if self.date:
00823          return self.__padDateField(self.date[1], 2);
00824       else:
00825          return None;
00826 
00827    def getDay(self):
00828       if self.date:
00829          return self.__padDateField(self.date[2], 2);
00830       else:
00831          return None;
00832 
00833    def getHour(self):
00834       if self.date:
00835          return self.__padDateField(self.date[3], 2);
00836       else:
00837          return None;
00838 
00839    def getMinute(self):
00840       if self.date:
00841          return self.__padDateField(self.date[4], 2);
00842       else:
00843          return None;
00844 
00845    def getSecond(self):
00846       if self.date:
00847          return self.__padDateField(self.date[5], 2);
00848       else:
00849          return None;
00850 
00851    def __padDateField(self, f, sz):
00852       fStr = str(f);
00853       if len(fStr) == sz:
00854          pass;
00855       elif len(fStr) < sz:
00856          fStr = ("0" * (sz - len(fStr))) + fStr;
00857       else:
00858          raise TagException("Invalid date field: " + fStr);
00859       return fStr;
00860 
00861    def render(self):
00862        # Conversion crap
00863        if self.header.minorVersion == 4 and\
00864           (self.header.id == OBSOLETE_DATE_FID or\
00865            self.header.id == OBSOLETE_YEAR_FID or\
00866            self.header.id == OBSOLETE_TIME_FID or\
00867            self.header.id == OBSOLETE_RECORDING_DATE_FID):
00868            self.header.id = "TDRC";
00869        elif self.header.minorVersion == 4 and\
00870             self.header.id == OBSOLETE_ORIG_RELEASE_FID:
00871            self.header.id = "TDOR";
00872        elif self.header.minorVersion == 3 and self.header.id == "TDOR":
00873            self.header.id = OBSOLETE_ORIG_RELEASE_FID;
00874        elif self.header.minorVersion == 3 and self.header.id == "TDEN":
00875            TRACE_MSG('Converting TDEN to TXXX(Encoding time) frame')
00876            self.header.id = "TXXX";
00877            self.description = "Encoding time";
00878            data = self.encoding +\
00879                   self.description.encode(id3EncodingToString(self.encoding)) +\
00880                   self.getTextDelim() +\
00881                   self.date_str.encode(id3EncodingToString(self.encoding));
00882            return self.assembleFrame(data)
00883 
00884        elif self.header.minorVersion == 3 and self.header.id[:2] == "TD":
00885            if self.header.id not in ['TDEN', 'TDLY', 'TDTG']:
00886               self.header.id = OBSOLETE_YEAR_FID;
00887 
00888        data = self.encoding +\
00889               self.date_str.encode(id3EncodingToString(self.encoding));
00890        data = self.assembleFrame(data);
00891        return data;
00892 
00893 
00894 ################################################################################
00895 class UserTextFrame(TextFrame):
00896    description = u""
00897 
00898    # Data string format:
00899    # encoding (one byte) + description + "\x00" + text
00900    def __init__(self, frameHeader, data=None, description=u"", text=u"",
00901                 encoding=DEFAULT_ENCODING, unsync_default=False):
00902        if data != None:
00903            TextFrame.__init__(self, frameHeader, data=data,
00904                               unsync_default=unsync_default)
00905            self._set(data, frameHeader)
00906        else:
00907            assert(isinstance(description, unicode) and\
00908                   isinstance(text, unicode))
00909            TextFrame.__init__(self, frameHeader, text=text, encoding=encoding,
00910                               unsync_default=unsync_default)
00911            self.description = description
00912 
00913    # Data string format:
00914    # encoding (one byte) + description + "\x00" + text
00915    def _set(self, data, frameHeader = None):
00916       assert(frameHeader)
00917       if not USERTEXT_FRAME_RX.match(frameHeader.id):
00918          raise FrameException("Invalid frame id for UserTextFrame: " +
00919                               frameHeader.id)
00920 
00921       data = self.disassembleFrame(data)
00922       self.encoding = data[0]
00923       TRACE_MSG("UserTextFrame encoding: %s" %\
00924                 id3EncodingToString(self.encoding))
00925       (d, t) = splitUnicode(data[1:], self.encoding)
00926       self.description = encodeUnicode(d, id3EncodingToString(self.encoding))
00927       TRACE_MSG("UserTextFrame description: %s" % self.description)
00928       self.text = encodeUnicode(t, id3EncodingToString(self.encoding))
00929       if not strictID3():
00930           self.text = cleanNulls(self.text)
00931       TRACE_MSG("UserTextFrame text: %s" % self.text)
00932 
00933    def render(self):
00934       if self.header.minorVersion == 4:
00935          if self.description.lower() == 'tagging time':
00936             TRACE_MSG("Converting TXXX(%s) to TDTG frame)" % self.description)
00937             return ""
00938          if self.description.lower() == 'encoding time':
00939             TRACE_MSG("Converting TXXX(%s) to TDEN frame" % self.description)
00940             self.header.id = 'TDEN'
00941             data = self.encoding +\
00942                    self.text.encode(id3EncodingToString(self.encoding))
00943             return self.assembleFrame(data)
00944       data = self.encoding +\
00945              self.description.encode(id3EncodingToString(self.encoding)) +\
00946              self.getTextDelim() +\
00947              self.text.encode(id3EncodingToString(self.encoding))
00948       return self.assembleFrame(data)
00949 
00950    def __unicode__(self):
00951        return u'<%s (%s): {Desc: %s} %s>' % (self.getFrameDesc(),
00952                                             self.header.id,
00953                                             self.description, self.text)
00954 
00955 ################################################################################
00956 class URLFrame(Frame):
00957    url = ""
00958 
00959    # Data string format:
00960    # url
00961    def __init__(self, frameHeader, data=None, url=None, unsync_default=False):
00962        Frame.__init__(self, frameHeader, unsync_default)
00963        if data != None:
00964            self._set(data, frameHeader)
00965        else:
00966            assert(url)
00967            self.url = url
00968 
00969    # Data string format:
00970    # url (ascii)
00971    def _set(self, data, frameHeader):
00972       fid = frameHeader.id
00973       if not URL_FRAME_RX.match(fid) or USERURL_FRAME_RX.match(fid):
00974           raise FrameException("Invalid frame id for URLFrame: " + fid)
00975       data = self.disassembleFrame(data)
00976       self.url = data
00977       if not strictID3():
00978           self.url = cleanNulls(self.url)
00979 
00980    def render(self):
00981       data = str(self.url)
00982       return self.assembleFrame(data)
00983 
00984    def __str__(self):
00985       return '<%s (%s): %s>' % (self.getFrameDesc(), self.header.id, self.url)
00986 
00987 ################################################################################
00988 class UserURLFrame(URLFrame):
00989    description = u""
00990    url = ""
00991 
00992    # Data string format:
00993    # encoding (one byte) + description + "\x00" + url
00994    def __init__(self, frameHeader, data=None, url="", description=u"",
00995                 encoding=DEFAULT_ENCODING, unsync_default=False):
00996        Frame.__init__(self, frameHeader, unsync_default)
00997        if data:
00998            self._set(data, frameHeader)
00999        else:
01000            assert(encoding)
01001            assert(description and isinstance(description, unicode))
01002            assert(url and isinstance(url, str))
01003            self.encoding = encoding
01004            self.url = url
01005            self.description = description
01006 
01007    # Data string format:
01008    # encoding (one byte) + description + "\x00" + url;
01009    def _set(self, data, frameHeader):
01010       assert(data and frameHeader)
01011       if not USERURL_FRAME_RX.match(frameHeader.id):
01012          raise FrameException("Invalid frame id for UserURLFrame: " +\
01013                               frameHeader.id)
01014 
01015       data = self.disassembleFrame(data);
01016       self.encoding = data[0]
01017       TRACE_MSG("UserURLFrame encoding: %s" %\
01018                 id3EncodingToString(self.encoding))
01019       try:
01020           (d, u) = splitUnicode(data[1:], self.encoding)
01021       except ValueError, ex:
01022           if strictID3():
01023               raise FrameException("Invalid WXXX frame, no null byte")
01024           d = data[1:]
01025           u = ""
01026       self.description = encodeUnicode(d, id3EncodingToString(self.encoding))
01027       TRACE_MSG("UserURLFrame description: %s" % self.description)
01028       self.url = u
01029       if not strictID3():
01030           self.url = cleanNulls(self.url)
01031       TRACE_MSG("UserURLFrame text: %s" % self.url)
01032 
01033    def render(self):
01034       data = self.encoding +\
01035              self.description.encode(id3EncodingToString(self.encoding)) +\
01036              self.getTextDelim() + self.url
01037       return self.assembleFrame(data)
01038 
01039    def __unicode__(self):
01040       return u'<%s (%s): %s [Encoding: %s] [Desc: %s]>' %\
01041              (self.getFrameDesc(), self.header.id,
01042               self.url, self.encoding, self.description)
01043 
01044 ################################################################################
01045 class CommentFrame(Frame):
01046    lang = ""
01047    description = u""
01048    comment = u""
01049 
01050    # Data string format:
01051    # encoding (one byte) + lang (three byte code) + description + "\x00" +
01052    # text
01053    def __init__(self, frameHeader, data=None, lang="",
01054                 description=u"", comment=u"", encoding=DEFAULT_ENCODING,
01055                 unsync_default=False):
01056        Frame.__init__(self, frameHeader, unsync_default)
01057        if data != None:
01058            self._set(data, frameHeader)
01059        else:
01060            assert(isinstance(description, unicode))
01061            assert(isinstance(comment, unicode))
01062            assert(isinstance(lang, str))
01063            self.encoding = encoding
01064            self.lang = lang
01065            self.description = description
01066            self.comment = comment
01067 
01068    # Data string format:
01069    # encoding (one byte) + lang (three byte code) + description + "\x00" +
01070    # text
01071    def _set(self, data, frameHeader = None):
01072       assert(frameHeader)
01073       if not COMMENT_FRAME_RX.match(frameHeader.id):
01074          raise FrameException("Invalid frame id for CommentFrame: " +
01075                               frameHeader.id)
01076 
01077       data = self.disassembleFrame(data)
01078       self.encoding = data[0]
01079       TRACE_MSG("CommentFrame encoding: " + id3EncodingToString(self.encoding))
01080       try:
01081           self.lang = str(data[1:4]).strip("\x00")
01082           # Test ascii encoding
01083           temp_lang = encodeUnicode(self.lang, "ascii")
01084           if self.lang and \
01085              not re.compile("[A-Z][A-Z][A-Z]", re.IGNORECASE).match(self.lang):
01086              if strictID3():
01087                  raise FrameException("[CommentFrame] Invalid language "\
01088                                        "code: %s" % self.lang)
01089       except UnicodeDecodeError, ex:
01090           if strictID3():
01091               raise FrameException("[CommentFrame] Invalid language code: "
01092                                    "[%s] %s" % (ex.object, ex.reason))
01093           else:
01094               self.lang = ""
01095       try:
01096          (d, c) = splitUnicode(data[4:], self.encoding)
01097          self.description = encodeUnicode(d, id3EncodingToString(self.encoding))
01098          self.comment = encodeUnicode(c, id3EncodingToString(self.encoding))
01099       except ValueError:
01100           if strictID3():
01101               raise FrameException("Invalid comment; no description/comment")
01102           else:
01103               self.description = u""
01104               self.comment = u""
01105       if not strictID3():
01106           self.description = cleanNulls(self.description)
01107           self.comment = cleanNulls(self.comment)
01108 
01109    def render(self):
01110       lang = self.lang.encode("ascii")
01111       if len(lang) > 3:
01112           lang = lang[0:3]
01113       elif len(lang) < 3:
01114           lang = lang + ('\x00' * (3 - len(lang)))
01115       data = self.encoding + lang +\
01116              self.description.encode(id3EncodingToString(self.encoding)) +\
01117              self.getTextDelim() +\
01118              self.comment.encode(id3EncodingToString(self.encoding));
01119       return self.assembleFrame(data);
01120 
01121    def __unicode__(self):
01122       return u"<%s (%s): %s [Lang: %s] [Desc: %s]>" %\
01123              (self.getFrameDesc(), self.header.id, self.comment,
01124               self.lang, self.description);
01125 
01126 ################################################################################
01127 class LyricsFrame(Frame):
01128    lang = "";
01129    description = u"";
01130    lyrics = u"";
01131 
01132    # Data string format:
01133    # encoding (one byte) + lang (three byte code) + description + "\x00" +
01134    # text
01135    def __init__(self, frameHeader, data=None, lang="",
01136                 description=u"", lyrics=u"", encoding=DEFAULT_ENCODING,
01137                 unsync_default=False):
01138        Frame.__init__(self, frameHeader, unsync_default)
01139        if data != None:
01140            self._set(data, frameHeader)
01141        else:
01142            assert(isinstance(description, unicode))
01143            assert(isinstance(lyrics, unicode))
01144            assert(isinstance(lang, str))
01145            self.encoding = encoding
01146            self.lang = lang
01147            self.description = description
01148            self.lyrics = lyrics
01149 
01150    # Data string format:
01151    # encoding (one byte) + lang (three byte code) + description + "\x00" +
01152    # text
01153    def _set(self, data, frameHeader = None):
01154       assert(frameHeader)
01155       if not LYRICS_FRAME_RX.match(frameHeader.id):
01156          raise FrameException("Invalid frame id for LyricsFrame: " +\
01157                               frameHeader.id)
01158 
01159       data = self.disassembleFrame(data)
01160       self.encoding = data[0]
01161       TRACE_MSG("LyricsFrame encoding: " + id3EncodingToString(self.encoding))
01162       try:
01163           self.lang = str(data[1:4]).strip("\x00")
01164           # Test ascii encoding
01165           temp_lang = encodeUnicode(self.lang, "ascii")
01166           if self.lang and \
01167              not re.compile("[A-Z][A-Z][A-Z]", re.IGNORECASE).match(self.lang):
01168              if strictID3():
01169                  raise FrameException("[LyricsFrame] Invalid language "\
01170                                        "code: %s" % self.lang)
01171       except UnicodeDecodeError, ex:
01172           if strictID3():
01173               raise FrameException("[LyricsFrame] Invalid language code: "\
01174                                    "[%s] %s" % (ex.object, ex.reason))
01175           else:
01176               self.lang = "";
01177       try:
01178          (d, c) = splitUnicode(data[4:], self.encoding)
01179          self.description = encodeUnicode(d, id3EncodingToString(self.encoding))
01180          self.lyrics = encodeUnicode(c, id3EncodingToString(self.encoding))
01181       except ValueError:
01182           if strictID3():
01183               raise FrameException("Invalid lyrics; no description/lyrics")
01184           else:
01185               self.description = u""
01186               self.lyrics = u""
01187       if not strictID3():
01188           self.description = cleanNulls(self.description)
01189           self.lyrics = cleanNulls(self.lyrics)
01190 
01191    def render(self):
01192       lang = self.lang.encode("ascii")
01193       if len(lang) > 3:
01194           lang = lang[0:3]
01195       elif len(lang) < 3:
01196           lang = lang + ('\x00' * (3 - len(lang)))
01197       data = self.encoding + lang +\
01198              self.description.encode(id3EncodingToString(self.encoding)) +\
01199              self.getTextDelim() +\
01200              self.lyrics.encode(id3EncodingToString(self.encoding))
01201       return self.assembleFrame(data)
01202 
01203    def __unicode__(self):
01204       return u"<%s (%s): %s [Lang: %s] [Desc: %s]>" %\
01205              (self.getFrameDesc(), self.header.id, self.lyrics,
01206               self.lang, self.description)
01207 
01208 ################################################################################
01209 # This class refers to the APIC frame, otherwise known as an "attached
01210 # picture".
01211 class ImageFrame(Frame):
01212    mimeType = None
01213    pictureType = None
01214    description = u""
01215    # Contains the image data when the mimetype is image type.
01216    # Otherwise it is None.
01217    imageData = None
01218    # Contains a URL for the image when the mimetype is "-->" per the spec.
01219    # Otherwise it is None.
01220    imageURL = None
01221    # Declared "picture types".
01222    OTHER               = 0x00
01223    ICON                = 0x01 # 32x32 png only.
01224    OTHER_ICON          = 0x02
01225    FRONT_COVER         = 0x03
01226    BACK_COVER          = 0x04
01227    LEAFLET             = 0x05
01228    MEDIA               = 0x06 # label side of cd, picture disc vinyl, etc.
01229    LEAD_ARTIST         = 0x07
01230    ARTIST              = 0x08
01231    CONDUCTOR           = 0x09
01232    BAND                = 0x0A
01233    COMPOSER            = 0x0B
01234    LYRICIST            = 0x0C
01235    RECORDING_LOCATION  = 0x0D
01236    DURING_RECORDING    = 0x0E
01237    DURING_PERFORMANCE  = 0x0F
01238    VIDEO               = 0x10
01239    BRIGHT_COLORED_FISH = 0x11 # There's always room for porno.
01240    ILLUSTRATION        = 0x12
01241    BAND_LOGO           = 0x13
01242    PUBLISHER_LOGO      = 0x14
01243    MIN_TYPE            = OTHER
01244    MAX_TYPE            = PUBLISHER_LOGO
01245 
01246    def __init__(self, frameHeader, data=None,
01247                 description=u"",
01248                 imageData=None, imageURL=None,
01249                 pictureType=None, mimeType=None,
01250                 encoding=DEFAULT_ENCODING, unsync_default=False):
01251        Frame.__init__(self, frameHeader, unsync_default)
01252        if data != None:
01253            self._set(data, frameHeader)
01254        else:
01255            assert(isinstance(description, unicode))
01256            self.description = description
01257            self.encoding = encoding
01258            assert(mimeType)
01259            self.mimeType = mimeType
01260            assert(pictureType != None)
01261            self.pictureType = pictureType
01262            if imageData:
01263                self.imageData = imageData
01264            else:
01265                self.imageURL = imageURL
01266            assert(self.imageData or self.imageURL)
01267 
01268 
01269    # Factory method
01270    def create(type, imgFile, desc = u"", encoding = DEFAULT_ENCODING):
01271        if not isinstance(desc, unicode) or \
01272           not isinstance(type, int):
01273            raise FrameException("Wrong description and/or image-type type.");
01274        # Load img
01275        fp = file(imgFile, "rb");
01276        imgData = fp.read();
01277        mt = guess_mime_type(imgFile);
01278        if not mt:
01279            raise FrameException("Unable to guess mime-type for %s" % (imgFile));
01280 
01281        frameData = DEFAULT_ENCODING;
01282        frameData += mt + "\x00";
01283        frameData += bin2bytes(dec2bin(type, 8));
01284        frameData += desc.encode(id3EncodingToString(encoding)) + "\x00";
01285        frameData += imgData;
01286 
01287        frameHeader = FrameHeader();
01288        frameHeader.id = IMAGE_FID;
01289        return ImageFrame(frameHeader, data = frameData);
01290    # Make create a static method.  Odd....
01291    create = staticmethod(create);
01292 
01293    # Data string format:
01294    # <Header for 'Attached picture', ID: "APIC">
01295    #  Text encoding      $xx
01296    #  MIME type          <text string> $00
01297    #  Picture type       $xx
01298    #  Description        <text string according to encoding> $00 (00)
01299    #  Picture data       <binary data>
01300    def _set(self, data, frameHeader = None):
01301       assert(frameHeader);
01302       if not IMAGE_FRAME_RX.match(frameHeader.id):
01303          raise FrameException("Invalid frame id for ImageFrame: " +\
01304                               frameHeader.id);
01305 
01306       data = self.disassembleFrame(data);
01307 
01308       input = StringIO(data);
01309       TRACE_MSG("APIC frame data size: " + str(len(data)));
01310       self.encoding = input.read(1);
01311       TRACE_MSG("APIC encoding: " + id3EncodingToString(self.encoding));
01312 
01313       # Mime type
01314       self.mimeType = "";
01315       if self.header.minorVersion != 2:
01316           ch = input.read(1)
01317           while ch and ch != "\x00":
01318               self.mimeType += ch
01319               ch = input.read(1)
01320       else:
01321           # v2.2 (OBSOLETE) special case
01322           self.mimeType = input.read(3);
01323       TRACE_MSG("APIC mime type: " + self.mimeType);
01324       if strictID3() and not self.mimeType:
01325          raise FrameException("APIC frame does not contain a mime type");
01326       if self.mimeType.find("/") == -1:
01327          self.mimeType = "image/" + self.mimeType;
01328 
01329       pt = ord(input.read(1));
01330       TRACE_MSG("Initial APIC picture type: " + str(pt));
01331       if pt < self.MIN_TYPE or pt > self.MAX_TYPE:
01332           if strictID3():
01333               raise FrameException("Invalid APIC picture type: %d" % (pt));
01334           # Rather than force this to UNKNOWN, let's assume that they put a
01335           # character literal instead of it's byte value.
01336           try:
01337               pt = int(chr(pt))
01338           except:
01339               pt = self.OTHER
01340           if pt < self.MIN_TYPE or pt > self.MAX_TYPE:
01341               self.pictureType = self.OTHER
01342       self.pictureType = pt
01343       TRACE_MSG("APIC picture type: " + str(self.pictureType))
01344 
01345       self.desciption = u""
01346 
01347       # Remaining data is a NULL separated description and image data
01348       buffer = input.read()
01349       input.close()
01350 
01351       (desc, img) = splitUnicode(buffer, self.encoding)
01352       TRACE_MSG("description len: %d" % len(desc))
01353       TRACE_MSG("image len: %d" % len(img))
01354       self.description = encodeUnicode(desc, id3EncodingToString(self.encoding))
01355       TRACE_MSG("APIC description: " + self.description);
01356 
01357       if self.mimeType.find("-->") != -1:
01358          self.imageData = None
01359          self.imageURL = img
01360          TRACE_MSG("APIC image data: %d bytes" % 0)
01361       else:
01362          self.imageData = img
01363          self.imageURL = None
01364          TRACE_MSG("APIC image data: %d bytes" % len(self.imageData))
01365       if strictID3() and not self.imageData and not self.imageURL:
01366          raise FrameException("APIC frame does not contain any image data")
01367 
01368 
01369    def writeFile(self, path = "./", name = None):
01370       if not self.imageData:
01371          raise IOError("Fetching remote image files is not implemented.")
01372       if not name:
01373          name = self.getDefaultFileName()
01374       imageFile = os.path.join(path, name)
01375 
01376       f = file(imageFile, "wb");
01377       f.write(self.imageData);
01378       f.flush();
01379       f.close();
01380    def getDefaultFileName(self, suffix = ""):
01381       nameStr = self.picTypeToString(self.pictureType)
01382       if suffix:
01383           nameStr += suffix
01384       nameStr = nameStr +  "." + self.mimeType.split("/")[1]
01385       return nameStr
01386 
01387    def render(self):
01388       data = self.encoding + self.mimeType + "\x00" +\
01389              bin2bytes(dec2bin(self.pictureType, 8)) +\
01390              self.description.encode(id3EncodingToString(self.encoding)) +\
01391              self.getTextDelim()
01392       if self.imageURL:
01393           data += self.imageURL.encode("ascii");
01394       else:
01395           data += self.imageData;
01396       return self.assembleFrame(data);
01397 
01398    def stringToPicType(s):
01399        if s == "OTHER":
01400            return ImageFrame.OTHER;
01401        elif s == "ICON":
01402            return ImageFrame.ICON;
01403        elif s == "OTHER_ICON":
01404            return ImageFrame.OTHER_ICON;
01405        elif s == "FRONT_COVER":
01406            return ImageFrame.FRONT_COVER
01407        elif s == "BACK_COVER":
01408            return ImageFrame.BACK_COVER;
01409        elif s == "LEAFLET":
01410            return ImageFrame.LEAFLET;
01411        elif s == "MEDIA":
01412            return ImageFrame.MEDIA;
01413        elif s == "LEAD_ARTIST":
01414            return ImageFrame.LEAD_ARTIST;
01415        elif s == "ARTIST":
01416            return ImageFrame.ARTIST;
01417        elif s == "CONDUCTOR":
01418            return ImageFrame.CONDUCTOR;
01419        elif s == "BAND":
01420            return ImageFrame.BAND;
01421        elif s == "COMPOSER":
01422            return ImageFrame.COMPOSER;
01423        elif s == "LYRICIST":
01424            return ImageFrame.LYRICIST;
01425        elif s == "RECORDING_LOCATION":
01426            return ImageFrame.RECORDING_LOCATION;
01427        elif s == "DURING_RECORDING":
01428            return ImageFrame.DURING_RECORDING;
01429        elif s == "DURING_PERFORMANCE":
01430            return ImageFrame.DURING_PERFORMANCE;
01431        elif s == "VIDEO":
01432            return ImageFrame.VIDEO;
01433        elif s == "BRIGHT_COLORED_FISH":
01434            return ImageFrame.BRIGHT_COLORED_FISH;
01435        elif s == "ILLUSTRATION":
01436            return ImageFrame.ILLUSTRATION;
01437        elif s == "BAND_LOGO":
01438            return ImageFrame.BAND_LOGO;
01439        elif s == "PUBLISHER_LOGO":
01440            return ImageFrame.PUBLISHER_LOGO;
01441        else:
01442          raise FrameException("Invalid APIC picture type: %s" % s);
01443    stringToPicType = staticmethod(stringToPicType);
01444 
01445    def picTypeToString(t):
01446       if t == ImageFrame.OTHER:
01447          return "OTHER";
01448       elif t == ImageFrame.ICON:
01449          return "ICON";
01450       elif t == ImageFrame.OTHER_ICON:
01451          return "OTHER_ICON";
01452       elif t == ImageFrame.FRONT_COVER:
01453          return "FRONT_COVER";
01454       elif t == ImageFrame.BACK_COVER:
01455          return "BACK_COVER";
01456       elif t == ImageFrame.LEAFLET:
01457          return "LEAFLET";
01458       elif t == ImageFrame.MEDIA:
01459          return "MEDIA";
01460       elif t == ImageFrame.LEAD_ARTIST:
01461          return "LEAD_ARTIST";
01462       elif t == ImageFrame.ARTIST:
01463          return "ARTIST";
01464       elif t == ImageFrame.CONDUCTOR:
01465          return "CONDUCTOR";
01466       elif t == ImageFrame.BAND:
01467          return "BAND";
01468       elif t == ImageFrame.COMPOSER:
01469          return "COMPOSER";
01470       elif t == ImageFrame.LYRICIST:
01471          return "LYRICIST";
01472       elif t == ImageFrame.RECORDING_LOCATION:
01473          return "RECORDING_LOCATION";
01474       elif t == ImageFrame.DURING_RECORDING:
01475          return "DURING_RECORDING";
01476       elif t == ImageFrame.DURING_PERFORMANCE:
01477          return "DURING_PERFORMANCE";
01478       elif t == ImageFrame.VIDEO:
01479          return "VIDEO";
01480       elif t == ImageFrame.BRIGHT_COLORED_FISH:
01481          return "BRIGHT_COLORED_FISH";
01482       elif t == ImageFrame.ILLUSTRATION:
01483          return "ILLUSTRATION";
01484       elif t == ImageFrame.BAND_LOGO:
01485          return "BAND_LOGO";
01486       elif t == ImageFrame.PUBLISHER_LOGO:
01487          return "PUBLISHER_LOGO";
01488       else:
01489          raise FrameException("Invalid APIC picture type: %d" % t);
01490    picTypeToString = staticmethod(picTypeToString);
01491 
01492 ################################################################################
01493 # This class refers to the GEOB frame
01494 class ObjectFrame(Frame):
01495    mimeType = None
01496    description = u""
01497    filename = u""
01498    objectData = None
01499 
01500    def __init__(self, frameHeader, data=None,
01501                 desc=u"", filename=u"",
01502                 objectData=None, mimeType=None,
01503                 encoding=DEFAULT_ENCODING, unsync_default=False):
01504        Frame.__init__(self, frameHeader, unsync_default)
01505        if data != None:
01506            self._set(data, frameHeader)
01507        else:
01508            assert(isinstance(desc, unicode))
01509            self.description = desc
01510            assert(isinstance(filename, unicode))
01511            self.filename = filename
01512            self.encoding = encoding
01513            assert(mimeType)
01514            self.mimeType = mimeType
01515            assert(objectData)
01516            self.objectData = objectData
01517 
01518    # Factory method
01519    def create(objFile, mime = u"", desc = u"", filename = None,
01520               encoding = DEFAULT_ENCODING):
01521        if filename == None:
01522            filename = encodeUnicode(os.path.basename(objFile),
01523                                     sys.getfilesystemencoding())
01524        if not isinstance(desc, unicode) or \
01525           (not isinstance(filename, unicode) and filename != ""):
01526            raise FrameException("Wrong description and/or filename type.")
01527        # Load file
01528        fp = file(objFile, "rb")
01529        objData = fp.read()
01530        if mime:
01531            TRACE_MSG("Using specified mime type %s" % mime)
01532        else:
01533            mime = guess_mime_type(objFile);
01534            if not mime:
01535                raise FrameException("Unable to guess mime-type for %s" %
01536                                     objFile)
01537            TRACE_MSG("Guessed mime type %s" % mime)
01538 
01539        frameData = DEFAULT_ENCODING
01540        frameData += mime + "\x00"
01541        frameData += filename.encode(id3EncodingToString(encoding)) + "\x00"
01542        frameData += desc.encode(id3EncodingToString(encoding)) + "\x00"
01543        frameData += objData
01544 
01545        frameHeader = FrameHeader()
01546        frameHeader.id = OBJECT_FID
01547        return ObjectFrame(frameHeader, data = frameData)
01548    # Make create a static method.  Odd....
01549    create = staticmethod(create)
01550 
01551    # Data string format:
01552    # <Header for 'General encapsulated object', ID: "GEOB">
01553    #  Text encoding          $xx
01554    #  MIME type              <text string> $00
01555    #  Filename               <text string according to encoding> $00 (00)
01556    #  Content description    <text string according to encoding> $00 (00)
01557    #  Encapsulated object    <binary data>
01558    def _set(self, data, frameHeader = None):
01559       assert(frameHeader);
01560       if not OBJECT_FRAME_RX.match(frameHeader.id):
01561          raise FrameException("Invalid frame id for ObjectFrame: " +\
01562                               frameHeader.id);
01563 
01564       data = self.disassembleFrame(data);
01565 
01566       input = StringIO(data);
01567       TRACE_MSG("GEOB frame data size: " + str(len(data)));
01568       self.encoding = input.read(1);
01569       TRACE_MSG("GEOB encoding: " + id3EncodingToString(self.encoding));
01570 
01571       # Mime type
01572       self.mimeType = ""
01573       if self.header.minorVersion != 2:
01574           ch = input.read(1)
01575           while ch and ch != "\x00":
01576               self.mimeType += ch
01577               ch = input.read(1)
01578           if not ch:
01579               raise FrameException("GEOB frame mime type is not NULL terminated")
01580 
01581       else:
01582           # v2.2 (OBSOLETE) special case
01583           self.mimeType = input.read(3)
01584       TRACE_MSG("GEOB mime type: " + self.mimeType)
01585       if strictID3() and not self.mimeType:
01586          raise FrameException("GEOB frame does not contain a mime type")
01587       if strictID3() and self.mimeType.find("/") == -1:
01588          raise FrameException("GEOB frame does not contain a valid mime type")
01589 
01590       self.filename = u""
01591       self.description = u""
01592 
01593       # Remaining data is a NULL separated filename, description and object data
01594       buffer = input.read()
01595       input.close()
01596 
01597       try:
01598           (filename, buffer) = splitUnicode(buffer, self.encoding)
01599           (desc, obj) = splitUnicode(buffer, self.encoding)
01600       except ValueError:
01601           raise FrameException("GEOB frame appears to be missing requisite NULL "
01602                                "terminators");
01603       TRACE_MSG("filename len: %d" % len(filename))
01604       TRACE_MSG("description len: %d" % len(desc))
01605       TRACE_MSG("data len: %d" % len(obj))
01606       self.filename = encodeUnicode(filename,
01607                                     id3EncodingToString(self.encoding))
01608       self.description = encodeUnicode(desc, id3EncodingToString(self.encoding))
01609       TRACE_MSG("GEOB filename: " + self.filename)
01610       TRACE_MSG("GEOB description: " + self.description)
01611 
01612       self.objectData = obj
01613       TRACE_MSG("GEOB data: " + str(len(self.objectData)) + " bytes")
01614       if strictID3() and not self.objectData:
01615          raise FrameException("GEOB frame does not contain any data")
01616 
01617 
01618    def writeFile(self, path = "./", name = None):
01619       if not self.objectData:
01620          raise IOError("Fetching remote object files is not implemented.")
01621       if not name:
01622          name = self.getDefaultFileName()
01623       objectFile = os.path.join(path, name)
01624 
01625       f = file(objectFile, "wb")
01626       f.write(self.objectData)
01627       f.flush()
01628       f.close()
01629    def getDefaultFileName(self, suffix = ""):
01630       nameStr = self.filename
01631       if suffix:
01632           nameStr += suffix
01633       nameStr = nameStr +  "." + self.mimeType.split("/")[1]
01634       return nameStr
01635 
01636    def render(self):
01637       data = self.encoding + self.mimeType + "\x00" +\
01638              self.filename.encode(id3EncodingToString(self.encoding)) +\
01639              self.getTextDelim() +\
01640              self.description.encode(id3EncodingToString(self.encoding)) +\
01641              self.getTextDelim() +\
01642              self.objectData
01643       return self.assembleFrame(data)
01644 
01645 class PlayCountFrame(Frame):
01646     count = None
01647 
01648     def __init__(self, frameHeader, data=None, count=None,
01649                  unsync_default=False):
01650         Frame.__init__(self, frameHeader, unsync_default)
01651         if data != None:
01652             self._set(data, frameHeader)
01653         else:
01654             assert(count != None and count >= 0)
01655             self.count = count
01656 
01657     def _set(self, data, frameHeader):
01658         assert(frameHeader)
01659         # data of less then 4 bytes is handled with with 'sz' arg
01660         self.count = long(bytes2dec(data, sz=(len(data) * 2)))
01661 
01662     def render(self):
01663         data = dec2bytes(self.count, 32)
01664         return self.assembleFrame(data)
01665 
01666 class UniqueFileIDFrame(Frame):
01667     owner_id = ""
01668     id = ""
01669 
01670     def __init__(self, frameHeader, data=None, owner_id=None, id=None,
01671                  unsync_default=False):
01672         Frame.__init__(self, frameHeader, unsync_default)
01673         if data != None:
01674             self._set(data, frameHeader)
01675         else:
01676             assert(owner_id != None and len(owner_id) > 0)
01677             assert(id != None and len(id) > 0 and len(id) <= 64)
01678             self.owner_id = owner_id
01679             self.id = id
01680 
01681     def _set(self, data, frameHeader):
01682         assert(frameHeader)
01683         # Data format
01684         # Owner identifier <text string> $00
01685         # Identifier       up to 64 bytes binary data>
01686         (self.owner_id, self.id) = data.split("\x00", 1)
01687         TRACE_MSG("UFID owner_id: " + self.owner_id)
01688         TRACE_MSG("UFID id: " + self.id)
01689         if strictID3() and (len(self.owner_id) == 0 or
01690                             len(self.id) == 0 or len(self.id) > 64):
01691             raise FrameException("Invalid UFID frame")
01692 
01693     def render(self):
01694         data = self.owner_id + "\x00" + self.id
01695         return self.assembleFrame(data)
01696 
01697 ################################################################################
01698 class UnknownFrame(Frame):
01699    data = "";
01700 
01701    def __init__(self, frameHeader, data, unsync_default=False):
01702        assert(frameHeader and data)
01703        Frame.__init__(self, frameHeader, unsync_default)
01704        self._set(data, frameHeader)
01705 
01706    def _set(self, data, frameHeader):
01707       self.data = self.disassembleFrame(data);
01708 
01709    def render(self):
01710       return self.assembleFrame(self.data)
01711 
01712 ################################################################################
01713 class MusicCDIdFrame(Frame):
01714    toc = "";
01715 
01716    def __init__(self, frameHeader, data=None, unsync_default=False):
01717        Frame.__init__(self, frameHeader, unsync_default)
01718        # XXX: Flesh this class out and add a toc arg
01719        assert(data != None);
01720        if data != None:
01721            self._set(data, frameHeader);
01722 
01723    # TODO: Parse the TOC and comment the format.
01724    def _set(self, data, frameHeader):
01725       if not CDID_FRAME_RX.match(frameHeader.id):
01726          raise FrameException("Invalid frame id for MusicCDIdFrame: " +\
01727                               frameHeader.id);
01728       data = self.disassembleFrame(data);
01729       self.toc = data;
01730 
01731    def render(self):
01732       data = self.toc;
01733       return self.assembleFrame(data);
01734 
01735 ################################################################################
01736 # A class for containing and managing ID3v2.Frame objects.
01737 class FrameSet(list):
01738    tagHeader = None;
01739 
01740    def __init__(self, tagHeader, l = None):
01741       self.tagHeader = tagHeader;
01742       if l:
01743          for f in l:
01744             if not isinstance(f, Frame):
01745                raise TypeError("Invalid type added to FrameSet: " +\
01746                                f.__class__);
01747             self.append(f);
01748 
01749    # Setting a FrameSet instance like this 'fs = []' morphs the instance into
01750    # a list object.
01751    def clear(self):
01752       del self[0:];
01753 
01754    # Read frames starting from the current read position of the file object.
01755    # Returns the amount of padding which occurs after the tag, but before the
01756    # audio content.  A return valule of 0 DOES NOT imply an error.
01757    def parse(self, f, tagHeader, extendedHeader):
01758       self.tagHeader = tagHeader;
01759       self.extendedHeader = extendedHeader
01760       paddingSize = 0;
01761       sizeLeft = tagHeader.tagSize - extendedHeader.size
01762       start_size = sizeLeft
01763       consumed_size = 0
01764 
01765       # Handle a tag-level unsync.  Some frames may have their own unsync bit
01766       # set instead.
01767       tagData = f.read(sizeLeft)
01768 
01769       # If the tag is 2.3 and the tag header unsync bit is set then all the
01770       # frame data is deunsync'd at once, otherwise it will happen on a per
01771       # frame basis.
01772       from eyeD3 import ID3_V2_3
01773       if tagHeader.unsync and tagHeader.version <= ID3_V2_3:
01774           TRACE_MSG("De-unsynching %d bytes at once (<= 2.3 tag)" %
01775                     len(tagData))
01776           og_size = len(tagData)
01777           tagData = deunsyncData(tagData)
01778           sizeLeft = len(tagData)
01779           TRACE_MSG("De-unsynch'd %d bytes at once (<= 2.3 tag) to %d bytes" %
01780                     (og_size, sizeLeft))
01781 
01782       # Adding bytes to simulate the tag header(s) in the buffer.  This keeps 
01783       # f.tell() values matching the file offsets.
01784       prepadding = '\x00' * 10  # Tag header
01785       prepadding += '\x00' * extendedHeader.size
01786       tagBuffer = StringIO(prepadding + tagData);
01787       tagBuffer.seek(len(prepadding));
01788 
01789       while sizeLeft > 0:
01790          TRACE_MSG("sizeLeft: " + str(sizeLeft));
01791          if sizeLeft < (10 + 1): # The size of the smallest frame.
01792             TRACE_MSG("FrameSet: Implied padding (sizeLeft < minFrameSize)");
01793             paddingSize = sizeLeft
01794             break;
01795 
01796          TRACE_MSG("+++++++++++++++++++++++++++++++++++++++++++++++++");
01797          TRACE_MSG("FrameSet: Reading Frame #" + str(len(self) + 1));
01798          frameHeader = FrameHeader(tagHeader);
01799          if not frameHeader.parse(tagBuffer):
01800             TRACE_MSG("No frame found, implied padding of %d bytes" % sizeLeft)
01801             paddingSize = sizeLeft
01802             break;
01803 
01804          # Frame data.
01805          if frameHeader.dataSize:
01806              TRACE_MSG("FrameSet: Reading %d (0x%X) bytes of data from byte "
01807                        "pos %d (0x%X)" % (frameHeader.dataSize,
01808                                           frameHeader.dataSize,
01809                                           tagBuffer.tell(),
01810                                           tagBuffer.tell()));
01811              data = tagBuffer.read(frameHeader.dataSize);
01812 
01813              TRACE_MSG("FrameSet: %d bytes of data read" % len(data));
01814 
01815              consumed_size += (frameHeader.FRAME_HEADER_SIZE +
01816                                frameHeader.dataSize)
01817              self.addFrame(createFrame(frameHeader, data, tagHeader))
01818 
01819          # Each frame contains dataSize + headerSize bytes.
01820          sizeLeft -= (frameHeader.FRAME_HEADER_SIZE + frameHeader.dataSize);
01821 
01822       return paddingSize
01823 
01824    # Returrns the size of the frame data.
01825    def getSize(self):
01826       sz = 0;
01827       for f in self:
01828          sz += len(f.render());
01829       return sz;
01830 
01831    def setTagHeader(self, tagHeader):
01832       self.tagHeader = tagHeader;
01833       for f in self:
01834          f.header.setVersion(tagHeader);
01835 
01836    # This methods adds the frame if it is addable per the ID3 spec.
01837    def addFrame(self, frame):
01838       fid = frame.header.id;
01839 
01840       # Text frame restrictions.
01841       # No multiples except for TXXX which must have unique descriptions.
01842       if strictID3() and TEXT_FRAME_RX.match(fid) and self[fid]:
01843          if not USERTEXT_FRAME_RX.match(fid):
01844             raise FrameException("Multiple %s frames not allowed." % fid);
01845          userTextFrames = self[fid];
01846          for frm in userTextFrames:
01847             if frm.description == frame.description:
01848                raise FrameException("Multiple %s frames with the same\
01849                                      description not allowed." % fid);
01850 
01851       # Comment frame restrictions.
01852       # Multiples must have a unique description/language combination.
01853       if strictID3() and COMMENT_FRAME_RX.match(fid) and self[fid]:
01854          commentFrames = self[fid];
01855          for frm in commentFrames:
01856             if frm.description == frame.description and\
01857                frm.lang == frame.lang:
01858                raise FrameException("Multiple %s frames with the same\
01859                                      language and description not allowed." %\
01860                                      fid);
01861 
01862       # Lyrics frame restrictions.
01863       # Multiples must have a unique description/language combination.
01864       if strictID3() and LYRICS_FRAME_RX.match(fid) and self[fid]:
01865          lyricsFrames = self[fid];
01866          for frm in lyricsFrames:
01867             if frm.description == frame.description and\
01868                frm.lang == frame.lang:
01869                raise FrameException("Multiple %s frames with the same\
01870                                      language and description not allowed." %\
01871                                      fid);
01872 
01873       # URL frame restrictions.
01874       # No multiples except for TXXX which must have unique descriptions.
01875       if strictID3() and URL_FRAME_RX.match(fid) and self[fid]:
01876          if not USERURL_FRAME_RX.match(fid):
01877             raise FrameException("Multiple %s frames not allowed." % fid);
01878          userUrlFrames = self[fid];
01879          for frm in userUrlFrames:
01880             if frm.description == frame.description:
01881                raise FrameException("Multiple %s frames with the same\
01882                                      description not allowed." % fid);
01883 
01884       # Music CD ID restrictions.
01885       # No multiples.
01886       if strictID3() and CDID_FRAME_RX.match(fid) and self[fid]:
01887          raise FrameException("Multiple %s frames not allowed." % fid);
01888 
01889       # Image (attached picture) frame restrictions.
01890       # Multiples must have a unique content desciptor.  I'm assuming that
01891       # the spec means the picture type.....
01892       if IMAGE_FRAME_RX.match(fid) and self[fid] and strictID3():
01893          imageFrames = self[fid];
01894          for frm in imageFrames:
01895             if frm.pictureType == frame.pictureType:
01896                raise FrameException("Multiple %s frames with the same "\
01897                                     "content descriptor not allowed." % fid);
01898 
01899       # Object (GEOB) frame restrictions.
01900       # Multiples must have a unique content desciptor.
01901       if OBJECT_FRAME_RX.match(fid) and self[fid] and strictID3():
01902          objectFrames = self[fid];
01903          for frm in objectFrames:
01904             if frm.description == frame.description:
01905                raise FrameException("Multiple %s frames with the same "\
01906                                     "content descriptor not allowed." % fid);
01907 
01908       # Play count frame (PCNT).  There may be only one
01909       if PLAYCOUNT_FRAME_RX.match(fid) and self[fid]:
01910          raise FrameException("Multiple %s frames not allowed." % fid);
01911 
01912       # Unique File identifier frame.  There may be only one with the same
01913       # owner_id
01914       if UNIQUE_FILE_ID_FRAME_RX.match(fid) and self[fid]:
01915           ufid_frames = self[fid];
01916           for frm in ufid_frames:
01917               if frm.owner_id == frame.owner_id:
01918                   raise FrameException("Multiple %s frames not allowed with "\
01919                                        "the same owner ID (%s)" %\
01920                                        (fid, frame.owner_id));
01921 
01922       self.append(frame);
01923 
01924    # Set a text frame value.  Text frame IDs must be unique.  If a frame with
01925    # the same Id is already in the list it's value is changed, otherwise
01926    # the frame is added.
01927    def setTextFrame(self, frameId, text, encoding=None):
01928       assert(type(text) == unicode)
01929 
01930       if not TEXT_FRAME_RX.match(frameId):
01931          raise FrameException("Invalid Frame ID: " + frameId)
01932 
01933       if self[frameId]:
01934           curr = self[frameId][0]
01935           if encoding:
01936               curr.encoding = encoding
01937 
01938           if isinstance(curr, DateFrame):
01939               curr.setDate(text)
01940           else:
01941               curr.text = text
01942       else:
01943           h = FrameHeader(self.tagHeader)
01944           h.id = frameId
01945           if not encoding:
01946               encoding = DEFAULT_ENCODING
01947           if frameId in DATE_FIDS:
01948               self.addFrame(DateFrame(h, encoding = encoding, date_str = text))
01949           else:
01950               self.addFrame(TextFrame(h, encoding = encoding, text = text))
01951 
01952    def setURLFrame(self, frame_id, url):
01953       assert(type(url) == str)
01954 
01955       if frame_id not in URL_FIDS:
01956          raise FrameException("Invalid URL frame ID: %s" % frame_id)
01957 
01958       if self[frame_id]:
01959           self[frame_id][0].url = url
01960       else:
01961           h = FrameHeader(self.tagHeader)
01962           h.id = frame_id
01963           self.addFrame(URLFrame(h, url=url))
01964 
01965    # If a user text frame with the same description exists then
01966    # the frame text is replaced, otherwise the frame is added.
01967    def setCommentFrame(self, comment, description, lang = DEFAULT_LANG,
01968                        encoding = None):
01969       assert(isinstance(comment, unicode) and isinstance(description, unicode))
01970 
01971       if self[COMMENT_FID]:
01972          found = 0
01973          for f in self[COMMENT_FID]:
01974             if f.lang == lang and f.description == description:
01975                f.comment = comment
01976                if encoding:
01977                    f.encoding = encoding
01978                found = 1
01979                break
01980          if not found:
01981             h = FrameHeader(self.tagHeader)
01982             h.id = COMMENT_FID
01983             if not encoding:
01984                 encoding = DEFAULT_ENCODING
01985             self.addFrame(CommentFrame(h, encoding = encoding, lang = lang,
01986                                        description = description,
01987                                        comment = comment))
01988       else:
01989         if not encoding:
01990             encoding = DEFAULT_ENCODING
01991         h = FrameHeader(self.tagHeader)
01992         h.id = COMMENT_FID
01993         self.addFrame(CommentFrame(h, encoding = encoding, lang = lang,
01994                                    description = description,
01995                                    comment = comment))
01996 
01997    # If a user text frame with the same description exists then
01998    # the frame text is replaced, otherwise the frame is added.
01999    def setLyricsFrame(self, lyrics, description, lang = DEFAULT_LANG,
02000                        encoding = None):
02001       assert(isinstance(lyrics, unicode) and isinstance(description, unicode));
02002 
02003       if self[LYRICS_FID]:
02004          found = 0;
02005          for f in self[LYRICS_FID]:
02006             if f.lang == lang and f.description == description:
02007                f.lyrics = lyrics;
02008                if encoding:
02009                    f.encoding = encoding;
02010                found = 1;
02011                break;
02012          if not found:
02013             h = FrameHeader(self.tagHeader);
02014             h.id = LYRICS_FID;
02015             if not encoding:
02016                 encoding = DEFAULT_ENCODING;
02017             self.addFrame(LyricsFrame(h, encoding = encoding, lang = lang,
02018                                        description = description,
02019                                        lyrics = lyrics));
02020       else:
02021         if not encoding:
02022             encoding = DEFAULT_ENCODING;
02023         h = FrameHeader(self.tagHeader);
02024         h.id = LYRICS_FID;
02025         self.addFrame(LyricsFrame(h, encoding = encoding, lang = lang,
02026                                    description = description,
02027                                    lyrics = lyrics))
02028 
02029    def setUniqueFileIDFrame(self, owner_id, id):
02030       assert(isinstance(owner_id, str) and isinstance(id, str))
02031 
02032       if self[UNIQUE_FILE_ID_FID]:
02033          found = 0
02034          for f in self[UNIQUE_FILE_ID_FID]:
02035              if f.owner_id == owner_id:
02036                  f.id = id
02037                  found = 1
02038                  break
02039          if not found:
02040             h = FrameHeader(self.tagHeader)
02041             h.id = UNIQUE_FILE_ID_FID
02042             self.addFrame(UniqueFileIDFrame(h, owner_id = owner_id, id = id))
02043       else:
02044         h = FrameHeader(self.tagHeader)
02045         h.id = UNIQUE_FILE_ID_FID
02046         self.addFrame(UniqueFileIDFrame(h, owner_id = owner_id, id = id))
02047 
02048    # If a comment frame with the same language and description exists then
02049    # the comment text is replaced, otherwise the frame is added.
02050    def setUserTextFrame(self, txt, description, encoding=None):
02051       assert(isinstance(txt, unicode))
02052       assert(isinstance(description, unicode));
02053 
02054       if self[USERTEXT_FID]:
02055          found = 0;
02056          for f in self[USERTEXT_FID]:
02057             if f.description == description:
02058                f.text = txt;
02059                if encoding:
02060                    f.encoding = encoding;
02061                found = 1;
02062                break;
02063          if not found:
02064              if not encoding:
02065                  encoding = DEFAULT_ENCODING;
02066              h = FrameHeader(self.tagHeader);
02067              h.id = USERTEXT_FID;
02068              self.addFrame(UserTextFrame(h, encoding = encoding,
02069                                          description = description,
02070                                          text = txt));
02071       else:
02072           if not encoding:
02073               encoding = DEFAULT_ENCODING;
02074           h = FrameHeader(self.tagHeader);
02075           h.id = USERTEXT_FID;
02076           self.addFrame(UserTextFrame(h, encoding = encoding,
02077                                       description = description,
02078                                       text = txt));
02079 
02080    def setUserURLFrame(self, url, description, encoding=None):
02081       assert(isinstance(url, str))
02082       assert(isinstance(description, unicode))
02083 
02084       if self[USERURL_FID]:
02085          found = 0
02086          for f in self[USERURL_FID]:
02087             if f.description == description:
02088                f.url = url
02089                if encoding:
02090                    f.encoding = encoding
02091                found = 1
02092                break
02093          if not found:
02094              if not encoding:
02095                  encoding = DEFAULT_ENCODING
02096              h = FrameHeader(self.tagHeader)
02097              h.id = USERURL_FID
02098              self.addFrame(UserURLFrame(h, encoding=encoding,
02099                                         description=description, url=url))
02100       else:
02101           if not encoding:
02102               encoding = DEFAULT_ENCODING
02103           h = FrameHeader(self.tagHeader)
02104           h.id = USERURL_FID
02105           self.addFrame(UserURLFrame(h, encoding=encoding,
02106                                      description=description, url=url))
02107 
02108    # This method removes all frames with the matching frame ID.
02109    # The number of frames removed is returned.
02110    # Note that calling this method with a key like "COMM" may remove more
02111    # frames then you really want.
02112    def removeFramesByID(self, fid):
02113       if not isinstance(fid, str):
02114          raise FrameException("removeFramesByID only operates on frame IDs")
02115 
02116       i = 0
02117       count = 0
02118       while i < len(self):
02119          if self[i].header.id == fid:
02120             del self[i]
02121             count += 1
02122          else:
02123             i += 1
02124       return count
02125 
02126    # Removes the frame at index.  True is returned if the element was
02127    # removed, and false otherwise.
02128    def removeFrameByIndex(self, index):
02129       if not isinstance(index, int):
02130          raise\
02131            FrameException("removeFrameByIndex only operates on a frame index")
02132       try:
02133          del self.frames[key]
02134          return 1
02135       except:
02136          return 0
02137 
02138    # Accepts both int (indexed access) and string keys (a valid frame Id).
02139    # A list of frames (commonly with only one element) is returned when the
02140    # FrameSet is accessed using frame IDs since some frames can appear
02141    # multiple times in a tag.  To sum it all up htis method returns
02142    # string or None when indexed using an integer, and a 0 to N length
02143    # list of strings when indexed with a frame ID.
02144    #
02145    # Throws IndexError and TypeError.
02146    def __getitem__(self, key):
02147       if isinstance(key, int):
02148          if key >= 0 and key < len(self):
02149             return list.__getitem__(self, key)
02150          else:
02151             raise IndexError("FrameSet index out of range")
02152       elif isinstance(key, str):
02153          retList = list()
02154          for f in self:
02155             if f.header.id == key:
02156                retList.append(f)
02157          return retList
02158       else:
02159          raise TypeError("FrameSet key must be type int or string")
02160 
02161 ##
02162 # Used for splitting user text tags that use null byte(s) to seperate a
02163 # description and its text.
02164 def splitUnicode(data, encoding):
02165     if encoding == LATIN1_ENCODING or encoding == UTF_8_ENCODING:
02166         retval = data.split("\x00", 1)
02167     elif encoding == UTF_16_ENCODING or encoding == UTF_16BE_ENCODING:
02168         # Two null bytes split, but since each utf16 char is also two 
02169         # bytes we need to ensure we found a proper boundary.
02170         (d, t) = data.split("\x00\x00", 1)
02171         if (len(d) % 2) != 0:
02172             (d, t) = data.split("\x00\x00\x00", 1)
02173             d += "\x00"
02174         retval = (d, t)
02175 
02176     if len(retval) != 2:
02177         # What we have here is an invalid tag in that contains only piece
02178         # of the information. In the spirit of not crashing on crap tags
02179         # return it as the ... description
02180         retval = (retval[0], "")
02181 
02182     return retval
02183 
02184 #######################################################################
02185 # Create and return the appropriate frame.
02186 # Exceptions: ....
02187 def createFrame(frameHeader, data, tagHeader):
02188   f = None
02189 
02190   # Text Frames
02191   if TEXT_FRAME_RX.match(frameHeader.id):
02192      if USERTEXT_FRAME_RX.match(frameHeader.id):
02193         f = UserTextFrame(frameHeader, data=data,
02194                           unsync_default=tagHeader.unsync)
02195      else:
02196         if frameHeader.id[:2] == "TD" or\
02197            frameHeader.id == OBSOLETE_DATE_FID or\
02198            frameHeader.id == OBSOLETE_YEAR_FID or \
02199            frameHeader.id == OBSOLETE_ORIG_RELEASE_FID:
02200            f = DateFrame(frameHeader, data=data,
02201                          unsync_default=tagHeader.unsync)
02202         else:
02203            f = TextFrame(frameHeader, data=data,
02204                          unsync_default=tagHeader.unsync)
02205   # Comment Frames.
02206   elif COMMENT_FRAME_RX.match(frameHeader.id):
02207      f = CommentFrame(frameHeader, data=data, unsync_default=tagHeader.unsync)
02208   # Lyrics Frames.
02209   elif LYRICS_FRAME_RX.match(frameHeader.id):
02210      f = LyricsFrame(frameHeader, data=data, unsync_default=tagHeader.unsync)
02211   # URL Frames.
02212   elif URL_FRAME_RX.match(frameHeader.id):
02213      if USERURL_FRAME_RX.match(frameHeader.id):
02214         f = UserURLFrame(frameHeader, data=data,
02215                          unsync_default=tagHeader.unsync)
02216      else:
02217         f = URLFrame(frameHeader, data=data, unsync_default=tagHeader.unsync)
02218   # CD Id frame.
02219   elif CDID_FRAME_RX.match(frameHeader.id):
02220      f = MusicCDIdFrame(frameHeader, data=data, unsync_default=tagHeader.unsync)
02221   # Attached picture
02222   elif IMAGE_FRAME_RX.match(frameHeader.id):
02223      f = ImageFrame(frameHeader, data=data, unsync_default=tagHeader.unsync)
02224   # Encapsulated object
02225   elif OBJECT_FRAME_RX.match(frameHeader.id):
02226      f = ObjectFrame(frameHeader, data=data, unsync_default=tagHeader.unsync)
02227   # Play count
02228   elif PLAYCOUNT_FRAME_RX.match(frameHeader.id):
02229      f = PlayCountFrame(frameHeader, data=data, unsync_default=tagHeader.unsync)
02230   # Unique file identifier
02231   elif UNIQUE_FILE_ID_FRAME_RX.match(frameHeader.id):
02232      f = UniqueFileIDFrame(frameHeader, data=data,
02233                            unsync_default=tagHeader.unsync)
02234 
02235   if f == None:
02236      f = UnknownFrame(frameHeader, data=data, unsync_default=tagHeader.unsync)
02237 
02238   return f
02239 
02240 
02241 def map2_2FrameId(originalId):
02242     if not TAGS2_2_TO_TAGS_2_3_AND_4.has_key(originalId):
02243         return originalId
02244     return TAGS2_2_TO_TAGS_2_3_AND_4[originalId]
02245 
02246 def dump_data_to_file(s, f):
02247     f = file(f, "w")
02248     for c in s:
02249         f.write("\\x%.2x" % ord(c))
02250     f.write("\n")
02251     f.close()
02252 
02253 ##
02254 # A suxtitute for Python's 'unicode' constructor which handles invalid
02255 # string encodings (as seen in the real world).
02256 def encodeUnicode(bytes, encoding):
02257     if (encoding == id3EncodingToString(UTF_16_ENCODING) and
02258             len(bytes) % 2 and
02259             bytes[-2:] == "\x00\x00"):
02260         # Fixes: utf16, odd number of bytes (including 2-byte BOM) where
02261         #        the final byte is \x00
02262         # Users have sent tags with an invalid utf16 encoding thus 
02263         # python's unicode type can't decode. Fix this edge case.
02264         bytes = bytes[:-1]
02265 
02266     return unicode(bytes, encoding)
02267