1 ################################################################################
2 # Copyright (C) 2002-2007 Travis Shirk <travis@pobox.com>
3 # Copyright (C) 2005 Michael Urman
4 # - Sync-safe encoding/decoding algorithms
6 # This program is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 2 of the License, or
9 # (at your option) any later version.
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
20 ################################################################################
21 import sys
, os
, os
.path
, re
, zlib
, StringIO
, time
, mimetypes
;
22 from StringIO
import StringIO
;
24 from binfuncs
import *;
26 # Valid time stamp formats per ISO 8601 and used by time.strptime.
27 timeStampFormats
= ["%Y",
36 CONDUCTOR_FID
= "TPE3";
38 COMPOSER_FID
= "TCOM";
39 ARTIST_FIDS
= [ARTIST_FID
, BAND_FID
, CONDUCTOR_FID
,
40 REMIXER_FID
, COMPOSER_FID
];
43 SUBTITLE_FID
= "TIT3";
44 CONTENT_TITLE_FID
= "TIT1";
45 TITLE_FIDS
= [TITLE_FID
, SUBTITLE_FID
, CONTENT_TITLE_FID
];
49 TRACKNUM_FID
= "TRCK";
51 USERTEXT_FID
= "TXXX";
55 URL_COMMERCIAL_FID
= "WCOM";
56 URL_COPYRIGHT_FID
= "WCOP";
57 URL_AUDIOFILE_FID
= "WOAF";
58 URL_ARTIST_FID
= "WOAR";
59 URL_AUDIOSRC_FID
= "WOAS";
60 URL_INET_RADIO_FID
= "WORS";
61 URL_PAYMENT_FID
= "WPAY";
62 URL_PUBLISHER_FID
= "WPUB";
63 URL_FIDS
= [URL_COMMERCIAL_FID
, URL_COPYRIGHT_FID
,
64 URL_AUDIOFILE_FID
, URL_ARTIST_FID
, URL_AUDIOSRC_FID
,
65 URL_INET_RADIO_FID
, URL_PAYMENT_FID
,
68 PLAYCOUNT_FID
= "PCNT";
69 UNIQUE_FILE_ID_FID
= "UFID";
71 PUBLISHER_FID
= "TPUB";
73 obsoleteFrames
= {"EQUA": "Equalisation",
74 "IPLS": "Involved people list",
75 "RVAD": "Relative volume adjustment",
77 "TORY": "Original release year",
78 "TRDA": "Recording dates",
80 # Both of these are "coerced" into a v2.4 TDRC frame when read, and
81 # recreated when saving v2.3.
82 OBSOLETE_DATE_FID
= "TDAT";
83 OBSOLETE_YEAR_FID
= "TYER";
84 OBSOLETE_TIME_FID
= "TIME";
85 OBSOLETE_ORIG_RELEASE_FID
= "TORY";
86 OBSOLETE_RECORDING_DATE_FID
= "TRDA";
88 DATE_FIDS
= ["TDRL", "TDOR", "TDRC", OBSOLETE_YEAR_FID
,
91 frameDesc
= { "AENC": "Audio encryption",
92 "APIC": "Attached picture",
93 "ASPI": "Audio seek point index",
96 "COMR": "Commercial frame",
98 "ENCR": "Encryption method registration",
99 "EQU2": "Equalisation (2)",
100 "ETCO": "Event timing codes",
102 "GEOB": "General encapsulated object",
103 "GRID": "Group identification registration",
105 "LINK": "Linked information",
107 "MCDI": "Music CD identifier",
108 "MLLT": "MPEG location lookup table",
110 "OWNE": "Ownership frame",
112 "PRIV": "Private frame",
113 "PCNT": "Play counter",
114 "POPM": "Popularimeter",
115 "POSS": "Position synchronisation frame",
117 "RBUF": "Recommended buffer size",
118 "RVA2": "Relative volume adjustment (2)",
121 "SEEK": "Seek frame",
122 "SIGN": "Signature frame",
123 "SYLT": "Synchronised lyric/text",
124 "SYTC": "Synchronised tempo codes",
126 "TALB": "Album/Movie/Show title",
127 "TBPM": "BPM (beats per minute)",
129 "TCON": "Content type",
130 "TCOP": "Copyright message",
131 "TDEN": "Encoding time",
132 "TDLY": "Playlist delay",
133 "TDOR": "Original release time",
134 "TDRC": "Recording time",
135 "TDRL": "Release time",
136 "TDTG": "Tagging time",
137 "TENC": "Encoded by",
138 "TEXT": "Lyricist/Text writer",
140 "TIPL": "Involved people list",
141 "TIT1": "Content group description",
142 "TIT2": "Title/songname/content description",
143 "TIT3": "Subtitle/Description refinement",
144 "TKEY": "Initial key",
145 "TLAN": "Language(s)",
147 "TMCL": "Musician credits list",
148 "TMED": "Media type",
150 "TOAL": "Original album/movie/show title",
151 "TOFN": "Original filename",
152 "TOLY": "Original lyricist(s)/text writer(s)",
153 "TOPE": "Original artist(s)/performer(s)",
154 "TOWN": "File owner/licensee",
155 "TPE1": "Lead performer(s)/Soloist(s)",
156 "TPE2": "Band/orchestra/accompaniment",
157 "TPE3": "Conductor/performer refinement",
158 "TPE4": "Interpreted, remixed, or otherwise modified by",
159 "TPOS": "Part of a set",
160 "TPRO": "Produced notice",
162 "TRCK": "Track number/Position in set",
163 "TRSN": "Internet radio station name",
164 "TRSO": "Internet radio station owner",
165 "TSOA": "Album sort order",
166 "TSOP": "Performer sort order",
167 "TSOT": "Title sort order",
168 "TSRC": "ISRC (international standard recording code)",
169 "TSSE": "Software/Hardware and settings used for encoding",
170 "TSST": "Set subtitle",
171 "TXXX": "User defined text information frame",
173 "UFID": "Unique file identifier",
174 "USER": "Terms of use",
175 "USLT": "Unsynchronised lyric/text transcription",
177 "WCOM": "Commercial information",
178 "WCOP": "Copyright/Legal information",
179 "WOAF": "Official audio file webpage",
180 "WOAR": "Official artist/performer webpage",
181 "WOAS": "Official audio source webpage",
182 "WORS": "Official Internet radio station homepage",
184 "WPUB": "Publishers official webpage",
185 "WXXX": "User defined URL link frame" };
188 # mapping of 2.2 frames to 2.3/2.4
189 TAGS2_2_TO_TAGS_2_3_AND_4
= {
190 "TT1" : "TIT1", # CONTENTGROUP content group description
191 "TT2" : "TIT2", # TITLE title/songname/content description
192 "TT3" : "TIT3", # SUBTITLE subtitle/description refinement
193 "TP1" : "TPE1", # ARTIST lead performer(s)/soloist(s)
194 "TP2" : "TPE2", # BAND band/orchestra/accompaniment
195 "TP3" : "TPE3", # CONDUCTOR conductor/performer refinement
196 "TP4" : "TPE4", # MIXARTIST interpreted, remixed, modified by
197 "TCM" : "TCOM", # COMPOSER composer
198 "TXT" : "TEXT", # LYRICIST lyricist/text writer
199 "TLA" : "TLAN", # LANGUAGE language(s)
200 "TCO" : "TCON", # CONTENTTYPE content type
201 "TAL" : "TALB", # ALBUM album/movie/show title
202 "TRK" : "TRCK", # TRACKNUM track number/position in set
203 "TPA" : "TPOS", # PARTINSET part of set
204 "TRC" : "TSRC", # ISRC international standard recording code
205 "TDA" : "TDAT", # DATE date
206 "TYE" : "TYER", # YEAR year
207 "TIM" : "TIME", # TIME time
208 "TRD" : "TRDA", # RECORDINGDATES recording dates
209 "TOR" : "TORY", # ORIGYEAR original release year
210 "TBP" : "TBPM", # BPM beats per minute
211 "TMT" : "TMED", # MEDIATYPE media type
212 "TFT" : "TFLT", # FILETYPE file type
213 "TCR" : "TCOP", # COPYRIGHT copyright message
214 "TPB" : "TPUB", # PUBLISHER publisher
215 "TEN" : "TENC", # ENCODEDBY encoded by
216 "TSS" : "TSSE", # ENCODERSETTINGS software/hardware + settings for encoding
217 "TLE" : "TLEN", # SONGLEN length (ms)
218 "TSI" : "TSIZ", # SIZE size (bytes)
219 "TDY" : "TDLY", # PLAYLISTDELAY playlist delay
220 "TKE" : "TKEY", # INITIALKEY initial key
221 "TOT" : "TOAL", # ORIGALBUM original album/movie/show title
222 "TOF" : "TOFN", # ORIGFILENAME original filename
223 "TOA" : "TOPE", # ORIGARTIST original artist(s)/performer(s)
224 "TOL" : "TOLY", # ORIGLYRICIST original lyricist(s)/text writer(s)
225 "TXX" : "TXXX", # USERTEXT user defined text information frame
226 "WAF" : "WOAF", # WWWAUDIOFILE official audio file webpage
227 "WAR" : "WOAR", # WWWARTIST official artist/performer webpage
228 "WAS" : "WOAS", # WWWAUDIOSOURCE official audion source webpage
229 "WCM" : "WCOM", # WWWCOMMERCIALINFO commercial information
230 "WCP" : "WCOP", # WWWCOPYRIGHT copyright/legal information
231 "WPB" : "WPUB", # WWWPUBLISHER publishers official webpage
232 "WXX" : "WXXX", # WWWUSER user defined URL link frame
233 "IPL" : "IPLS", # INVOLVEDPEOPLE involved people list
234 "ULT" : "USLT", # UNSYNCEDLYRICS unsynchronised lyrics/text transcription
235 "COM" : "COMM", # COMMENT comments
236 "UFI" : "UFID", # UNIQUEFILEID unique file identifier
237 "MCI" : "MCDI", # CDID music CD identifier
238 "ETC" : "ETCO", # EVENTTIMING event timing codes
239 "MLL" : "MLLT", # MPEGLOOKUP MPEG location lookup table
240 "STC" : "SYTC", # SYNCEDTEMPO synchronised tempo codes
241 "SLT" : "SYLT", # SYNCEDLYRICS synchronised lyrics/text
242 "RVA" : "RVAD", # VOLUMEADJ relative volume adjustment
243 "EQU" : "EQUA", # EQUALIZATION equalization
244 "REV" : "RVRB", # REVERB reverb
245 "PIC" : "APIC", # PICTURE attached picture
246 "GEO" : "GEOB", # GENERALOBJECT general encapsulated object
247 "CNT" : "PCNT", # PLAYCOUNTER play counter
248 "POP" : "POPM", # POPULARIMETER popularimeter
249 "BUF" : "RBUF", # BUFFERSIZE recommended buffer size
250 "CRA" : "AENC", # AUDIOCRYPTO audio encryption
251 "LNK" : "LINK", # LINKEDINFO linked information
252 # Extension workarounds i.e., ignore them
253 "TCP" : "TCP ", # iTunes "extension" for compilation marking
254 "CM1" : "CM1 " # Seems to be some script kiddie tagging the tag.
255 # For example, [rH] join #rH on efnet [rH]
259 NULL_FRAME_FLAGS
= [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
261 TEXT_FRAME_RX
= re
.compile("^T[A-Z0-9][A-Z0-9][A-Z0-9]$");
262 USERTEXT_FRAME_RX
= re
.compile("^" + USERTEXT_FID
+ "$");
263 URL_FRAME_RX
= re
.compile("^W[A-Z0-9][A-Z0-9][A-Z0-9]$");
264 USERURL_FRAME_RX
= re
.compile("^" + USERURL_FID
+ "$");
265 COMMENT_FRAME_RX
= re
.compile("^" + COMMENT_FID
+ "$");
266 LYRICS_FRAME_RX
= re
.compile("^" + LYRICS_FID
+ "$");
267 CDID_FRAME_RX
= re
.compile("^" + CDID_FID
+ "$");
268 IMAGE_FRAME_RX
= re
.compile("^" + IMAGE_FID
+ "$");
269 OBJECT_FRAME_RX
= re
.compile("^" + OBJECT_FID
+ "$");
270 PLAYCOUNT_FRAME_RX
= re
.compile("^" + PLAYCOUNT_FID
+ "$");
271 UNIQUE_FILE_ID_FRAME_RX
= re
.compile("^" + UNIQUE_FILE_ID_FID
+ "$");
273 # MP3ext causes illegal frames to be inserted, which must be ignored.
274 # Copied from http://shell.lab49.com/~vivake/python/MP3Info.py
275 # Henning Kiel <henning.kiel@rwth-aachen.de>
286 "CM1 " # Script kiddie
289 LATIN1_ENCODING
= "\x00";
290 UTF_16_ENCODING
= "\x01";
291 UTF_16BE_ENCODING
= "\x02";
292 UTF_8_ENCODING
= "\x03";
294 DEFAULT_ENCODING
= LATIN1_ENCODING
;
295 DEFAULT_ID3_MAJOR_VERSION
= 2;
296 DEFAULT_ID3_MINOR_VERSION
= 4;
297 DEFAULT_LANG
= "eng";
300 return "/".join([x
for x
in s
.split('\x00') if x
])
302 def id3EncodingToString(encoding
):
303 if encoding
== LATIN1_ENCODING
:
305 elif encoding
== UTF_8_ENCODING
:
307 elif encoding
== UTF_16_ENCODING
:
309 elif encoding
== UTF_16BE_ENCODING
:
317 ################################################################################
318 class FrameException(Exception):
319 '''Thrown by invalid frames'''
322 ################################################################################
324 FRAME_HEADER_SIZE
= 10;
326 majorVersion
= DEFAULT_ID3_MAJOR_VERSION
;
327 minorVersion
= DEFAULT_ID3_MINOR_VERSION
;
328 # The 4 character frame ID.
330 # An array of 16 "bits"...
331 flags
= NULL_FRAME_FLAGS
;
332 # ...and the info they store.
340 dataLenIndicator
= 0;
341 # The size of the data following this header.
344 # 2.4 not only added flag bits, but also reordered the previously defined
345 # flags. So these are mapped once we know the version.
356 def __init__(self
, tagHeader
= None):
358 self
.setVersion(tagHeader
);
360 self
.setVersion([DEFAULT_ID3_MAJOR_VERSION
,
361 DEFAULT_ID3_MINOR_VERSION
]);
363 def setVersion(self
, tagHeader
):
364 # A slight hack to make the default ctor work.
365 if isinstance(tagHeader
, list):
366 self
.majorVersion
= tagHeader
[0];
367 self
.minorVersion
= tagHeader
[1];
369 self
.majorVersion
= tagHeader
.majorVersion
;
370 self
.minorVersion
= tagHeader
.minorVersion
;
371 # Correctly set size of header
372 if self
.minorVersion
== 2:
373 self
.FRAME_HEADER_SIZE
= 6;
375 self
.FRAME_HEADER_SIZE
= 10;
378 def setBitMask(self
):
379 major
= self
.majorVersion
;
380 minor
= self
.minorVersion
;
382 # 1.x tags are converted to 2.4 frames internally. These frames are
383 # created with frame flags \x00.
384 if (major
== 2 and minor
== 2):
385 # no flags for 2.2 frames
387 elif (major
== 2 and minor
== 3):
391 self
.COMPRESSION
= 8;
394 # This is not really in 2.3 frame header flags, but there is
395 # a "global" unsync bit in the tag header and that is written here
396 # so access to the tag header is not required.
398 # And this is mapped to an used bit, so that 0 is returned.
400 elif (major
== 2 and minor
== 4) or \
401 (major
== 1 and (minor
== 0 or minor
== 1)):
405 self
.COMPRESSION
= 12;
406 self
.ENCRYPTION
= 13;
411 raise ValueError("ID3 v" + str(major
) + "." + str(minor
) +\
412 " is not supported.");
414 def render(self
, dataSize
):
417 if self
.minorVersion
== 3:
418 data
+= bin2bytes(dec2bin(dataSize
, 32));
420 data
+= bin2bytes(bin2synchsafe(dec2bin(dataSize
, 32)));
423 self
.flags
= NULL_FRAME_FLAGS
;
424 self
.flags
[self
.TAG_ALTER
] = self
.tagAlter
;
425 self
.flags
[self
.FILE_ALTER
] = self
.fileAlter
;
426 self
.flags
[self
.READ_ONLY
] = self
.readOnly
;
427 self
.flags
[self
.COMPRESSION
] = self
.compressed
;
428 self
.flags
[self
.COMPRESSION
] = self
.compressed
;
429 self
.flags
[self
.ENCRYPTION
] = self
.encrypted
;
430 self
.flags
[self
.GROUPING
] = self
.grouped
;
431 self
.flags
[self
.UNSYNC
] = self
.unsync
;
432 self
.flags
[self
.DATA_LEN
] = self
.dataLenIndicator
;
434 data
+= bin2bytes(self
.flags
);
438 def parse2_2(self
, f
):
439 frameId_22
= f
.read(3);
440 frameId
= map2_2FrameId(frameId_22
);
441 if self
.isFrameIdValid(frameId
):
442 TRACE_MSG("FrameHeader [id]: %s (0x%x%x%x)" % (frameId_22
,
445 ord(frameId_22
[2])));
447 # dataSize corresponds to the size of the data segment after
448 # encryption, compression, and unsynchronization.
450 self
.dataSize
= bin2dec(bytes2bin(sz
, 8));
451 TRACE_MSG("FrameHeader [data size]: %d (0x%X)" % (self
.dataSize
,
454 elif frameId
== '\x00\x00\x00':
455 TRACE_MSG("FrameHeader: Null frame id found at byte " +\
457 elif not strictID3() and frameId
in KNOWN_BAD_FRAMES
:
458 TRACE_MSG("FrameHeader: Illegal but known frame found; "\
459 "Happily ignoring" + str(f
.tell()));
461 raise FrameException("FrameHeader: Illegal Frame ID: " + frameId
);
466 # Returns 1 on success and 0 when a null tag (marking the beginning of
467 # padding). In the case of an invalid frame header, a FrameException is
470 TRACE_MSG("FrameHeader [start byte]: %d (0x%X)" % (f
.tell(),
472 if self
.minorVersion
== 2:
473 return self
.parse2_2(f
)
476 if self
.isFrameIdValid(frameId
):
477 TRACE_MSG("FrameHeader [id]: %s (0x%x%x%x%x)" % (frameId
,
483 # dataSize corresponds to the size of the data segment after
484 # encryption, compression, and unsynchronization.
486 # In ID3 v2.4 this value became a synch-safe integer, meaning only
487 # the low 7 bits are used per byte.
488 if self
.minorVersion
== 3:
489 self
.dataSize
= bin2dec(bytes2bin(sz
, 8));
491 self
.dataSize
= bin2dec(bytes2bin(sz
, 7));
492 TRACE_MSG("FrameHeader [data size]: %d (0x%X)" % (self
.dataSize
,
497 self
.flags
= bytes2bin(flags
);
498 self
.tagAlter
= self
.flags
[self
.TAG_ALTER
];
499 self
.fileAlter
= self
.flags
[self
.FILE_ALTER
];
500 self
.readOnly
= self
.flags
[self
.READ_ONLY
];
501 self
.compressed
= self
.flags
[self
.COMPRESSION
];
502 self
.encrypted
= self
.flags
[self
.ENCRYPTION
];
503 self
.grouped
= self
.flags
[self
.GROUPING
];
504 self
.unsync
= self
.flags
[self
.UNSYNC
];
505 self
.dataLenIndicator
= self
.flags
[self
.DATA_LEN
];
506 TRACE_MSG("FrameHeader [flags]: ta(%d) fa(%d) ro(%d) co(%d) "\
507 "en(%d) gr(%d) un(%d) dl(%d)" % (self
.tagAlter
,
514 self
.dataLenIndicator
));
515 if self
.minorVersion
>= 4 and self
.compressed
and \
516 not self
.dataLenIndicator
:
517 raise FrameException("Invalid frame; compressed with no data "
521 elif frameId
== '\x00\x00\x00\x00':
522 TRACE_MSG("FrameHeader: Null frame id found at byte " +\
524 elif not strictID3() and frameId
in KNOWN_BAD_FRAMES
:
525 TRACE_MSG("FrameHeader: Illegal but known "\
526 "(possibly created by the shitty mp3ext) frame found; "\
527 "Happily ignoring!" + str(f
.tell()));
529 raise FrameException("FrameHeader: Illegal Frame ID: " + frameId
);
533 def isFrameIdValid(self
, id):
534 return re
.compile(r
"^[A-Z0-9][A-Z0-9][A-Z0-9][A-Z0-9]$").match(id);
536 def clearFlags(self
):
539 ################################################################################
540 def unsyncData(data
):
548 elif val
== '\x00' or val
>= '\xe0':
549 output
.append('\x00')
551 safe
= (val
!= '\xff')
556 output
.append('\x00')
557 return ''.join(output
)
559 def deunsyncData(data
):
565 safe
= (val
!= '\xff')
570 return ''.join(output
)
573 ################################################################################
576 def __init__(self
, frameHeader
, unsync_default
):
578 self
.decompressedSize
= 0
580 self
.encryptionMethod
= 0
582 self
.encoding
= DEFAULT_ENCODING
583 self
.header
= frameHeader
584 self
.unsync_default
= unsync_default
587 desc
= self
.getFrameDesc();
588 return '<%s Frame (%s)>' % (desc
, self
.header
.id);
590 def unsync(self
, data
):
591 data
= unsyncData(data
)
594 def deunsync(self
, data
):
595 data
= deunsyncData(data
)
598 def decompress(self
, data
):
599 TRACE_MSG("before decompression: %d bytes" % len(data
));
600 data
= zlib
.decompress(data
, 15, self
.decompressedSize
);
601 TRACE_MSG("after decompression: %d bytes" % len(data
));
604 def compress(self
, data
):
605 TRACE_MSG("before compression: %d bytes" % len(data
));
606 data
= zlib
.compress(data
);
607 TRACE_MSG("after compression: %d bytes" % len(data
));
610 def decrypt(self
, data
):
611 raise FrameException("Encryption not supported");
613 def encrypt(self
, data
):
614 raise FrameException("Encryption not supported");
616 def disassembleFrame(self
, data
):
617 # Format flags in the frame header may add extra data to the
618 # beginning of this data.
619 if self
.header
.minorVersion
<= 3:
620 # 2.3: compression(4), encryption(1), group(1)
621 if self
.header
.compressed
:
622 self
.decompressedSize
= bin2dec(bytes2bin(data
[:4]));
624 TRACE_MSG("Decompressed Size: %d" % self
.decompressedSize
);
625 if self
.header
.encrypted
:
626 self
.encryptionMethod
= bin2dec(bytes2bin(data
[0]));
628 TRACE_MSG("Encryption Method: %d" % self
.encryptionMethod
);
629 if self
.header
.grouped
:
630 self
.groupId
= bin2dec(bytes2bin(data
[0]));
632 TRACE_MSG("Group ID: %d" % self
.groupId
);
634 # 2.4: group(1), encrypted(1), dataLenIndicator(4,7)
635 if self
.header
.grouped
:
636 self
.groupId
= bin2dec(bytes2bin(data
[0]));
638 if self
.header
.encrypted
:
639 self
.encryptionMethod
= bin2dec(bytes2bin(data
[0]));
641 TRACE_MSG("Encryption Method: %d" % self
.encryptionMethod
);
642 TRACE_MSG("Group ID: %d" % self
.groupId
);
643 if self
.header
.dataLenIndicator
:
644 self
.dataLen
= bin2dec(bytes2bin(data
[:4], 7));
646 TRACE_MSG("Data Length: %d" % self
.dataLen
);
647 if self
.header
.compressed
:
648 self
.decompressedSize
= self
.dataLen
;
649 TRACE_MSG("Decompressed Size: %d" % self
.decompressedSize
);
651 if self
.header
.unsync
or self
.unsync_default
:
652 data
= self
.deunsync(data
)
653 if self
.header
.encrypted
:
654 data
= self
.decrypt(data
);
655 if self
.header
.compressed
:
656 data
= self
.decompress(data
);
659 def assembleFrame (self
, data
):
661 if self
.header
.minorVersion
== 3:
662 if self
.header
.compressed
:
663 formatFlagData
+= bin2bytes(dec2bin(len(data
), 32));
664 if self
.header
.encrypted
:
665 formatFlagData
+= bin2bytes(dec2bin(self
.encryptionMethod
, 8));
666 if self
.header
.grouped
:
667 formatFlagData
+= bin2bytes(dec2bin(self
.groupId
, 8));
669 if self
.header
.grouped
:
670 formatFlagData
+= bin2bytes(dec2bin(self
.groupId
, 8));
671 if self
.header
.encrypted
:
672 formatFlagData
+= bin2bytes(dec2bin(self
.encryptionMethod
, 8));
673 if self
.header
.compressed
or self
.header
.dataLenIndicator
:
674 # Just in case, not sure about this?
675 self
.header
.dataLenIndicator
= 1;
676 formatFlagData
+= bin2bytes(dec2bin(len(data
), 32));
678 if self
.header
.compressed
:
679 data
= self
.compress(data
);
680 if self
.header
.encrypted
:
681 data
= self
.encrypt(data
);
682 if self
.header
.unsync
or self
.unsync_default
:
683 data
= self
.unsync(data
)
685 data
= formatFlagData
+ data
;
686 return self
.header
.render(len(data
)) + data
;
688 def getFrameDesc(self
):
690 return frameDesc
[self
.header
.id];
693 return obsoleteFrames
[self
.header
.id];
695 return "UNKOWN FRAME";
697 def getTextDelim(self
):
698 if self
.encoding
== UTF_16_ENCODING
or \
699 self
.encoding
== UTF_16BE_ENCODING
:
704 ################################################################################
705 class TextFrame(Frame
):
708 # Data string format:
709 # encoding (one byte) + text
710 def __init__(self
, frameHeader
, data
=None, text
=u
"",
711 encoding
=DEFAULT_ENCODING
, unsync_default
=False):
712 Frame
.__init
__(self
, frameHeader
, unsync_default
)
714 self
._set
(data
, frameHeader
);
717 assert(text
!= None and isinstance(text
, unicode));
718 self
.encoding
= encoding
;
721 # Data string format:
722 # encoding (one byte) + text;
723 def _set(self
, data
, frameHeader
):
724 fid
= frameHeader
.id;
725 if not TEXT_FRAME_RX
.match(fid
) or USERTEXT_FRAME_RX
.match(fid
):
726 raise FrameException("Invalid frame id for TextFrame: " + fid
);
728 data
= self
.disassembleFrame(data
);
729 self
.encoding
= data
[0];
730 TRACE_MSG("TextFrame encoding: %s" % id3EncodingToString(self
.encoding
));
732 self
.text
= unicode(data
[1:], id3EncodingToString(self
.encoding
));
734 self
.text
= cleanNulls(self
.text
)
735 except TypeError, excArg
:
736 # if data is already unicode, just copy it
737 if excArg
.args
== ("decoding Unicode is not supported",):
738 self
.text
= data
[1:];
740 self
.text
= cleanNulls(self
.text
)
744 TRACE_MSG("TextFrame text: %s" % self
.text
);
746 def __unicode__(self
):
747 return u
'<%s (%s): %s>' % (self
.getFrameDesc(), self
.header
.id,
751 if self
.header
.minorVersion
== 4 and self
.header
.id == "TSIZ":
752 TRACE_MSG("Dropping deprecated frame TSIZ")
754 data
= self
.encoding
+\
755 self
.text
.encode(id3EncodingToString(self
.encoding
));
756 return self
.assembleFrame(data
);
758 ################################################################################
759 class DateFrame(TextFrame
):
763 def __init__(self
, frameHeader
, data
=None, date_str
=None,
764 encoding
=DEFAULT_ENCODING
, unsync_default
=False):
766 TextFrame
.__init
__(self
, frameHeader
, data
=data
,
767 encoding
=encoding
, unsync_default
=unsync_default
)
768 self
._set
(data
, frameHeader
)
770 assert(date_str
and isinstance(date_str
, unicode))
771 TextFrame
.__init
__(self
, frameHeader
, text
=date_str
,
772 encoding
=encoding
, unsync_default
=unsync_default
)
773 self
.setDate(self
.text
)
775 def _set(self
, data
, frameHeader
):
776 TextFrame
._set
(self
, data
, frameHeader
);
777 if self
.header
.id[:2] != "TD" and self
.header
.minorVersion
>= 4:
778 raise FrameException("Invalid frame id for DateFrame: " + \
781 def setDate(self
, d
):
787 for fmt
in timeStampFormats
:
789 if isinstance(d
, tuple):
790 self
.date_str
= unicode(time
.strftime(fmt
, d
));
793 assert(isinstance(d
, unicode));
794 # Witnessed oddball tags with NULL bytes (ozzy.tag from id3lib)
798 self
.date
= time
.strptime(d
, fmt
);
799 except TypeError, ex
:
807 if strictID3() and not self
.date
:
808 raise FrameException("Invalid Date: " + str(d
));
809 self
.text
= self
.date_str
;
812 return self
.date_str
;
816 return self
.__padDateField
(self
.date
[0], 4);
822 return self
.__padDateField
(self
.date
[1], 2);
828 return self
.__padDateField
(self
.date
[2], 2);
834 return self
.__padDateField
(self
.date
[3], 2);
840 return self
.__padDateField
(self
.date
[4], 2);
846 return self
.__padDateField
(self
.date
[5], 2);
850 def __padDateField(self
, f
, sz
):
855 fStr
= ("0" * (sz
- len(fStr
))) + fStr
;
857 raise TagException("Invalid date field: " + fStr
);
862 if self
.header
.minorVersion
== 4 and\
863 (self
.header
.id == OBSOLETE_DATE_FID
or\
864 self
.header
.id == OBSOLETE_YEAR_FID
or\
865 self
.header
.id == OBSOLETE_TIME_FID
or\
866 self
.header
.id == OBSOLETE_RECORDING_DATE_FID
):
867 self
.header
.id = "TDRC";
868 elif self
.header
.minorVersion
== 4 and\
869 self
.header
.id == OBSOLETE_ORIG_RELEASE_FID
:
870 self
.header
.id = "TDOR";
871 elif self
.header
.minorVersion
== 3 and self
.header
.id == "TDOR":
872 self
.header
.id = OBSOLETE_ORIG_RELEASE_FID
;
873 elif self
.header
.minorVersion
== 3 and self
.header
.id == "TDEN":
874 TRACE_MSG('Converting TDEN to TXXX(Encoding time) frame')
875 self
.header
.id = "TXXX";
876 self
.description
= "Encoding time";
877 data
= self
.encoding
+\
878 self
.description
.encode(id3EncodingToString(self
.encoding
)) +\
879 self
.getTextDelim() +\
880 self
.date_str
.encode(id3EncodingToString(self
.encoding
));
881 return self
.assembleFrame(data
)
883 elif self
.header
.minorVersion
== 3 and self
.header
.id[:2] == "TD":
884 if self
.header
.id not in ['TDEN', 'TDLY', 'TDTG']:
885 self
.header
.id = OBSOLETE_YEAR_FID
;
887 data
= self
.encoding
+\
888 self
.date_str
.encode(id3EncodingToString(self
.encoding
));
889 data
= self
.assembleFrame(data
);
893 ################################################################################
894 class UserTextFrame(TextFrame
):
897 # Data string format:
898 # encoding (one byte) + description + "\x00" + text
899 def __init__(self
, frameHeader
, data
=None, description
=u
"", text
=u
"",
900 encoding
=DEFAULT_ENCODING
, unsync_default
=False):
902 TextFrame
.__init
__(self
, frameHeader
, data
=data
,
903 unsync_default
=unsync_default
)
904 self
._set
(data
, frameHeader
)
906 assert(isinstance(description
, unicode) and\
907 isinstance(text
, unicode))
908 TextFrame
.__init
__(self
, frameHeader
, text
=text
, encoding
=encoding
,
909 unsync_default
=unsync_default
)
910 self
.description
= description
912 # Data string format:
913 # encoding (one byte) + description + "\x00" + text;
914 def _set(self
, data
, frameHeader
= None):
916 if not USERTEXT_FRAME_RX
.match(frameHeader
.id):
917 raise FrameException("Invalid frame id for UserTextFrame: " +\
920 data
= self
.disassembleFrame(data
);
921 self
.encoding
= data
[0];
922 TRACE_MSG("UserTextFrame encoding: %s" %\
923 id3EncodingToString(self
.encoding
));
924 (d
, t
) = splitUnicode(data
[1:], self
.encoding
);
925 self
.description
= unicode(d
, id3EncodingToString(self
.encoding
));
926 TRACE_MSG("UserTextFrame description: %s" % self
.description
);
927 self
.text
= unicode(t
, id3EncodingToString(self
.encoding
));
929 self
.text
= cleanNulls(self
.text
)
930 TRACE_MSG("UserTextFrame text: %s" % self
.text
);
933 if self
.header
.minorVersion
== 4:
934 if self
.description
.lower() == 'tagging time':
935 TRACE_MSG("Converting TXXX(%s) to TDTG frame)" % self
.description
)
937 if self
.description
.lower() == 'encoding time':
938 TRACE_MSG("Converting TXXX(%s) to TDEN frame" % self
.description
)
939 self
.header
.id = 'TDEN'
940 data
= self
.encoding
+\
941 self
.text
.encode(id3EncodingToString(self
.encoding
))
942 return self
.assembleFrame(data
);
943 data
= self
.encoding
+\
944 self
.description
.encode(id3EncodingToString(self
.encoding
)) +\
945 self
.getTextDelim() +\
946 self
.text
.encode(id3EncodingToString(self
.encoding
));
947 return self
.assembleFrame(data
);
949 def __unicode__(self
):
950 return u
'<%s (%s): {Desc: %s} %s>' % (self
.getFrameDesc(),
952 self
.description
, self
.text
);
954 ################################################################################
955 class URLFrame(Frame
):
958 # Data string format:
960 def __init__(self
, frameHeader
, data
=None, url
=None, unsync_default
=False):
961 Frame
.__init
__(self
, frameHeader
, unsync_default
)
963 self
._set
(data
, frameHeader
)
968 # Data string format:
970 def _set(self
, data
, frameHeader
):
971 fid
= frameHeader
.id;
972 if not URL_FRAME_RX
.match(fid
) or USERURL_FRAME_RX
.match(fid
):
973 raise FrameException("Invalid frame id for URLFrame: " + fid
);
974 data
= self
.disassembleFrame(data
);
977 self
.url
= cleanNulls(self
.url
)
980 data
= str(self
.url
);
981 return self
.assembleFrame(data
);
984 return '<%s (%s): %s>' % (self
.getFrameDesc(), self
.header
.id,
987 ################################################################################
988 class UserURLFrame(URLFrame
):
991 # Data string format:
992 # encoding (one byte) + description + "\x00" + url
993 def __init__(self
, frameHeader
, data
=None, url
="", description
=u
"",
994 encoding
=DEFAULT_ENCODING
, unsync_default
=False):
995 Frame
.__init
__(self
, frameHeader
, unsync_default
)
997 self
._set
(data
, frameHeader
);
1000 assert(description
and isinstance(description
, unicode));
1001 assert(url
and isinstance(url
, str));
1002 self
.encoding
= encoding
;
1004 self
.description
= description
;
1006 # Data string format:
1007 # encoding (one byte) + description + "\x00" + url;
1008 def _set(self
, data
, frameHeader
):
1009 assert(data
and frameHeader
);
1010 if not USERURL_FRAME_RX
.match(frameHeader
.id):
1011 raise FrameException("Invalid frame id for UserURLFrame: " +\
1014 data
= self
.disassembleFrame(data
);
1015 self
.encoding
= data
[0];
1016 TRACE_MSG("UserURLFrame encoding: %s" %\
1017 id3EncodingToString(self
.encoding
));
1019 (d
, u
) = splitUnicode(data
[1:], self
.encoding
);
1020 except ValueError, ex
:
1022 raise FrameException("Invalid WXXX frame, no null byte")
1025 self
.description
= unicode(d
, id3EncodingToString(self
.encoding
));
1026 TRACE_MSG("UserURLFrame description: %s" % self
.description
);
1029 self
.url
= cleanNulls(self
.url
)
1030 TRACE_MSG("UserURLFrame text: %s" % self
.url
);
1033 data
= self
.encoding
+\
1034 self
.description
.encode(id3EncodingToString(self
.encoding
)) +\
1035 self
.getTextDelim() + self
.url
;
1036 return self
.assembleFrame(data
);
1038 def __unicode__(self
):
1039 return u
'<%s (%s): %s [Encoding: %s] [Desc: %s]>' %\
1040 (self
.getFrameDesc(), self
.header
.id,
1041 self
.url
, self
.encoding
, self
.description
)
1043 ################################################################################
1044 class CommentFrame(Frame
):
1049 # Data string format:
1050 # encoding (one byte) + lang (three byte code) + description + "\x00" +
1052 def __init__(self
, frameHeader
, data
=None, lang
="",
1053 description
=u
"", comment
=u
"", encoding
=DEFAULT_ENCODING
,
1054 unsync_default
=False):
1055 Frame
.__init
__(self
, frameHeader
, unsync_default
)
1057 self
._set
(data
, frameHeader
)
1059 assert(isinstance(description
, unicode))
1060 assert(isinstance(comment
, unicode))
1061 assert(isinstance(lang
, str))
1062 self
.encoding
= encoding
1064 self
.description
= description
1065 self
.comment
= comment
1067 # Data string format:
1068 # encoding (one byte) + lang (three byte code) + description + "\x00" +
1070 def _set(self
, data
, frameHeader
= None):
1071 assert(frameHeader
);
1072 if not COMMENT_FRAME_RX
.match(frameHeader
.id):
1073 raise FrameException("Invalid frame id for CommentFrame: " +\
1076 data
= self
.disassembleFrame(data
);
1077 self
.encoding
= data
[0];
1078 TRACE_MSG("CommentFrame encoding: " + id3EncodingToString(self
.encoding
));
1080 self
.lang
= str(data
[1:4]).strip("\x00");
1081 # Test ascii encoding
1082 temp_lang
= unicode(self
.lang
, "ascii");
1084 not re
.compile("[A-Z][A-Z][A-Z]", re
.IGNORECASE
).match(self
.lang
):
1086 raise FrameException("[CommentFrame] Invalid language "\
1087 "code: %s" % self
.lang
);
1088 except UnicodeDecodeError, ex
:
1090 raise FrameException("[CommentFrame] Invalid language code: "\
1091 "[%s] %s" % (ex
.object, ex
.reason
));
1095 (d
, c
) = splitUnicode(data
[4:], self
.encoding
);
1096 self
.description
= unicode(d
, id3EncodingToString(self
.encoding
));
1097 self
.comment
= unicode(c
, id3EncodingToString(self
.encoding
));
1100 raise FrameException("Invalid comment; no description/comment");
1102 self
.description
= u
"";
1105 self
.description
= cleanNulls(self
.description
)
1106 self
.comment
= cleanNulls(self
.comment
)
1109 lang
= self
.lang
.encode("ascii");
1113 lang
= lang
+ ('\x00' * (3 - len(lang
)));
1114 data
= self
.encoding
+ lang
+\
1115 self
.description
.encode(id3EncodingToString(self
.encoding
)) +\
1116 self
.getTextDelim() +\
1117 self
.comment
.encode(id3EncodingToString(self
.encoding
));
1118 return self
.assembleFrame(data
);
1120 def __unicode__(self
):
1121 return u
"<%s (%s): %s [Lang: %s] [Desc: %s]>" %\
1122 (self
.getFrameDesc(), self
.header
.id, self
.comment
,
1123 self
.lang
, self
.description
);
1125 ################################################################################
1126 class LyricsFrame(Frame
):
1131 # Data string format:
1132 # encoding (one byte) + lang (three byte code) + description + "\x00" +
1134 def __init__(self
, frameHeader
, data
=None, lang
="",
1135 description
=u
"", lyrics
=u
"", encoding
=DEFAULT_ENCODING
,
1136 unsync_default
=False):
1137 Frame
.__init
__(self
, frameHeader
, unsync_default
)
1139 self
._set
(data
, frameHeader
)
1141 assert(isinstance(description
, unicode))
1142 assert(isinstance(lyrics
, unicode))
1143 assert(isinstance(lang
, str))
1144 self
.encoding
= encoding
1146 self
.description
= description
1147 self
.lyrics
= lyrics
1149 # Data string format:
1150 # encoding (one byte) + lang (three byte code) + description + "\x00" +
1152 def _set(self
, data
, frameHeader
= None):
1153 assert(frameHeader
);
1154 if not LYRICS_FRAME_RX
.match(frameHeader
.id):
1155 raise FrameException("Invalid frame id for LyricsFrame: " +\
1158 data
= self
.disassembleFrame(data
);
1159 self
.encoding
= data
[0];
1160 TRACE_MSG("LyricsFrame encoding: " + id3EncodingToString(self
.encoding
));
1162 self
.lang
= str(data
[1:4]).strip("\x00");
1163 # Test ascii encoding
1164 temp_lang
= unicode(self
.lang
, "ascii");
1166 not re
.compile("[A-Z][A-Z][A-Z]", re
.IGNORECASE
).match(self
.lang
):
1168 raise FrameException("[LyricsFrame] Invalid language "\
1169 "code: %s" % self
.lang
);
1170 except UnicodeDecodeError, ex
:
1172 raise FrameException("[LyricsFrame] Invalid language code: "\
1173 "[%s] %s" % (ex
.object, ex
.reason
));
1177 (d
, c
) = splitUnicode(data
[4:], self
.encoding
);
1178 self
.description
= unicode(d
, id3EncodingToString(self
.encoding
));
1179 self
.lyrics
= unicode(c
, id3EncodingToString(self
.encoding
));
1182 raise FrameException("Invalid lyrics; no description/lyrics");
1184 self
.description
= u
"";
1187 self
.description
= cleanNulls(self
.description
)
1188 self
.lyrics
= cleanNulls(self
.lyrics
)
1191 lang
= self
.lang
.encode("ascii");
1195 lang
= lang
+ ('\x00' * (3 - len(lang
)));
1196 data
= self
.encoding
+ lang
+\
1197 self
.description
.encode(id3EncodingToString(self
.encoding
)) +\
1198 self
.getTextDelim() +\
1199 self
.lyrics
.encode(id3EncodingToString(self
.encoding
));
1200 return self
.assembleFrame(data
);
1202 def __unicode__(self
):
1203 return u
"<%s (%s): %s [Lang: %s] [Desc: %s]>" %\
1204 (self
.getFrameDesc(), self
.header
.id, self
.lyrics
,
1205 self
.lang
, self
.description
);
1207 ################################################################################
1208 # This class refers to the APIC frame, otherwise known as an "attached
1210 class ImageFrame(Frame
):
1214 # Contains the image data when the mimetype is image type.
1215 # Otherwise it is None.
1217 # Contains a URL for the image when the mimetype is "-->" per the spec.
1218 # Otherwise it is None.
1220 # Declared "picture types".
1222 ICON
= 0x01 # 32x32 png only.
1227 MEDIA
= 0x06 # label side of cd, picture disc vinyl, etc.
1234 RECORDING_LOCATION
= 0x0D
1235 DURING_RECORDING
= 0x0E
1236 DURING_PERFORMANCE
= 0x0F
1238 BRIGHT_COLORED_FISH
= 0x11 # There's always room for porno.
1241 PUBLISHER_LOGO
= 0x14
1243 MAX_TYPE
= PUBLISHER_LOGO
1245 def __init__(self
, frameHeader
, data
=None,
1247 imageData
=None, imageURL
=None,
1248 pictureType
=None, mimeType
=None,
1249 encoding
=DEFAULT_ENCODING
, unsync_default
=False):
1250 Frame
.__init
__(self
, frameHeader
, unsync_default
)
1252 self
._set
(data
, frameHeader
);
1254 assert(isinstance(description
, unicode));
1255 self
.description
= description
;
1256 self
.encoding
= encoding
;
1258 self
.mimeType
= mimeType
;
1259 assert(pictureType
!= None);
1260 self
.pictureType
= pictureType
;
1262 self
.imageData
= imageData
;
1264 self
.imageURL
= imageURL
;
1265 assert(self
.imageData
or self
.imageURL
);
1269 def create(type, imgFile
, desc
= u
"", encoding
= DEFAULT_ENCODING
):
1270 if not isinstance(desc
, unicode) or \
1271 not isinstance(type, int):
1272 raise FrameException("Wrong description and/or image-type type.");
1274 fp
= file(imgFile
, "rb");
1275 imgData
= fp
.read();
1276 mt
= mimetypes
.guess_type(imgFile
);
1278 raise FrameException("Unable to guess mime-type for %s" % (imgFile
));
1280 frameData
= DEFAULT_ENCODING
;
1281 frameData
+= mt
[0] + "\x00";
1282 frameData
+= bin2bytes(dec2bin(type, 8));
1283 frameData
+= desc
.encode(id3EncodingToString(encoding
)) + "\x00";
1284 frameData
+= imgData
;
1286 frameHeader
= FrameHeader();
1287 frameHeader
.id = IMAGE_FID
;
1288 return ImageFrame(frameHeader
, data
= frameData
);
1289 # Make create a static method. Odd....
1290 create
= staticmethod(create
);
1292 # Data string format:
1293 # <Header for 'Attached picture', ID: "APIC">
1295 # MIME type <text string> $00
1297 # Description <text string according to encoding> $00 (00)
1298 # Picture data <binary data>
1299 def _set(self
, data
, frameHeader
= None):
1300 assert(frameHeader
);
1301 if not IMAGE_FRAME_RX
.match(frameHeader
.id):
1302 raise FrameException("Invalid frame id for ImageFrame: " +\
1305 data
= self
.disassembleFrame(data
);
1307 input = StringIO(data
);
1308 TRACE_MSG("APIC frame data size: " + str(len(data
)));
1309 self
.encoding
= input.read(1);
1310 TRACE_MSG("APIC encoding: " + id3EncodingToString(self
.encoding
));
1314 if self
.header
.minorVersion
!= 2:
1316 while ch
and ch
!= "\x00":
1320 # v2.2 (OBSOLETE) special case
1321 self
.mimeType
= input.read(3);
1322 TRACE_MSG("APIC mime type: " + self
.mimeType
);
1323 if strictID3() and not self
.mimeType
:
1324 raise FrameException("APIC frame does not contain a mime type");
1325 if self
.mimeType
.find("/") == -1:
1326 self
.mimeType
= "image/" + self
.mimeType
;
1328 pt
= ord(input.read(1));
1329 TRACE_MSG("Initial APIC picture type: " + str(pt
));
1330 if pt
< self
.MIN_TYPE
or pt
> self
.MAX_TYPE
:
1332 raise FrameException("Invalid APIC picture type: %d" % (pt
));
1333 # Rather than force this to UNKNOWN, let's assume that they put a
1334 # character literal instead of it's byte value.
1339 if pt
< self
.MIN_TYPE
or pt
> self
.MAX_TYPE
:
1340 self
.pictureType
= self
.OTHER
;
1341 self
.pictureType
= pt
;
1342 TRACE_MSG("APIC picture type: " + str(self
.pictureType
));
1344 self
.desciption
= u
"";
1346 # Remaining data is a NULL separated description and image data
1347 buffer = input.read();
1350 (desc
, img
) = splitUnicode(buffer, self
.encoding
);
1351 TRACE_MSG("description len: %d" % len(desc
));
1352 TRACE_MSG("description len: %d" % len(img
));
1353 self
.description
= unicode(desc
, id3EncodingToString(self
.encoding
));
1354 TRACE_MSG("APIC description: " + self
.description
);
1356 if self
.mimeType
.find("-->") != -1:
1357 self
.imageData
= None;
1358 self
.imageURL
= img
;
1360 self
.imageData
= img
;
1361 self
.imageURL
= None;
1362 TRACE_MSG("APIC image data: " + str(len(self
.imageData
)) + " bytes");
1363 if strictID3() and not self
.imageData
and not self
.imageURL
:
1364 raise FrameException("APIC frame does not contain any image data");
1367 def writeFile(self
, path
= "./", name
= None):
1368 if not self
.imageData
:
1369 raise IOError("Fetching remote image files is not implemented.");
1371 name
= self
.getDefaultFileName();
1372 imageFile
= os
.path
.join(path
, name
);
1374 f
= file(imageFile
, "wb");
1375 f
.write(self
.imageData
);
1378 def getDefaultFileName(self
, suffix
= ""):
1379 nameStr
= self
.picTypeToString(self
.pictureType
);
1382 nameStr
= nameStr
+ "." + self
.mimeType
.split("/")[1];
1386 data
= self
.encoding
+ self
.mimeType
+ "\x00" +\
1387 bin2bytes(dec2bin(self
.pictureType
, 8)) +\
1388 self
.description
.encode(id3EncodingToString(self
.encoding
)) +\
1389 self
.getTextDelim();
1391 data
+= self
.imageURL
.encode("ascii");
1393 data
+= self
.imageData
;
1394 return self
.assembleFrame(data
);
1396 def stringToPicType(s
):
1398 return ImageFrame
.OTHER
;
1400 return ImageFrame
.ICON
;
1401 elif s
== "OTHER_ICON":
1402 return ImageFrame
.OTHER_ICON
;
1403 elif s
== "FRONT_COVER":
1404 return ImageFrame
.FRONT_COVER
1405 elif s
== "BACK_COVER":
1406 return ImageFrame
.BACK_COVER
;
1407 elif s
== "LEAFLET":
1408 return ImageFrame
.LEAFLET
;
1410 return ImageFrame
.MEDIA
;
1411 elif s
== "LEAD_ARTIST":
1412 return ImageFrame
.LEAD_ARTIST
;
1414 return ImageFrame
.ARTIST
;
1415 elif s
== "CONDUCTOR":
1416 return ImageFrame
.CONDUCTOR
;
1418 return ImageFrame
.BAND
;
1419 elif s
== "COMPOSER":
1420 return ImageFrame
.COMPOSER
;
1421 elif s
== "LYRICIST":
1422 return ImageFrame
.LYRICIST
;
1423 elif s
== "RECORDING_LOCATION":
1424 return ImageFrame
.RECORDING_LOCATION
;
1425 elif s
== "DURING_RECORDING":
1426 return ImageFrame
.DURING_RECORDING
;
1427 elif s
== "DURING_PERFORMANCE":
1428 return ImageFrame
.DURING_PERFORMANCE
;
1430 return ImageFrame
.VIDEO
;
1431 elif s
== "BRIGHT_COLORED_FISH":
1432 return ImageFrame
.BRIGHT_COLORED_FISH
;
1433 elif s
== "ILLUSTRATION":
1434 return ImageFrame
.ILLUSTRATION
;
1435 elif s
== "BAND_LOGO":
1436 return ImageFrame
.BAND_LOGO
;
1437 elif s
== "PUBLISHER_LOGO":
1438 return ImageFrame
.PUBLISHER_LOGO
;
1440 raise FrameException("Invalid APIC picture type: %s" % s
);
1441 stringToPicType
= staticmethod(stringToPicType
);
1443 def picTypeToString(t
):
1444 if t
== ImageFrame
.OTHER
:
1446 elif t
== ImageFrame
.ICON
:
1448 elif t
== ImageFrame
.OTHER_ICON
:
1449 return "OTHER_ICON";
1450 elif t
== ImageFrame
.FRONT_COVER
:
1451 return "FRONT_COVER";
1452 elif t
== ImageFrame
.BACK_COVER
:
1453 return "BACK_COVER";
1454 elif t
== ImageFrame
.LEAFLET
:
1456 elif t
== ImageFrame
.MEDIA
:
1458 elif t
== ImageFrame
.LEAD_ARTIST
:
1459 return "LEAD_ARTIST";
1460 elif t
== ImageFrame
.ARTIST
:
1462 elif t
== ImageFrame
.CONDUCTOR
:
1464 elif t
== ImageFrame
.BAND
:
1466 elif t
== ImageFrame
.COMPOSER
:
1468 elif t
== ImageFrame
.LYRICIST
:
1470 elif t
== ImageFrame
.RECORDING_LOCATION
:
1471 return "RECORDING_LOCATION";
1472 elif t
== ImageFrame
.DURING_RECORDING
:
1473 return "DURING_RECORDING";
1474 elif t
== ImageFrame
.DURING_PERFORMANCE
:
1475 return "DURING_PERFORMANCE";
1476 elif t
== ImageFrame
.VIDEO
:
1478 elif t
== ImageFrame
.BRIGHT_COLORED_FISH
:
1479 return "BRIGHT_COLORED_FISH";
1480 elif t
== ImageFrame
.ILLUSTRATION
:
1481 return "ILLUSTRATION";
1482 elif t
== ImageFrame
.BAND_LOGO
:
1484 elif t
== ImageFrame
.PUBLISHER_LOGO
:
1485 return "PUBLISHER_LOGO";
1487 raise FrameException("Invalid APIC picture type: %d" % t
);
1488 picTypeToString
= staticmethod(picTypeToString
);
1490 ################################################################################
1491 # This class refers to the GEOB frame
1492 class ObjectFrame(Frame
):
1498 def __init__(self
, frameHeader
, data
=None,
1499 desc
=u
"", filename
=u
"",
1500 objectData
=None, mimeType
=None,
1501 encoding
=DEFAULT_ENCODING
, unsync_default
=False):
1502 Frame
.__init
__(self
, frameHeader
, unsync_default
)
1504 self
._set
(data
, frameHeader
);
1506 assert(isinstance(desc
, unicode));
1507 self
.description
= desc
;
1508 assert(isinstance(filename
, unicode));
1509 self
.filename
= filename
;
1510 self
.encoding
= encoding
;
1512 self
.mimeType
= mimeType
;
1514 self
.objectData
= objectData
;
1517 def create(objFile
, mime
= u
"", desc
= u
"", filename
= None,
1518 encoding
= DEFAULT_ENCODING
):
1519 if filename
== None:
1520 filename
= unicode(os
.path
.basename(objFile
));
1521 if not isinstance(desc
, unicode) or \
1522 (not isinstance(filename
, unicode) and filename
!= ""):
1523 raise FrameException("Wrong description and/or filename type.");
1525 fp
= file(objFile
, "rb");
1526 objData
= fp
.read();
1528 print("Using specified mime type %s" % mime
);
1530 mt
= mimetypes
.guess_type(objFile
);
1532 raise FrameException("Unable to guess mime-type for %s" %
1535 print("Guessing mime type %s" % mime
);
1537 frameData
= DEFAULT_ENCODING
;
1538 frameData
+= mime
+ "\x00";
1539 frameData
+= filename
.encode(id3EncodingToString(encoding
)) + "\x00";
1540 frameData
+= desc
.encode(id3EncodingToString(encoding
)) + "\x00";
1541 frameData
+= objData
;
1543 frameHeader
= FrameHeader();
1544 frameHeader
.id = OBJECT_FID
;
1545 return ObjectFrame(frameHeader
, data
= frameData
);
1546 # Make create a static method. Odd....
1547 create
= staticmethod(create
);
1549 # Data string format:
1550 # <Header for 'General encapsulated object', ID: "GEOB">
1552 # MIME type <text string> $00
1553 # Filename <text string according to encoding> $00 (00)
1554 # Content description <text string according to encoding> $00 (00)
1555 # Encapsulated object <binary data>
1556 def _set(self
, data
, frameHeader
= None):
1557 assert(frameHeader
);
1558 if not OBJECT_FRAME_RX
.match(frameHeader
.id):
1559 raise FrameException("Invalid frame id for ObjectFrame: " +\
1562 data
= self
.disassembleFrame(data
);
1564 input = StringIO(data
);
1565 TRACE_MSG("GEOB frame data size: " + str(len(data
)));
1566 self
.encoding
= input.read(1);
1567 TRACE_MSG("GEOB encoding: " + id3EncodingToString(self
.encoding
));
1571 if self
.header
.minorVersion
!= 2:
1574 self
.mimeType
+= ch
;
1577 # v2.2 (OBSOLETE) special case
1578 self
.mimeType
= input.read(3);
1579 TRACE_MSG("GEOB mime type: " + self
.mimeType
);
1580 if strictID3() and not self
.mimeType
:
1581 raise FrameException("GEOB frame does not contain a mime type");
1582 if strictID3() and self
.mimeType
.find("/") == -1:
1583 raise FrameException("GEOB frame does not contain a valid mime type");
1585 self
.filename
= u
"";
1586 self
.description
= u
"";
1588 # Remaining data is a NULL separated filename, description and object data
1589 buffer = input.read();
1592 (filename
, buffer) = splitUnicode(buffer, self
.encoding
);
1593 (desc
, obj
) = splitUnicode(buffer, self
.encoding
);
1594 TRACE_MSG("filename len: %d" % len(filename
));
1595 TRACE_MSG("description len: %d" % len(desc
));
1596 TRACE_MSG("data len: %d" % len(obj
));
1597 self
.filename
= unicode(filename
, id3EncodingToString(self
.encoding
));
1598 self
.description
= unicode(desc
, id3EncodingToString(self
.encoding
));
1599 TRACE_MSG("GEOB filename: " + self
.filename
);
1600 TRACE_MSG("GEOB description: " + self
.description
);
1602 self
.objectData
= obj
;
1603 TRACE_MSG("GEOB data: " + str(len(self
.objectData
)) + " bytes");
1604 if strictID3() and not self
.objectData
:
1605 raise FrameException("GEOB frame does not contain any data");
1608 def writeFile(self
, path
= "./", name
= None):
1609 if not self
.objectData
:
1610 raise IOError("Fetching remote object files is not implemented.");
1612 name
= self
.getDefaultFileName();
1613 objectFile
= os
.path
.join(path
, name
);
1615 f
= file(objectFile
, "wb");
1616 f
.write(self
.objectData
);
1619 def getDefaultFileName(self
, suffix
= ""):
1620 nameStr
= self
.filename
;
1623 nameStr
= nameStr
+ "." + self
.mimeType
.split("/")[1];
1627 data
= self
.encoding
+ self
.mimeType
+ "\x00" +\
1628 self
.filename
.encode(id3EncodingToString(self
.encoding
)) +\
1629 self
.getTextDelim() +\
1630 self
.description
.encode(id3EncodingToString(self
.encoding
)) +\
1631 self
.getTextDelim() +\
1633 return self
.assembleFrame(data
);
1635 class PlayCountFrame(Frame
):
1638 def __init__(self
, frameHeader
, data
=None, count
=None,
1639 unsync_default
=False):
1640 Frame
.__init
__(self
, frameHeader
, unsync_default
)
1642 self
._set
(data
, frameHeader
);
1644 assert(count
!= None and count
>= 0);
1647 def _set(self
, data
, frameHeader
):
1648 assert(frameHeader
);
1649 assert(len(data
) >= 4);
1650 self
.count
= long(bytes2dec(data
));
1653 data
= dec2bytes(self
.count
, 32);
1654 return self
.assembleFrame(data
);
1656 class UniqueFileIDFrame(Frame
):
1660 def __init__(self
, frameHeader
, data
=None, owner_id
=None, id=None,
1661 unsync_default
=False):
1662 Frame
.__init
__(self
, frameHeader
, unsync_default
)
1664 self
._set
(data
, frameHeader
);
1666 assert(owner_id
!= None and len(owner_id
) > 0);
1667 assert(id != None and len(id) > 0 and len(id) <= 64);
1668 self
.owner_id
= owner_id
;
1671 def _set(self
, data
, frameHeader
):
1672 assert(frameHeader
);
1674 # Owner identifier <text string> $00
1675 # Identifier up to 64 bytes binary data>
1676 (self
.owner_id
, self
.id) = data
.split("\x00", 1);
1677 TRACE_MSG("UFID owner_id: " + self
.owner_id
);
1678 TRACE_MSG("UFID id: " + self
.id);
1679 if strictID3() and (len(self
.owner_id
) == 0 or
1680 len(self
.id) == 0 or len(self
.id) > 64):
1681 raise FrameException("Invalid UFID frame");
1684 data
= self
.owner_id
+ "\x00" + self
.id;
1685 return self
.assembleFrame(data
);
1687 ################################################################################
1688 class UnknownFrame(Frame
):
1691 def __init__(self
, frameHeader
, data
, unsync_default
=False):
1692 assert(frameHeader
and data
)
1693 Frame
.__init
__(self
, frameHeader
, unsync_default
)
1694 self
._set
(data
, frameHeader
)
1696 def _set(self
, data
, frameHeader
):
1697 self
.data
= self
.disassembleFrame(data
);
1700 return self
.assembleFrame(self
.data
)
1702 ################################################################################
1703 class MusicCDIdFrame(Frame
):
1706 def __init__(self
, frameHeader
, data
=None, unsync_default
=False):
1707 Frame
.__init
__(self
, frameHeader
, unsync_default
)
1708 # XXX: Flesh this class out and add a toc arg
1709 assert(data
!= None);
1711 self
._set
(data
, frameHeader
);
1713 # TODO: Parse the TOC and comment the format.
1714 def _set(self
, data
, frameHeader
):
1715 if not CDID_FRAME_RX
.match(frameHeader
.id):
1716 raise FrameException("Invalid frame id for MusicCDIdFrame: " +\
1718 data
= self
.disassembleFrame(data
);
1723 return self
.assembleFrame(data
);
1725 ################################################################################
1726 # A class for containing and managing ID3v2.Frame objects.
1727 class FrameSet(list):
1730 def __init__(self
, tagHeader
, l
= None):
1731 self
.tagHeader
= tagHeader
;
1734 if not isinstance(f
, Frame
):
1735 raise TypeError("Invalid type added to FrameSet: " +\
1739 # Setting a FrameSet instance like this 'fs = []' morphs the instance into
1744 # Read frames starting from the current read position of the file object.
1745 # Returns the amount of padding which occurs after the tag, but before the
1746 # audio content. A return valule of 0 DOES NOT imply an error.
1747 def parse(self
, f
, tagHeader
, extendedHeader
):
1748 self
.tagHeader
= tagHeader
;
1749 self
.extendedHeader
= extendedHeader
1751 sizeLeft
= tagHeader
.tagSize
- extendedHeader
.size
1752 start_size
= sizeLeft
1755 # Handle a tag-level unsync. Some frames may have their own unsync bit
1757 tagData
= f
.read(sizeLeft
)
1759 # If the tag is 2.3 and the tag header unsync bit is set then all the
1760 # frame data is deunsync'd at once, otherwise it will happen on a per
1762 from eyeD3
import ID3_V2_3
1763 if tagHeader
.unsync
and tagHeader
.version
<= ID3_V2_3
:
1764 TRACE_MSG("De-unsynching %d bytes at once (<= 2.3 tag)" %
1766 og_size
= len(tagData
)
1767 tagData
= deunsyncData(tagData
)
1768 sizeLeft
= len(tagData
)
1769 TRACE_MSG("De-unsynch'd %d bytes at once (<= 2.3 tag) to %d bytes" %
1770 (og_size
, sizeLeft
))
1772 # Adding bytes to simulate the tag header(s) in the buffer. This keeps
1773 # f.tell() values matching the file offsets.
1774 prepadding
= '\x00' * 10 # Tag header
1775 prepadding
+= '\x00' * extendedHeader
.size
1776 tagBuffer
= StringIO(prepadding
+ tagData
);
1777 tagBuffer
.seek(len(prepadding
));
1780 TRACE_MSG("sizeLeft: " + str(sizeLeft
));
1781 if sizeLeft
< (10 + 1): # The size of the smallest frame.
1782 TRACE_MSG("FrameSet: Implied padding (sizeLeft < minFrameSize)");
1783 paddingSize
= sizeLeft
1786 TRACE_MSG("+++++++++++++++++++++++++++++++++++++++++++++++++");
1787 TRACE_MSG("FrameSet: Reading Frame #" + str(len(self
) + 1));
1788 frameHeader
= FrameHeader(tagHeader
);
1789 if not frameHeader
.parse(tagBuffer
):
1790 TRACE_MSG("No frame found, implied padding of %d bytes" % sizeLeft
)
1791 paddingSize
= sizeLeft
1795 if frameHeader
.dataSize
:
1796 TRACE_MSG("FrameSet: Reading %d (0x%X) bytes of data from byte "
1797 "pos %d (0x%X)" % (frameHeader
.dataSize
,
1798 frameHeader
.dataSize
,
1801 data
= tagBuffer
.read(frameHeader
.dataSize
);
1803 TRACE_MSG("FrameSet: %d bytes of data read" % len(data
));
1805 consumed_size
+= (frameHeader
.FRAME_HEADER_SIZE
+
1806 frameHeader
.dataSize
)
1807 self
.addFrame(createFrame(frameHeader
, data
, tagHeader
))
1809 # Each frame contains dataSize + headerSize bytes.
1810 sizeLeft
-= (frameHeader
.FRAME_HEADER_SIZE
+ frameHeader
.dataSize
);
1814 # Returrns the size of the frame data.
1818 sz
+= len(f
.render());
1821 def setTagHeader(self
, tagHeader
):
1822 self
.tagHeader
= tagHeader
;
1824 f
.header
.setVersion(tagHeader
);
1826 # This methods adds the frame if it is addable per the ID3 spec.
1827 def addFrame(self
, frame
):
1828 fid
= frame
.header
.id;
1830 # Text frame restrictions.
1831 # No multiples except for TXXX which must have unique descriptions.
1832 if strictID3() and TEXT_FRAME_RX
.match(fid
) and self
[fid
]:
1833 if not USERTEXT_FRAME_RX
.match(fid
):
1834 raise FrameException("Multiple %s frames not allowed." % fid
);
1835 userTextFrames
= self
[fid
];
1836 for frm
in userTextFrames
:
1837 if frm
.description
== frame
.description
:
1838 raise FrameException("Multiple %s frames with the same\
1839 description not allowed." % fid
);
1841 # Comment frame restrictions.
1842 # Multiples must have a unique description/language combination.
1843 if strictID3() and COMMENT_FRAME_RX
.match(fid
) and self
[fid
]:
1844 commentFrames
= self
[fid
];
1845 for frm
in commentFrames
:
1846 if frm
.description
== frame
.description
and\
1847 frm
.lang
== frame
.lang
:
1848 raise FrameException("Multiple %s frames with the same\
1849 language and description not allowed." %\
1852 # Lyrics frame restrictions.
1853 # Multiples must have a unique description/language combination.
1854 if strictID3() and LYRICS_FRAME_RX
.match(fid
) and self
[fid
]:
1855 lyricsFrames
= self
[fid
];
1856 for frm
in lyricsFrames
:
1857 if frm
.description
== frame
.description
and\
1858 frm
.lang
== frame
.lang
:
1859 raise FrameException("Multiple %s frames with the same\
1860 language and description not allowed." %\
1863 # URL frame restrictions.
1864 # No multiples except for TXXX which must have unique descriptions.
1865 if strictID3() and URL_FRAME_RX
.match(fid
) and self
[fid
]:
1866 if not USERURL_FRAME_RX
.match(fid
):
1867 raise FrameException("Multiple %s frames not allowed." % fid
);
1868 userUrlFrames
= self
[fid
];
1869 for frm
in userUrlFrames
:
1870 if frm
.description
== frame
.description
:
1871 raise FrameException("Multiple %s frames with the same\
1872 description not allowed." % fid
);
1874 # Music CD ID restrictions.
1876 if strictID3() and CDID_FRAME_RX
.match(fid
) and self
[fid
]:
1877 raise FrameException("Multiple %s frames not allowed." % fid
);
1879 # Image (attached picture) frame restrictions.
1880 # Multiples must have a unique content desciptor. I'm assuming that
1881 # the spec means the picture type.....
1882 if IMAGE_FRAME_RX
.match(fid
) and self
[fid
] and strictID3():
1883 imageFrames
= self
[fid
];
1884 for frm
in imageFrames
:
1885 if frm
.pictureType
== frame
.pictureType
:
1886 raise FrameException("Multiple %s frames with the same "\
1887 "content descriptor not allowed." % fid
);
1889 # Object (GEOB) frame restrictions.
1890 # Multiples must have a unique content desciptor.
1891 if OBJECT_FRAME_RX
.match(fid
) and self
[fid
] and strictID3():
1892 objectFrames
= self
[fid
];
1893 for frm
in objectFrames
:
1894 if frm
.description
== frame
.description
:
1895 raise FrameException("Multiple %s frames with the same "\
1896 "content descriptor not allowed." % fid
);
1898 # Play count frame (PCNT). There may be only one
1899 if PLAYCOUNT_FRAME_RX
.match(fid
) and self
[fid
]:
1900 raise FrameException("Multiple %s frames not allowed." % fid
);
1902 # Unique File identifier frame. There may be only one with the same
1904 if UNIQUE_FILE_ID_FRAME_RX
.match(fid
) and self
[fid
]:
1905 ufid_frames
= self
[fid
];
1906 for frm
in ufid_frames
:
1907 if frm
.owner_id
== frame
.owner_id
:
1908 raise FrameException("Multiple %s frames not allowed with "\
1909 "the same owner ID (%s)" %\
1910 (fid
, frame
.owner_id
));
1914 # Set a text frame value. Text frame IDs must be unique. If a frame with
1915 # the same Id is already in the list it's value is changed, otherwise
1916 # the frame is added.
1917 def setTextFrame(self
, frameId
, text
, encoding
= None):
1918 assert(type(text
) == unicode);
1920 if not TEXT_FRAME_RX
.match(frameId
):
1921 raise FrameException("Invalid Frame ID: " + frameId
);
1922 if USERTEXT_FRAME_RX
.match(frameId
):
1923 raise FrameException("Wrong method, use setUserTextFrame");
1926 curr
= self
[frameId
][0];
1928 curr
.encoding
= encoding
;
1930 if isinstance(curr
, DateFrame
):
1935 h
= FrameHeader(self
.tagHeader
);
1938 encoding
= DEFAULT_ENCODING
;
1939 if frameId
in DATE_FIDS
:
1940 self
.addFrame(DateFrame(h
, encoding
= encoding
, date_str
= text
));
1942 self
.addFrame(TextFrame(h
, encoding
= encoding
, text
= text
));
1944 # If a user text frame with the same description exists then
1945 # the frame text is replaced, otherwise the frame is added.
1946 def setCommentFrame(self
, comment
, description
, lang
= DEFAULT_LANG
,
1948 assert(isinstance(comment
, unicode) and isinstance(description
, unicode));
1950 if self
[COMMENT_FID
]:
1952 for f
in self
[COMMENT_FID
]:
1953 if f
.lang
== lang
and f
.description
== description
:
1954 f
.comment
= comment
;
1956 f
.encoding
= encoding
;
1960 h
= FrameHeader(self
.tagHeader
);
1963 encoding
= DEFAULT_ENCODING
;
1964 self
.addFrame(CommentFrame(h
, encoding
= encoding
, lang
= lang
,
1965 description
= description
,
1966 comment
= comment
));
1969 encoding
= DEFAULT_ENCODING
;
1970 h
= FrameHeader(self
.tagHeader
);
1972 self
.addFrame(CommentFrame(h
, encoding
= encoding
, lang
= lang
,
1973 description
= description
,
1974 comment
= comment
));
1976 # If a user text frame with the same description exists then
1977 # the frame text is replaced, otherwise the frame is added.
1978 def setLyricsFrame(self
, lyrics
, description
, lang
= DEFAULT_LANG
,
1980 assert(isinstance(lyrics
, unicode) and isinstance(description
, unicode));
1982 if self
[LYRICS_FID
]:
1984 for f
in self
[LYRICS_FID
]:
1985 if f
.lang
== lang
and f
.description
== description
:
1988 f
.encoding
= encoding
;
1992 h
= FrameHeader(self
.tagHeader
);
1995 encoding
= DEFAULT_ENCODING
;
1996 self
.addFrame(LyricsFrame(h
, encoding
= encoding
, lang
= lang
,
1997 description
= description
,
2001 encoding
= DEFAULT_ENCODING
;
2002 h
= FrameHeader(self
.tagHeader
);
2004 self
.addFrame(LyricsFrame(h
, encoding
= encoding
, lang
= lang
,
2005 description
= description
,
2008 def setUniqueFileIDFrame(self
, owner_id
, id):
2009 assert(isinstance(owner_id
, str) and isinstance(id, str));
2011 if self
[UNIQUE_FILE_ID_FID
]:
2013 for f
in self
[UNIQUE_FILE_ID_FID
]:
2014 if f
.owner_id
== owner_id
:
2019 h
= FrameHeader(self
.tagHeader
);
2020 h
.id = UNIQUE_FILE_ID_FID
;
2021 self
.addFrame(UniqueFileIDFrame(h
, owner_id
= owner_id
, id = id));
2023 h
= FrameHeader(self
.tagHeader
);
2024 h
.id = UNIQUE_FILE_ID_FID
;
2025 self
.addFrame(UniqueFileIDFrame(h
, owner_id
= owner_id
, id = id));
2027 # If a comment frame with the same language and description exists then
2028 # the comment text is replaced, otherwise the frame is added.
2029 def setUserTextFrame(self
, txt
, description
, encoding
= None):
2030 assert(isinstance(txt
, unicode));
2031 assert(isinstance(description
, unicode));
2033 if self
[USERTEXT_FID
]:
2035 for f
in self
[USERTEXT_FID
]:
2036 if f
.description
== description
:
2039 f
.encoding
= encoding
;
2044 encoding
= DEFAULT_ENCODING
;
2045 h
= FrameHeader(self
.tagHeader
);
2046 h
.id = USERTEXT_FID
;
2047 self
.addFrame(UserTextFrame(h
, encoding
= encoding
,
2048 description
= description
,
2052 encoding
= DEFAULT_ENCODING
;
2053 h
= FrameHeader(self
.tagHeader
);
2054 h
.id = USERTEXT_FID
;
2055 self
.addFrame(UserTextFrame(h
, encoding
= encoding
,
2056 description
= description
,
2059 # This method removes all frames with the matching frame ID.
2060 # The number of frames removed is returned.
2061 # Note that calling this method with a key like "COMM" may remove more
2062 # frames then you really want.
2063 def removeFramesByID(self
, fid
):
2064 if not isinstance(fid
, str):
2065 raise FrameException("removeFramesByID only operates on frame IDs");
2069 while i
< len(self
):
2070 if self
[i
].header
.id == fid
:
2077 # Removes the frame at index. True is returned if the element was
2078 # removed, and false otherwise.
2079 def removeFrameByIndex(self
, index
):
2080 if not isinstance(index
, int):
2082 FrameException("removeFrameByIndex only operates on a frame index");
2084 del self
.frames
[key
];
2089 # Accepts both int (indexed access) and string keys (a valid frame Id).
2090 # A list of frames (commonly with only one element) is returned when the
2091 # FrameSet is accessed using frame IDs since some frames can appear
2092 # multiple times in a tag. To sum it all up htis method returns
2093 # string or None when indexed using an integer, and a 0 to N length
2094 # list of strings when indexed with a frame ID.
2096 # Throws IndexError and TypeError.
2097 def __getitem__(self
, key
):
2098 if isinstance(key
, int):
2099 if key
>= 0 and key
< len(self
):
2100 return list.__getitem
__(self
, key
);
2102 raise IndexError("FrameSet index out of range");
2103 elif isinstance(key
, str):
2106 if f
.header
.id == key
:
2110 raise TypeError("FrameSet key must be type int or string");
2112 def splitUnicode(data
, encoding
):
2113 if encoding
== LATIN1_ENCODING
or encoding
== UTF_8_ENCODING
:
2114 return data
.split("\x00", 1);
2115 elif encoding
== UTF_16_ENCODING
or encoding
== UTF_16BE_ENCODING
:
2116 # Two null bytes split, but since each utf16 char is also two
2117 # bytes we need to ensure we found a proper boundary.
2118 (d
, t
) = data
.split("\x00\x00", 1);
2119 if (len(d
) % 2) != 0:
2120 (d
, t
) = data
.split("\x00\x00\x00", 1);
2124 #######################################################################
2125 # Create and return the appropriate frame.
2127 def createFrame(frameHeader
, data
, tagHeader
):
2131 if TEXT_FRAME_RX
.match(frameHeader
.id):
2132 if USERTEXT_FRAME_RX
.match(frameHeader
.id):
2133 f
= UserTextFrame(frameHeader
, data
=data
,
2134 unsync_default
=tagHeader
.unsync
)
2136 if frameHeader
.id[:2] == "TD" or\
2137 frameHeader
.id == OBSOLETE_DATE_FID
or\
2138 frameHeader
.id == OBSOLETE_YEAR_FID
or \
2139 frameHeader
.id == OBSOLETE_ORIG_RELEASE_FID
:
2140 f
= DateFrame(frameHeader
, data
=data
,
2141 unsync_default
=tagHeader
.unsync
)
2143 f
= TextFrame(frameHeader
, data
=data
,
2144 unsync_default
=tagHeader
.unsync
)
2146 elif COMMENT_FRAME_RX
.match(frameHeader
.id):
2147 f
= CommentFrame(frameHeader
, data
=data
, unsync_default
=tagHeader
.unsync
)
2149 elif LYRICS_FRAME_RX
.match(frameHeader
.id):
2150 f
= LyricsFrame(frameHeader
, data
=data
, unsync_default
=tagHeader
.unsync
)
2152 elif URL_FRAME_RX
.match(frameHeader
.id):
2153 if USERURL_FRAME_RX
.match(frameHeader
.id):
2154 f
= UserURLFrame(frameHeader
, data
=data
,
2155 unsync_default
=tagHeader
.unsync
)
2157 f
= URLFrame(frameHeader
, data
=data
, unsync_default
=tagHeader
.unsync
)
2159 elif CDID_FRAME_RX
.match(frameHeader
.id):
2160 f
= MusicCDIdFrame(frameHeader
, data
=data
, unsync_default
=tagHeader
.unsync
)
2162 elif IMAGE_FRAME_RX
.match(frameHeader
.id):
2163 f
= ImageFrame(frameHeader
, data
=data
, unsync_default
=tagHeader
.unsync
)
2164 # Encapsulated object
2165 elif OBJECT_FRAME_RX
.match(frameHeader
.id):
2166 f
= ObjectFrame(frameHeader
, data
=data
, unsync_default
=tagHeader
.unsync
)
2168 elif PLAYCOUNT_FRAME_RX
.match(frameHeader
.id):
2169 f
= PlayCountFrame(frameHeader
, data
=data
, unsync_default
=tagHeader
.unsync
)
2170 # Unique file identifier
2171 elif UNIQUE_FILE_ID_FRAME_RX
.match(frameHeader
.id):
2172 f
= UniqueFileIDFrame(frameHeader
, data
=data
,
2173 unsync_default
=tagHeader
.unsync
)
2176 f
= UnknownFrame(frameHeader
, data
=data
, unsync_default
=tagHeader
.unsync
)
2181 def map2_2FrameId(originalId
):
2182 if not TAGS2_2_TO_TAGS_2_3_AND_4
.has_key(originalId
):
2184 return TAGS2_2_TO_TAGS_2_3_AND_4
[originalId
]