176
184
"WPUB": "Publishers official webpage",
177
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]
179
259
NULL_FRAME_FLAGS = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
181
261
TEXT_FRAME_RX = re.compile("^T[A-Z0-9][A-Z0-9][A-Z0-9]$");
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,
453
elif frameId == '\x00\x00\x00':
454
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 "\
459
"(possibly created by the shitty mp3ext) frame found; "\
460
"Happily ignoring!" + str(f.tell()));
463
raise FrameException("FrameHeader: Illegal Frame ID: " + frameId);
345
467
# Returns 1 on success and 0 when a null tag (marking the beginning of
346
468
# padding). In the case of an invalid frame header, a FrameException is
348
470
def parse(self, f):
349
471
TRACE_MSG("FrameHeader [start byte]: %d (0x%X)" % (f.tell(),
473
if self.minorVersion == 2:
474
return self.parse2_2(f)
351
476
frameId = f.read(4);
352
477
if self.isFrameIdValid(frameId):
353
478
TRACE_MSG("FrameHeader [id]: %s (0x%x%x%x%x)" % (frameId,
842
977
self.encoding = data[0];
843
978
TRACE_MSG("UserURLFrame encoding: %s" %\
844
979
id3EncodingToString(self.encoding));
845
(d, u) = splitUnicode(data[1:], self.encoding);
981
(d, u) = splitUnicode(data[1:], self.encoding);
982
except ValueError, ex:
984
raise FrameException("Invalid WXXX frame, no null byte")
846
987
self.description = unicode(d, id3EncodingToString(self.encoding));
847
988
TRACE_MSG("UserURLFrame description: %s" % self.description);
991
self.url = cleanNulls(self.url)
849
992
TRACE_MSG("UserURLFrame text: %s" % self.url);
851
994
def render(self):
852
995
data = self.encoding +\
853
996
self.description.encode(id3EncodingToString(self.encoding)) +\
997
self.getTextDelim() + self.url;
855
998
return self.assembleFrame(data);
858
return '<%s (%s): %s [Encoding: %s] [Desc: %s]>' %\
1000
def __unicode__(self):
1001
return u'<%s (%s): %s [Encoding: %s] [Desc: %s]>' %\
859
1002
(self.getFrameDesc(), self.header.id,
860
1003
self.url, self.encoding, self.description)
862
1005
################################################################################
863
1006
class CommentFrame(Frame):
865
1008
description = u"";
868
1011
# Data string format:
869
1012
# encoding (one byte) + lang (three byte code) + description + "\x00" +
871
def __init__(self, frameHeader, data = None, lang = u"",
1014
def __init__(self, frameHeader, data = None, lang = "",
872
1015
description = u"", comment = u"", encoding = DEFAULT_ENCODING):
873
1016
Frame.__init__(self, frameHeader);
874
1017
if data != None:
928
1074
lang = lang + ('\x00' * (3 - len(lang)));
929
1075
data = self.encoding + lang +\
930
1076
self.description.encode(id3EncodingToString(self.encoding)) +\
1077
self.getTextDelim() +\
932
1078
self.comment.encode(id3EncodingToString(self.encoding));
933
1079
return self.assembleFrame(data);
936
return "<%s (%s): %s [Lang: %s] [Desc: %s]>" %\
1081
def __unicode__(self):
1082
return u"<%s (%s): %s [Lang: %s] [Desc: %s]>" %\
937
1083
(self.getFrameDesc(), self.header.id, self.comment,
938
1084
self.lang, self.description);
940
1086
################################################################################
1087
class LyricsFrame(Frame):
1092
# Data string format:
1093
# encoding (one byte) + lang (three byte code) + description + "\x00" +
1095
def __init__(self, frameHeader, data = None, lang = "",
1096
description = u"", lyrics = u"", encoding = DEFAULT_ENCODING):
1097
Frame.__init__(self, frameHeader);
1099
self._set(data, frameHeader);
1101
assert(isinstance(description, unicode));
1102
assert(isinstance(lyrics, unicode));
1103
assert(isinstance(lang, str));
1104
self.encoding = encoding;
1106
self.description = description;
1107
self.lyrics = lyrics;
1109
# Data string format:
1110
# encoding (one byte) + lang (three byte code) + description + "\x00" +
1112
def _set(self, data, frameHeader = None):
1113
assert(frameHeader);
1114
if not LYRICS_FRAME_RX.match(frameHeader.id):
1115
raise FrameException("Invalid frame id for LyricsFrame: " +\
1118
data = self.disassembleFrame(data);
1119
self.encoding = data[0];
1120
TRACE_MSG("LyricsFrame encoding: " + id3EncodingToString(self.encoding));
1122
self.lang = str(data[1:4]).strip("\x00");
1123
# Test ascii encoding
1124
temp_lang = unicode(self.lang, "ascii");
1126
not re.compile("[A-Z][A-Z][A-Z]", re.IGNORECASE).match(self.lang):
1128
raise FrameException("[LyricsFrame] Invalid language "\
1129
"code: %s" % self.lang);
1130
except UnicodeDecodeError, ex:
1132
raise FrameException("[LyricsFrame] Invalid language code: "\
1133
"[%s] %s" % (ex.object, ex.reason));
1137
(d, c) = splitUnicode(data[4:], self.encoding);
1138
self.description = unicode(d, id3EncodingToString(self.encoding));
1139
self.lyrics = unicode(c, id3EncodingToString(self.encoding));
1142
raise FrameException("Invalid lyrics; no description/lyrics");
1144
self.description = u"";
1147
self.description = cleanNulls(self.description)
1148
self.lyrics = cleanNulls(self.lyrics)
1151
lang = self.lang.encode("ascii");
1155
lang = lang + ('\x00' * (3 - len(lang)));
1156
data = self.encoding + lang +\
1157
self.description.encode(id3EncodingToString(self.encoding)) +\
1158
self.getTextDelim() +\
1159
self.lyrics.encode(id3EncodingToString(self.encoding));
1160
return self.assembleFrame(data);
1162
def __unicode__(self):
1163
return u"<%s (%s): %s [Lang: %s] [Desc: %s]>" %\
1164
(self.getFrameDesc(), self.header.id, self.lyrics,
1165
self.lang, self.description);
1167
################################################################################
941
1168
# This class refers to the APIC frame, otherwise known as an "attached
943
1170
class ImageFrame(Frame):
1211
1447
raise FrameException("Invalid APIC picture type: %d" % t);
1212
1448
picTypeToString = staticmethod(picTypeToString);
1450
################################################################################
1451
# This class refers to the GEOB frame
1452
class ObjectFrame(Frame):
1458
def __init__(self, frameHeader, data = None,
1459
desc = u"", filename = u"",
1460
objectData = None, mimeType = None,
1461
encoding = DEFAULT_ENCODING):
1462
Frame.__init__(self, frameHeader);
1464
self._set(data, frameHeader);
1466
assert(isinstance(desc, unicode));
1467
self.description = desc;
1468
assert(isinstance(filename, unicode));
1469
self.filename = filename;
1470
self.encoding = encoding;
1472
self.mimeType = mimeType;
1474
self.objectData = objectData;
1477
def create(objFile, mime = u"", desc = u"", filename = None,
1478
encoding = DEFAULT_ENCODING):
1479
if filename == None:
1480
filename = unicode(os.path.basename(objFile));
1481
if not isinstance(desc, unicode) or \
1482
(not isinstance(filename, unicode) and filename != ""):
1483
raise FrameException("Wrong description and/or filename type.");
1485
fp = file(objFile, "rb");
1486
objData = fp.read();
1488
print("Using specified mime type %s" % mime);
1490
mt = mimetypes.guess_type(objFile);
1492
raise FrameException("Unable to guess mime-type for %s" %
1495
print("Guessing mime type %s" % mime);
1497
frameData = DEFAULT_ENCODING;
1498
frameData += mime + "\x00";
1499
frameData += filename.encode(id3EncodingToString(encoding)) + "\x00";
1500
frameData += desc.encode(id3EncodingToString(encoding)) + "\x00";
1501
frameData += objData;
1503
frameHeader = FrameHeader();
1504
frameHeader.id = OBJECT_FID;
1505
return ObjectFrame(frameHeader, data = frameData);
1506
# Make create a static method. Odd....
1507
create = staticmethod(create);
1509
# Data string format:
1510
# <Header for 'General encapsulated object', ID: "GEOB">
1512
# MIME type <text string> $00
1513
# Filename <text string according to encoding> $00 (00)
1514
# Content description <text string according to encoding> $00 (00)
1515
# Encapsulated object <binary data>
1516
def _set(self, data, frameHeader = None):
1517
assert(frameHeader);
1518
if not OBJECT_FRAME_RX.match(frameHeader.id):
1519
raise FrameException("Invalid frame id for ObjectFrame: " +\
1522
data = self.disassembleFrame(data);
1524
input = StringIO(data);
1525
TRACE_MSG("GEOB frame data size: " + str(len(data)));
1526
self.encoding = input.read(1);
1527
TRACE_MSG("GEOB encoding: " + id3EncodingToString(self.encoding));
1531
if self.header.minorVersion != 2:
1534
self.mimeType += ch;
1537
# v2.2 (OBSOLETE) special case
1538
self.mimeType = input.read(3);
1539
TRACE_MSG("GEOB mime type: " + self.mimeType);
1540
if strictID3() and not self.mimeType:
1541
raise FrameException("GEOB frame does not contain a mime type");
1542
if strictID3() and self.mimeType.find("/") == -1:
1543
raise FrameException("GEOB frame does not contain a valid mime type");
1545
self.filename = u"";
1546
self.description = u"";
1548
# Remaining data is a NULL separated filename, description and object data
1549
buffer = input.read();
1552
(filename, buffer) = splitUnicode(buffer, self.encoding);
1553
(desc, obj) = splitUnicode(buffer, self.encoding);
1554
TRACE_MSG("filename len: %d" % len(filename));
1555
TRACE_MSG("description len: %d" % len(desc));
1556
TRACE_MSG("data len: %d" % len(obj));
1557
self.filename = unicode(filename, id3EncodingToString(self.encoding));
1558
self.description = unicode(desc, id3EncodingToString(self.encoding));
1559
TRACE_MSG("GEOB filename: " + self.filename);
1560
TRACE_MSG("GEOB description: " + self.description);
1562
self.objectData = obj;
1563
TRACE_MSG("GEOB data: " + str(len(self.objectData)) + " bytes");
1564
if strictID3() and not self.objectData:
1565
raise FrameException("GEOB frame does not contain any data");
1568
def writeFile(self, path = "./", name = None):
1569
if not self.objectData:
1570
raise IOError("Fetching remote object files is not implemented.");
1572
name = self.getDefaultFileName();
1573
objectFile = os.path.join(path, name);
1575
f = file(objectFile, "wb");
1576
f.write(self.objectData);
1579
def getDefaultFileName(self, suffix = ""):
1580
nameStr = self.filename;
1583
nameStr = nameStr + "." + self.mimeType.split("/")[1];
1587
data = self.encoding + self.mimeType + "\x00" +\
1588
self.filename.encode(id3EncodingToString(self.encoding)) +\
1589
self.getTextDelim() +\
1590
self.description.encode(id3EncodingToString(self.encoding)) +\
1591
self.getTextDelim() +\
1593
return self.assembleFrame(data);
1214
1595
class PlayCountFrame(Frame):
1410
1790
if frm.description == frame.description and\
1411
1791
frm.lang == frame.lang:
1412
1792
raise FrameException("Multiple %s frames with the same\
1413
language and description now allowed." %\
1793
language and description not allowed." %\
1796
# Lyrics frame restrictions.
1797
# Multiples must have a unique description/language combination.
1798
if strictID3() and LYRICS_FRAME_RX.match(fid) and self[fid]:
1799
lyricsFrames = self[fid];
1800
for frm in lyricsFrames:
1801
if frm.description == frame.description and\
1802
frm.lang == frame.lang:
1803
raise FrameException("Multiple %s frames with the same\
1804
language and description not allowed." %\
1416
1807
# URL frame restrictions.
1417
1808
# No multiples except for TXXX which must have unique descriptions.
1418
1809
if strictID3() and URL_FRAME_RX.match(fid) and self[fid]:
1419
1810
if not USERURL_FRAME_RX.match(fid):
1420
raise FrameException("Multiple %s frames now allowed." % fid);
1811
raise FrameException("Multiple %s frames not allowed." % fid);
1421
1812
userUrlFrames = self[fid];
1422
1813
for frm in userUrlFrames:
1423
1814
if frm.description == frame.description:
1424
1815
raise FrameException("Multiple %s frames with the same\
1425
description now allowed." % fid);
1816
description not allowed." % fid);
1427
1818
# Music CD ID restrictions.
1428
1819
# No multiples.
1429
1820
if strictID3() and CDID_FRAME_RX.match(fid) and self[fid]:
1430
raise FrameException("Multiple %s frames now allowed." % fid);
1821
raise FrameException("Multiple %s frames not allowed." % fid);
1432
1823
# Image (attached picture) frame restrictions.
1433
1824
# Multiples must have a unique content desciptor. I'm assuming that
1434
1825
# the spec means the picture type.....
1435
if IMAGE_FRAME_RX.match(fid) and self[fid]:
1826
if IMAGE_FRAME_RX.match(fid) and self[fid] and strictID3():
1436
1827
imageFrames = self[fid];
1437
1828
for frm in imageFrames:
1438
1829
if frm.pictureType == frame.pictureType:
1439
raise FrameException("Multiple %s frames with the same\
1440
content descriptor now allowed." % fid);
1830
raise FrameException("Multiple %s frames with the same "\
1831
"content descriptor not allowed." % fid);
1833
# Object (GEOB) frame restrictions.
1834
# Multiples must have a unique content desciptor.
1835
if OBJECT_FRAME_RX.match(fid) and self[fid] and strictID3():
1836
objectFrames = self[fid];
1837
for frm in objectFrames:
1838
if frm.description == frame.description:
1839
raise FrameException("Multiple %s frames with the same "\
1840
"content descriptor not allowed." % fid);
1442
1842
# Play count frame (PCNT). There may be only one
1443
1843
if PLAYCOUNT_FRAME_RX.match(fid) and self[fid]:
1444
raise FrameException("Multiple %s frames now allowed." % fid);
1844
raise FrameException("Multiple %s frames not allowed." % fid);
1446
1846
# Unique File identifier frame. There may be only one with the same
1514
1917
description = description,
1515
1918
comment = comment));
1920
# If a user text frame with the same description exists then
1921
# the frame text is replaced, otherwise the frame is added.
1922
def setLyricsFrame(self, lyrics, description, lang = DEFAULT_LANG,
1924
assert(isinstance(lyrics, unicode) and isinstance(description, unicode));
1926
if self[LYRICS_FID]:
1928
for f in self[LYRICS_FID]:
1929
if f.lang == lang and f.description == description:
1932
f.encoding = encoding;
1936
h = FrameHeader(self.tagHeader);
1939
encoding = DEFAULT_ENCODING;
1940
self.addFrame(LyricsFrame(h, encoding = encoding, lang = lang,
1941
description = description,
1945
encoding = DEFAULT_ENCODING;
1946
h = FrameHeader(self.tagHeader);
1948
self.addFrame(LyricsFrame(h, encoding = encoding, lang = lang,
1949
description = description,
1517
1952
def setUniqueFileIDFrame(self, owner_id, id):
1518
1953
assert(isinstance(owner_id, str) and isinstance(id, str));
1619
2054
raise TypeError("FrameSet key must be type int or string");
1622
2056
def splitUnicode(data, encoding):
1623
if encoding == LATIN1_ENCODING or encoding == UTF_8_ENCODING or\
1624
encoding == UTF_16BE_ENCODING:
2057
if encoding == LATIN1_ENCODING or encoding == UTF_8_ENCODING:
1625
2058
return data.split("\x00", 1);
1626
elif encoding == UTF_16_ENCODING:
1627
(d, t) = data.split("\x00\x00\x00", 1);
2059
elif encoding == UTF_16_ENCODING or encoding == UTF_16BE_ENCODING:
2060
# Two null bytes split, but since each utf16 char is also two
2061
# bytes we need to ensure we found a proper boundary.
2062
(d, t) = data.split("\x00\x00", 1);
2063
if (len(d) % 2) != 0:
2064
(d, t) = data.split("\x00\x00\x00", 1);
1633
2068
#######################################################################
1634
2069
# Create and return the appropriate frame.
1635
2070
# Exceptions: ....