def fixup_ID3(fname: Union[str, MusicFileType]) -> None: '''Convert RVA2 tags to TXXX:replaygain_* tags. Argument should be an MusicFile (instance of mutagen.FileType) or a string, which will be loaded by mutagen.MusicFile. If it is an instance of mutagen.id3.ID3FileType, the ReplayGain information in the RVA2 tags (if any) will be propagated to 'TXXX:replaygain_*' tags. Thus the resulting file will have the ReplayGain information encoded both ways for maximum compatibility. If the track is an instance of 'mutagen.mp3.EasyMP3', it will be re-opened as the non-easy equivalent, since EasyMP3 maps the replaygain tags to RVA2, preventing the editing of the TXXX tags. This function modifies the file on disk. ''' # Make sure we have the non-easy variant. if isinstance(fname, MusicFileType): fname = fname.filename # type: ignore track = MusicFile(fname, easy=False) # Only operate on ID3 if not isinstance(track, id3.ID3FileType): return # Get the RVA2 frames try: track_rva2 = track['RVA2:track'] if track_rva2.channel != 1: track_rva2 = None except KeyError: track_rva2 = None try: album_rva2 = track['RVA2:album'] if album_rva2.channel != 1: album_rva2 = None except KeyError: album_rva2 = None # Add the other tags based on RVA2 values if track_rva2: track['TXXX:replaygain_track_peak'] = \ id3.TXXX(encoding=id3.Encoding.UTF8, desc='replaygain_track_peak', text=format_peak(track_rva2.peak)) track['TXXX:replaygain_track_gain'] = \ id3.TXXX(encoding=id3.Encoding.UTF8, desc='replaygain_track_gain', text=format_gain(track_rva2.gain)) if album_rva2: track['TXXX:replaygain_album_peak'] = \ id3.TXXX(encoding=id3.Encoding.UTF8, desc='replaygain_album_peak', text=format_peak(album_rva2.peak)) track['TXXX:replaygain_album_gain'] = \ id3.TXXX(encoding=id3.Encoding.UTF8, desc='replaygain_album_gain', text=format_gain(album_rva2.gain)) track.save()
def build_TXXX(self, encoding, desc, values): """Construct and return a TXXX frame.""" # This is here so that plugins can customize the behavior of TXXX # frames in particular via subclassing. # discussion: https://github.com/metabrainz/picard/pull/634 # discussion: https://github.com/metabrainz/picard/pull/635 # Used in the plugin "Compatible TXXX frames" # PR: https://github.com/metabrainz/picard-plugins/pull/83 return id3.TXXX(encoding=encoding, desc=desc, text=values)
def build_compliant_TXXX(self, encoding, desc, values): """Return a TXXX frame with only a single value. Use id3v23_join_with as the sperator if using id3v2.3, otherwise the value set in this plugin (default "; "). """ if config.setting['write_id3v23']: sep = config.setting['id3v23_join_with'] else: sep = id3v24_join_with joined_values = [sep.join(values)] return id3.TXXX(encoding=encoding, desc=desc, text=joined_values)
def get_id3_frame(tag_name, tag_value): """ :param tag_name: str. :param tag_value: str. :return: mutagen ID3 frame """ if tag_name in vorbis_to_id3_map: frame_name = vorbis_to_id3_map[tag_name] assert frame_name in id3.Frames, f"'{frame_name}' in vorbis_to_id3_map is not a valid frame type" frame_type = id3.Frames[frame_name] return frame_type(encoding=3, text=tag_value) else: return id3.TXXX(encoding=3, desc=tag_name, text=tag_value)
def _save(self, filename, metadata): """Save metadata to the file.""" log.debug("Saving file %r", filename) tags = self._get_tags(filename) if config.setting['clear_existing_tags']: tags.clear() if metadata.images_to_be_saved_to_tags: tags.delall('APIC') encoding = { 'utf-8': 3, 'utf-16': 1 }.get(config.setting['id3v2_encoding'], 0) if 'tracknumber' in metadata: if 'totaltracks' in metadata: text = '%s/%s' % (metadata['tracknumber'], metadata['totaltracks']) else: text = metadata['tracknumber'] tags.add(id3.TRCK(encoding=0, text=id3text(text, 0))) if 'discnumber' in metadata: if 'totaldiscs' in metadata: text = '%s/%s' % (metadata['discnumber'], metadata['totaldiscs']) else: text = metadata['discnumber'] tags.add(id3.TPOS(encoding=0, text=id3text(text, 0))) # This is necessary because mutagens HashKey for APIC frames only # includes the FrameID (APIC) and description - it's basically # impossible to save two images, even of different types, without # any description. counters = defaultdict(lambda: 0) for image in metadata.images_to_be_saved_to_tags: desc = desctag = image.comment if counters[desc] > 0: if desc: desctag = "%s (%i)" % (desc, counters[desc]) else: desctag = "(%i)" % counters[desc] counters[desc] += 1 tags.add( id3.APIC(encoding=0, mime=image.mimetype, type=image_type_as_id3_num(image.maintype), desc=id3text(desctag, 0), data=image.data)) tmcl = mutagen.id3.TMCL(encoding=encoding, people=[]) tipl = mutagen.id3.TIPL(encoding=encoding, people=[]) tags.delall('TCMP') for name, values in metadata.rawitems(): values = [id3text(v, encoding) for v in values] name = id3text(name, encoding) if name.startswith('performer:'): role = name.split(':', 1)[1] for value in values: tmcl.people.append([role, value]) elif name.startswith('comment:'): desc = name.split(':', 1)[1] if desc.lower()[:4] == "itun": tags.delall('COMM:' + desc) tags.add( id3.COMM(encoding=0, desc=desc, lang='eng', text=[v + u'\x00' for v in values])) else: tags.add( id3.COMM(encoding=encoding, desc=desc, lang='eng', text=values)) elif name.startswith('lyrics:') or name == 'lyrics': if ':' in name: desc = name.split(':', 1)[1] else: desc = '' for value in values: tags.add(id3.USLT(encoding=encoding, desc=desc, text=value)) elif name in self._rtipl_roles: for value in values: tipl.people.append([self._rtipl_roles[name], value]) elif name == 'musicbrainz_recordingid': tags.add( id3.UFID(owner='http://musicbrainz.org', data=str(values[0]))) elif name == '~rating': # Search for an existing POPM frame to get the current playcount for frame in tags.values(): if frame.FrameID == 'POPM' and frame.email == config.setting[ 'rating_user_email']: count = getattr(frame, 'count', 0) break else: count = 0 # Convert rating to range between 0 and 255 rating = int( round( float(values[0]) * 255 / (config.setting['rating_steps'] - 1))) tags.add( id3.POPM(email=config.setting['rating_user_email'], rating=rating, count=count)) elif name in self.__rtranslate: frameid = self.__rtranslate[name] if frameid.startswith('W'): valid_urls = all([all(urlparse(v)[:2]) for v in values]) if frameid == 'WCOP': # Only add WCOP if there is only one license URL, otherwise use TXXX:LICENSE if len(values) > 1 or not valid_urls: tags.add( id3.TXXX(encoding=encoding, desc=self.__rtranslate_freetext[name], text=values)) else: tags.add(id3.WCOP(url=values[0])) elif frameid == 'WOAR' and valid_urls: for url in values: tags.add(id3.WOAR(url=url)) elif frameid.startswith('T'): tags.add( getattr(id3, frameid)(encoding=encoding, text=values)) if frameid == 'TSOA': tags.delall('XSOA') elif frameid == 'TSOP': tags.delall('XSOP') elif frameid == 'TSO2': tags.delall('TXXX:ALBUMARTISTSORT') elif name in self.__rtranslate_freetext: tags.add( id3.TXXX(encoding=encoding, desc=self.__rtranslate_freetext[name], text=values)) elif name.startswith('~id3:'): name = name[5:] if name.startswith('TXXX:'): tags.add( id3.TXXX(encoding=encoding, desc=name[5:], text=values)) else: frameclass = getattr(id3, name[:4], None) if frameclass: tags.add(frameclass(encoding=encoding, text=values)) # don't save private / already stored tags elif not name.startswith( "~") and not name in self.__other_supported_tags: tags.add(id3.TXXX(encoding=encoding, desc=name, text=values)) if tmcl.people: tags.add(tmcl) if tipl.people: tags.add(tipl) self._save_tags(tags, encode_filename(filename)) if self._IsMP3 and config.setting["remove_ape_from_mp3"]: try: mutagen.apev2.delete(encode_filename(filename)) except: pass
def create_usertext(title, value): frame = id3.TXXX(encoding, title, value) frame.get_value = lambda: get_text(frame) frame.set_value = partial(set_text, frame) return {title: frame}
def _save(self, filename, metadata, settings): """Save metadata to the file.""" self.log.debug("Saving file %r", filename) try: tags = compatid3.CompatID3(encode_filename(filename)) except mutagen.id3.ID3NoHeaderError: tags = compatid3.CompatID3() if settings['clear_existing_tags']: tags.clear() if settings['save_images_to_tags'] and metadata.images: tags.delall('APIC') if settings['write_id3v1']: v1 = 2 else: v1 = 0 encoding = {'utf-8': 3, 'utf-16': 1}.get(settings['id3v2_encoding'], 0) if 'tracknumber' in metadata: if 'totaltracks' in metadata: text = '%s/%s' % (metadata['tracknumber'], metadata['totaltracks']) else: text = metadata['tracknumber'] tags.add(id3.TRCK(encoding=0, text=text)) if 'discnumber' in metadata: if 'totaldiscs' in metadata: text = '%s/%s' % (metadata['discnumber'], metadata['totaldiscs']) else: text = metadata['discnumber'] tags.add(id3.TPOS(encoding=0, text=text)) if settings['save_images_to_tags']: for mime, data in metadata.images: tags.add( id3.APIC(encoding=0, mime=mime, type=3, desc='', data=data)) tmcl = mutagen.id3.TMCL(encoding=encoding, people=[]) tipl = mutagen.id3.TIPL(encoding=encoding, people=[]) id3.TCMP = compatid3.TCMP tags.delall('TCMP') for name, values in metadata.rawitems(): if name.startswith('performer:'): role = name.split(':', 1)[1] for value in values: tmcl.people.append([role, value]) elif name.startswith('comment:'): desc = name.split(':', 1)[1] if desc.lower()[:4] == "itun": tags.delall('COMM:' + desc) tags.add( id3.COMM(encoding=0, desc=desc, lang='eng', text=[v + u'\x00' for v in values])) else: tags.add( id3.COMM(encoding=encoding, desc=desc, lang='eng', text=values)) elif name.startswith('lyrics:') or name == 'lyrics': if ':' in name: desc = name.split(':', 1)[1] else: desc = '' for value in values: tags.add(id3.USLT(encoding=encoding, desc=desc, text=value)) elif name in self.__rtipl_roles: for value in values: tipl.people.append([self.__rtipl_roles[name], value]) elif name == 'musicbrainz_trackid': tags.add( id3.UFID(owner='http://musicbrainz.org', data=str(values[0]))) elif name == '~rating': # Search for an existing POPM frame to get the current playcount for frame in tags.values(): if frame.FrameID == 'POPM' and frame.email == settings[ 'rating_user_email']: count = getattr(frame, 'count', 0) break else: count = 0 # Convert rating to range between 0 and 255 rating = int( round( float(values[0]) * 255 / (settings['rating_steps'] - 1))) tags.add( id3.POPM(email=settings['rating_user_email'], rating=rating, count=count)) elif name in self.__rtranslate: frameid = self.__rtranslate[name] if frameid.startswith('W'): tags.add(getattr(id3, frameid)(url=values[0])) elif frameid.startswith('T'): tags.add( getattr(id3, frameid)(encoding=encoding, text=values)) elif name in self.__rtranslate_freetext: tags.add( id3.TXXX(encoding=encoding, desc=self.__rtranslate_freetext[name], text=values)) elif name.startswith('~id3:'): name = name[5:] if name.startswith('TXXX:'): tags.add( id3.TXXX(encoding=encoding, desc=name[5:], text=values)) else: frameclass = getattr(id3, name[:4], None) if frameclass: tags.add(frameclass(encoding=encoding, text=values)) if tmcl.people: tags.add(tmcl) if tipl.people: tags.add(tipl) if settings['write_id3v23']: tags.update_to_v23() tags.save(encode_filename(filename), v2=3, v1=v1) else: # remove all custom 2.3 frames for old in self.__upgrade.keys(): tags.delall(old) tags.update_to_v24() tags.save(encode_filename(filename), v2=4, v1=v1) if self._IsMP3 and settings["remove_ape_from_mp3"]: try: mutagen.apev2.delete(encode_filename(filename)) except: pass
def _write_tags(self, tags): for tag, value in tags.items(): if tag == "releaseName" or tag == "release_name" or tag == "TALB": self._add_tag(id3.TALB(encoding=UTF_8, text=value)) elif tag == "front_cover" or tag == "APIC_FRONT": mime, desc, data = value self._add_tag(id3.APIC(UTF_8, mime, FRONT_COVER, desc, data)) elif tag == "backCover" or tag == "APIC_BACK": mime, desc, data = value self._add_tag(id3.APIC(UTF_8, mime, BACK_COVER, desc, data)) elif tag == "lead_performer" or tag == "TPE1": self._add_tag(id3.TPE1(encoding=UTF_8, text=value)) elif tag == "lead_performer_region" or tag == "TXXX_LEAD_PERFORMER_REGION": self._add_tag( id3.TXXX(encoding=UTF_8, desc="LEAD-PERFORMER-REGION:UN/LOCODE", text=value)) elif tag == "lead_performer_date_of_birth" or tag == "TXXX_LEAD_PERFORMER_DATE_OF_BIRTH": self._add_tag( id3.TXXX(encoding=UTF_8, desc="LEAD-PERFORMER-DATE-OF-BIRTH", text=value)) elif tag == "guestPerformers" or tag == "TMCL": self._add_tag(id3.TMCL(encoding=UTF_8, people=value)) elif tag == "label_name" or tag == "TOWN": self._add_tag(id3.TOWN(encoding=UTF_8, text=value)) elif tag == "catalogNumber" or tag == "TXXX_CATALOG_NUMBER": self._add_tag( id3.TXXX(encoding=UTF_8, desc="Catalog Number", text=value)) elif tag == "upc" or tag == "TXXX_UPC": """ Deprecated and replaced with BARCODE """ self._add_tag(id3.TXXX(encoding=UTF_8, desc="UPC", text=value)) elif tag == "barcode" or tag == "TXXX_BARCODE": self._add_tag( id3.TXXX(encoding=UTF_8, desc="BARCODE", text=value)) elif tag == "recording_time" or tag == "TDRC": self._add_tag( id3.TDRC(encoding=UTF_8, text=[id3.ID3TimeStamp(value)])) elif tag == "releaseTime" or tag == "TDRL": self._add_tag( id3.TDRL(encoding=UTF_8, text=[id3.ID3TimeStamp(value)])) elif tag == "originalReleaseTime" or tag == "TDOR": self._add_tag( id3.TDOR(encoding=UTF_8, text=[id3.ID3TimeStamp(value)])) elif tag == "tagging_time" or tag == "TDTG": self._add_tag( id3.TDTG(encoding=UTF_8, text=[id3.ID3TimeStamp(value)])) elif tag == "recordingStudios" or tag == "TXXX_RECORDING_STUDIO": self._add_tag( id3.TXXX(encoding=UTF_8, desc="Recording Studio", text=value)) elif tag == "TIPL": self._add_tag(id3.TIPL(encoding=UTF_8, people=value)) elif tag == "comments" or tag == "COMM": text, lang = value self._add_tag( id3.COMM(encoding=UTF_8, text=text, desc="", lang=lang)) elif tag == "track_title" or tag == "TIT2": self._add_tag(id3.TIT2(encoding=UTF_8, text=value)) elif tag == "version_info" or tag == "TPE4": self._add_tag(id3.TPE4(encoding=UTF_8, text=value)) elif tag == "featured_guest" or tag == "TXXX_FEATURED_GUEST": self._add_tag( id3.TXXX(encoding=UTF_8, desc="Featured Guest", text=value)) elif tag == "lyricist" or tag == "TEXT": self._add_tag(id3.TEXT(encoding=UTF_8, text=value)) elif tag == "composer" or tag == "TCOM": self._add_tag(id3.TCOM(encoding=UTF_8, text=value)) elif tag == "publisher" or tag == "TPUB": self._add_tag(id3.TPUB(encoding=UTF_8, text=value)) elif tag == "isrc" or tag == "TSRC": self._add_tag(id3.TSRC(encoding=UTF_8, text=value)) elif tag == "labels" or tag == "TXXX_TAGS": self._add_tag(id3.TXXX(encoding=UTF_8, desc="Tags", text=value)) elif tag == "isni" or tag == "TXXX_ISNI_Joel_Miller": self._add_tag( id3.TXXX(encoding=UTF_8, desc="ISNI:Joel Miller", text=value)) elif tag == "TXXX_ISNI_Rebecca_Ann_Maloy": self._add_tag( id3.TXXX(encoding=UTF_8, desc="ISNI:Rebecca Ann Maloy", text=value)) elif tag == "TXXX_IPI_Joel_Miller": self._add_tag( id3.TXXX(encoding=UTF_8, desc="IPI:Joel Miller", text=value)) elif tag == "TXXX_IPI_Rebecca_Ann_Maloy": self._add_tag( id3.TXXX(encoding=UTF_8, desc="IPI:Rebecca Ann Maloy", text=value)) elif tag == "TXXX_ISNI": self._add_tag(id3.TXXX(encoding=UTF_8, desc="ISNI", text=value)) elif tag == "iswc" or tag == "TXXX_ISWC": self._add_tag(id3.TXXX(encoding=UTF_8, desc="ISWC", text=value)) elif tag == "lyrics" or tag == "USLT": text, lang = value self._add_tag( id3.USLT(encoding=UTF_8, text=text, desc="", lang=lang)) elif tag == "language" or tag == "TLAN": self._add_tag(id3.TLAN(encoding=UTF_8, text=value)) elif tag == "primary_style" or tag == "TCON": self._add_tag(id3.TCON(encoding=UTF_8, text=value)) elif tag == "compilation" or tag == "TCMP": self._add_tag(id3.TCMP(encoding=UTF_8, text=value)) elif tag == "tagger" or tag == "TXXX_TAGGER": self._add_tag( id3.TXXX(encoding=UTF_8, desc="TAGGER", text=value)) elif tag == "TXXX_Tagger": """ Deprecated and replaced with separate TAGGER and VERSION tags """ self._add_tag( id3.TXXX(encoding=UTF_8, desc="Tagger", text=value)) elif tag == "tagger_version" or tag == "TXXX_TAGGER_VERSION": self._add_tag( id3.TXXX(encoding=UTF_8, desc="TAGGER_VERSION", text=value)) elif tag == "TXXX_Tagging_Time": """ Deprecated and replaced with TAGGING_TIME """ self._add_tag( id3.TXXX(encoding=UTF_8, desc="Tagging Time", text=value)) elif tag == "TXXX_TAGGING_TIME": """ Deprecated and replaced with TDTG """ self._add_tag( id3.TXXX(encoding=UTF_8, desc="TAGGING_TIME", text=value)) elif tag == "TXXX_PRODUCTION_COMPANY": self._add_tag( id3.TXXX(encoding=UTF_8, desc="PRODUCTION-COMPANY", text=value)) elif tag == "TXXX_PRODUCTION_COMPANY_REGION": self._add_tag( id3.TXXX(encoding=UTF_8, desc="PRODUCTION-COMPANY-REGION:UN/LOCODE", text=value)) elif tag == "TXXX_RECORDING_STUDIO_REGION": self._add_tag( id3.TXXX(encoding=UTF_8, desc="RECORDING-STUDIO-REGION:UN/LOCODE", text=value)) elif tag == "TXXX_RECORDING_STUDIO_ADDRESS": self._add_tag( id3.TXXX(encoding=UTF_8, desc="RECORDING-STUDIO-ADDRESS", text=value)) elif tag == "TRCK": self._add_tag(id3.TRCK(encoding=UTF_8, text=value)) else: raise AssertionError("Knows nothing about '{0}'".format(tag))