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