Tasks rework mostly done, replaygain now uses tasks. Start of style/docs cleanup.
[audiomangler.git] / audiomangler / tag.py
blob798a24f374dc265754c195432348f8fface75d0b
1 # -*- coding: utf-8 -*-
2 ###########################################################################
3 # Copyright (C) 2008 by Andrew Mahone
4 # <andrew.mahone@gmail.com>
6 # Copyright: See COPYING file that comes with this distribution
8 ###########################################################################
9 from mutagen.id3 import ID3, USLT
10 from mutagen._vorbis import VCommentDict
11 from mutagen.apev2 import APEv2
12 from mutagen import id3
13 import re
14 from operator import and_
15 from audiomangler.expression import evaluate
17 def splitnumber(num, label):
18 if isinstance(num, (list, tuple)):
19 num = num[0]
20 if not num:
21 num = ''
22 num = re.search(r'^(\d*)(?:/(\d*))?', num)
23 num = num and num.groups() or [0, 0]
24 try:
25 index = int(num[0])
26 except:
27 index = 0
28 try:
29 total = int(num[1])
30 except:
31 total = 0
32 ret = []
33 if index:
34 ret.append((label+'number', index))
35 if total:
36 ret.append(('total'+label+'s', total))
37 return ret
39 def joinnumber(input, label, outlabel = None):
40 try:
41 index = input.get(label+'number', 0)
42 if isinstance(index, (list, tuple)):
43 index = index[0]
44 index = int(index)
45 except ValueError:
46 index = 0
47 try:
48 total = input.get('total'+label+'s', 0)
49 if isinstance(total, (list, tuple)):
50 total = total[0]
51 total = int(total)
52 except ValueError:
53 total = 0
54 ret = []
55 if index:
56 index = str(index)
57 if total:
58 index = index + '/' + str(total)
59 if outlabel is None:
60 outlabel = label
61 ret.append((outlabel, index))
62 return ret
64 def id3tiplin(i, o, k, v):
65 pmap = {
66 'arranger':'arranger',
67 'engineer':'engineer',
68 'producer':'producer',
69 'DJ-mix':'djmixer',
70 'mix':'mixer'
72 for key, value in v.people:
73 if key in pmap:
74 o.setdefault(pmap[key], []).append(value)
76 def id3usltin(i, o, k, v):
77 text = u'\n'.join(v.text.splitlines())
78 return [('lyrics', [text])]
80 def id3ufidin(i, o, k, v):
81 return (('musicbrainz_trackid', [v.data]), )
83 def id3tiplout(i, o, k, v):
84 pmap = {
85 'arranger':'arranger',
86 'engineer':'engineer',
87 'producer':'producer',
88 'djmixer':'DJ-mix',
89 'mixer':'mix'
91 if k not in pmap:
92 return
93 k = pmap[k]
94 t = o.setdefault('TIPL', [])
95 t.extend(zip([k]*len(v), v))
97 def id3rva2in(i, o, k, v):
98 if v.channel != 1:
99 return
100 if not v.desc:
101 if 'replaygain_track_gain' in o:
102 return
103 else:
104 target = 'track'
105 elif v.desc.lower() == 'track':
106 target = 'track'
107 elif v.desc.lower() == 'album':
108 target = 'album'
109 o['_'.join(('replaygain', target, 'gain'))] = "%.3f dB" % v.gain
110 o['_'.join(('replaygain', target, 'peak'))] = "%.8f" % v.peak
112 def id3rva2out(i, o, k, v):
113 if 'track' in k:
114 target = 'track'
115 elif 'album' in k:
116 target = 'album'
117 try:
118 gain = float(re.search('[+-]?[0-9]*(\.[0-9]*)?([^0-9]|$)', i.get('_'.join(('replaygain', target, 'gain')), '0.0')).group(0))
119 except Exception:
120 gain = 0.0
121 try:
122 peak = float(re.search('[+-]?[0-9]*(\.[0-9]*)?([^0-9]|$)', i.get('_'.join(('replaygain', target, 'peak')), '0.0')).group(0))
123 peak = abs(peak)
124 except Exception:
125 peak = 0.0
126 o[':'.join(('RVA2', target))] = id3.RVA2(desc=target, peak=peak, gain=gain, channel=1)
128 id3_encodings=(
129 'iso-8859-1',
130 'utf-16',
131 'utf-16be',
132 'utf-8'
135 def best_encoding(txt):
136 r = []
137 for n in range(4):
138 try:
139 r.append((len(txt.encode(id3_encodings[n])), n))
140 except UnicodeError:
141 pass
142 r.sort()
143 return r[0][1]
145 def id3itemout(k, v):
146 if isinstance(v, id3.Frame):
147 return k, v
148 fid = k[:4]
149 if fid.startswith('T'):
150 if isinstance(v, basestring):
151 v = [v]
152 if fid == 'TIPL':
153 enc = best_encoding(u'\0'.join(reduce(lambda x, y: x+y, v)))
154 else:
155 enc = best_encoding(u'\0'.join(v))
156 if fid == 'TXXX':
157 return k, id3.TXXX(encoding=enc, desc=k.split(':', 1)[1], text=v)
158 else:
159 return k, getattr(id3, fid)(encoding=enc, text=v)
160 elif fid == 'USLT':
161 if isinstance(v, (list, tuple)):
162 v = v[0]
163 enc = best_encoding(v)
164 return "USLT::'und'", id3.USLT(encoding=enc, text=v, lang='und')
165 elif k == 'UFID:http://musicbrainz.org':
166 if isinstance(v, (list, tuple)):
167 v = v[0]
168 return k, id3.UFID(owner='http://musicbrainz.org', data=v)
169 tagmap = {
170 APEv2:{
171 'keysasis':(
172 'album',
173 'title',
174 'artist',
175 'composer',
176 'lyricist',
177 'conductor',
178 'arranger',
179 'engineer',
180 'producer',
181 'djmixer',
182 'mixer',
183 'grouping',
184 'subtitle',
185 'discsubtitle',
186 'compilation',
187 'comment',
188 'genre',
189 'bpm',
190 'mood',
191 'isrc',
192 'copyright',
193 'lyrics',
194 'media',
195 'label',
196 'catalognumber',
197 'barcode',
198 'encodedby',
199 'albumsort',
200 'albumartistsort',
201 'artistsort',
202 'titlesort',
203 'musicbrainz_trackid',
204 'musicbrainz_albumid',
205 'musicbrainz_artistid',
206 'musicbrainz_albumartistid',
207 'musicbrainz_trmid',
208 'musicbrainz_discid',
209 'musicip_puid',
210 'replaygain_album_gain',
211 'replaygain_album_peak',
212 'replaygain_track_gain',
213 'replaygain_track_peak',
214 'releasecountry',
215 'asin',
217 'in':{
218 'keytrans': lambda k: k.lower(),
219 'valuetrans': lambda v: list(v),
220 'keymap': {
221 'album artist':'albumartist',
222 'year':'date',
223 'mixartist':'remixer',
224 'musicbrainz_albumstatus':'releasestatus',
225 'musicbrainz_albumtype':'releasetype',
226 'track': lambda i, o, k, v: splitnumber(v.value, 'track'),
227 'disc': lambda i, o, k, v: splitnumber(v.value, 'disc'),
230 'out':{
231 'keytrans': lambda k: {
232 'mixartist':'MixArtist',
233 'djmixer':'DJMixer',
234 'discsubtitle':'DiscSubtitle',
235 'bpm':'BPM',
236 'isrc':'ISRC',
237 'catalognumber':'CatalogNumber',
238 'encodedby':'EncodedBy',
239 'albumsort':'ALBUMSORT',
240 'albumartistsort':'ALBUMARTISTSORT',
241 'artistsort':'ARTISTSORT',
242 'titlesort':'TITLESORT',
243 'musicbrainz_trackid':'MUSICBRAINZ_TRACKID',
244 'musicbrainz_albumid':'MUSICBRAINZ_ALBUMID',
245 'musicbrainz_artistid':'MUSICBRAINZ_ARTISTID',
246 'musicbrainz_albumartistid':'MUSICBRAINZ_ALBUMARTISTID',
247 'musicbrainz_trmid':'MUSICBRAINZ_TRMID',
248 'musicbrainz_discid':'MUSICBRAINZ_DISCID',
249 'musicip_puid':'MUSICIP_PUID',
250 'releasecountry':'RELEASECOUNTRY',
251 'asin':'ASIN',
252 'MUSICBRAINZ_ALBUMSTATUS':'MUSICBRAINZ_ALBUMSTATUS',
253 'MUSICBRAINZ_ALBUMTYPE':'MUSICBRAINZ_ALBUMTYPE',
254 'replaygain_album_gain':'replaygain_album_gain',
255 'replaygain_album_peak':'replaygain_album_peak',
256 'replaygain_track_gain':'replaygain_track_gain',
257 'replaygain_track_peak':'replaygain_track_peak',
258 }.get(k, None) or k.title(),
259 'valuetrans': lambda v: isinstance(v, (tuple, list)) and u'\0'.join(v) or v,
260 'keymap': {
261 'albumartist':'album artist',
262 'releasestatus':'MUSICBRAINZ_ALBUMSTATUS',
263 'releasetype':'MUSICBRAINZ_ALBUMTYPE',
264 'date':'Year',
265 'remixer':'mixartist',
266 'tracknumber': lambda i, o, k, v: joinnumber(i, 'track'),
267 'discnumber': lambda i, o, k, v: joinnumber(i, 'disc'),
272 VCommentDict:{
273 'keysasis':(
274 'album',
275 'title',
276 'artist',
277 'albumartist',
278 'date',
279 'composer',
280 'lyricist',
281 'conductor',
282 'remixer',
283 'arranger',
284 'engineer',
285 'producer',
286 'djmixer',
287 'mixer',
288 'grouping',
289 'subtitle',
290 'discsubtitle',
291 'compilation',
292 'comment',
293 'genre',
294 'bpm',
295 'mood',
296 'isrc',
297 'copyright',
298 'lyrics',
299 'media',
300 'label',
301 'catalognumber',
302 'barcode',
303 'encodedby',
304 'albumsort',
305 'albumartistsort',
306 'artistsort',
307 'titlesort',
308 'musicbrainz_trackid',
309 'musicbrainz_albumid',
310 'musicbrainz_artistid',
311 'musicbrainz_albumartistid',
312 'musicbrainz_trmid',
313 'musicbrainz_discid',
314 'musicip_puid',
315 'replaygain_album_gain',
316 'replaygain_album_peak',
317 'replaygain_track_gain',
318 'replaygain_track_peak',
319 'releasecountry',
320 'asin',
322 'in':{
323 'keytrans': lambda k: k.lower(),
324 'keymap': {
325 'musicbrainz_albumstatus':'releasestatus',
326 'musicbrainz_albumtype':'releasetype',
327 'tracknumber': lambda i, o, k, v: splitnumber(v, 'track'),
328 'discnumber': lambda i, o, k, v: splitnumber(v, 'disc'),
331 'out':{
332 'keytrans': lambda k: k.upper(),
333 'valuetrans': lambda v: isinstance(v, basestring) and [v] or v,
334 'keymap': {
335 'releasestatus':'MUSICBRAINZ_ALBUMSTATUS',
336 'releasetype':'MUSICBRAINZ_ALBUMTYPE',
337 'tracknumber': lambda i, o, k, v: joinnumber(i, 'track', 'tracknumber'),
338 'discnumber': lambda i, o, k, v: joinnumber(i, 'disc', 'discnumber'),
342 ID3:{
343 'in':{
344 'keytrans': lambda k: (k.startswith('TXXX') or k.startswith('UFID')) and k or k.split(':', 1)[0],
345 'valuetrans': lambda v: [unicode(i) for i in v.text],
346 'keymap': {
347 'TALB':'album',
348 'TIT2':'title',
349 'TPE1':'artist',
350 'TPE2':'albumartist',
351 'TDRC':'date',
352 'TCOM':'composer',
353 'TEXT':'lyricist',
354 'TPE3':'conductor',
355 'TPE4':'remixer',
356 'TIPL':id3tiplin,
357 'TIT1':'grouping',
358 'TIT3':'subtitle',
359 'TSST':'discsubtitle',
360 'TRCK': lambda i, o, k, v: splitnumber(v.text, 'track'),
361 'TPOS': lambda i, o, k, v: splitnumber(v.text, 'disc'),
362 'TCMP':'compilation',
363 'TCON':'genre',
364 'TBPM':'bpm',
365 'TMOO':'mood',
366 'TSRC':'isrc',
367 'TCOP':'copyright',
368 'USLT':id3usltin,
369 'TMED':'media',
370 'TPUB':'label',
371 'TXXX:CATALOGNUMBER':'catalognumber',
372 'TXXX:BARCODE':'barcode',
373 'TENC':'encodedby',
374 'TSOA':'albumsort',
375 'TXXX:ALBUMARTISTSORT':'albumartistsort',
376 'TSOP':'artistsort',
377 'TSOT':'titlesort',
378 'UFID:http://musicbrainz.org':id3ufidin,
379 'TXXX:MusicBrainz Album Id':'musicbrainz_albumid',
380 'TXXX:MusicBrainz Artist Id':'musicbrainz_artistid',
381 'TXXX:MusicBrainz Album Artist Id':'musicbrainz_albumartistid',
382 'TXXX:MusicBrainz TRM Id':'musicbrainz_trmid',
383 'TXXX:MusicBrainz Disc Id':'musicbrainz_discid',
384 'TXXX:MusicIP PUID':'musicip_puid',
385 'TXXX:MusicBrainz Album Status':'releasestatus',
386 'TXXX:MusicBrainz Album Type':'releasetype',
387 'TXXX:MusicBrainz Album Release Country':'releasecountry',
388 'TXXX:ASIN':'asin',
389 'RVA2': id3rva2in
392 'out':{
393 'itemtrans':id3itemout,
394 'keymap': {
395 'album':'TALB',
396 'title':'TIT2',
397 'artist':'TPE1',
398 'albumartist':'TPE2',
399 'date':'TDRC',
400 'composer':'TCOM',
401 'lyricist':'TEXT',
402 'conductor':'TPE3',
403 'remixer':'TPE4',
404 'arranger':id3tiplout,
405 'engineer':id3tiplout,
406 'producer':id3tiplout,
407 'djmixer':id3tiplout,
408 'mixer':id3tiplout,
409 'grouping':'TIT1',
410 'subtitle':'TIT3',
411 'discsubtitle':'TSST',
412 'tracknumber': lambda i, o, k, v: joinnumber(i, 'track', 'TRCK'),
413 'discnumber': lambda i, o, k, v: joinnumber(i, 'disc', 'TPOS'),
414 'compilation':'TCMP',
415 'genre':'TCON',
416 'bmp':'TBPM',
417 'mood':'TMOO',
418 'isrc':'TSRC',
419 'copyright':'TCOP',
420 'lyrics':'USLT',
421 'media':'TMED',
422 'label':'TPUB',
423 'catalognumber':'TXXX:CATALOGNUMBER',
424 'barcode':'TXXX:BARCODE',
425 'encodedby':'TENC',
426 'albumsort':'TSOA',
427 'albumartistsort':'TXXX:ALBUMARTISTSORT',
428 'artistsort':'TSOP',
429 'titlesort':'TSOT',
430 'musicbrainz_trackid':'UFID:http://musicbrainz.org',
431 'musicbrainz_albumid':'TXXX:MusicBrainz Album Id',
432 'musicbrainz_artistid':'TXXX:MusicBrainz Artist Id',
433 'musicbrainz_albumartistid':'TXXX:MusicBrainz Album Artist Id',
434 'musicbrainz_trmid':'TXXX:MusicBrainz TRM Id',
435 'musicbrainz_discid':'TXXX:MusicBrainz Disc Id',
436 'musicip_puid':'TXXX:MusicIP PUID',
437 'releasestatus':'TXXX:MusicBrainz Album Status',
438 'releasetype':'TXXX:MusicBrainz Album Type',
439 'releasecountry':'TXXX:MusicBrainz Album Release Country',
440 'replaygain_track_gain':id3rva2out,
441 'replaygain_album_gain':id3rva2out,
442 'asin':'TXXX:ASIN',
449 for value in tagmap.values():
450 if 'keysasis' in value:
451 value['keysasis'] = frozenset(value['keysasis'])
453 class NormMetaData(dict):
455 def copy(self):
456 return self.__class__(self)
458 @classmethod
459 def tagmapfor(cls, meta):
460 global tagmap
461 for c in (type(meta), ) + type(meta).__bases__:
462 if c in tagmap:
463 return tagmap[c]
464 raise TypeError("No mapping specified for tag type %s"%type(meta))
466 @classmethod
467 def converted(cls, meta):
468 if hasattr(meta, 'tags'):
469 meta = meta.tags
470 if isinstance(meta, cls):
471 return cls(meta)
472 else:
473 tagmap = cls.tagmapfor(meta)
474 newmeta = {}
475 for key, value in meta.items():
476 if 'keytrans' in tagmap['in']:
477 key = tagmap['in']['keytrans'](key)
478 if 'keysasis' in tagmap and key in tagmap['keysasis']:
479 if 'valuetrans' in tagmap['in']:
480 value = tagmap['in']['valuetrans'](value)
481 newmeta[key] = value
482 elif 'keymap' in tagmap['in'] and key in tagmap['in']['keymap']:
483 keymap = tagmap['in']['keymap'][key]
484 if isinstance(keymap, basestring):
485 if 'valuetrans' in tagmap['in']:
486 value = tagmap['in']['valuetrans'](value)
487 newmeta[keymap] = value
488 else:
489 l = keymap(meta, newmeta, key, value)
490 if l is not None:
491 newmeta.update(l)
492 for k in newmeta.keys():
493 if isinstance(newmeta[k], (list, tuple)):
494 newmeta[k] = [i for i in newmeta[k] if i]
495 if not newmeta[k]:
496 del newmeta[k]
497 return cls(newmeta)
499 def flat(self, newmeta = None):
500 if newmeta is None:
501 newmeta = self.__class__()
502 #we assume here that all items are numeric, a string, a list of
503 #strings, or a list of associations.
504 for key, value in self.iteritems():
505 if isinstance(value, (list, tuple)):
506 if not reduce(and_, (isinstance(i, basestring) for i in value)):
507 value = (': '.join(i) for i in value)
508 value = u'; '.join(value)
509 newmeta[key] = value
510 #make sure the numeric members *always* have numeric values
511 for k in ('tracknumber', 'totaltracks', 'discnumber', 'totaldiscs'):
512 newmeta.setdefault(k, 0)
513 return newmeta
515 def evaluate(self, expr, d = None):
516 return evaluate(expr, self.flat(d))
518 def apply(self, target, clear=False):
519 if target.tags is None:
520 target.add_tags()
521 if clear:
522 target.tags.clear
523 tagmap = self.tagmapfor(target.tags)
524 newmeta = {}
525 for key, value in self.items():
526 if 'keysasis' in tagmap and key in tagmap['keysasis']:
527 newmeta[key] = value
528 elif 'keymap' in tagmap['out'] and key in tagmap['out']['keymap']:
529 keymap = tagmap['out']['keymap'][key]
530 if isinstance(keymap, basestring):
531 newmeta[keymap] = value
532 else:
533 l = keymap(self, newmeta, key, value)
534 if l is not None:
535 newmeta.update(l)
536 itemtrans = None
537 if 'itemtrans' in tagmap['out']:
538 itemtrans = tagmap['out']['itemtrans']
539 elif 'keytrans' in tagmap['out'] or 'valuetrans' in tagmap['out']:
540 keytrans = 'keytrans' in tagmap['out'] and tagmap['out']['keytrans'] or (lambda x: x)
541 valuetrans = 'valuetrans' in tagmap['out'] and tagmap['out']['valuetrans'] or (lambda x: x)
542 itemtrans = lambda k, v: (keytrans(k), valuetrans(v))
543 if itemtrans:
544 target.tags.update(itemtrans(k, v) for k, v in newmeta.items())
545 else:
546 print newmeta
547 target.tags.update(newmeta)
549 __all__ = ['NormMetaData']