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 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 %f Track file name
247 %x File extension """
248 if not self._lock.acquire(block) and not block:
249 return False
250
251 esc = {'n': 'TRACKNUMBER',
252 'a': 'ARTIST',
253 't': 'TITLE',
254 'l': 'ALBUM',
255 'y': 'YEAR',
256 'g': 'GENRE_NAME'}
257 #'x': file extension
258 #'f': filename
259 #'z': Used for literal '%'
260
261 out = ""
262 fmt = fmt.replace('%%', '%z')
263 fmt = fmt.split('%')
264 while fmt:
265 out += fmt[0]
266 if len(fmt) <= 1:
267 break
268 fmt = fmt[1:]
269 code = fmt[0] and fmt[0][0]
270 if code == 'z':
271 fmt[0] = '%' + fmt[0][1:]
272 elif code == 'f':
273 fmt[0] = self._file + fmt[0][1:]
274 elif code == 'x':
275 fmt[0] = self._type + 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