Fixed uncaught exception thrown by eyeD3 on 2 digit years.
mp3togo/tags.py
1 # - tags.py -
2 # This file is part of mp3togo
3
4 # Convert audio files to play on a mp3 player
5 # Manage program options
6 #
7 # (c) Simeon Veldstra 2004, 2006 <reallifesim@gmail.com>
8 #
9 # This software is free.
10 #
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.
15 #
16 # You may redistribute this program under the terms of the
17 # GNU General Public Licence version 2
18 # Available in this package or at http://www.fsf.org
19
20
21 import os, sys
22 import UserDict
23 import threading
24
25 import mp3togo.conf as setup
26
27 # This mess needs to be fixed,
28 # Tags() is just going to have to
29 # take an Options() instance.
30 try:
31 import ogg.vorbis
32 except ImportError:
33 HAVE_VORBIS = False
34 else:
35 HAVE_VORBIS = True
36
37 try:
38 import eyeD3
39 except ImportError:
40 HAVE_eyeD3 = False
41 else:
42 HAVE_eyeD3 = True
43
44 try:
45 import ID3
46 except ImportError:
47 HAVE_ID3 = False
48 else:
49 HAVE_ID3 = True
50
51 import codecs
52
53 HAVE_METAFLAC=False
54 for path in os.environ['PATH'].split(':'):
55 if os.path.exists(os.path.join(path, 'metaflac')):
56 HAVE_METAFLAC=True
57 break
58
59
60 class Tags(UserDict.DictMixin):
61 """Manage per track metadata.
62
63 Provide a filename in the constructor
64 Call read() to read in the tags
65 modify tags if necessary
66 Call write(name) with the output file the tags
67 are to be written to
68 Tags are available through a dictionary interface
69 format(fmt) returns a formatted string of metadata
70 """
71
72 def __init__(self, filename, opts):
73 if not os.path.exists(filename):
74 raise setup.ErrorNoFile
75 self._opts = opts
76 self._file = filename
77 self._type = opts.getfiletype(filename)
78 self._tags = {}
79 self._readok = threading.Event()
80 self._readok.clear()
81 self._lock = threading.Lock()
82
83
84 def read(self, block=True):
85 if not self._lock.acquire(block) and not block:
86 return False
87 self._readok.clear()
88
89 def copytags(d):
90 o = {}
91 for k in d.keys():
92 o[k] = list(d[k])
93 if not o.has_key('GENRE'):
94 o['GENRE'] = 'Other'
95 return o
96
97 if self._type == 'mp3' and HAVE_ID3:
98 if HAVE_eyeD3:
99 eye = eyeD3.Tag()
100 eye.link(self._file)
101 self._tags['ARTIST'] = [eye.getArtist() or '']
102 self._tags['ALBUM'] = [eye.getAlbum() or '']
103 self._tags['TITLE'] = [eye.getTitle() or '']
104 try:
105 self._tags['GENRE'] = [getattr(eye.getGenre(), 'name', 'Other')]
106 except eyeD3.tag.GenreException:
107 self._tags['GENRE'] = 'Other'
108 del eye
109 elif HAVE_ID3:
110 info = ID3.ID3(self._file, as_tuple=1).as_dict()
111 self._tags = copytags(info)
112 del info
113 elif self._type == 'ogg' and HAVE_VORBIS:
114 info = ogg.vorbis.VorbisFile(self._file).comment().as_dict()
115 self._tags = copytags(info)
116 del info
117 elif self._type == 'flac' and HAVE_METAFLAC:
118 cmd = '%s --export-tags-to=- "%s" ' % ('metaflac', self._file)
119 fd = os.popen(cmd)
120 info = fd.read()
121 fd.close()
122 info = map(lambda x: x.split('=', 1), info.split('\n'))
123 info = filter(lambda x: len(x) == 2, info)
124 info = map(lambda x: [x[0].upper(), x[1:]], info)
125 self._tags = dict(info)
126 elif self._type == 'wav':
127 pass
128
129
130 # Try to guess from the file's path - better than nothing
131 path = os.path.splitext(self._file)[0]
132 path = path.replace('_', ' ')
133 for id, depth in [('ARTIST', -3), ('ALBUM', -2), ('TITLE', -1)]:
134 if not id in self._tags or self._tags[id][0] == '':
135 try:
136 self._tags[id] = [path.split(os.sep)[depth]]
137 except IndexError:
138 self._tags[id] = "%s unknown" % id.lower()
139
140 self._readok.set()
141 self._lock.release()
142 return True
143
144
145 def write(self, filename, block=True):
146 if not self._lock.acquire(block) and not block:
147 return False
148
149 if not os.path.exists(filename):
150 self._lock.release()
151 raise setup.ErrorNoFile
152
153 def puttags(d):
154 for key in self._tags.keys(): # Not self.keys()
155 if key == 'GENRE' and fmt == 'mp3':
156 if type(self._tags[key][0]) is type(1) and 0 <= self._tags[key][0] < 256:
157 d[key] = self._tags[key][0]
158 elif self._tags[key][0] in map(str, range(256)):
159 d[key] = int(self._tags[key][0])
160 else:
161 d[key] = int(genres.get(self._tags[key][0], '255'))
162 else:
163 d[key] = self._tags[key][0]
164 # No! don't unlock here dumbass! return from puttags
165 return d
166
167 try:
168 fmt = self._opts.getfiletype(filename)
169 except setup.ErrorUnknownFileType:
170 self._lock.release()
171 raise
172
173 if fmt == 'ogg':
174 vf = ogg.vorbis.VorbisFile(filename)
175 out = vf.comment()
176 puttags(out)
177 try:
178 out.write_to(filename)
179 except:
180 pass
181 del out
182 del vf
183 elif fmt == 'mp3':
184 if HAVE_eyeD3:
185 out = eyeD3.Tag()
186 out.link(filename, eyeD3.ID3_V1)
187 out.setVersion(eyeD3.ID3_V1)
188 d = {}
189 d = puttags(d)
190 out.setArtist(d['ARTIST'])
191 out.setAlbum(d['ALBUM'])
192 out.setTitle(d['TITLE'])
193 g = eyeD3.Genre()
194 g.setId(d.get('GENRE', 12))
195 out.setGenre(g)
196 if d.has_key('COMMENT'):
197 out.addComment(d['COMMENT'])
198 out.update()
199 del out
200 elif HAVE_ID3:
201 out = ID3.ID3(filename, as_tuple=1)
202 puttags(out)
203 try:
204 out.write()
205 except:
206 # Tagging failed, but the file should be okay.
207 pass
208 del out
209 else:
210 self._lock.release()
211 raise setup.ErrorUnknownFileType
212
213 self._lock.release()
214 return True
215
216
217 def writeindex(self, filename, indexname, block=True):
218 if not self._lock.acquire(block) and not block:
219 return False
220
221 try:
222 ifile = codecs.open(indexname, 'a', 'utf-8')
223 ifile.write(filename + "\n")
224 keys = self._tags.keys()
225 keys.sort()
226 for k in keys:
227 for q in self._tags[k]:
228 ifile.write("%s: %s\n" % (k, q))
229 ifile.write("\n")
230 ifile.close()
231 except:
232 self._lock.release()
233 raise
234
235 self._lock.release()
236 return True
237
238 def format(self, fmt, block=True):
239 """Pretty print the tag information
240
241 The following format strings apply:
242 %% Literal %
243 %n Track number
244 %a Artist
245 %t Track title
246 %l Album title
247 %y Album release year
248 %g Album genre"""
249 if not self._lock.acquire(block) and not block:
250 return False
251
252 esc = {'n': 'TRACKNUMBER',
253 'a': 'ARTIST',
254 't': 'TITLE',
255 'l': 'ALBUM',
256 'y': 'YEAR',
257 'g': 'GENRE_NAME'}
258 #'z': Used for literal '%'
259
260 out = ""
261 fmt = fmt.replace('%%', '%z')
262 fmt = fmt.split('%')
263 while fmt:
264 out += fmt[0]
265 if len(fmt) <= 1:
266 break
267 fmt = fmt[1:]
268 code = fmt[0] and fmt[0][0]
269 if code == 'z':
270 fmt[0] = '%' + fmt[0][1:]
271 elif code in esc.keys():
272 fmt[0] = self._tags.get(esc[code], ('',))[0] + fmt[0][1:]
273 else:
274 self._lock.release()
275 raise setup.ErrorBadFormat
276
277 self._lock.release()
278 return out
279
280
281 def __getitem__(self, key, block=True):
282
283 if not self._lock.acquire(block) and not block:
284 return False
285
286 if key == 'GENRE_NAME':
287 if not self._tags.has_key('GENRE'):
288 out = ''
289 else:
290 out = [genrenumbers.get(self._tags['GENRE'][0], self._tags['GENRE'][0])]
291 else:
292 try:
293 out = self._tags[key.upper()]
294 except KeyError:
295 self._lock.release()
296 raise
297
298 self._lock.release()
299 return out
300
301
302 def __setitem__(self, key, value, block=True):
303
304 if not self._lock.acquire(block) and not block:
305 return False
306
307 if type(value) != type([]):
308 value = [value]
309 self._tags[key.upper()] = value
310
311 self._lock.release()
312 return True
313
314
315 def setorappend(self, key, value, block=True):
316 if not self._lock.acquire(block) and not block:
317 return False
318
319 key = key.upper()
320 if self._tags.has_key(key):
321 self._tags[key].append(value)
322 else:
323 self[key] = value
324
325 self._lock.release()
326 return True
327
328
329 def __delitem__(self, key, block=True):
330 if not self._lock.acquire(block) and not block:
331 return False
332
333 try:
334 del self._tags[key]
335 except KeyError:
336 self._lock.release()
337 raise
338
339 self._lock.release()
340 return True
341
342
343 def keys(self, block=True):
344 if not self._lock.acquire(block) and not block:
345 return False
346
347 if self._tags.has_key('GENRE'):
348 out = self._tags.keys() + ['GENRE_NAME']
349 else:
350 out = self._tags.keys()
351
352 self._lock.release()
353 return out
354
355
356 def remove_from_index(trackname, indexname):
357 """Remove an entry from an index file."""
358 if not os.path.exists(indexname):
359 return True
360 fp = file(indexname, 'r')
361 lines = fp.readlines()
362 fp.close()
363 i = 0
364 new = []
365 while i < len(lines):
366 if lines[i] == trackname + '\n':
367 while 1:
368 i += 1
369 if i >= len(lines):
370 break
371 if lines[i] == '\n':
372 i += 1
373 break
374 if i >= len(lines):
375 break
376 new.append(lines[i])
377 i += 1
378 if new:
379 fp = file(indexname, 'w')
380 fp.writelines(new)
381 fp.close()
382 else:
383 os.unlink(indexname)
384 return True
385
386
387 #DATA
388
389 genres = {
390 'Blues': '0',
391 'Classic Rock': '1',
392 'Country': '2',
393 'Dance': '3',
394 'Disco': '4',
395 'Funk': '5',
396 'Grunge': '6',
397 'Hip-Hop': '7',
398 'Jazz': '8',
399 'Metal': '9',
400 'New Age': '10',
401 'Oldies': '11',
402 'Other': '12',
403 'Pop': '13',
404 'R&B': '14',
405 'Rap': '15',
406 'Reggae': '16',
407 'Rock': '17',
408 'Techno': '18',
409 'Industrial': '19',
410 'Alternative': '20',
411 'Ska': '21',
412 'Death Metal': '22',
413 'Pranks': '23',
414 'Soundtrack': '24',
415 'Euro-Techno': '25',
416 'Ambient': '26',
417 'Trip-Hop': '27',
418 'Vocal': '28',
419 'Jazz+Funk': '29',
420 'Fusion': '30',
421 'Trance': '31',
422 'Classical': '32',
423 'Instrumental': '33',
424 'Acid': '34',
425 'House': '35',
426 'Game': '36',
427 'Sound Clip': '37',
428 'Gospel': '38',
429 'Noise': '39',
430 'Alt. Rock': '40',
431 'Bass': '41',
432 'Soul': '42',
433 'Punk': '43',
434 'Space': '44',
435 'Meditative': '45',
436 'Instrum. Pop': '46',
437 'Instrum. Rock': '47',
438 'Ethnic': '48',
439 'Gothic': '49',
440 'Darkwave': '50',
441 'Techno-Indust.': '51',
442 'Electronic': '52',
443 'Pop-Folk': '53',
444 'Eurodance': '54',
445 'Dream': '55',
446 'Southern Rock': '56',
447 'Comedy': '57',
448 'Cult': '58',
449 'Gangsta': '59',
450 'Top 40': '60',
451 'Christian Rap': '61',
452 'Pop/Funk': '62',
453 'Jungle': '63',
454 'Native American': '64',
455 'Cabaret': '65',
456 'New Wave': '66',
457 'Psychedelic': '67',
458 'Rave': '68',
459 'Showtunes': '69',
460 'Trailer': '70',
461 'Lo-Fi': '71',
462 'Tribal': '72',
463 'Acid Punk': '73',
464 'Acid Jazz': '74',
465 'Polka': '75',
466 'Retro': '76',
467 'Musical': '77',
468 'Rock & Roll': '78',
469 'Hard Rock': '79',
470 'Folk': '80',
471 'Folk/Rock': '81',
472 'National Folk': '82',
473 'Swing': '83',
474 'Fusion': '84',
475 'Bebob': '85',
476 'Latin': '86',
477 'Revival': '87',
478 'Celtic': '88',
479 'Bluegrass': '89',
480 'Avantgarde': '90',
481 'Gothic Rock': '91',
482 'Progress. Rock': '92',
483 'Psychedel. Rock': '93',
484 'Symphonic Rock': '94',
485 'Slow Rock': '95',
486 'Big Band': '96',
487 'Chorus': '97',
488 'Easy Listening': '98',
489 'Acoustic': '99',
490 'Humour': '100',
491 'Speech': '101',
492 'Chanson': '102',
493 'Opera': '103',
494 'Chamber Music': '104',
495 'Sonata': '105',
496 'Symphony': '106',
497 'Booty Bass': '107',
498 'Primus': '108',
499 'Porn Groove': '109',
500 'Satire': '110',
501 'Slow Jam': '111',
502 'Club': '112',
503 'Tango': '113',
504 'Samba': '114',
505 'Folklore': '115',
506 'Ballad': '116',
507 'Power Ballad': '117',
508 'Rhythmic Soul': '118',
509 'Freestyle': '119',
510 'Duet': '120',
511 'Punk Rock': '121',
512 'Drum Solo': '122',
513 'A Capella': '123',
514 'Euro-House': '124',
515 'Dance Hall': '125',
516 'Goa': '126',
517 'Drum & Bass': '127',
518 'Club-House': '128',
519 'Hardcore': '129',
520 'Terror': '130',
521 'Indie': '131',
522 'BritPop': '132',
523 'Negerpunk': '133',
524 'Polsk Punk': '134',
525 'Beat': '135',
526 'Christian Gangsta Rap': '136',
527 'Heavy Metal': '137',
528 'Black Metal': '138',
529 'Crossover': '139',
530 'Contemporary Christian': '140',
531 'Christian Rock': '141',
532 'Merengue': '142',
533 'Salsa': '143',
534 'Thrash Metal': '144',
535 'Anime': '145',
536 'Jpop': '146',
537 'Synthpop': '147'}
538
539 genrenumbers = {}
540 for key, value in genres.items():
541 genrenumbers[value] = key
542
543 #END