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
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]
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)):
48 total
= input.get('total'+label
+'s', 0)
49 if isinstance(total
, (list, tuple)):
58 index
= index
+ '/' + str(total
)
61 ret
.append((outlabel
, index
))
64 def id3tiplin(i
, o
, k
, v
):
66 'arranger':'arranger',
67 'engineer':'engineer',
68 'producer':'producer',
72 for key
, value
in v
.people
:
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
):
85 'arranger':'arranger',
86 'engineer':'engineer',
87 'producer':'producer',
94 t
= o
.setdefault('TIPL', [])
95 t
.extend(zip([k
]*len(v
), v
))
97 def id3rva2in(i
, o
, k
, v
):
101 if 'replaygain_track_gain' in o
:
105 elif v
.desc
.lower() == 'track':
107 elif v
.desc
.lower() == '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
):
118 gain
= float(re
.search('[+-]?[0-9]*(\.[0-9]*)?([^0-9]|$)', i
.get('_'.join(('replaygain', target
, 'gain')), '0.0')).group(0))
122 peak
= float(re
.search('[+-]?[0-9]*(\.[0-9]*)?([^0-9]|$)', i
.get('_'.join(('replaygain', target
, 'peak')), '0.0')).group(0))
126 o
[':'.join(('RVA2', target
))] = id3
.RVA2(desc
=target
, peak
=peak
, gain
=gain
, channel
=1)
135 def best_encoding(txt
):
139 r
.append((len(txt
.encode(id3_encodings
[n
])), n
))
145 def id3itemout(k
, v
):
146 if isinstance(v
, id3
.Frame
):
149 if fid
.startswith('T'):
150 if isinstance(v
, basestring
):
153 enc
= best_encoding(u
'\0'.join(reduce(lambda x
, y
: x
+y
, v
)))
155 enc
= best_encoding(u
'\0'.join(v
))
157 return k
, id3
.TXXX(encoding
=enc
, desc
=k
.split(':', 1)[1], text
=v
)
159 return k
, getattr(id3
, fid
)(encoding
=enc
, text
=v
)
161 if isinstance(v
, (list, tuple)):
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)):
168 return k
, id3
.UFID(owner
='http://musicbrainz.org', data
=v
)
203 'musicbrainz_trackid',
204 'musicbrainz_albumid',
205 'musicbrainz_artistid',
206 'musicbrainz_albumartistid',
208 'musicbrainz_discid',
210 'replaygain_album_gain',
211 'replaygain_album_peak',
212 'replaygain_track_gain',
213 'replaygain_track_peak',
218 'keytrans': lambda k
: k
.lower(),
219 'valuetrans': lambda v
: list(v
),
221 'album artist':'albumartist',
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'),
231 'keytrans': lambda k
: {
232 'mixartist':'MixArtist',
234 'discsubtitle':'DiscSubtitle',
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',
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
,
261 'albumartist':'album artist',
262 'releasestatus':'MUSICBRAINZ_ALBUMSTATUS',
263 'releasetype':'MUSICBRAINZ_ALBUMTYPE',
265 'remixer':'mixartist',
266 'tracknumber': lambda i
, o
, k
, v
: joinnumber(i
, 'track'),
267 'discnumber': lambda i
, o
, k
, v
: joinnumber(i
, 'disc'),
308 'musicbrainz_trackid',
309 'musicbrainz_albumid',
310 'musicbrainz_artistid',
311 'musicbrainz_albumartistid',
313 'musicbrainz_discid',
315 'replaygain_album_gain',
316 'replaygain_album_peak',
317 'replaygain_track_gain',
318 'replaygain_track_peak',
323 'keytrans': lambda k
: k
.lower(),
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'),
332 'keytrans': lambda k
: k
.upper(),
333 'valuetrans': lambda v
: isinstance(v
, basestring
) and [v
] or v
,
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'),
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
],
350 'TPE2':'albumartist',
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',
371 'TXXX:CATALOGNUMBER':'catalognumber',
372 'TXXX:BARCODE':'barcode',
375 'TXXX:ALBUMARTISTSORT':'albumartistsort',
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',
393 'itemtrans':id3itemout
,
398 'albumartist':'TPE2',
404 'arranger':id3tiplout
,
405 'engineer':id3tiplout
,
406 'producer':id3tiplout
,
407 'djmixer':id3tiplout
,
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',
423 'catalognumber':'TXXX:CATALOGNUMBER',
424 'barcode':'TXXX:BARCODE',
427 'albumartistsort':'TXXX:ALBUMARTISTSORT',
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
,
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 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
))
544 target
.tags
.update(itemtrans(k
, v
) for k
, v
in newmeta
.items())
547 target
.tags
.update(newmeta
)
549 __all__
= ['NormMetaData']