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