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