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"))))
class __TIFF__(ImageMetrics): HEADER = Con.Struct('header', Con.String('byte_order', 2), Con.Switch('order', lambda ctx: ctx['byte_order'], {"II": Con.Embed( Con.Struct('little_endian', Con.Const(Con.ULInt16('version'), 42), Con.ULInt32('offset'))), "MM": Con.Embed( Con.Struct('big_endian', Con.Const(Con.UBInt16('version'), 42), Con.UBInt32('offset')))})) L_IFD = Con.Struct('ifd', Con.PrefixedArray( length_field=Con.ULInt16('length'), subcon=Con.Struct('tags', Con.ULInt16('id'), Con.ULInt16('type'), Con.ULInt32('count'), Con.ULInt32('offset'))), Con.ULInt32('next')) B_IFD = Con.Struct('ifd', Con.PrefixedArray( length_field=Con.UBInt16('length'), subcon=Con.Struct('tags', Con.UBInt16('id'), Con.UBInt16('type'), Con.UBInt32('count'), Con.UBInt32('offset'))), Con.UBInt32('next')) def __init__(self, width, height, bits_per_pixel, color_count): ImageMetrics.__init__(self, width, height, bits_per_pixel, color_count, u'image/tiff') @classmethod def b_tag_value(cls, file, tag): subtype = {1: Con.Byte("data"), 2: Con.CString("data"), 3: Con.UBInt16("data"), 4: Con.UBInt32("data"), 5: Con.Struct("data", Con.UBInt32("high"), Con.UBInt32("low"))}[tag.type] data = Con.StrictRepeater(tag.count, subtype) if ((tag.type != 2) and (data.sizeof() <= 4)): return tag.offset else: file.seek(tag.offset, 0) return data.parse_stream(file) @classmethod def l_tag_value(cls, file, tag): subtype = {1: Con.Byte("data"), 2: Con.CString("data"), 3: Con.ULInt16("data"), 4: Con.ULInt32("data"), 5: Con.Struct("data", Con.ULInt32("high"), Con.ULInt32("low"))}[tag.type] data = Con.StrictRepeater(tag.count, subtype) if ((tag.type != 2) and (data.sizeof() <= 4)): return tag.offset else: file.seek(tag.offset, 0) return data.parse_stream(file) @classmethod def parse(cls, file): width = 0 height = 0 bits_per_sample = 0 color_count = 0 try: header = cls.HEADER.parse_stream(file) if (header.byte_order == 'II'): IFD = cls.L_IFD tag_value = cls.l_tag_value elif (header.byte_order == 'MM'): IFD = cls.B_IFD tag_value = cls.b_tag_value else: raise InvalidTIFF(_(u'Invalid byte order')) file.seek(header.offset, 0) ifd = IFD.parse_stream(file) while (True): for tag in ifd.tags: if (tag.id == 0x0100): width = tag_value(file, tag) elif (tag.id == 0x0101): height = tag_value(file, tag) elif (tag.id == 0x0102): try: bits_per_sample = sum(tag_value(file, tag)) except TypeError: bits_per_sample = tag_value(file, tag) elif (tag.id == 0x0140): color_count = tag.count / 3 else: pass if (ifd.next == 0x00): break else: file.seek(ifd.next, 0) ifd = IFD.parse_stream(file) return __TIFF__(width, height, bits_per_sample, color_count) except Con.ConstError: raise InvalidTIFF(_(u'Invalid TIFF'))
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"), Con.UBInt16("quicktime_audio_encoding_version"), Con.UBInt16("quicktime_audio_encoding_revision"), Con.String("quicktime_audio_encoding_vendor", 4), Con.UBInt16("channels"), Con.UBInt16("sample_size"), Con.UBInt16("audio_compression_id"), Con.UBInt16("quicktime_audio_packet_size"), Con.String("sample_rate", 4))
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)