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