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