class UnframedVorbisComment(VorbisComment): """An implementation of VorbisComment without the framing bit.""" 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"))))
ATOM_MDHD = Con.Struct( "mdhd", Con.Byte("version"), Con.String("flags", 3), VersionLength("created_mac_UTC_date"), VersionLength("modified_mac_UTC_date"), Con.UBInt32("time_scale"), VersionLength("duration"), Con.BitStruct("languages", Con.Padding(1), Con.StrictRepeater(3, Con.Bits("language", 5))), Con.UBInt16("quicktime_quality")) ATOM_HDLR = Con.Struct("hdlr", Con.Byte("version"), Con.String("flags", 3), Con.String("quicktime_type", 4), Con.String("subtype", 4), Con.String("quicktime_manufacturer", 4), Con.UBInt32("quicktime_component_reserved_flags"), Con.UBInt32("quicktime_component_reserved_flags_mask"), Con.PascalString("component_name"), Con.Padding(1)) ATOM_SMHD = Con.Struct('smhd', Con.Byte("version"), Con.String("flags", 3), Con.String("audio_balance", 2), Con.Padding(2)) ATOM_DREF = Con.Struct( 'dref', Con.Byte("version"), Con.String("flags", 3), Con.PrefixedArray(length_field=Con.UBInt32("num_references"), subcon=Atom("references"))) ATOM_STSD = Con.Struct( 'stsd', Con.Byte("version"), Con.String("flags", 3), Con.PrefixedArray(length_field=Con.UBInt32("num_descriptions"), subcon=Atom("descriptions"))) ATOM_MP4A = Con.Struct("mp4a", Con.Padding(6), Con.UBInt16("reference_index"),
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)