Merge lp:~eh/coverlovin/mutagen into lp:coverlovin

Proposed by Eetu Huisman
Status: Merged
Merged at revision: 7
Proposed branch: lp:~eh/coverlovin/mutagen
Merge into: lp:coverlovin
Diff against target: 670 lines (+10/-632)
2 files modified
coverlovin.py (+10/-5)
id3reader.py (+0/-627)
To merge this branch: bzr merge lp:~eh/coverlovin/mutagen
Reviewer Review Type Date Requested Status
James Stewart Approve
Review via email: mp+102965@code.launchpad.net

Description of the change

Added support for other types besides mp3 by replacing id3reader with a dependency to Mutagen.

To post a comment you must log in.
Revision history for this message
James Stewart (amorphic) :
review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'coverlovin.py'
--- coverlovin.py 2012-02-06 14:08:17 +0000
+++ coverlovin.py 2012-04-21 08:59:19 +0000
@@ -10,7 +10,7 @@
10import os, sys10import os, sys
11import urllib, urllib211import urllib, urllib2
12import simplejson12import simplejson
13import id3reader13import mutagen
14import logging14import logging
15from optparse import OptionParser15from optparse import OptionParser
1616
@@ -147,16 +147,21 @@
147 fileFullPath = os.path.join(thisDir, file)147 fileFullPath = os.path.join(thisDir, file)
148 if os.path.getsize(fileFullPath) > 128:148 if os.path.getsize(fileFullPath) > 128:
149149
150 # check file for id3 tag info150 # check file for tag info
151 try:151 try:
152 id3r = id3reader.Reader(fileFullPath)152 fileInfo = mutagen.File(fileFullPath, easy=True)
153 except:153 except:
154 log.info("Skipping " + file + " due to unicode error")154 log.info("Skipping " + file + " due to unicode error")
155 continue155 continue
156156
157 artist = id3r.getValue('performer')157 try:
158 album = id3r.getValue('album')158 artist = fileInfo["artist"][0]
159 album = fileInfo["album"][0]
160 except:
161 log.debug("Artist or album of " + file + " not found, continuing")
162 continue
159163
164 log.info("artist: " + artist + ", album: " + album)
160 # sanitise None values165 # sanitise None values
161 if artist == None: artist = ''166 if artist == None: artist = ''
162 if album == None: album = ''167 if album == None: album = ''
163168
=== removed file 'id3reader.py'
--- id3reader.py 2011-02-06 03:46:54 +0000
+++ id3reader.py 1970-01-01 00:00:00 +0000
@@ -1,627 +0,0 @@
1""" Read ID3 tags from a file.
2 Ned Batchelder, http://nedbatchelder.com/code/modules/id3reader.html
3"""
4
5__version__ = '1.53.20070415' # History at the end of the file.
6
7# ID3 specs: http://www.id3.org/develop.html
8
9import struct, sys, zlib
10
11# These are the text encodings, indexed by the first byte of a text value.
12_encodings = ['iso8859-1', 'utf-16', 'utf-16be', 'utf-8']
13
14# Simple pseudo-id's, mapped to their various representations.
15# Use these ids with getValue, and you don't need to know what
16# version of ID3 the file contains.
17_simpleDataMapping = {
18 'album': ('TALB', 'TAL', 'v1album', 'TOAL'),
19 'performer': ('TPE1', 'TP1', 'v1performer', 'TOPE'),
20 'title': ('TIT2', 'TT2', 'v1title'),
21 'track': ('TRCK', 'TRK', 'v1track'),
22 'year': ('TYER', 'TYE', 'v1year'),
23 'genre': ('TCON', 'TCO', 'v1genre'),
24 'comment': ('COMM', 'COM', 'v1comment'),
25}
26
27# Provide booleans for older Pythons.
28try:
29 True, False
30except NameError:
31 True, False = 1==1, 1==0
32
33# Tracing
34_t = False
35def _trace(msg):
36 print msg
37
38# Coverage
39_c = False
40_features = {}
41def _coverage(feat):
42 #if _t: _trace('feature '+feat)
43 _features[feat] = _features.setdefault(feat, 0)+1
44
45def _safestr(s):
46 """ Get a good string for printing, that won't throw exceptions,
47 no matter what's in it.
48 """
49 try:
50 return unicode(s).encode(sys.getdefaultencoding())
51 except UnicodeError:
52 return '?: '+repr(s)
53
54# Can I just say that I think the whole concept of genres is bogus,
55# since they are so subjective? And the idea of letting someone else pick
56# one of these things and then have it affect the categorization of my music
57# is extra bogus. And the list itself is absurd. Polsk Punk?
58_genres = [
59 # 0-19
60 'Blues', 'Classic Rock', 'Country', 'Dance', 'Disco', 'Funk', 'Grunge', 'Hip - Hop', 'Jazz', 'Metal',
61 'New Age', 'Oldies', 'Other', 'Pop', 'R&B', 'Rap', 'Reggae', 'Rock', 'Techno', 'Industrial',
62 # 20-39
63 'Alternative', 'Ska', 'Death Metal', 'Pranks', 'Soundtrack', 'Euro - Techno', 'Ambient', 'Trip - Hop', 'Vocal', 'Jazz + Funk',
64 'Fusion', 'Trance', 'Classical', 'Instrumental', 'Acid', 'House', 'Game', 'Sound Clip', 'Gospel', 'Noise',
65 # 40-59
66 'Alt Rock', 'Bass', 'Soul', 'Punk', 'Space', 'Meditative', 'Instrumental Pop', 'Instrumental Rock', 'Ethnic', 'Gothic',
67 'Darkwave', 'Techno - Industrial', 'Electronic', 'Pop - Folk', 'Eurodance', 'Dream', 'Southern Rock', 'Comedy', 'Cult', 'Gangsta Rap',
68 # 60-79
69 'Top 40', 'Christian Rap', 'Pop / Funk', 'Jungle', 'Native American', 'Cabaret', 'New Wave', 'Psychedelic', 'Rave', 'Showtunes',
70 'Trailer', 'Lo - Fi', 'Tribal', 'Acid Punk', 'Acid Jazz', 'Polka', 'Retro', 'Musical', 'Rock & Roll', 'Hard Rock',
71 # 80-99
72 'Folk', 'Folk / Rock', 'National Folk', 'Swing', 'Fast - Fusion', 'Bebob', 'Latin', 'Revival', 'Celtic', 'Bluegrass',
73 'Avantgarde', 'Gothic Rock', 'Progressive Rock', 'Psychedelic Rock', 'Symphonic Rock', 'Slow Rock', 'Big Band', 'Chorus', 'Easy Listening', 'Acoustic',
74 # 100-119
75 'Humour', 'Speech', 'Chanson', 'Opera', 'Chamber Music', 'Sonata', 'Symphony', 'Booty Bass', 'Primus', 'Porn Groove',
76 'Satire', 'Slow Jam', 'Club', 'Tango', 'Samba', 'Folklore', 'Ballad', 'Power Ballad', 'Rhythmic Soul', 'Freestyle',
77 # 120-139
78 'Duet', 'Punk Rock', 'Drum Solo', 'A Cappella', 'Euro - House', 'Dance Hall', 'Goa', 'Drum & Bass', 'Club - House', 'Hardcore',
79 'Terror', 'Indie', 'BritPop', 'Negerpunk', 'Polsk Punk', 'Beat', 'Christian Gangsta Rap', 'Heavy Metal', 'Black Metal', 'Crossover',
80 # 140-147
81 'Contemporary Christian', 'Christian Rock', 'Merengue', 'Salsa', 'Thrash Metal', 'Anime', 'JPop', 'Synthpop'
82 ]
83
84class Id3Error(Exception):
85 """ An exception caused by id3reader properly handling a bad ID3 tag.
86 """
87 pass
88
89class _Header:
90 """ Represent the ID3 header in a tag.
91 """
92 def __init__(self):
93 self.majorVersion = 0
94 self.revision = 0
95 self.flags = 0
96 self.size = 0
97 self.bUnsynchronized = False
98 self.bExperimental = False
99 self.bFooter = False
100
101 def __str__(self):
102 return str(self.__dict__)
103
104class _Frame:
105 """ Represent an ID3 frame in a tag.
106 """
107 def __init__(self):
108 self.id = ''
109 self.size = 0
110 self.flags = 0
111 self.rawData = ''
112 self.bTagAlterPreserve = False
113 self.bFileAlterPreserve = False
114 self.bReadOnly = False
115 self.bCompressed = False
116 self.bEncrypted = False
117 self.bInGroup = False
118
119 def __str__(self):
120 return str(self.__dict__)
121
122 def __repr__(self):
123 return str(self.__dict__)
124
125 def _interpret(self):
126 """ Examine self.rawData and create a self.value from it.
127 """
128 if len(self.rawData) == 0:
129 # This is counter to the spec, but seems harmless enough.
130 #if _c: _coverage('zero data')
131 return
132
133 if self.bCompressed:
134 # Decompress the compressed data.
135 self.rawData = zlib.decompress(self.rawData)
136
137 if self.id[0] == 'T':
138 # Text fields start with T
139 encoding = ord(self.rawData[0])
140 if 0 <= encoding < len(_encodings):
141 #if _c: _coverage('encoding%d' % encoding)
142 value = self.rawData[1:].decode(_encodings[encoding])
143 else:
144 #if _c: _coverage('bad encoding')
145 value = self.rawData[1:]
146 # Don't let trailing zero bytes fool you.
147 if value:
148 value = value.strip('\0')
149 # The value can actually be a list.
150 if '\0' in value:
151 value = value.split('\0')
152 #if _c: _coverage('textlist')
153 self.value = value
154 elif self.id[0] == 'W':
155 # URL fields start with W
156 self.value = self.rawData.strip('\0')
157 if self.id == 'WXXX':
158 self.value = self.value.split('\0')
159 elif self.id == 'CDM':
160 # ID3v2.2.1 Compressed Data Metaframe
161 if self.rawData[0] == 'z':
162 self.rawData = zlib.decompress(self.rawData[5:])
163 else:
164 #if _c: _coverage('badcdm!')
165 raise Id3Error, 'Unknown CDM compression: %02x' % self.rawData[0]
166 #@TODO: re-interpret the decompressed frame.
167
168 elif self.id in _simpleDataMapping['comment']:
169 # comment field
170
171 # In limited testing a typical comment looks like
172 # '\x00XXXID3v1 Comment\x00comment test' so in this
173 # case we need to find the second \x00 to know where
174 # where we start for a comment. In case we only find
175 # one \x00, lets just start at the beginning for the
176 # value
177 s = str(self.rawData)
178
179 pos = 0
180 count = 0
181 while pos < len(s) and count < 2:
182 if ord(s[pos]) == 0:
183 count = count + 1
184 pos = pos + 1
185 if count < 2:
186 pos = 1
187
188 if pos > 0 and pos < len(s):
189 s = s[pos:]
190 if ord(s[-1]) == 0:
191 s = s[:-1]
192
193 self.value = s
194
195class Reader:
196 """ An ID3 reader.
197 Create one on a file object, and then use getValue('TIT2') (for example)
198 to pull values.
199 """
200 def __init__(self, file):
201 """ Create a reader from a file or filename. """
202 self.file = file
203 self.header = None
204 self.frames = {}
205 self.allFrames = []
206 self.bytesLeft = 0
207 self.padbytes = ''
208
209 bCloseFile = False
210 # If self.file is a string of some sort, then open it to get a file.
211 if isinstance(self.file, (type(''), type(u''))):
212 self.file = open(self.file, 'rb')
213 bCloseFile = True
214
215 self._readId3()
216
217 if bCloseFile:
218 self.file.close()
219
220 def _readBytes(self, num, desc=''):
221 """ Read some bytes from the file.
222 This method implements the "unsynchronization" scheme,
223 where 0xFF bytes may have had 0x00 bytes stuffed after
224 them. These zero bytes have to be removed transparently.
225 """
226 #if _t: _trace("ask %d (%s)" % (num,desc))
227 if num > self.bytesLeft:
228 #if _c: _coverage('long!')
229 raise Id3Error, 'Long read (%s): (%d > %d)' % (desc, num, self.bytesLeft)
230 bytes = self.file.read(num)
231 self.bytesLeft -= num
232
233 if len(bytes) < num:
234 #if _t: _trace("short read with %d left, %d total" % (self.bytesLeft, self.header.size))
235 #if _c: _coverage('short!')
236 raise Id3Error, 'Short read (%s): (%d < %d)' % (desc, len(bytes), num)
237
238 if self.header.bUnsynchronized:
239 nUnsync = 0
240 i = 0
241 while True:
242 i = bytes.find('\xFF\x00', i)
243 if i == -1:
244 break
245 #if _t: _trace("unsync at %d" % (i+1))
246 #if _c: _coverage('unsyncbyte')
247 nUnsync += 1
248 # This is a stuffed byte to remove
249 bytes = bytes[:i+1] + bytes[i+2:]
250 # Have to read one more byte from the file to adjust
251 bytes += self.file.read(1)
252 self.bytesLeft -= 1
253 i += 1
254 #if _t: _trace("unsync'ed %d" % (nUnsync))
255
256 return bytes
257
258 def _unreadBytes(self, num):
259 self.file.seek(-num, 1)
260 self.bytesLeft += num
261
262 def _getSyncSafeInt(self, bytes):
263 assert len(bytes) == 4
264 if type(bytes) == type(''):
265 bytes = [ ord(c) for c in bytes ]
266 return (bytes[0] << 21) + (bytes[1] << 14) + (bytes[2] << 7) + bytes[3]
267
268 def _getInteger(self, bytes):
269 i = 0;
270 if type(bytes) == type(''):
271 bytes = [ ord(c) for c in bytes ]
272 for b in bytes:
273 i = i*256+b
274 return i
275
276 def _addV1Frame(self, id, rawData):
277 if id == 'v1genre':
278 assert len(rawData) == 1
279 nGenre = ord(rawData)
280 try:
281 value = _genres[nGenre]
282 except IndexError:
283 value = "(%d)" % nGenre
284 else:
285 value = rawData.strip(' \t\r\n').split('\0')[0]
286 if value:
287 frame = _Frame()
288 frame.id = id
289 frame.rawData = rawData
290 frame.value = value
291 self.frames[id] = frame
292 self.allFrames.append(frame)
293
294 def _pass(self):
295 """ Do nothing, for when we need to plug in a no-op function.
296 """
297 pass
298
299 def _readId3(self):
300 header = self.file.read(10)
301 if len(header) < 10:
302 return
303 hstuff = struct.unpack('!3sBBBBBBB', header)
304 if hstuff[0] != "ID3":
305 # Doesn't look like an ID3v2 tag,
306 # Try reading an ID3v1 tag.
307 self._readId3v1()
308 return
309
310 self.header = _Header()
311 self.header.majorVersion = hstuff[1]
312 self.header.revision = hstuff[2]
313 self.header.flags = hstuff[3]
314 self.header.size = self._getSyncSafeInt(hstuff[4:8])
315
316 self.bytesLeft = self.header.size
317
318 self._readExtHeader = self._pass
319
320 if self.header.majorVersion == 2:
321 #if _c: _coverage('id3v2.2.%d' % self.header.revision)
322 self._readFrame = self._readFrame_rev2
323 elif self.header.majorVersion == 3:
324 #if _c: _coverage('id3v2.3.%d' % self.header.revision)
325 self._readFrame = self._readFrame_rev3
326 elif self.header.majorVersion == 4:
327 #if _c: _coverage('id3v2.4.%d' % self.header.revision)
328 self._readFrame = self._readFrame_rev4
329 else:
330 #if _c: _coverage('badmajor!')
331 raise Id3Error, "Unsupported major version: %d" % self.header.majorVersion
332
333 # Interpret the flags
334 self._interpretFlags()
335
336 # Read any extended header
337 self._readExtHeader()
338
339 # Read the frames
340 while self.bytesLeft > 0:
341 frame = self._readFrame()
342 if frame:
343 frame._interpret()
344 self.frames[frame.id] = frame
345 self.allFrames.append(frame)
346 else:
347 #if _c: _coverage('padding')
348 break
349
350 def _interpretFlags(self):
351 """ Interpret ID3v2.x flags.
352 """
353 if self.header.flags & 0x80:
354 self.header.bUnsynchronized = True
355 #if _c: _coverage('unsynctag')
356
357 if self.header.majorVersion == 2:
358 if self.header.flags & 0x40:
359 #if _c: _coverage('compressed')
360 # "Since no compression scheme has been decided yet,
361 # the ID3 decoder (for now) should just ignore the entire
362 # tag if the compression bit is set."
363 self.header.bCompressed = True
364
365 if self.header.majorVersion >= 3:
366 if self.header.flags & 0x40:
367 #if _c: _coverage('extheader')
368 if self.header.majorVersion == 3:
369 self._readExtHeader = self._readExtHeader_rev3
370 else:
371 self._readExtHeader = self._readExtHeader_rev4
372 if self.header.flags & 0x20:
373 #if _c: _coverage('experimental')
374 self.header.bExperimental = True
375
376 if self.header.majorVersion >= 4:
377 if self.header.flags & 0x10:
378 #if _c: _coverage('footer')
379 self.header.bFooter = True
380
381 def _readExtHeader_rev3(self):
382 """ Read the ID3v2.3 extended header.
383 """
384 # We don't interpret this yet, just eat the bytes.
385 size = self._getInteger(self._readBytes(4, 'rev3ehlen'))
386 self._readBytes(size, 'rev3ehdata')
387
388 def _readExtHeader_rev4(self):
389 """ Read the ID3v2.4 extended header.
390 """
391 # We don't interpret this yet, just eat the bytes.
392 size = self._getSyncSafeInt(self._readBytes(4, 'rev4ehlen'))
393 self._readBytes(size-4, 'rev4ehdata')
394
395 def _readId3v1(self):
396 """ Read the ID3v1 tag.
397 spec: http://www.id3.org/id3v1.html
398 """
399 self.file.seek(-128, 2)
400 tag = self.file.read(128)
401 if len(tag) != 128:
402 return
403 if tag[0:3] != 'TAG':
404 return
405 self.header = _Header()
406 self.header.majorVersion = 1
407 self.header.revision = 0
408
409 self._addV1Frame('v1title', tag[3:33])
410 self._addV1Frame('v1performer', tag[33:63])
411 self._addV1Frame('v1album', tag[63:93])
412 self._addV1Frame('v1year', tag[93:97])
413 self._addV1Frame('v1comment', tag[97:127])
414 self._addV1Frame('v1genre', tag[127])
415 if tag[125] == '\0' and tag[126] != '\0':
416 #if _c: _coverage('id3v1.1')
417 self.header.revision = 1
418 self._addV1Frame('v1track', str(ord(tag[126])))
419 else:
420 #if _c: _coverage('id3v1.0')
421 pass
422 return
423
424 _validIdChars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
425
426 def _isValidId(self, id):
427 """ Determine if the id bytes make a valid ID3 id.
428 """
429 for c in id:
430 if not c in self._validIdChars:
431 #if _c: _coverage('bad id')
432 return False
433 #if _c: _coverage('id '+id)
434 return True
435
436 def _readFrame_rev2(self):
437 """ Read a frame for ID3v2.2: three-byte ids and lengths.
438 spec: http://www.id3.org/id3v2-00.txt
439 """
440 if self.bytesLeft < 6:
441 return None
442 id = self._readBytes(3, 'rev2id')
443 if len(id) < 3 or not self._isValidId(id):
444 self._unreadBytes(len(id))
445 return None
446 hstuff = struct.unpack('!BBB', self._readBytes(3, 'rev2len'))
447 frame = _Frame()
448 frame.id = id
449 frame.size = self._getInteger(hstuff[0:3])
450 frame.rawData = self._readBytes(frame.size, 'rev2data')
451 return frame
452
453 def _readFrame_rev3(self):
454 """ Read a frame for ID3v2.3: four-byte ids and lengths.
455 """
456 if self.bytesLeft < 10:
457 return None
458 id = self._readBytes(4,'rev3id')
459 if len(id) < 4 or not self._isValidId(id):
460 self._unreadBytes(len(id))
461 return None
462 hstuff = struct.unpack('!BBBBh', self._readBytes(6,'rev3head'))
463 frame = _Frame()
464 frame.id = id
465 frame.size = self._getInteger(hstuff[0:4])
466 cbData = frame.size
467 frame.flags = hstuff[4]
468 #if _t: _trace('flags = %x' % frame.flags)
469 frame.bTagAlterPreserve = (frame.flags & 0x8000 != 0)
470 frame.bFileAlterPreserve = (frame.flags & 0x4000 != 0)
471 frame.bReadOnly = (frame.flags & 0x2000 != 0)
472 frame.bCompressed = (frame.flags & 0x0080 != 0)
473 if frame.bCompressed:
474 frame.decompressedSize = self._getInteger(self._readBytes(4, 'decompsize'))
475 cbData -= 4
476 #if _c: _coverage('compress')
477 frame.bEncrypted = (frame.flags & 0x0040 != 0)
478 if frame.bEncrypted:
479 frame.encryptionMethod = self._readBytes(1, 'encrmethod')
480 cbData -= 1
481 #if _c: _coverage('encrypt')
482 frame.bInGroup = (frame.flags & 0x0020 != 0)
483 if frame.bInGroup:
484 frame.groupid = self._readBytes(1, 'groupid')
485 cbData -= 1
486 #if _c: _coverage('groupid')
487
488 frame.rawData = self._readBytes(cbData, 'rev3data')
489 return frame
490
491 def _readFrame_rev4(self):
492 """ Read a frame for ID3v2.4: four-byte ids and lengths.
493 """
494 if self.bytesLeft < 10:
495 return None
496 id = self._readBytes(4,'rev4id')
497 if len(id) < 4 or not self._isValidId(id):
498 self._unreadBytes(len(id))
499 return None
500 hstuff = struct.unpack('!BBBBh', self._readBytes(6,'rev4head'))
501 frame = _Frame()
502 frame.id = id
503 frame.size = self._getSyncSafeInt(hstuff[0:4])
504 cbData = frame.size
505 frame.flags = hstuff[4]
506 frame.bTagAlterPreserve = (frame.flags & 0x4000 != 0)
507 frame.bFileAlterPreserve = (frame.flags & 0x2000 != 0)
508 frame.bReadOnly = (frame.flags & 0x1000 != 0)
509 frame.bInGroup = (frame.flags & 0x0040 != 0)
510 if frame.bInGroup:
511 frame.groupid = self._readBytes(1, 'groupid')
512 cbData -= 1
513 #if _c: _coverage('groupid')
514
515 frame.bCompressed = (frame.flags & 0x0008 != 0)
516 if frame.bCompressed:
517 #if _c: _coverage('compress')
518 pass
519 frame.bEncrypted = (frame.flags & 0x0004 != 0)
520 if frame.bEncrypted:
521 frame.encryptionMethod = self._readBytes(1, 'encrmethod')
522 cbData -= 1
523 #if _c: _coverage('encrypt')
524 frame.bUnsynchronized = (frame.flags & 0x0002 != 0)
525 if frame.bUnsynchronized:
526 #if _c: _coverage('unsyncframe')
527 pass
528 if frame.flags & 0x0001:
529 frame.datalen = self._getSyncSafeInt(self._readBytes(4, 'datalen'))
530 cbData -= 4
531 #if _c: _coverage('datalenindic')
532
533 frame.rawData = self._readBytes(cbData, 'rev3data')
534
535 return frame
536
537 def getValue(self, id):
538 """ Return the value for an ID3 tag id, or for a
539 convenience label ('title', 'performer', ...),
540 or return None if there is no such value.
541 """
542 if self.frames.has_key(id):
543 if hasattr(self.frames[id], 'value'):
544 return self.frames[id].value
545 if _simpleDataMapping.has_key(id):
546 for id2 in _simpleDataMapping[id]:
547 v = self.getValue(id2)
548 if v:
549 return v
550 return None
551
552 def getRawData(self, id):
553 if self.frames.has_key(id):
554 return self.frames[id].rawData
555 return None
556
557 def dump(self):
558 import pprint
559 print "Header:"
560 print self.header
561 print "Frames:"
562 for fr in self.allFrames:
563 if len(fr.rawData) > 30:
564 fr.rawData = fr.rawData[:30]
565 pprint.pprint(self.allFrames)
566 for fr in self.allFrames:
567 if hasattr(fr, 'value'):
568 print '%s: %s' % (fr.id, _safestr(fr.value))
569 else:
570 print '%s= %s' % (fr.id, _safestr(fr.rawData))
571 for label in _simpleDataMapping.keys():
572 v = self.getValue(label)
573 if v:
574 print 'Label %s: %s' % (label, _safestr(v))
575
576 def dumpCoverage(self):
577 feats = _features.keys()
578 feats.sort()
579 for feat in feats:
580 print "Feature %-12s: %d" % (feat, _features[feat])
581
582if __name__ == '__main__':
583 if len(sys.argv) < 2 or '-?' in sys.argv:
584 print "Give me a filename"
585 else:
586 id3 = Reader(sys.argv[1])
587 id3.dump()
588 #if _c: id3.dumpCoverage()
589
590# History:
591# 20040104: Created.
592# 20040105: Two bugs: didn't read v1 properly, and didn't like empty strings in values.
593#
594# 20040109: Properly reads v2.3 properly (4-byte lens, but not synchsafe)
595# Handles unsynchronized tags properly.
596#
597# 20040110: Total length was wrong for unsynchronized tags.
598# Treat input filename better so path module can be used.
599# Frame ids are more closely scrutinized for validity.
600# Errors are now thrown as our own exception.
601# Pad bytes aren't retained any more.
602# Frame.value is not set if there is no interpretation performed.
603#
604# 20040111: Tracing and code coverage more formalized.
605# Exceptions are now all Id3Error.
606# Zero-length data in frames is handled pleasantly.
607# Compressed frames are decompressed.
608# Extended headers are read (but uninterpreted).
609# Non-zero pad bytes are handled.
610# Frame flags are read and interpreted.
611# W*** frames are interpreted.
612# Multi-string frames set .value to a list of strings.
613#
614# 20040113: Strip all trailing zero bytes from text strings.
615# If we opened the file, we should close the file.
616#
617# 20040205: Do a better job printing strings without throwing.
618# Support genre information, even if it is stupid.
619#
620# 20040913: When dumping strings, be more robust when trying to print
621# non-character data. Thanks to Duane Harkness for the fix.
622#
623# 20061230: Fix ommission of self. in a few places.
624#
625# 20070415: Extended headers in ID3v2.4 weren't skipped properly, throwing
626# everything out of whack.
627# Be more generous about finding album and performer names in the tag.

Subscribers

People subscribed via source and target branches

to all changes: