1
################################################################################
1
###############################################################################
3
# Copyright (C) 2002-2005 Travis Shirk <travis@pobox.com>
3
# Copyright (C) 2002-2006 Travis Shirk <travis@pobox.com>
5
5
# This program is free software; you can redistribute it and/or modify
6
6
# it under the terms of the GNU General Public License as published by
26
26
from frames import *;
27
27
from binfuncs import *;
29
30
ID3_V1_COMMENT_DESC = "ID3 v1 Comment";
31
32
################################################################################
35
def __init__(self, msg):
33
class TagException(Exception):
34
'''error reading tag'''
41
36
################################################################################
120
115
TRACE_MSG("TagHeader [major]: " + str(major));
121
116
TRACE_MSG("TagHeader [minor]: " + str(minor));
122
117
TRACE_MSG("TagHeader [revis]: " + str(rev));
123
if not (major == 2 and (minor == 3 or minor == 4)):
118
if not (major == 2 and (minor >= 2 and minor <= 4)):
124
119
raise TagException("ID3 v" + str(major) + "." + str(minor) +\
125
120
" is not supported.");
126
121
# Get all the version madness in sync.
174
169
restrictions = 0;
176
171
def isUpdate(self):
177
return self.flags & 0x40;
172
return self.flags & 0x40;
178
173
def hasCRC(self):
179
return self.flags & 0x20;
180
def hasRestrictions(self):
181
return self.flags & 0x10;
174
return self.flags & 0x20;
175
def hasRestrictions(self, minor_version = None):
176
return self.flags & 0x10;
183
178
def setSizeRestrictions(self, v):
184
179
assert(v >= 0 and v <= 3);
252
247
return "All images are exactly 64x64 pixels, unless required "\
250
def _syncsafeCRC(self):
252
bites += chr((self.crc >> 28) & 0x7f);
253
bites += chr((self.crc >> 21) & 0x7f);
254
bites += chr((self.crc >> 14) & 0x7f);
255
bites += chr((self.crc >> 7) & 0x7f);
256
bites += chr((self.crc >> 0) & 0x7f);
255
260
def render(self, header, frameData, padding = 0):
256
261
assert(header.majorVersion == 2);
259
265
if header.minorVersion == 4:
263
268
# Extended flags.
264
269
if self.isUpdate():
266
271
if self.hasCRC():
268
self.crc = binascii.crc32(data);
269
data += bin2bytes(dec2bin(self.crc, 40));
273
# XXX: Using the absolute value of the CRC. The spec is unclear
274
# about the type of this data.
275
self.crc = int(math.fabs(binascii.crc32(frameData +\
276
("\x00" * padding))));
277
crc_data = self._syncsafeCRC();
278
if len(crc_data) < 5:
279
crc_data = ("\x00" * (5 - len(crc_data))) + crc_data
280
assert(len(crc_data) == 5)
270
282
if self.hasRestrictions():
272
284
assert(len(self.restrictions) == 1);
273
285
data += self.restrictions;
286
TRACE_MSG("Rendered extended header data (%d bytes)" % len(data));
275
288
# Extended header size.
276
289
size = bin2bytes(bin2synchsafe(dec2bin(len(data) + 6, 32)))
277
290
assert(len(size) == 4);
279
assert(len(self.flags) == 2);
280
data = size + "\x01" + self.flags + data;
292
data = size + "\x01" + bin2bytes(dec2bin(self.flags)) + data;
293
TRACE_MSG("Rendered extended header of size %d" % len(data));
285
297
# Extended flags.
287
299
if self.hasCRC():
289
self.crc = binascii.crc32(data);
301
# XXX: Using the absolute value of the CRC. The spec is unclear
302
# about the type of this type.
303
self.crc = int(math.fabs(binascii.crc32(frameData +\
304
("\x00" * padding))));
290
305
crc = bin2bytes(dec2bin(self.crc));
291
306
assert(len(crc) == 4);
301
316
data = size + flags + paddingSize;
306
321
# Only call this when you *know* there is an extened header.
307
322
def parse(self, fp, header):
308
323
assert(header.majorVersion == 2);
325
TRACE_MSG("Parsing extended header @ 0x%x" % fp.tell());
310
326
# First 4 bytes is the size of the extended header.
311
327
data = fp.read(4);
312
328
if header.minorVersion == 4:
313
TRACE_MSG("Parsing extended header for v2.4");
315
330
sz = bin2dec(bytes2bin(data, 7));
316
TRACE_MSG("Extended header size: %d" % (sz - 4));
331
TRACE_MSG("Extended header size (not including 4 byte size): %d" %\
317
333
data = fp.read(sz - 4);
319
335
if ord(data[0]) != 1 or (ord(data[1]) & 0x8f):
345
361
self.restrictions = ord(data[offset]);
348
TRACE_MSG("Parsing extended header for v2.3");
349
364
# v2.3 is totally different... *sigh*
350
365
sz = bin2dec(bytes2bin(data));
351
TRACE_MSG("Extended header size: %d" % sz);
366
TRACE_MSG("Extended header size (not including 4 bytes size): %d" %\
353
368
tmpFlags = fp.read(2);
369
# Read the padding size, but it'll be computed during the parse.
371
TRACE_MSG("Extended header says there is %d bytes of padding" %\
372
bin2dec(bytes2bin(ps)));
354
373
# Make this look like a v2.4 mask.
355
374
self.flags = ord(tmpFlags[0]) >> 2;
356
375
if self.hasCRC():
358
377
crcData = fp.read(4);
359
378
self.crc = bin2dec(bytes2bin(crcData));
360
379
TRACE_MSG("Extended header CRC: %d" % self.crc);
361
# Read the padding size, but it'll be computed during the parse.
365
382
################################################################################
427
443
# Converts all ID3v1 data into ID3v2 frames internally.
428
444
# May throw IOError, or TagException if parsing fails.
429
445
def link(self, f, v = ID3_ANY_VERSION):
446
self.linkedFile = None;
433
450
if isinstance(f, file):
434
451
fileName = f.name;
435
elif isinstance(f, str):
452
elif isinstance(f, str) or isinstance(f, unicode):
438
raise TagException("Invalid type passed to Tag.link: " +
455
raise TagException("Invalid type passed to Tag.link: " +
441
458
if v != ID3_V1 and v != ID3_V2 and v != ID3_ANY_VERSION:
483
500
self.setVersion(version);
484
501
version = self.getVersion();
485
# If v1.0 is being for explicitly then so be it, if not and there is
502
if version == ID3_V2_2:
503
raise TagException("Unable to write ID3 v2.2");
504
# If v1.0 is being requested explicitly then so be it, if not and there is
486
505
# a track number then bumping to v1.1 is /probably/ best.
487
506
if self.header.majorVersion == 1 and self.header.minorVersion == 0 and\
488
507
self.getTrackNum()[0] != None and version != ID3_V1_0:
489
508
version = ID3_V1_1;
490
509
self.setVersion(version);
492
511
# If there are no frames then simply remove the current tag.
493
512
if len(self.frames) == 0:
494
513
self.remove(version);
520
539
if version == ID3_CURRENT_VERSION:
521
540
version = self.getVersion();
524
543
if version & ID3_V1 or version == ID3_ANY_VERSION:
525
544
tagFile = file(self.linkedFile.name, "r+b");
526
545
tagFile.seek(-128, 2);
535
554
self.header.tagSize:
536
555
tagFile = file(self.linkedFile.name, "r+b");
537
556
if tagFile.read(3) == "ID3":
538
559
TRACE_MSG("Removing ID3 v2.x Tag");
539
560
tagSize = self.header.tagSize + self.header.SIZE;
540
561
tagFile.seek(tagSize);
541
data = tagFile.read();
564
tmpName = tempfile.mktemp();
565
tmpFile = file(tmpName, "w+b");
567
# Write audio data in chunks
568
self.__copyRemaining(tagFile, tmpFile);
544
569
tagFile.truncate();
575
shutil.copyfile(tmpName, self.linkedFile.name);
552
582
# Get artist. There are a few frames that can contain this information,
553
583
# and they are subtley different.
554
584
# eyeD3.frames.ARTIST_FID - Lead performer(s)/Soloist(s)
597
627
def getDate(self, fid = None):
599
for fid in ["TDRL", "TDOR", "TDRC",
600
OBSOLETE_YEAR_FID, OBSOLETE_DATE_FID]:
629
for fid in DATE_FIDS:
601
630
if self.frames[fid]:
602
631
return self.frames[fid];
653
def _getNum(self, fid):
656
f = self.frames[fid];
658
n = f[0].text.split('/')
660
tn = self.toInt(n[0])
662
tn = self.toInt(n[0])
663
tt = self.toInt(n[1])
624
666
# Returns a tuple with the first value containing the track number and the
625
667
# second the total number of tracks. One or both of these values may be
626
668
# None depending on what is available in the tag.
627
669
def getTrackNum(self):
628
f = self.frames[TRACKNUM_FID];
630
n = string.split(f[0].text, '/');
631
tn = self.toInt(n[0]);
634
tt = self.toInt(n[1]);
635
if tn != None and tt != None:
670
return self._getNum(TRACKNUM_FID)
672
# Like TrackNum, except for DiscNum--that is, position in a set. Most
673
# songs won't have this or it will be 1/1.
674
def getDiscNum(self):
675
return self._getNum(DISCNUM_FID)
641
677
# Since multiple comment frames are allowed this returns a list with 0
642
678
# or more elements. The elements are not the comment strings, they are
644
680
def getComments(self):
645
681
return self.frames[COMMENT_FID];
683
# Since multiple lyrics frames are allowed this returns a list with 0
684
# or more elements. The elements are not the lyrics strings, they are
685
# eyeD3.frames.LyricsFrame objects.
687
return self.frames[LYRICS_FID];
647
689
# Returns a list (possibly empty) of eyeD3.frames.ImageFrame objects.
648
690
def getImages(self):
649
691
return self.frames[IMAGE_FID];
693
# Returns a list (possibly empty) of eyeD3.frames.ObjectFrame objects.
694
def getObjects(self):
695
return self.frames[OBJECT_FID];
651
697
# Returns a list (possibly empty) of eyeD3.frames.URLFrame objects.
652
698
# Both URLFrame and UserURLFrame objects are returned. UserURLFrames
653
699
# add a description and encoding, and have a different frame ID.
694
740
def setDate(self, year, month = None, dayOfMonth = None,
695
741
hour = None, minute = None, second = None, fid = None):
696
742
if not year and not fid:
697
dateFrame = self.getDate;
698
self.frames.removeFramesByID(dateFrame.id);
743
dateFrames = self.getDate();
745
self.frames.removeFramesByID(dateFrames[0].header.id);
701
748
self.frames.removeFramesByID(fid);
758
805
# second the total number of tracks. One or both of these values may be
759
806
# None. If both values are None, the frame is removed.
760
807
def setTrackNum(self, n):
808
self.setNum(TRACKNUM_FID, n)
810
def setDiscNum(self, n):
811
self.setNum(DISCNUM_FID, n)
813
def setNum(self, fid, n):
761
814
if n[0] == None and n[1] == None:
762
self.frames.removeFramesByID(TRACKNUM_FID);
815
self.frames.removeFramesByID(fid);
788
841
elif trackStr and not totalStr:
791
self.frames.setTextFrame(TRACKNUM_FID, self.strToUnicode(s),
844
self.frames.setTextFrame(fid, self.strToUnicode(s),
794
848
# Add a comment. This adds a comment unless one is already present with
795
849
# the same language and description in which case the current value is
796
850
# either changed (cmt != "") or removed (cmt equals "" or None).
808
862
self.strToUnicode(desc),
809
863
lang, self.encoding);
865
# Add lyrics. Semantics similar to addComment
866
def addLyrics(self, lyr, desc = u"", lang = DEFAULT_LANG):
868
# A little more than a call to removeFramesByID is involved since we
869
# need to look at more than the frame ID.
870
lyrics = self.frames[LYRICS_FID];
872
if l.lang == lang and l.description == desc:
873
self.frames.remove(l);
876
self.frames.setLyricsFrame(self.strToUnicode(lyr),
877
self.strToUnicode(desc),
878
lang, self.encoding);
811
880
# Semantics similar to addComment
812
881
def addUserTextFrame(self, desc, text):
823
892
def removeComments(self):
824
893
return self.frames.removeFramesByID(COMMENT_FID);
895
def removeLyrics(self):
896
return self.frames.removeFramesByID(LYRICS_FID);
826
898
def addImage(self, type, image_file_path, desc = u""):
827
image_frame = ImageFrame.create(type, image_file_path, desc);
828
self.frames.addFrame(image_frame);
900
image_frame = ImageFrame.create(type, image_file_path, desc);
901
self.frames.addFrame(image_frame);
903
image_frames = self.frames[IMAGE_FID];
904
for i in image_frames:
905
if i.pictureType == type:
906
self.frames.remove(i);
909
def addObject(self, object_file_path, mime = "", desc = u"",
911
object_frames = self.frames[OBJECT_FID];
912
for i in object_frames:
913
if i.description == desc:
914
self.frames.remove(i);
916
object_frame = ObjectFrame.create(object_file_path, mime, desc,
918
self.frames.addFrame(object_frame);
830
920
def getPlayCount(self):
831
921
if self.frames[PLAYCOUNT_FID]:
846
936
frameHeader.id = PLAYCOUNT_FID;
847
937
pc = PlayCountFrame(frameHeader, count = count);
848
938
self.frames.addFrame(pc);
850
940
def incrementPlayCount(self, n = 1):
851
941
pc = self.getPlayCount();
868
958
self.frames.setUniqueFileIDFrame(owner_id, id);
961
bpm = self.frames[BPM_FID];
963
return int(bpm[0].text);
967
def setBPM(self, bpm):
968
self.setTextFrame(BPM_FID, self.strToUnicode(str(bpm)));
970
def getPublisher(self):
971
pub = self.frames[PUBLISHER_FID];
973
return pub[0].text or None;
975
def setPublisher(self, p):
976
self.setTextFrame(PUBLISHER_FID, self.strToUnicode(str(p)));
870
978
# Test ID3 major version.
872
980
return self.header.majorVersion == 1;
882
990
if v != ID3_CURRENT_VERSION:
883
991
self.header.setVersion(v);
884
992
self.frames.setTagHeader(self.header);
886
994
def setTextFrame(self, fid, txt):
888
996
self.frames.removeFramesByID(fid);
890
998
self.frames.setTextFrame(fid, self.strToUnicode(txt), self.encoding);
892
1000
def setTextEncoding(self, enc):
893
1001
if enc != LATIN1_ENCODING and enc != UTF_16_ENCODING and\
894
1002
enc != UTF_16BE_ENCODING and enc != UTF_8_ENCODING:
994
1102
# Returns false when an ID3 v1 tag is not present, or contains no data.
995
1103
def __loadV1Tag(self, f):
996
if isinstance(f, str):
1104
if isinstance(f, str) or isinstance(f, unicode):
997
1105
fp = file(f, "rb")
1119
1228
rewriteFile = 1;
1120
1229
paddingSize = 2048;
1121
1230
TRACE_MSG("Rendering extended header");
1122
headerData += self.extendedHeader.render(self.header, frameData,
1231
extHeaderData += self.extendedHeader.render(self.header, frameData,
1125
if rewriteFile or (10 + len(headerData) + len(frameData)) >= currTagSize:
1234
if rewriteFile or (10 + len(headerData) + len(extHeaderData) +\
1235
len(frameData)) >= currTagSize:
1126
1236
TRACE_MSG("File rewrite required");
1127
1237
rewriteFile = 1;
1128
1238
paddingSize = 2048;
1130
paddingSize = currTagSize - (len(headerData) + len(frameData));
1240
paddingSize = currTagSize - (len(headerData) + len(extHeaderData) +\
1131
1242
frameData += ("\x00" * paddingSize);
1133
1244
# Render the tag header.
1145
1256
tagFile.write(tagData);
1146
1257
tagFile.close();
1149
tagFile = file(self.linkedFile.name, "rb");
1150
# Read all audio data
1151
tagFile.seek(currTagSize);
1152
audioData = tagFile.read();
1155
1261
# Open tmp file
1156
1262
tmpName = tempfile.mktemp();
1157
1263
tmpFile = file(tmpName, "w+b");
1158
1264
TRACE_MSG("Writing %d bytes of tag data" % len(tagData));
1159
1265
tmpFile.write(tagData);
1160
tmpFile.write(audioData);
1267
# Write audio data in chunks
1268
tagFile = file(self.linkedFile.name, "rb");
1269
tagFile.seek(currTagSize);
1270
self.__copyRemaining(tagFile, tmpFile);
1161
1273
tmpFile.close();
1163
1275
# Move tmp to orig.
1174
1286
# Returns >= 0 to indicate the padding size of the read frame; -1 returned
1175
1287
# when not tag was found.
1176
1288
def __loadV2Tag(self, f):
1177
if isinstance(f, str):
1289
if isinstance(f, str) or isinstance(f, unicode):
1178
1290
fp = file(f, "rb")
1196
1308
padding = self.frames.parse(fp, self.header);
1197
1309
TRACE_MSG("Tag contains %d bytes of padding." % padding);
1198
1310
except FrameException, ex:
1200
raise TagException(str(ex));
1311
if utils.strictID3():
1313
raise TagException(str(ex));
1201
1314
except TagException:
1224
1337
raise TagException("Invalid date field: " + fStr);
1340
def __copyRemaining(self, src_fp, dest_fp):
1341
# Write audio data in chunks
1345
data = src_fp.read(amt)
1228
1353
# This method will return the first comment in the FrameSet
1229
1354
# and not all of them. Multiple COMM frames are common and useful. Use
1239
1364
################################################################################
1240
1365
class GenreException(Exception):
1243
def __init__(self, msg):
1366
'''Problem looking up genre'''
1250
1368
################################################################################
1373
def __init__(self, id = None, name = None):
1376
elif name is not None:
1258
1379
def getId(self):
1259
1380
return self.id;
1327
1448
raise GenreException("eyeD3.genres[" + str(id) + "] " +\
1328
1449
"does not match " + name);
1330
1451
# Parses genre information from genreStr.
1331
1452
# The following formats are supported:
1332
1453
# 01, 2, 23, 125 - ID3 v1 style.
1377
1498
# Non standard, but witnessed.
1378
1499
# Match genreName alone. e.g. Rap, Rock, blues.
1379
regex = re.compile("^[A-Z 0-9+/\-&]+\00*$", re.IGNORECASE);
1500
regex = re.compile("^[A-Z 0-9'+/\-&]+\00*$", re.IGNORECASE);
1380
1501
if regex.match(genreStr):
1381
1502
self.setName(genreStr);
1394
1515
################################################################################
1395
class InvalidAudioFormatException:
1398
def __init__(self, msg):
1516
class InvalidAudioFormatException(Exception):
1517
'''Problems with audio format'''
1404
1519
################################################################################
1406
1521
fileName = str("");
1407
1522
fileSize = int(0);
1524
# Number of seconds required to play the audio file.
1410
1527
def __init__(self, fileName):
1411
1528
self.fileName = fileName;
1418
1535
self.fileSize = os.stat(self.fileName)[ST_SIZE];
1419
1536
return self.fileSize;
1421
def rename(self, name):
1538
def rename(self, name, fsencoding):
1422
1539
base = os.path.basename(self.fileName);
1423
1540
base_ext = os.path.splitext(base)[1];
1424
1541
dir = os.path.dirname(self.fileName);
1425
new_name = dir + os.sep + name.encode("iso8859-1") + base_ext;
1544
new_name = dir + os.sep + name.encode(fsencoding) + base_ext;
1427
1546
os.rename(self.fileName, new_name);
1428
1547
self.fileName = new_name;
1430
1549
raise TagException("Error renaming '%s' to '%s'" % (self.fileName,
1552
def getPlayTime(self):
1553
return self.play_time;
1555
def getPlayTimeString(self):
1556
total = self.getPlayTime();
1558
m = (total % 3600) / 60;
1559
s = (total % 3600) % 60;
1561
timeStr = "%d:%.2d:%.2d" % (h, m, s);
1563
timeStr = "%d:%.2d" % (m, s);
1434
1567
################################################################################
1435
1568
class Mp3AudioFile(TagFile):
1436
1569
header = mp3.Header();
1437
1570
xingHeader = None;
1438
1571
invalidFileExc = InvalidAudioFormatException("File is not mp3");
1439
# Number of seconds required to play the audio file.
1442
1573
def __init__(self, fileName, tagVersion = ID3_ANY_VERSION):
1443
1574
TagFile.__init__(self, fileName);
1445
self.playTime = None;
1447
1576
if not isMp3File(fileName):
1448
1577
raise self.invalidFileExc;
1450
1579
# Parse ID3 tag.
1451
f = file(fileName, "rb");
1580
f = file(self.fileName, "rb");
1453
1582
hasTag = tag.link(f, tagVersion);
1454
1583
# Find the first mp3 frame.
1470
1598
frameHead = bin2dec(bytes2bin(bString));
1471
1599
header = mp3.Header();
1472
1601
# Keep reading until we find a valid mp3 frame header.
1473
1602
while not header.isValid(frameHead):
1603
# Originally, the search was one byte at a time. Occasionally a tag
1604
# was heavily, and incorrectly, padded or a corrupt mp3 would cause
1605
# a byte by byte search to take a long time. This algorithm speeds
1606
# up this particular case.
1608
f.seek(f.tell() + 128);
1609
bString = f.read(4);
1610
if len(bString) < 4:
1611
raise InvalidAudioFormatException("Unable to find a valid mp3 "\
1613
frameHead = bin2dec(bytes2bin(bString));
1474
1617
frameHead <<= 8;
1475
1618
bString = f.read(1);
1476
1619
if len(bString) != 1:
1477
1620
raise InvalidAudioFormatException("Unable to find a valid mp3 "\
1479
1622
frameHead |= ord(bString[0]);
1480
1624
TRACE_MSG("mp3 header %x found at position: %d (0x%x)" % \
1481
1625
(frameHead, f.tell() - 4, f.tell() - 4));
1511
1655
elif tag and tag.isV1():
1513
self.playTime = int((length / header.frameLength) * tpf);
1657
self.play_time = int((length / header.frameLength) * tpf);
1515
1659
self.header = header;
1516
1660
self.xingHeader = xingHeader;
1517
1661
self.tag = tag;
1520
def getPlayTime(self):
1521
return self.playTime;
1523
def getPlayTimeString(self):
1524
total = self.getPlayTime();
1526
m = (total % 3600) / 60;
1527
s = (total % 3600) % 60;
1529
timeStr = "%d:%.2d:%.2d" % (h, m, s);
1531
timeStr = "%d:%.2d" % (m, s);
1534
1664
# Returns a tuple. The first value is a boolean which if true means the
1535
1665
# bit rate returned in the second value is variable.
1536
1666
def getBitRate(self):
1764
1894
tagSize = 0; # This includes the padding byte count.
1766
1896
def __init__(self, fileName):
1767
self.name = fileName;
1897
if isinstance(fileName, str):
1899
self.name = unicode(fileName, sys.getfilesystemencoding());
1901
# Work around the local encoding not matching that of a mounted
1903
self.name = fileName
1905
self.name = fileName;
1769
1907
def tagToUserTune(tag):
1770
1908
audio_file = None;
1771
1909
if isinstance(tag, Mp3AudioFile):
1772
1910
audio_file = tag;
1773
1911
tag = audio_file.getTag();
1775
tune = "<tune xmlns='http://jabber.org/protocol/tune'>\n";
1913
tune = u"<tune xmlns='http://jabber.org/protocol/tune'>\n";
1776
1914
if tag.getArtist():
1777
1915
tune += " <artist>" + tag.getArtist() + "</artist>\n";
1778
1916
if tag.getTitle():
1779
1917
tune += " <title>" + tag.getTitle() + "</title>\n";
1780
1918
if tag.getAlbum():
1781
1919
tune += " <source>" + tag.getAlbum() + "</source>\n";
1782
tune += " <track>" + "file://" + os.path.abspath(tag.linkedFile.name) +\
1920
tune += " <track>" +\
1921
"file://" + unicode(os.path.abspath(tag.linkedFile.name)) +\
1785
tune += " <length>" + str(audio_file.getPlayTime()) + "</length>\n";
1924
tune += " <length>" + unicode(audio_file.getPlayTime()) +\
1786
1926
tune += "</tune>\n";