Cleanups, fixes, use decorator lib for argspec-preserving decorators.
[audiomangler.git] / audiomangler / tag.py
blob61ed9677b38e80cefd420e5fbf3247e8b80738a4
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
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 (ValueError, TypeError):
27 index = 0
28 try:
29 total = int(num[1])
30 except (ValueError, TypeError):
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, TypeError):
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, TypeError):
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__, out_dict, k__, in_val):
65 pmap = {
66 'arranger':'arranger',
67 'engineer':'engineer',
68 'producer':'producer',
69 'DJ-mix':'djmixer',
70 'mix':'mixer'
72 for key, val in in_val.people:
73 if key in pmap:
74 out_dict.setdefault(pmap[key], []).append(val)
76 def id3usltin(i__, o__, k__, val):
77 text = u'\n'.join(val.text.splitlines())
78 return [('lyrics', [text])]
80 def id3ufidin(i__, o__, k__, val):
81 return (('musicbrainz_trackid', [val.data]), )
83 def id3tiplout(i__, out_dict, key, val):
84 pmap = {
85 'arranger':'arranger',
86 'engineer':'engineer',
87 'producer':'producer',
88 'djmixer':'DJ-mix',
89 'mixer':'mix'
91 if key not in pmap:
92 return
93 key = pmap[key]
94 tag = out_dict.setdefault('TIPL', [])
95 tag.extend(zip([key]*len(val), val))
97 def id3rva2in(i__, out_dict, k__, val):
98 if val.channel != 1:
99 return
100 if not val.desc:
101 if 'replaygain_track_gain' in out_dict:
102 return
103 else:
104 target = 'track'
105 elif val.desc.lower() == 'track':
106 target = 'track'
107 elif val.desc.lower() == 'album':
108 target = 'album'
109 out_dict['_'.join(('replaygain', target, 'gain'))] = "%.3f dB" % val.gain
110 out_dict['_'.join(('replaygain', target, 'peak'))] = "%.8f" % val.peak
112 def id3rva2out(in_dict, out_dict, key, v__):
113 if 'track' in key:
114 target = 'track'
115 elif 'album' in key:
116 target = 'album'
117 try:
118 gain = float(re.search('[+-]?[0-9]*(\.[0-9]*)?([^0-9]|$)', in_dict.get('_'.join(('replaygain', target, 'gain')), '0.0')).group(0))
119 except (AttributeError, TypeError, ValueError):
120 gain = 0.0
121 try:
122 peak = float(re.search('[+-]?[0-9]*(\.[0-9]*)?([^0-9]|$)', in_dict.get('_'.join(('replaygain', target, 'peak')), '0.0')).group(0))
123 peak = abs(peak)
124 except (AttributeError, TypeError, ValueError):
125 peak = 0.0
126 out_dict[':'.join(('RVA2', target))] = id3.RVA2(desc=target, peak=peak, gain=gain, channel=1)
128 def best_encoding(txt):
129 id3_encodings = (
130 'iso-8859-1',
131 'utf-16',
132 'utf-16be',
133 'utf-8'
135 results = []
136 for enc in range(4):
137 try:
138 results.append((len(txt.encode(id3_encodings[enc])), enc))
139 except UnicodeError:
140 pass
141 results.sort()
142 return results[0][1]
144 def id3itemout(key, val):
145 if isinstance(val, id3.Frame):
146 return key, val
147 fid = key[:4]
148 if fid.startswith('T'):
149 if isinstance(val, basestring):
150 val = [val]
151 if fid == 'TIPL':
152 enc = best_encoding(u'\0'.join(reduce(lambda x, y: x+y, val)))
153 else:
154 enc = best_encoding(u'\0'.join(val))
155 if fid == 'TXXX':
156 return key, id3.TXXX(encoding=enc, desc=key.split(':', 1)[1], text=val)
157 else:
158 return key, getattr(id3, fid)(encoding=enc, text=val)
159 elif fid == 'USLT':
160 if isinstance(val, (list, tuple)):
161 val = val[0]
162 enc = best_encoding(val)
163 return "USLT::'und'", id3.USLT(encoding=enc, text=val, lang='und')
164 elif key == 'UFID:http://musicbrainz.org':
165 if isinstance(val, (list, tuple)):
166 val = val[0]
167 return key, id3.UFID(owner='http://musicbrainz.org', data=val)
168 TAGMAP = {
169 APEv2:{
170 'keysasis':(
171 'album',
172 'title',
173 'artist',
174 'composer',
175 'lyricist',
176 'conductor',
177 'arranger',
178 'engineer',
179 'producer',
180 'djmixer',
181 'mixer',
182 'grouping',
183 'subtitle',
184 'discsubtitle',
185 'compilation',
186 'comment',
187 'genre',
188 'bpm',
189 'mood',
190 'isrc',
191 'copyright',
192 'lyrics',
193 'media',
194 'label',
195 'catalognumber',
196 'barcode',
197 'encodedby',
198 'albumsort',
199 'albumartistsort',
200 'artistsort',
201 'titlesort',
202 'musicbrainz_trackid',
203 'musicbrainz_albumid',
204 'musicbrainz_artistid',
205 'musicbrainz_albumartistid',
206 'musicbrainz_trmid',
207 'musicbrainz_discid',
208 'musicip_puid',
209 'replaygain_album_gain',
210 'replaygain_album_peak',
211 'replaygain_track_gain',
212 'replaygain_track_peak',
213 'releasecountry',
214 'asin',
216 'in':{
217 'keytrans': lambda k: k.lower(),
218 'valuetrans': lambda v: list(v),
219 'keymap': {
220 'album artist':'albumartist',
221 'year':'date',
222 'mixartist':'remixer',
223 'musicbrainz_albumstatus':'releasestatus',
224 'musicbrainz_albumtype':'releasetype',
225 'track': lambda i, o, k, v: splitnumber(v.value, 'track'),
226 'disc': lambda i, o, k, v: splitnumber(v.value, 'disc'),
229 'out':{
230 'keytrans': lambda k: {
231 'mixartist':'MixArtist',
232 'djmixer':'DJMixer',
233 'discsubtitle':'DiscSubtitle',
234 'bpm':'BPM',
235 'isrc':'ISRC',
236 'catalognumber':'CatalogNumber',
237 'encodedby':'EncodedBy',
238 'albumsort':'ALBUMSORT',
239 'albumartistsort':'ALBUMARTISTSORT',
240 'artistsort':'ARTISTSORT',
241 'titlesort':'TITLESORT',
242 'musicbrainz_trackid':'MUSICBRAINZ_TRACKID',
243 'musicbrainz_albumid':'MUSICBRAINZ_ALBUMID',
244 'musicbrainz_artistid':'MUSICBRAINZ_ARTISTID',
245 'musicbrainz_albumartistid':'MUSICBRAINZ_ALBUMARTISTID',
246 'musicbrainz_trmid':'MUSICBRAINZ_TRMID',
247 'musicbrainz_discid':'MUSICBRAINZ_DISCID',
248 'musicip_puid':'MUSICIP_PUID',
249 'releasecountry':'RELEASECOUNTRY',
250 'asin':'ASIN',
251 'MUSICBRAINZ_ALBUMSTATUS':'MUSICBRAINZ_ALBUMSTATUS',
252 'MUSICBRAINZ_ALBUMTYPE':'MUSICBRAINZ_ALBUMTYPE',
253 'replaygain_album_gain':'replaygain_album_gain',
254 'replaygain_album_peak':'replaygain_album_peak',
255 'replaygain_track_gain':'replaygain_track_gain',
256 'replaygain_track_peak':'replaygain_track_peak',
257 }.get(k, None) or k.title(),
258 'valuetrans': lambda v: isinstance(v, (tuple, list)) and u'\0'.join(v) or v,
259 'keymap': {
260 'albumartist':'album artist',
261 'releasestatus':'MUSICBRAINZ_ALBUMSTATUS',
262 'releasetype':'MUSICBRAINZ_ALBUMTYPE',
263 'date':'Year',
264 'remixer':'mixartist',
265 'tracknumber': lambda i, o, k, v: joinnumber(i, 'track'),
266 'discnumber': lambda i, o, k, v: joinnumber(i, 'disc'),
271 VCommentDict:{
272 'keysasis':(
273 'album',
274 'title',
275 'artist',
276 'albumartist',
277 'date',
278 'composer',
279 'lyricist',
280 'conductor',
281 'remixer',
282 'arranger',
283 'engineer',
284 'producer',
285 'djmixer',
286 'mixer',
287 'grouping',
288 'subtitle',
289 'discsubtitle',
290 'compilation',
291 'comment',
292 'genre',
293 'bpm',
294 'mood',
295 'isrc',
296 'copyright',
297 'lyrics',
298 'media',
299 'label',
300 'catalognumber',
301 'barcode',
302 'encodedby',
303 'albumsort',
304 'albumartistsort',
305 'artistsort',
306 'titlesort',
307 'musicbrainz_trackid',
308 'musicbrainz_albumid',
309 'musicbrainz_artistid',
310 'musicbrainz_albumartistid',
311 'musicbrainz_trmid',
312 'musicbrainz_discid',
313 'musicip_puid',
314 'replaygain_album_gain',
315 'replaygain_album_peak',
316 'replaygain_track_gain',
317 'replaygain_track_peak',
318 'releasecountry',
319 'asin',
321 'in':{
322 'keytrans': lambda k: k.lower(),
323 'keymap': {
324 'musicbrainz_albumstatus':'releasestatus',
325 'musicbrainz_albumtype':'releasetype',
326 'tracknumber': lambda i, o, k, v: splitnumber(v, 'track'),
327 'discnumber': lambda i, o, k, v: splitnumber(v, 'disc'),
330 'out':{
331 'keytrans': lambda k: k.upper(),
332 'valuetrans': lambda v: isinstance(v, basestring) and [v] or v,
333 'keymap': {
334 'releasestatus':'MUSICBRAINZ_ALBUMSTATUS',
335 'releasetype':'MUSICBRAINZ_ALBUMTYPE',
336 'tracknumber': lambda i, o, k, v: joinnumber(i, 'track', 'tracknumber'),
337 'discnumber': lambda i, o, k, v: joinnumber(i, 'disc', 'discnumber'),
341 ID3:{
342 'in':{
343 'keytrans': lambda k: (k.startswith('TXXX') or k.startswith('UFID')) and k or k.split(':', 1)[0],
344 'valuetrans': lambda v: [unicode(i) for i in v.text],
345 'keymap': {
346 'TALB':'album',
347 'TIT2':'title',
348 'TPE1':'artist',
349 'TPE2':'albumartist',
350 'TDRC':'date',
351 'TCOM':'composer',
352 'TEXT':'lyricist',
353 'TPE3':'conductor',
354 'TPE4':'remixer',
355 'TIPL':id3tiplin,
356 'TIT1':'grouping',
357 'TIT3':'subtitle',
358 'TSST':'discsubtitle',
359 'TRCK': lambda i, o, k, v: splitnumber(v.text, 'track'),
360 'TPOS': lambda i, o, k, v: splitnumber(v.text, 'disc'),
361 'TCMP':'compilation',
362 'TCON':'genre',
363 'TBPM':'bpm',
364 'TMOO':'mood',
365 'TSRC':'isrc',
366 'TCOP':'copyright',
367 'USLT':id3usltin,
368 'TMED':'media',
369 'TPUB':'label',
370 'TXXX:CATALOGNUMBER':'catalognumber',
371 'TXXX:BARCODE':'barcode',
372 'TENC':'encodedby',
373 'TSOA':'albumsort',
374 'TXXX:ALBUMARTISTSORT':'albumartistsort',
375 'TSOP':'artistsort',
376 'TSOT':'titlesort',
377 'UFID:http://musicbrainz.org':id3ufidin,
378 'TXXX:MusicBrainz Album Id':'musicbrainz_albumid',
379 'TXXX:MusicBrainz Artist Id':'musicbrainz_artistid',
380 'TXXX:MusicBrainz Album Artist Id':'musicbrainz_albumartistid',
381 'TXXX:MusicBrainz TRM Id':'musicbrainz_trmid',
382 'TXXX:MusicBrainz Disc Id':'musicbrainz_discid',
383 'TXXX:MusicIP PUID':'musicip_puid',
384 'TXXX:MusicBrainz Album Status':'releasestatus',
385 'TXXX:MusicBrainz Album Type':'releasetype',
386 'TXXX:MusicBrainz Album Release Country':'releasecountry',
387 'TXXX:ASIN':'asin',
388 'RVA2': id3rva2in
391 'out':{
392 'itemtrans':id3itemout,
393 'keymap': {
394 'album':'TALB',
395 'title':'TIT2',
396 'artist':'TPE1',
397 'albumartist':'TPE2',
398 'date':'TDRC',
399 'composer':'TCOM',
400 'lyricist':'TEXT',
401 'conductor':'TPE3',
402 'remixer':'TPE4',
403 'arranger':id3tiplout,
404 'engineer':id3tiplout,
405 'producer':id3tiplout,
406 'djmixer':id3tiplout,
407 'mixer':id3tiplout,
408 'grouping':'TIT1',
409 'subtitle':'TIT3',
410 'discsubtitle':'TSST',
411 'tracknumber': lambda i, o, k, v: joinnumber(i, 'track', 'TRCK'),
412 'discnumber': lambda i, o, k, v: joinnumber(i, 'disc', 'TPOS'),
413 'compilation':'TCMP',
414 'genre':'TCON',
415 'bmp':'TBPM',
416 'mood':'TMOO',
417 'isrc':'TSRC',
418 'copyright':'TCOP',
419 'lyrics':'USLT',
420 'media':'TMED',
421 'label':'TPUB',
422 'catalognumber':'TXXX:CATALOGNUMBER',
423 'barcode':'TXXX:BARCODE',
424 'encodedby':'TENC',
425 'albumsort':'TSOA',
426 'albumartistsort':'TXXX:ALBUMARTISTSORT',
427 'artistsort':'TSOP',
428 'titlesort':'TSOT',
429 'musicbrainz_trackid':'UFID:http://musicbrainz.org',
430 'musicbrainz_albumid':'TXXX:MusicBrainz Album Id',
431 'musicbrainz_artistid':'TXXX:MusicBrainz Artist Id',
432 'musicbrainz_albumartistid':'TXXX:MusicBrainz Album Artist Id',
433 'musicbrainz_trmid':'TXXX:MusicBrainz TRM Id',
434 'musicbrainz_discid':'TXXX:MusicBrainz Disc Id',
435 'musicip_puid':'TXXX:MusicIP PUID',
436 'releasestatus':'TXXX:MusicBrainz Album Status',
437 'releasetype':'TXXX:MusicBrainz Album Type',
438 'releasecountry':'TXXX:MusicBrainz Album Release Country',
439 'replaygain_track_gain':id3rva2out,
440 'replaygain_album_gain':id3rva2out,
441 '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 tagmap = 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
541 tagmap['out']['keytrans'] or (lambda x: x))
542 valuetrans = ('valuetrans' in tagmap['out'] and
543 tagmap['out']['valuetrans'] or (lambda x: x))
544 itemtrans = lambda k, v: (keytrans(k), valuetrans(v))
545 if itemtrans:
546 target.tags.update(itemtrans(k, v) for k, v in newmeta.items())
547 else:
548 print newmeta
549 target.tags.update(newmeta)
551 __all__ = ['NormMetaData']