Melodia/Melodia/resources/audiotools/__vorbiscomment__.py

295 lines
11 KiB
Python

#!/usr/bin/python
#Audio Tools, a module and set of tools for manipulating audio data
#Copyright (C) 2007-2011 Brian Langenberger
#This program is free software; you can redistribute it and/or modify
#it under the terms of the GNU General Public License as published by
#the Free Software Foundation; either version 2 of the License, or
#(at your option) any later version.
#This program is distributed in the hope that it will be useful,
#but WITHOUT ANY WARRANTY; without even the implied warranty of
#MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
#GNU General Public License for more details.
#You should have received a copy of the GNU General Public License
#along with this program; if not, write to the Free Software
#Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
from audiotools import MetaData, Con, VERSION, re
class VorbisComment(MetaData, dict):
"""A complete Vorbis Comment tag."""
VORBIS_COMMENT = Con.Struct(
"vorbis_comment",
Con.PascalString("vendor_string",
length_field=Con.ULInt32("length")),
Con.PrefixedArray(
length_field=Con.ULInt32("length"),
subcon=Con.PascalString("value",
length_field=Con.ULInt32("length"))),
Con.Const(Con.Byte("framing"), 1))
ATTRIBUTE_MAP = {'track_name': 'TITLE',
'track_number': 'TRACKNUMBER',
'track_total': 'TRACKTOTAL',
'album_name': 'ALBUM',
'artist_name': 'ARTIST',
'performer_name': 'PERFORMER',
'composer_name': 'COMPOSER',
'conductor_name': 'CONDUCTOR',
'media': 'SOURCE MEDIUM',
'ISRC': 'ISRC',
'catalog': 'CATALOG',
'copyright': 'COPYRIGHT',
'publisher': 'PUBLISHER',
'year': 'DATE',
'album_number': 'DISCNUMBER',
'album_total': 'DISCTOTAL',
'comment': 'COMMENT'}
ITEM_MAP = dict(map(reversed, ATTRIBUTE_MAP.items()))
def __init__(self, vorbis_data, vendor_string=u""):
"""Initialized with a key->[value1,value2] dict.
keys are generally upper case.
values are unicode string.
vendor_string is an optional unicode string."""
dict.__init__(self, [(key.upper(), values)
for (key, values) in vorbis_data.items()])
self.vendor_string = vendor_string
def __setitem__(self, key, value):
dict.__setitem__(self, key.upper(), value)
def __getattr__(self, key):
if (key == 'track_number'):
match = re.match(r'^\d+$',
self.get('TRACKNUMBER', [u''])[0])
if (match):
return int(match.group(0))
else:
match = re.match('^(\d+)/\d+$',
self.get('TRACKNUMBER', [u''])[0])
if (match):
return int(match.group(1))
else:
return 0
elif (key == 'track_total'):
match = re.match(r'^\d+$',
self.get('TRACKTOTAL', [u''])[0])
if (match):
return int(match.group(0))
else:
match = re.match('^\d+/(\d+)$',
self.get('TRACKNUMBER', [u''])[0])
if (match):
return int(match.group(1))
else:
return 0
elif (key == 'album_number'):
match = re.match(r'^\d+$',
self.get('DISCNUMBER', [u''])[0])
if (match):
return int(match.group(0))
else:
match = re.match('^(\d+)/\d+$',
self.get('DISCNUMBER', [u''])[0])
if (match):
return int(match.group(1))
else:
return 0
elif (key == 'album_total'):
match = re.match(r'^\d+$',
self.get('DISCTOTAL', [u''])[0])
if (match):
return int(match.group(0))
else:
match = re.match('^\d+/(\d+)$',
self.get('DISCNUMBER', [u''])[0])
if (match):
return int(match.group(1))
else:
return 0
elif (key in self.ATTRIBUTE_MAP):
return self.get(self.ATTRIBUTE_MAP[key], [u''])[0]
elif (key in MetaData.__FIELDS__):
return u''
else:
try:
return self.__dict__[key]
except KeyError:
raise AttributeError(key)
def __delattr__(self, key):
if (key == 'track_number'):
track_number = self.get('TRACKNUMBER', [u''])[0]
if (re.match(r'^\d+$', track_number)):
del(self['TRACKNUMBER'])
elif (re.match('^\d+/(\d+)$', track_number)):
self['TRACKNUMBER'] = u"0/%s" % (
re.match('^\d+/(\d+)$', track_number).group(1))
elif (key == 'track_total'):
track_number = self.get('TRACKNUMBER', [u''])[0]
if (re.match('^(\d+)/\d+$', track_number)):
self['TRACKNUMBER'] = u"%s" % (
re.match('^(\d+)/\d+$', track_number).group(1))
if ('TRACKTOTAL' in self):
del(self['TRACKTOTAL'])
elif (key == 'album_number'):
album_number = self.get('DISCNUMBER', [u''])[0]
if (re.match(r'^\d+$', album_number)):
del(self['DISCNUMBER'])
elif (re.match('^\d+/(\d+)$', album_number)):
self['DISCNUMBER'] = u"0/%s" % (
re.match('^\d+/(\d+)$', album_number).group(1))
elif (key == 'album_total'):
album_number = self.get('DISCNUMBER', [u''])[0]
if (re.match('^(\d+)/\d+$', album_number)):
self['DISCNUMBER'] = u"%s" % (
re.match('^(\d+)/\d+$', album_number).group(1))
if ('DISCTOTAL' in self):
del(self['DISCTOTAL'])
elif (key in self.ATTRIBUTE_MAP):
if (self.ATTRIBUTE_MAP[key] in self):
del(self[self.ATTRIBUTE_MAP[key]])
elif (key in MetaData.__FIELDS__):
pass
else:
try:
del(self.__dict__[key])
except KeyError:
raise AttributeError(key)
@classmethod
def supports_images(cls):
"""Returns False."""
#There's actually a (proposed?) standard to add embedded covers
#to Vorbis Comments by base64 encoding them.
#This strikes me as messy and convoluted.
#In addition, I'd have to perform a special case of
#image extraction and re-insertion whenever converting
#to FlacMetaData. The whole thought gives me a headache.
return False
def images(self):
"""Returns an empty list of Image objects."""
return list()
#if an attribute is updated (e.g. self.track_name)
#make sure to update the corresponding dict pair
def __setattr__(self, key, value):
if (key in self.ATTRIBUTE_MAP):
if (key not in MetaData.__INTEGER_FIELDS__):
self[self.ATTRIBUTE_MAP[key]] = [value]
else:
self[self.ATTRIBUTE_MAP[key]] = [unicode(value)]
else:
self.__dict__[key] = value
@classmethod
def converted(cls, metadata):
"""Converts a MetaData object to a VorbisComment object."""
if ((metadata is None) or (isinstance(metadata, VorbisComment))):
return metadata
elif (metadata.__class__.__name__ == 'FlacMetaData'):
return cls(vorbis_data=dict(metadata.vorbis_comment.items()),
vendor_string=metadata.vorbis_comment.vendor_string)
else:
values = {}
for key in cls.ATTRIBUTE_MAP.keys():
if (key in cls.__INTEGER_FIELDS__):
if (getattr(metadata, key) != 0):
values[cls.ATTRIBUTE_MAP[key]] = \
[unicode(getattr(metadata, key))]
elif (getattr(metadata, key) != u""):
values[cls.ATTRIBUTE_MAP[key]] = \
[unicode(getattr(metadata, key))]
return VorbisComment(values)
def merge(self, metadata):
"""Updates any currently empty entries from metadata's values."""
metadata = self.__class__.converted(metadata)
if (metadata is None):
return
for (key, values) in metadata.items():
if ((len(values) > 0) and
(len(self.get(key, [])) == 0)):
self[key] = values
def __comment_name__(self):
return u'Vorbis'
#takes two (key,value) vorbiscomment pairs
#returns cmp on the weighted set of them
#(title first, then artist, album, tracknumber, ... , replaygain)
@classmethod
def __by_pair__(cls, pair1, pair2):
KEY_MAP = {"TITLE": 1,
"ALBUM": 2,
"TRACKNUMBER": 3,
"TRACKTOTAL": 4,
"DISCNUMBER": 5,
"DISCTOTAL": 6,
"ARTIST": 7,
"PERFORMER": 8,
"COMPOSER": 9,
"CONDUCTOR": 10,
"CATALOG": 11,
"PUBLISHER": 12,
"ISRC": 13,
"SOURCE MEDIUM": 14,
#"YEAR": 15,
"DATE": 16,
"COPYRIGHT": 17,
"REPLAYGAIN_ALBUM_GAIN": 19,
"REPLAYGAIN_ALBUM_PEAK": 19,
"REPLAYGAIN_TRACK_GAIN": 19,
"REPLAYGAIN_TRACK_PEAK": 19,
"REPLAYGAIN_REFERENCE_LOUDNESS": 20}
return cmp((KEY_MAP.get(pair1[0].upper(), 18),
pair1[0].upper(),
pair1[1]),
(KEY_MAP.get(pair2[0].upper(), 18),
pair2[0].upper(),
pair2[1]))
def __comment_pairs__(self):
pairs = []
for (key, values) in self.items():
for value in values:
pairs.append((key, value))
pairs.sort(VorbisComment.__by_pair__)
return pairs
def build(self):
"""Returns this VorbisComment as a binary string."""
comment = Con.Container(vendor_string=self.vendor_string,
framing=1,
value=[])
for (key, values) in self.items():
for value in values:
if ((value != u"") and not
((key in ("TRACKNUMBER", "TRACKTOTAL",
"DISCNUMBER", "DISCTOTAL")) and
(value == u"0"))):
comment.value.append("%s=%s" % (key,
value.encode('utf-8')))
return self.VORBIS_COMMENT.build(comment)