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