Exemplo n.º 1
0
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"),
Exemplo n.º 3
0
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)