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