295 lines
11 KiB
Python
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)
|