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
14 from operator
import and_
15 from audiomangler
.expression
import evaluate
17 def splitnumber(num
, label
):
18 if isinstance(num
, (list, tuple)):
22 num
= re
.search(r
'^(\d*)(?:/(\d*))?', num
)
23 num
= num
and num
.groups() or [0, 0]
26 except (ValueError, TypeError):
30 except (ValueError, TypeError):
34 ret
.append((label
+'number', index
))
36 ret
.append(('total'+label
+'s', total
))
39 def joinnumber(input_
, label
, outlabel
= None):
41 index
= input_
.get(label
+'number', 0)
42 if isinstance(index
, (list, tuple)):
45 except (ValueError, TypeError):
48 total
= input_
.get('total'+label
+'s', 0)
49 if isinstance(total
, (list, tuple)):
52 except (ValueError, TypeError):
58 index
= index
+ '/' + str(total
)
61 ret
.append((outlabel
, index
))
64 def id3tiplin(i__
, out_dict
, k__
, in_val
):
66 'arranger':'arranger',
67 'engineer':'engineer',
68 'producer':'producer',
72 for key
, val
in in_val
.people
:
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
):
85 'arranger':'arranger',
86 'engineer':'engineer',
87 'producer':'producer',
94 tag
= out_dict
.setdefault('TIPL', [])
95 tag
.extend(zip([key
]*len(val
), val
))
97 def id3rva2in(i__
, out_dict
, k__
, val
):
101 if 'replaygain_track_gain' in out_dict
:
105 elif val
.desc
.lower() == 'track':
107 elif val
.desc
.lower() == '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__
):
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):
122 peak
= float(re
.search('[+-]?[0-9]*(\.[0-9]*)?([^0-9]|$)', in_dict
.get('_'.join(('replaygain', target
, 'peak')), '0.0')).group(0))
124 except (AttributeError, TypeError, ValueError):
126 out_dict
[':'.join(('RVA2', target
))] = id3
.RVA2(desc
=target
, peak
=peak
, gain
=gain
, channel
=1)
128 def best_encoding(txt
):
138 results
.append((len(txt
.encode(id3_encodings
[enc
])), enc
))
144 def id3itemout(key
, val
):
145 if isinstance(val
, id3
.Frame
):
148 if fid
.startswith('T'):
149 if isinstance(val
, basestring
):
152 enc
= best_encoding(u
'\0'.join(reduce(lambda x
, y
: x
+y
, val
)))
154 enc
= best_encoding(u
'\0'.join(val
))
156 return key
, id3
.TXXX(encoding
=enc
, desc
=key
.split(':', 1)[1], text
=val
)
158 return key
, getattr(id3
, fid
)(encoding
=enc
, text
=val
)
160 if isinstance(val
, (list, tuple)):
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)):
167 return key
, id3
.UFID(owner
='http://musicbrainz.org', data
=val
)
202 'musicbrainz_trackid',
203 'musicbrainz_albumid',
204 'musicbrainz_artistid',
205 'musicbrainz_albumartistid',
207 'musicbrainz_discid',
209 'replaygain_album_gain',
210 'replaygain_album_peak',
211 'replaygain_track_gain',
212 'replaygain_track_peak',
217 'keytrans': lambda k
: k
.lower(),
218 'valuetrans': lambda v
: list(v
),
220 'album artist':'albumartist',
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'),
230 'keytrans': lambda k
: {
231 'mixartist':'MixArtist',
233 'discsubtitle':'DiscSubtitle',
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',
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
,
260 'albumartist':'album artist',
261 'releasestatus':'MUSICBRAINZ_ALBUMSTATUS',
262 'releasetype':'MUSICBRAINZ_ALBUMTYPE',
264 'remixer':'mixartist',
265 'tracknumber': lambda i
, o
, k
, v
: joinnumber(i
, 'track'),
266 'discnumber': lambda i
, o
, k
, v
: joinnumber(i
, 'disc'),
307 'musicbrainz_trackid',
308 'musicbrainz_albumid',
309 'musicbrainz_artistid',
310 'musicbrainz_albumartistid',
312 'musicbrainz_discid',
314 'replaygain_album_gain',
315 'replaygain_album_peak',
316 'replaygain_track_gain',
317 'replaygain_track_peak',
322 'keytrans': lambda k
: k
.lower(),
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'),
331 'keytrans': lambda k
: k
.upper(),
332 'valuetrans': lambda v
: isinstance(v
, basestring
) and [v
] or v
,
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'),
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
],
349 'TPE2':'albumartist',
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',
370 'TXXX:CATALOGNUMBER':'catalognumber',
371 'TXXX:BARCODE':'barcode',
374 'TXXX:ALBUMARTISTSORT':'albumartistsort',
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',
392 'itemtrans':id3itemout
,
397 'albumartist':'TPE2',
403 'arranger':id3tiplout
,
404 'engineer':id3tiplout
,
405 'producer':id3tiplout
,
406 'djmixer':id3tiplout
,
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',
422 'catalognumber':'TXXX:CATALOGNUMBER',
423 'barcode':'TXXX:BARCODE',
426 'albumartistsort':'TXXX:ALBUMARTISTSORT',
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
,
449 for value
in TAGMAP
.values():
450 if 'keysasis' in value
:
451 value
['keysasis'] = frozenset(value
['keysasis'])
453 class NormMetaData(dict):
456 return self
.__class
__(self
)
459 def tagmapfor(cls
, meta
):
461 for c
in (type(meta
), ) + type(meta
).__bases
__:
464 raise TypeError("No mapping specified for tag type %s"%type(meta
))
467 def converted(cls
, meta
):
468 if hasattr(meta
, 'tags'):
470 if isinstance(meta
, cls
):
473 tagmap
= cls
.tagmapfor(meta
)
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
)
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
489 l
= keymap(meta
, newmeta
, key
, value
)
492 for k
in newmeta
.keys():
493 if isinstance(newmeta
[k
], (list, tuple)):
494 newmeta
[k
] = [i
for i
in newmeta
[k
] if i
]
499 def flat(self
, newmeta
= 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
)
510 #make sure the numeric members *always* have numeric values
511 for k
in ('tracknumber', 'totaltracks', 'discnumber', 'totaldiscs'):
512 newmeta
.setdefault(k
, 0)
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:
523 tagmap
= self
.tagmapfor(target
.tags
)
525 for key
, value
in self
.items():
526 if 'keysasis' in tagmap
and key
in tagmap
['keysasis']:
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
533 l
= keymap(self
, newmeta
, key
, value
)
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
))
546 target
.tags
.update(itemtrans(k
, v
) for k
, v
in newmeta
.items())
549 target
.tags
.update(newmeta
)
551 __all__
= ['NormMetaData']