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