Merge with bugfix-0.5
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 %f Track file name
250 %x File extension """
251 if not self._lock.acquire(block) and not block:
252 return False
253
254 esc = {'n': 'TRACKNUMBER',
255 'a': 'ARTIST',
256 't': 'TITLE',
257 'l': 'ALBUM',
258 'y': 'YEAR',
259 'g': 'GENRE_NAME'}
260 #'x': file extension
261 #'f': filename
262 #'z': Used for literal '%'
263
264 out = ""
265 fmt = fmt.replace('%%', '%z')
266 fmt = fmt.split('%')
267 while fmt:
268 out += fmt[0]
269 if len(fmt) <= 1:
270 break
271 fmt = fmt[1:]
272 code = fmt[0] and fmt[0][0]
273 if code == 'z':
274 fmt[0] = '%' + fmt[0][1:]
275 elif code == 'f':
276 fmt[0] = self._file + fmt[0][1:]
277 elif code == 'x':
278 fmt[0] = self._type + fmt[0][1:]
279 elif code in esc.keys():
280 fmt[0] = self._tags.get(esc[code], ('',))[0] + fmt[0][1:]
281 else:
282 self._lock.release()
283 raise setup.ErrorBadFormat
284
285 self._lock.release()
286 return out
287
288
289 def __getitem__(self, key, block=True):
290
291 if not self._lock.acquire(block) and not block:
292 return False
293
294 if key == 'GENRE_NAME':
295 if not self._tags.has_key('GENRE'):
296 out = ''
297 else:
298 out = [genrenumbers.get(self._tags['GENRE'][0], self._tags['GENRE'][0])]
299 else:
300 try:
301 out = self._tags[key.upper()]
302 except KeyError:
303 self._lock.release()
304 raise
305
306 self._lock.release()
307 return out
308
309
310 def __setitem__(self, key, value, block=True):
311
312 if not self._lock.acquire(block) and not block:
313 return False
314
315 if type(value) != type([]):
316 value = [value]
317 self._tags[key.upper()] = value
318
319 self._lock.release()
320 return True
321
322
323 def setorappend(self, key, value, block=True):
324 if not self._lock.acquire(block) and not block:
325 return False
326
327 key = key.upper()
328 if self._tags.has_key(key):
329 self._tags[key].append(value)
330 else:
331 self[key] = value
332
333 self._lock.release()
334 return True
335
336
337 def __delitem__(self, key, block=True):
338 if not self._lock.acquire(block) and not block:
339 return False
340
341 try:
342 del self._tags[key]
343 except KeyError:
344 self._lock.release()
345 raise
346
347 self._lock.release()
348 return True
349
350
351 def keys(self, block=True):
352 if not self._lock.acquire(block) and not block:
353 return False
354
355 if self._tags.has_key('GENRE'):
356 out = self._tags.keys() + ['GENRE_NAME']
357 else:
358 out = self._tags.keys()
359
360 self._lock.release()
361 return out
362
363
364 def remove_from_index(trackname, indexname):
365 """Remove an entry from an index file."""
366 if not os.path.exists(indexname):
367 return True
368 fp = file(indexname, 'r')
369 lines = fp.readlines()
370 fp.close()
371 i = 0
372 new = []
373 while i < len(lines):
374 if lines[i] == trackname + '\n':
375 while 1:
376 i += 1
377 if i >= len(lines):
378 break
379 if lines[i] == '\n':
380 i += 1
381 break
382 if i >= len(lines):
383 break
384 new.append(lines[i])
385 i += 1
386 if new:
387 fp = file(indexname, 'w')
388 fp.writelines(new)
389 fp.close()
390 else:
391 os.unlink(indexname)
392 return True
393
394
395 #DATA
396
397 genres = {
398 'Blues': '0',
399 'Classic Rock': '1',
400 'Country': '2',
401 'Dance': '3',
402 'Disco': '4',
403 'Funk': '5',
404 'Grunge': '6',
405 'Hip-Hop': '7',
406 'Jazz': '8',
407 'Metal': '9',
408 'New Age': '10',
409 'Oldies': '11',
410 'Other': '12',
411 'Pop': '13',
412 'R&B': '14',
413 'Rap': '15',
414 'Reggae': '16',
415 'Rock': '17',
416 'Techno': '18',
417 'Industrial': '19',
418 'Alternative': '20',
419 'Ska': '21',
420 'Death Metal': '22',
421 'Pranks': '23',
422 'Soundtrack': '24',
423 'Euro-Techno': '25',
424 'Ambient': '26',
425 'Trip-Hop': '27',
426 'Vocal': '28',
427 'Jazz+Funk': '29',
428 'Fusion': '30',
429 'Trance': '31',
430 'Classical': '32',
431 'Instrumental': '33',
432 'Acid': '34',
433 'House': '35',
434 'Game': '36',
435 'Sound Clip': '37',
436 'Gospel': '38',
437 'Noise': '39',
438 'Alt. Rock': '40',
439 'Bass': '41',
440 'Soul': '42',
441 'Punk': '43',
442 'Space': '44',
443 'Meditative': '45',
444 'Instrum. Pop': '46',
445 'Instrum. Rock': '47',
446 'Ethnic': '48',
447 'Gothic': '49',
448 'Darkwave': '50',
449 'Techno-Indust.': '51',
450 'Electronic': '52',
451 'Pop-Folk': '53',
452 'Eurodance': '54',
453 'Dream': '55',
454 'Southern Rock': '56',
455 'Comedy': '57',
456 'Cult': '58',
457 'Gangsta': '59',
458 'Top 40': '60',
459 'Christian Rap': '61',
460 'Pop/Funk': '62',
461 'Jungle': '63',
462 'Native American': '64',
463 'Cabaret': '65',
464 'New Wave': '66',
465 'Psychedelic': '67',
466 'Rave': '68',
467 'Showtunes': '69',
468 'Trailer': '70',
469 'Lo-Fi': '71',
470 'Tribal': '72',
471 'Acid Punk': '73',
472 'Acid Jazz': '74',
473 'Polka': '75',
474 'Retro': '76',
475 'Musical': '77',
476 'Rock & Roll': '78',
477 'Hard Rock': '79',
478 'Folk': '80',
479 'Folk/Rock': '81',
480 'National Folk': '82',
481 'Swing': '83',
482 'Fusion': '84',
483 'Bebob': '85',
484 'Latin': '86',
485 'Revival': '87',
486 'Celtic': '88',
487 'Bluegrass': '89',
488 'Avantgarde': '90',
489 'Gothic Rock': '91',
490 'Progress. Rock': '92',
491 'Psychedel. Rock': '93',
492 'Symphonic Rock': '94',
493 'Slow Rock': '95',
494 'Big Band': '96',
495 'Chorus': '97',
496 'Easy Listening': '98',
497 'Acoustic': '99',
498 'Humour': '100',
499 'Speech': '101',
500 'Chanson': '102',
501 'Opera': '103',
502 'Chamber Music': '104',
503 'Sonata': '105',
504 'Symphony': '106',
505 'Booty Bass': '107',
506 'Primus': '108',
507 'Porn Groove': '109',
508 'Satire': '110',
509 'Slow Jam': '111',
510 'Club': '112',
511 'Tango': '113',
512 'Samba': '114',
513 'Folklore': '115',
514 'Ballad': '116',
515 'Power Ballad': '117',
516 'Rhythmic Soul': '118',
517 'Freestyle': '119',
518 'Duet': '120',
519 'Punk Rock': '121',
520 'Drum Solo': '122',
521 'A Capella': '123',
522 'Euro-House': '124',
523 'Dance Hall': '125',
524 'Goa': '126',
525 'Drum & Bass': '127',
526 'Club-House': '128',
527 'Hardcore': '129',
528 'Terror': '130',
529 'Indie': '131',
530 'BritPop': '132',
531 'Negerpunk': '133',
532 'Polsk Punk': '134',
533 'Beat': '135',
534 'Christian Gangsta Rap': '136',
535 'Heavy Metal': '137',
536 'Black Metal': '138',
537 'Crossover': '139',
538 'Contemporary Christian': '140',
539 'Christian Rock': '141',
540 'Merengue': '142',
541 'Salsa': '143',
542 'Thrash Metal': '144',
543 'Anime': '145',
544 'Jpop': '146',
545 'Synthpop': '147'}
546
547 genrenumbers = {}
548 for key, value in genres.items():
549 genrenumbers[value] = key
550
551 #END