def save(self): metadata = Metadata() for i in range(self.ui.tags.topLevelItemCount()): item = self.ui.tags.topLevelItem(i) name = unicode(item.data(0, QtCore.Qt.UserRole).toString()) if name in self.changed: value = unicode(item.text(1)) metadata.add(name, value) # Rate the different tracks if self.config.setting['enable_ratings']: rating = self.ui.rating.getRating() metadata['~rating'] = unicode(rating) tracks = set([ file.parent for file in self.files if isinstance(file.parent, Track) ]) ratings = {} for track in tracks: ratings[('recording', track.id)] = rating track.metadata['~rating'] = rating if self.config.setting['submit_ratings']: self.tagger.xmlws.submit_ratings(ratings, None) for file in self.files: for name in self.changed: try: del file.metadata[name] except KeyError: pass file.metadata.update(metadata) file.update()
def save(self): metadata = Metadata() for i in range(self.ui.tags.topLevelItemCount()): item = self.ui.tags.topLevelItem(i) name = unicode(item.data(0, QtCore.Qt.UserRole).toString()) if name in self.changed: value = unicode(item.text(1)) metadata.add(name, value) # Rate the different tracks if self.config.setting['enable_ratings']: rating = self.ui.rating.getRating() metadata['~rating'] = unicode(rating) tracks = set([file.parent for file in self.files if isinstance(file.parent, Track)]) ratings = {} for track in tracks: ratings[('recording', track.id)] = rating track.metadata['~rating'] = rating if self.config.setting['submit_ratings']: self.tagger.xmlws.submit_ratings(ratings, None) for file in self.files: for name in self.changed: try: del file.metadata[name] except KeyError: pass file.metadata.update(metadata) file.update()
def _load(self, filename): self.log.debug("Loading file %r", filename) file = self._File(encode_filename(filename)) file.tags = file.tags or {} metadata = Metadata() for origname, values in file.tags.items(): for value in values: name = origname if name == "date" or name == "originaldate": # YYYY-00-00 => YYYY value = sanitize_date(value) elif name == 'performer' or name == 'comment': # transform "performer=Joe Barr (Piano)" to "performer:Piano=Joe Barr" name += ':' if value.endswith(')'): start = value.rfind(' (') if start > 0: name += value[start + 2:-1] value = value[:start] elif name.startswith('rating'): try: name, email = name.split(':', 1) except ValueError: email = '' if email != self.config.setting['rating_user_email']: continue name = '~rating' value = unicode( int( round( (float(value) * (self.config.setting['rating_steps'] - 1))))) elif name == "fingerprint" and value.startswith( "MusicMagic Fingerprint"): name = "musicip_fingerprint" value = value[22:] elif name == "tracktotal" and "totaltracks" not in file.tags: name = "totaltracks" elif name == "metadata_block_picture": image = mutagen.flac.Picture( base64.standard_b64decode(value)) metadata.add_image(image.mime, image.data) continue metadata.add(name, value) if self._File == mutagen.flac.FLAC: for image in file.pictures: metadata.add_image(image.mime, image.data) # Read the unofficial COVERART tags, for backward compatibillity only if not "metadata_block_picture" in file.tags: try: for index, data in enumerate(file["COVERART"]): metadata.add_image(file["COVERARTMIME"][index], base64.standard_b64decode(data)) except KeyError: pass self._info(metadata, file) return metadata
def _load(self, filename): log.debug("Loading file %r", filename) self.__casemap = {} file = self._File(encode_filename(filename)) metadata = Metadata() if file.tags: for origname, values in file.tags.items(): name_lower = origname.lower() if (values.kind == mutagen.apev2.BINARY and name_lower.startswith("cover art")): if b'\0' in values.value: descr, data = values.value.split(b'\0', 1) try: coverartimage = TagCoverArtImage( file=filename, tag=name_lower, data=data, ) except CoverArtImageError as e: log.error('Cannot load image from %r: %s' % (filename, e)) else: metadata.images.append(coverartimage) # skip EXTERNAL and BINARY values if values.kind != mutagen.apev2.TEXT: continue for value in values: name = name_lower if name == "year": name = "date" value = sanitize_date(value) elif name == "track": name = "tracknumber" track = value.split("/") if len(track) > 1: metadata["totaltracks"] = track[1] value = track[0] elif name == "disc": name = "discnumber" disc = value.split("/") if len(disc) > 1: metadata["totaldiscs"] = disc[1] value = disc[0] elif name == 'performer' or name == 'comment': name = name + ':' if value.endswith(')'): start = value.rfind(' (') if start > 0: name += value[start + 2:-1] value = value[:start] elif name in self.__rtranslate: name = self.__rtranslate[name] self.__casemap[name] = origname metadata.add(name, value) self._info(metadata, file) return metadata
def _load(self, filename): log.debug("Loading file %r", filename) file = self._File(encode_filename(filename)) metadata = Metadata() if file.tags: for origname, values in file.tags.items(): if origname.lower().startswith("cover art") and values.kind == mutagen.apev2.BINARY: if '\0' in values.value: descr, data = values.value.split('\0', 1) try: coverartimage = TagCoverArtImage( file=filename, tag=origname, data=data, ) except CoverArtImageError as e: log.error('Cannot load image from %r: %s' % (filename, e)) else: metadata.append_image(coverartimage) # skip EXTERNAL and BINARY values if values.kind != mutagen.apev2.TEXT: continue for value in values: name = origname if name == "Year": name = "date" value = sanitize_date(value) elif name == "Track": name = "tracknumber" track = value.split("/") if len(track) > 1: metadata["totaltracks"] = track[1] value = track[0] elif name == "Disc": name = "discnumber" disc = value.split("/") if len(disc) > 1: metadata["totaldiscs"] = disc[1] value = disc[0] elif name == 'Performer' or name == 'Comment': name = name.lower() + ':' if value.endswith(')'): start = value.rfind(' (') if start > 0: name += value[start + 2:-1] value = value[:start] elif name in self.__translate: name = self.__translate[name] else: name = name.lower() metadata.add(name, value) self._info(metadata, file) return metadata
def _load(self, filename): self.log.debug("Loading file %r", filename) file = self._File(encode_filename(filename)) file.tags = file.tags or {} metadata = Metadata() for origname, values in file.tags.items(): for value in values: name = origname if name == "date" or name == "originaldate": # YYYY-00-00 => YYYY value = sanitize_date(value) elif name == 'performer' or name == 'comment': # transform "performer=Joe Barr (Piano)" to "performer:Piano=Joe Barr" name += ':' if value.endswith(')'): start = value.rfind(' (') if start > 0: name += value[start + 2:-1] value = value[:start] elif name.startswith('rating'): try: name, email = name.split(':', 1) except ValueError: email = '' if email != self.config.setting['rating_user_email']: continue name = '~rating' value = unicode(int(round((float(value) * (self.config.setting['rating_steps'] - 1))))) elif name == "fingerprint" and value.startswith("MusicMagic Fingerprint"): name = "musicip_fingerprint" value = value[22:] elif name == "tracktotal": if "totaltracks" in file.tags: continue name = "totaltracks" elif name == "disctotal": if "totaldiscs" in file.tags: continue name = "totaldiscs" elif name == "metadata_block_picture": image = mutagen.flac.Picture(base64.standard_b64decode(value)) metadata.add_image(image.mime, image.data) continue metadata.add(name, value) if self._File == mutagen.flac.FLAC: for image in file.pictures: metadata.add_image(image.mime, image.data) # Read the unofficial COVERART tags, for backward compatibillity only if not "metadata_block_picture" in file.tags: try: for index, data in enumerate(file["COVERART"]): metadata.add_image(file["COVERARTMIME"][index], base64.standard_b64decode(data)) except KeyError: pass self._info(metadata, file) return metadata
def _load(self, filename): log.debug("Loading file %r", filename) file = MP4(encode_filename(filename)) tags = file.tags if tags is None: file.add_tags() metadata = Metadata() for name, values in tags.items(): if name in self.__text_tags: for value in values: metadata.add(self.__text_tags[name], value) elif name in self.__bool_tags: metadata.add(self.__bool_tags[name], values and '1' or '0') elif name in self.__int_tags: for value in values: metadata.add(self.__int_tags[name], unicode(value)) elif name in self.__freeform_tags: for value in values: value = value.strip("\x00").decode("utf-8", "replace") metadata.add(self.__freeform_tags[name], value) elif name == "----:com.apple.iTunes:fingerprint": for value in values: value = value.strip("\x00").decode("utf-8", "replace") if value.startswith("MusicMagic Fingerprint"): metadata.add("musicip_fingerprint", value[22:]) elif name == "trkn": metadata["tracknumber"] = str(values[0][0]) metadata["totaltracks"] = str(values[0][1]) elif name == "disk": metadata["discnumber"] = str(values[0][0]) metadata["totaldiscs"] = str(values[0][1]) elif name == "covr": for value in values: if value.imageformat not in (value.FORMAT_JPEG, value.FORMAT_PNG): continue try: coverartimage = TagCoverArtImage( file=filename, tag=name, data=value, ) except CoverArtImageError as e: log.error('Cannot load image from %r: %s' % (filename, e)) else: metadata.append_image(coverartimage) self._info(metadata, file) return metadata
def _load(self, filename): log.debug("Loading file %r", filename) file = MP4(encode_filename(filename)) tags = file.tags if tags is None: file.add_tags() metadata = Metadata() for name, values in tags.items(): if name in self.__text_tags: for value in values: metadata.add(self.__text_tags[name], value) elif name in self.__bool_tags: metadata.add(self.__bool_tags[name], values and '1' or '0') elif name in self.__int_tags: for value in values: metadata.add(self.__int_tags[name], string_(value)) elif name in self.__freeform_tags: for value in values: value = value.decode("utf-8", "replace").strip("\x00") metadata.add(self.__freeform_tags[name], value) elif name == "----:com.apple.iTunes:fingerprint": for value in values: value = value.decode("utf-8", "replace").strip("\x00") if value.startswith("MusicMagic Fingerprint"): metadata.add("musicip_fingerprint", value[22:]) elif name == "trkn": metadata["tracknumber"] = string_(values[0][0]) metadata["totaltracks"] = string_(values[0][1]) elif name == "disk": metadata["discnumber"] = string_(values[0][0]) metadata["totaldiscs"] = string_(values[0][1]) elif name == "covr": for value in values: if value.imageformat not in (value.FORMAT_JPEG, value.FORMAT_PNG): continue try: coverartimage = TagCoverArtImage( file=filename, tag=name, data=value, ) except CoverArtImageError as e: log.error('Cannot load image from %r: %s' % (filename, e)) else: metadata.append_image(coverartimage) self._info(metadata, file) return metadata
def _load(self, lines): metadata = Metadata() metadata.copy(self.metadata) metadata.add("album", self.metadata.get("title")) del metadata["title"] for line in lines: splitline = line.split() # linetokv? key = splitline[0] value = " ".join(splitline[1:]) if key == "INDEX": index, value = value.split() self.indexes[index] = value elif not key == "TRACK": self.kv_to_metadata(key, value, metadata) return metadata
def _load(self, filename): self.log.debug("Loading file %r", filename) file = self._File(encode_filename(filename)) metadata = Metadata() if file.tags: for origname, values in file.tags.items(): if origname.lower().startswith( "cover art") and values.kind == mutagen.apev2.BINARY: if '\0' in values.value: descr, data = values.value.split('\0', 1) mime = mimetype.get_from_data(data, descr, 'image/jpeg') metadata.add_image(mime, data) # skip EXTERNAL and BINARY values if values.kind != mutagen.apev2.TEXT: continue for value in values: name = origname if name == "Year": name = "date" value = sanitize_date(value) elif name == "Track": name = "tracknumber" track = value.split("/") if len(track) > 1: metadata["totaltracks"] = track[1] value = track[0] elif name == "Disc": name = "discnumber" disc = value.split("/") if len(disc) > 1: metadata["totaldiscs"] = disc[1] value = disc[0] elif name == 'Performer' or name == 'Comment': name = name.lower() + ':' if value.endswith(')'): start = value.rfind(' (') if start > 0: name += value[start + 2:-1] value = value[:start] elif name in self.__translate: name = self.__translate[name] else: name = name.lower() metadata.add(name, value) self._info(metadata, file) return metadata
def _load(self, filename): self.log.debug("Loading file %r", filename) file = MP4(encode_filename(filename)) if file.tags is None: file.add_tags() metadata = Metadata() for name, values in file.tags.items(): if name in self.__text_tags: for value in values: metadata.add(self.__text_tags[name], value) elif name in self.__bool_tags: metadata.add(self.__bool_tags[name], values and "1" or "0") elif name in self.__int_tags: for value in values: metadata.add(self.__int_tags[name], unicode(value)) elif name in self.__freeform_tags: for value in values: value = value.strip("\x00").decode("utf-8", "replace") metadata.add(self.__freeform_tags[name], value) elif name == "----:com.apple.iTunes:fingerprint": for value in values: value = value.strip("\x00").decode("utf-8", "replace") if value.startswith("MusicMagic Fingerprint"): metadata.add("musicip_fingerprint", value[22:]) elif name == "trkn": metadata["tracknumber"] = str(values[0][0]) metadata["totaltracks"] = str(values[0][1]) elif name == "disk": metadata["discnumber"] = str(values[0][0]) metadata["totaldiscs"] = str(values[0][1]) elif name == "covr": for value in values: value = MP4Cover(value) if value.imageformat == value.FORMAT_JPEG: metadata.add_image("image/jpeg", value) elif value.imageformat == value.FORMAT_PNG: metadata.add_image("image/png", value) self._info(metadata, file) return metadata
def _load(self, filename): log.debug("Loading file %r", filename) file = MP4(encode_filename(filename)) if file.tags is None: file.add_tags() metadata = Metadata() for name, values in file.tags.items(): if name in self.__text_tags: for value in values: metadata.add(self.__text_tags[name], value) elif name in self.__bool_tags: metadata.add(self.__bool_tags[name], values and '1' or '0') elif name in self.__int_tags: for value in values: metadata.add(self.__int_tags[name], unicode(value)) elif name in self.__freeform_tags: for value in values: value = value.strip("\x00").decode("utf-8", "replace") metadata.add(self.__freeform_tags[name], value) elif name == "----:com.apple.iTunes:fingerprint": for value in values: value = value.strip("\x00").decode("utf-8", "replace") if value.startswith("MusicMagic Fingerprint"): metadata.add("musicip_fingerprint", value[22:]) elif name == "trkn": metadata["tracknumber"] = str(values[0][0]) metadata["totaltracks"] = str(values[0][1]) elif name == "disk": metadata["discnumber"] = str(values[0][0]) metadata["totaldiscs"] = str(values[0][1]) elif name == "covr": for value in values: if value.imageformat == value.FORMAT_JPEG: metadata.add_image("image/jpeg", value) elif value.imageformat == value.FORMAT_PNG: metadata.add_image("image/png", value) self._info(metadata, file) return metadata
def _finalize_loading(self, error): if error: self.metadata.clear() self.status = _("[could not load album %s]") % self.id del self._new_metadata del self._new_tracks self.update() return if self._requests > 0: return if not self._tracks_loaded: artists = set() all_media = [] absolutetracknumber = 0 va = self._new_metadata['musicbrainz_albumartistid'] == VARIOUS_ARTISTS_ID djmix_ars = {} if hasattr(self._new_metadata, "_djmix_ars"): djmix_ars = self._new_metadata._djmix_ars for medium_node in self._release_node['media']: mm = Metadata() mm.copy(self._new_metadata) medium_to_metadata(medium_node, mm) discpregap = False format = medium_node.get('format') if format: all_media.append(format) for dj in djmix_ars.get(mm["discnumber"], []): mm.add("djmixer", dj) if 'discs' in medium_node: discids = [disc.get('id') for disc in medium_node['discs']] mm['~musicbrainz_discids'] = discids mm['musicbrainz_discid'] = list(self._discids.intersection(discids)) if "pregap" in medium_node: discpregap = True absolutetracknumber += 1 track = self._finalize_loading_track(medium_node['pregap'], mm, artists, va, absolutetracknumber, discpregap) track.metadata['~pregap'] = "1" track_count = medium_node['track-count'] if track_count: tracklist_node = medium_node['tracks'] for track_node in tracklist_node: absolutetracknumber += 1 track = self._finalize_loading_track(track_node, mm, artists, va, absolutetracknumber, discpregap) if "data-tracks" in medium_node: for track_node in medium_node['data-tracks']: absolutetracknumber += 1 track = self._finalize_loading_track(track_node, mm, artists, va, absolutetracknumber, discpregap) track.metadata['~datatrack'] = "1" totalalbumtracks = absolutetracknumber self._new_metadata['~totalalbumtracks'] = totalalbumtracks # Generate a list of unique media, but keep order of first appearance self._new_metadata['media'] = " / ".join(list(OrderedDict.fromkeys(all_media))) for track in self._new_tracks: track.metadata["~totalalbumtracks"] = totalalbumtracks if len(artists) > 1: track.metadata["~multiartist"] = "1" del self._release_node del self._release_artist_nodes self._tracks_loaded = True if not self._requests: self.enable_update_metadata_images(False) # Prepare parser for user's script for s_name, s_text in enabled_tagger_scripts_texts(): parser = ScriptParser() for track in self._new_tracks: # Run tagger script for each track try: parser.eval(s_text, track.metadata) except ScriptError: log.exception("Failed to run tagger script %s on track", s_name) track.metadata.strip_whitespace() # Run tagger script for the album itself try: parser.eval(s_text, self._new_metadata) except ScriptError: log.exception("Failed to run tagger script %s on album", s_name) self._new_metadata.strip_whitespace() for track in self.tracks: track.metadata_images_changed.connect(self.update_metadata_images) for file in list(track.linked_files): file.move(self.unmatched_files) self.metadata = self._new_metadata self.tracks = self._new_tracks del self._new_metadata del self._new_tracks self.loaded = True self.status = None self.match_files(self.unmatched_files.files) self.enable_update_metadata_images(True) self.update() self.tagger.window.set_statusbar_message( N_('Album %(id)s loaded: %(artist)s - %(album)s'), { 'id': self.id, 'artist': self.metadata['albumartist'], 'album': self.metadata['album'] }, timeout=3000 ) for func in self._after_load_callbacks: func() self._after_load_callbacks = []
def _load(self, filename): log.debug("Loading file %r", filename) file = self._get_file(encode_filename(filename)) tags = file.tags or {} # upgrade custom 2.3 frames to 2.4 for old, new in self.__upgrade.items(): if old in tags and new not in tags: f = tags.pop(old) tags.add(getattr(id3, new)(encoding=f.encoding, text=f.text)) metadata = Metadata() for frame in tags.values(): frameid = frame.FrameID if frameid in self.__translate: name = self.__translate[frameid] if frameid.startswith('T') or frameid == 'GRP1': for text in frame.text: if text: metadata.add(name, string_(text)) elif frameid == 'COMM': for text in frame.text: if text: metadata.add('%s:%s' % (name, frame.desc), string_(text)) else: metadata.add(name, string_(frame)) elif frameid == 'TIT1': itunes_compatible = config.setting[ 'itunes_compatible_grouping'] name = 'work' if itunes_compatible else 'grouping' for text in frame.text: if text: metadata.add(name, string_(text)) elif frameid == "TMCL": for role, name in frame.people: if role or name: metadata.add('performer:%s' % role, name) elif frameid == "TIPL": # If file is ID3v2.3, TIPL tag could contain TMCL # so we will test for TMCL values and add to TIPL if not TMCL for role, name in frame.people: if role in self._tipl_roles and name: metadata.add(self._tipl_roles[role], name) else: metadata.add('performer:%s' % role, name) elif frameid == 'TXXX': name = frame.desc if name in self.__translate_freetext: name = self.__translate_freetext[name] elif ((name in self.__rtranslate) != (name in self.__rtranslate_freetext)): # If the desc of a TXXX frame conflicts with the name of a # Picard tag, load it into ~id3:TXXX:desc rather than desc. # # This basically performs an XOR, making sure that 'name' # is in __rtranslate or __rtranslate_freetext, but not # both. (Being in both implies we support reading it both # ways.) Currently, the only tag in both is license. name = '~id3:TXXX:' + name for text in frame.text: metadata.add(name, string_(text)) elif frameid == 'USLT': name = 'lyrics' if frame.desc: name += ':%s' % frame.desc metadata.add(name, string_(frame.text)) elif frameid == 'UFID' and frame.owner == 'http://musicbrainz.org': metadata['musicbrainz_recordingid'] = frame.data.decode( 'ascii', 'ignore') elif frameid in self.__tag_re_parse.keys(): m = self.__tag_re_parse[frameid].search(frame.text[0]) if m: for name, value in m.groupdict().items(): if value is not None: metadata[name] = value else: log.error("Invalid %s value '%s' dropped in %r", frameid, frame.text[0], filename) elif frameid == 'APIC': try: coverartimage = TagCoverArtImage( file=filename, tag=frameid, types=types_from_id3(frame.type), comment=frame.desc, support_types=True, data=frame.data, ) except CoverArtImageError as e: log.error('Cannot load image from %r: %s' % (filename, e)) else: metadata.append_image(coverartimage) elif frameid == 'POPM': # Rating in ID3 ranges from 0 to 255, normalize this to the range 0 to 5 if frame.email == config.setting['rating_user_email']: rating = string_( int( round(frame.rating / 255.0 * (config.setting['rating_steps'] - 1)))) metadata.add('~rating', rating) if 'date' in metadata: sanitized = sanitize_date(metadata.getall('date')[0]) if sanitized: metadata['date'] = sanitized self._info(metadata, file) return metadata
def _load(self, filename): log.debug("Loading file %r", filename) self.__casemap = {} file = MP4(encode_filename(filename)) tags = file.tags or {} metadata = Metadata() for name, values in tags.items(): name_lower = name.lower() if name in self.__text_tags: for value in values: metadata.add(self.__text_tags[name], value) elif name in self.__bool_tags: metadata.add(self.__bool_tags[name], values and '1' or '0') elif name in self.__int_tags: for value in values: metadata.add(self.__int_tags[name], value) elif name in self.__freeform_tags: tag_name = self.__freeform_tags[name] _add_text_values_to_metadata(metadata, tag_name, values) elif name_lower in self.__freeform_tags_ci: tag_name = self.__freeform_tags_ci[name_lower] self.__casemap[tag_name] = name _add_text_values_to_metadata(metadata, tag_name, values) elif name == "----:com.apple.iTunes:fingerprint": for value in values: value = value.decode("utf-8", "replace").strip("\x00") if value.startswith("MusicMagic Fingerprint"): metadata.add("musicip_fingerprint", value[22:]) elif name == "trkn": try: metadata["tracknumber"] = values[0][0] metadata["totaltracks"] = values[0][1] except IndexError: log.debug('trkn is invalid, ignoring') elif name == "disk": try: metadata["discnumber"] = values[0][0] metadata["totaldiscs"] = values[0][1] except IndexError: log.debug('disk is invalid, ignoring') elif name == "covr": for value in values: if value.imageformat not in (value.FORMAT_JPEG, value.FORMAT_PNG): continue try: coverartimage = TagCoverArtImage( file=filename, tag=name, data=value, ) except CoverArtImageError as e: log.error('Cannot load image from %r: %s' % (filename, e)) else: metadata.images.append(coverartimage) # Read other freeform tags always case insensitive elif name.startswith('----:com.apple.iTunes:'): tag_name = name_lower[22:] self.__casemap[tag_name] = name[22:] if (name not in self.__r_text_tags and name not in self.__r_bool_tags and name not in self.__r_int_tags and name not in self.__r_freeform_tags and name_lower not in self.__r_freeform_tags_ci and name not in self.__other_supported_tags): _add_text_values_to_metadata(metadata, tag_name, values) self._info(metadata, file) return metadata
def _load(self, filename): log.debug("Loading file %r", filename) file = self._File(encode_filename(filename)) file.tags = file.tags or {} metadata = Metadata() for origname, values in file.tags.items(): for value in values: name = origname if name == "date" or name == "originaldate": # YYYY-00-00 => YYYY value = sanitize_date(value) elif name == "performer" or name == "comment": # transform "performer=Joe Barr (Piano)" to "performer:Piano=Joe Barr" name += ":" if value.endswith(")"): start = len(value) - 2 count = 1 while count > 0 and start > 0: if value[start] == ")": count += 1 elif value[start] == "(": count -= 1 start -= 1 if start > 0: name += value[start + 2 : -1] value = value[:start] elif name.startswith("rating"): try: name, email = name.split(":", 1) except ValueError: email = "" if email != config.setting["rating_user_email"]: continue name = "~rating" value = unicode(int(round((float(value) * (config.setting["rating_steps"] - 1))))) elif name == "fingerprint" and value.startswith("MusicMagic Fingerprint"): name = "musicip_fingerprint" value = value[22:] elif name == "tracktotal": if "totaltracks" in file.tags: continue name = "totaltracks" elif name == "disctotal": if "totaldiscs" in file.tags: continue name = "totaldiscs" elif name == "metadata_block_picture": image = mutagen.flac.Picture(base64.standard_b64decode(value)) imagetype = ID3_REVERSE_IMAGE_TYPE_MAP.get(image.type, "other") metadata.add_image(image.mime, image.data, description=image.desc, type_=imagetype) continue metadata.add(name, value) if self._File == mutagen.flac.FLAC: for image in file.pictures: imagetype = ID3_REVERSE_IMAGE_TYPE_MAP.get(image.type, "other") metadata.add_image(image.mime, image.data, description=image.desc, type_=imagetype) # Read the unofficial COVERART tags, for backward compatibillity only if not "metadata_block_picture" in file.tags: try: for index, data in enumerate(file["COVERART"]): metadata.add_image(file["COVERARTMIME"][index], base64.standard_b64decode(data)) except KeyError: pass self._info(metadata, file) return metadata
def _finalize_loading(self, error): if error: self.metadata.clear() self.status = _("[could not load album %s]") % self.id del self._new_metadata del self._new_tracks self.update() return if self._requests > 0: return if not self._tracks_loaded: artists = set() totalalbumtracks = 0 absolutetracknumber = 0 va = self._new_metadata[ 'musicbrainz_albumartistid'] == VARIOUS_ARTISTS_ID djmix_ars = {} if hasattr(self._new_metadata, "_djmix_ars"): djmix_ars = self._new_metadata._djmix_ars for medium_node in self._release_node.medium_list[0].medium: mm = Metadata() mm.copy(self._new_metadata) medium_to_metadata(medium_node, mm) discpregap = False for dj in djmix_ars.get(mm["discnumber"], []): mm.add("djmixer", dj) if "pregap" in medium_node.children: discpregap = True absolutetracknumber += 1 track = self._finalize_loading_track( medium_node.pregap[0], mm, artists, va, absolutetracknumber, discpregap) track.metadata['~pregap'] = "1" for track_node in medium_node.track_list[0].track: absolutetracknumber += 1 track = self._finalize_loading_track( track_node, mm, artists, va, absolutetracknumber, discpregap) if "data_track_list" in medium_node.children: for track_node in medium_node.data_track_list[0].track: absolutetracknumber += 1 track = self._finalize_loading_track( track_node, mm, artists, va, absolutetracknumber, discpregap) track.metadata['~datatrack'] = "1" totalalbumtracks = str(absolutetracknumber) for track in self._new_tracks: track.metadata["~totalalbumtracks"] = totalalbumtracks if len(artists) > 1: track.metadata["~multiartist"] = "1" del self._release_node self._tracks_loaded = True if not self._requests: # Prepare parser for user's script if config.setting["enable_tagger_script"]: script = config.setting["tagger_script"] if script: parser = ScriptParser() for track in self._new_tracks: # Run tagger script for each track try: parser.eval(script, track.metadata) except: self.error_append(traceback.format_exc()) # Strip leading/trailing whitespace track.metadata.strip_whitespace() # Run tagger script for the album itself try: parser.eval(script, self._new_metadata) except: self.error_append(traceback.format_exc()) self._new_metadata.strip_whitespace() for track in self.tracks: for file in list(track.linked_files): file.move(self.unmatched_files) self.metadata = self._new_metadata self.tracks = self._new_tracks del self._new_metadata del self._new_tracks self.loaded = True self.status = None self.match_files(self.unmatched_files.files) self.update() self.tagger.window.set_statusbar_message( N_('Album %(id)s loaded: %(artist)s - %(album)s'), { 'id': self.id, 'artist': self.metadata['albumartist'], 'album': self.metadata['album'] }, timeout=3000) for func in self._after_load_callbacks: func() self._after_load_callbacks = []
class MetadataTest(unittest.TestCase): original = None tags = [] def setUp(self): config.setting = settings.copy() self.metadata = Metadata() self.metadata["single1"] = "single1-value" self.metadata.add_unique("single2", "single2-value") self.metadata.add_unique("single2", "single2-value") self.multi1 = ["multi1-value", "multi1-value"] self.metadata.add("multi1", self.multi1[0]) self.metadata.add("multi1", self.multi1[1]) self.multi2 = ["multi2-value1", "multi2-value2"] self.metadata["multi2"] = self.multi2 self.multi3 = ["multi3-value1", "multi3-value2"] self.metadata.set("multi3", self.multi3) self.metadata["~hidden"] = "hidden-value" def tearDown(self): pass def test_metadata_set(self): self.assertEqual(["single1-value"], dict.get(self.metadata, "single1")) self.assertEqual(["single2-value"], dict.get(self.metadata, "single2")) self.assertEqual(self.multi1, dict.get(self.metadata, "multi1")) self.assertEqual(self.multi2, dict.get(self.metadata, "multi2")) self.assertEqual(self.multi3, dict.get(self.metadata, "multi3")) self.assertEqual(["hidden-value"], dict.get(self.metadata, "~hidden")) def test_metadata_get(self): self.assertEqual("single1-value", self.metadata["single1"]) self.assertEqual("single1-value", self.metadata.get("single1")) self.assertEqual(["single1-value"], self.metadata.getall("single1")) self.assertEqual(MULTI_VALUED_JOINER.join(self.multi1), self.metadata["multi1"]) self.assertEqual(MULTI_VALUED_JOINER.join(self.multi1), self.metadata.get("multi1")) self.assertEqual(self.multi1, self.metadata.getall("multi1")) self.assertEqual("", self.metadata["nonexistent"]) self.assertEqual(None, self.metadata.get("nonexistent")) self.assertEqual([], self.metadata.getall("nonexistent")) self.assertEqual(dict.items(self.metadata), self.metadata.rawitems()) metadata_items = [(x, z) for (x, y) in dict.items(self.metadata) for z in y] self.assertEqual(metadata_items, list(self.metadata.items())) def test_metadata_delete(self): self.metadata.delete("single1") self.assertNotIn("single1", self.metadata) self.assertIn("single1", self.metadata.deleted_tags) self.metadata["single2"] = "" self.assertNotIn("single2", self.metadata) self.assertIn("single2", self.metadata.deleted_tags) def test_metadata_update(self): m = Metadata() m["old"] = "old-value" self.metadata.delete("single1") m.update(self.metadata) self.assertIn("old", m) self.assertNotIn("single1", m) self.assertIn("single1", m.deleted_tags) self.assertEqual("single2-value", m["single2"]) self.assertEqual(self.metadata.deleted_tags, m.deleted_tags) self.metadata["old"] = "old-value" for (key, value) in dict.items(self.metadata): self.assertIn(key, m) self.assertEqual(value, dict.get(m, key)) for (key, value) in dict.items(m): self.assertIn(key, self.metadata) self.assertEqual(value, dict.get(self.metadata, key)) def test_metadata_clear(self): self.metadata.clear() self.assertEqual(0, len(self.metadata)) def test_metadata_applyfunc(self): func = lambda x: x[1:] self.metadata.apply_func(func) self.assertEqual("ingle1-value", self.metadata["single1"]) self.assertEqual("ingle1-value", self.metadata.get("single1")) self.assertEqual(["ingle1-value"], self.metadata.getall("single1")) self.assertEqual(MULTI_VALUED_JOINER.join(map(func, self.multi1)), self.metadata["multi1"]) self.assertEqual(MULTI_VALUED_JOINER.join(map(func, self.multi1)), self.metadata.get("multi1")) self.assertEqual(list(map(func, self.multi1)), self.metadata.getall("multi1")) self.assertEqual("", self.metadata["nonexistent"]) self.assertEqual(None, self.metadata.get("nonexistent")) self.assertEqual([], self.metadata.getall("nonexistent")) self.assertEqual(dict.items(self.metadata), self.metadata.rawitems()) metadata_items = [(x, z) for (x, y) in dict.items(self.metadata) for z in y] self.assertEqual(metadata_items, list(self.metadata.items())) def test_length_score(self): results = [(20000, 0, 0.333333333333), (20000, 10000, 0.666666666667), (20000, 20000, 1.0), (20000, 30000, 0.666666666667), (20000, 40000, 0.333333333333), (20000, 50000, 0.0)] for (a, b, expected) in results: actual = Metadata.length_score(a, b) self.assertAlmostEqual(expected, actual, msg="a={a}, b={b}".format(a=a, b=b))
class MetadataTest(PicardTestCase): original = None tags = [] def setUp(self): super().setUp() config.setting = settings.copy() self.metadata = Metadata() self.metadata["single1"] = "single1-value" self.metadata.add_unique("single2", "single2-value") self.metadata.add_unique("single2", "single2-value") self.multi1 = ["multi1-value", "multi1-value"] self.metadata.add("multi1", self.multi1[0]) self.metadata.add("multi1", self.multi1[1]) self.multi2 = ["multi2-value1", "multi2-value2"] self.metadata["multi2"] = self.multi2 self.multi3 = ["multi3-value1", "multi3-value2"] self.metadata.set("multi3", self.multi3) self.metadata["~hidden"] = "hidden-value" self.metadata_d1 = Metadata({ 'a': 'b', 'c': 2, 'd': ['x', 'y'], 'x': '' }) self.metadata_d2 = Metadata({ 'a': 'b', 'c': 2, 'd': ['x', 'y'], 'x': 'z' }) self.metadata_d3 = Metadata({'c': 3, 'd': ['u', 'w'], 'x': 'p'}) def tearDown(self): pass def test_metadata_setitem(self): self.assertEqual(["single1-value"], self.metadata.getraw("single1")) self.assertEqual(["single2-value"], self.metadata.getraw("single2")) self.assertEqual(self.multi1, self.metadata.getraw("multi1")) self.assertEqual(self.multi2, self.metadata.getraw("multi2")) self.assertEqual(self.multi3, self.metadata.getraw("multi3")) self.assertEqual(["hidden-value"], self.metadata.getraw("~hidden")) def test_metadata_get(self): self.assertEqual("single1-value", self.metadata["single1"]) self.assertEqual("single1-value", self.metadata.get("single1")) self.assertEqual(["single1-value"], self.metadata.getall("single1")) self.assertEqual(["single1-value"], self.metadata.getraw("single1")) self.assertEqual(MULTI_VALUED_JOINER.join(self.multi1), self.metadata["multi1"]) self.assertEqual(MULTI_VALUED_JOINER.join(self.multi1), self.metadata.get("multi1")) self.assertEqual(self.multi1, self.metadata.getall("multi1")) self.assertEqual(self.multi1, self.metadata.getraw("multi1")) self.assertEqual("", self.metadata["nonexistent"]) self.assertEqual(None, self.metadata.get("nonexistent")) self.assertEqual([], self.metadata.getall("nonexistent")) self.assertRaises(KeyError, self.metadata.getraw, "nonexistent") self.assertEqual(self.metadata._store.items(), self.metadata.rawitems()) metadata_items = [(x, z) for (x, y) in self.metadata.rawitems() for z in y] self.assertEqual(metadata_items, list(self.metadata.items())) def test_metadata_delete(self): self.metadata.delete("single1") self.assertNotIn("single1", self.metadata) self.assertIn("single1", self.metadata.deleted_tags) def test_metadata_implicit_delete(self): self.metadata["single2"] = "" self.assertNotIn("single2", self.metadata) self.assertIn("single2", self.metadata.deleted_tags) self.metadata["unknown"] = "" self.assertNotIn("unknown", self.metadata) self.assertNotIn("unknown", self.metadata.deleted_tags) def test_metadata_set_explicit_empty(self): self.metadata.delete("single1") self.metadata.set("single1", []) self.assertIn("single1", self.metadata) self.assertNotIn("single1", self.metadata.deleted_tags) self.assertEqual([], self.metadata.getall("single1")) def test_metadata_undelete(self): self.metadata.delete("single1") self.assertNotIn("single1", self.metadata) self.assertIn("single1", self.metadata.deleted_tags) self.metadata["single1"] = "value1" self.assertIn("single1", self.metadata) self.assertNotIn("single1", self.metadata.deleted_tags) def test_metadata_copy(self): m = Metadata() m["old"] = "old-value" self.metadata.delete("single1") m.copy(self.metadata) self.assertEqual(self.metadata._store, m._store) self.assertEqual(self.metadata.deleted_tags, m.deleted_tags) self.assertEqual(self.metadata.length, m.length) self.assertEqual(self.metadata.images, m.images) def test_metadata_copy_without_images(self): m = Metadata() m.copy(self.metadata, copy_images=False) self.assertEqual(self.metadata._store, m._store) self.assertEqual(self.metadata.deleted_tags, m.deleted_tags) self.assertEqual(self.metadata.length, m.length) self.assertEqual(ImageList(), m.images) def test_metadata_update(self): m = Metadata() m["old"] = "old-value" self.metadata.delete("single1") m.update(self.metadata) self.assertIn("old", m) self.assertNotIn("single1", m) self.assertIn("single1", m.deleted_tags) self.assertEqual("single2-value", m["single2"]) self.assertEqual(self.metadata.deleted_tags, m.deleted_tags) self.assertEqual(self.metadata.images, m.images) self.metadata["old"] = "old-value" self.assertEqual(self.metadata._store, m._store) def test_metadata_clear(self): self.metadata.clear() self.assertEqual(0, len(self.metadata)) def test_metadata_clear_deleted(self): self.metadata.delete("single1") self.assertIn("single1", self.metadata.deleted_tags) self.metadata.clear_deleted() self.assertNotIn("single1", self.metadata.deleted_tags) def test_metadata_applyfunc(self): def func(x): return x[1:] self.metadata.apply_func(func) self.assertEqual("ingle1-value", self.metadata["single1"]) self.assertEqual("ingle1-value", self.metadata.get("single1")) self.assertEqual(["ingle1-value"], self.metadata.getall("single1")) self.assertEqual(MULTI_VALUED_JOINER.join(map(func, self.multi1)), self.metadata["multi1"]) self.assertEqual(MULTI_VALUED_JOINER.join(map(func, self.multi1)), self.metadata.get("multi1")) self.assertEqual(list(map(func, self.multi1)), self.metadata.getall("multi1")) def test_metadata_applyfunc_preserve_tags(self): self.assertTrue(len(PRESERVED_TAGS) > 0) m = Metadata() m[PRESERVED_TAGS[0]] = 'value1' m['not_preserved'] = 'value2' def func(x): return x[1:] m.apply_func(func) self.assertEqual("value1", m[PRESERVED_TAGS[0]]) self.assertEqual("alue2", m['not_preserved']) def test_metadata_applyfunc_delete_tags(self): def func(x): return None metadata = Metadata(self.metadata) metadata.apply_func(func) self.assertEqual(0, len(metadata.rawitems())) self.assertEqual(self.metadata.keys(), metadata.deleted_tags) def test_length_score(self): results = [(20000, 0, 0.333333333333), (20000, 10000, 0.666666666667), (20000, 20000, 1.0), (20000, 30000, 0.666666666667), (20000, 40000, 0.333333333333), (20000, 50000, 0.0)] for (a, b, expected) in results: actual = Metadata.length_score(a, b) self.assertAlmostEqual(expected, actual, msg="a={a}, b={b}".format(a=a, b=b)) def test_compare_is_equal(self): m1 = Metadata() m1["title"] = "title1" m1["tracknumber"] = "2" m1.length = 360 m2 = Metadata() m2["title"] = "title1" m2["tracknumber"] = "2" m2.length = 360 self.assertEqual(m1.compare(m2), m2.compare(m1)) self.assertEqual(m1.compare(m2), 1) def test_compare_lengths(self): m1 = Metadata() m1.length = 360 m2 = Metadata() m2.length = 300 self.assertAlmostEqual(m1.compare(m2), 0.998) def test_compare_tracknumber_difference(self): m1 = Metadata() m1["tracknumber"] = "1" m2 = Metadata() m2["tracknumber"] = "2" self.assertEqual(m1.compare(m2), 0) def test_compare_deleted(self): m1 = Metadata() m1["artist"] = "TheArtist" m1["title"] = "title1" m2 = Metadata() m2["artist"] = "TheArtist" m2.delete("title") self.assertTrue(m1.compare(m2) < 1) def test_strip_whitespace(self): m1 = Metadata() m1["artist"] = " TheArtist " m1["title"] = "\t\u00A0 tit le1 \r\n" m1["genre"] = " \t" m1.strip_whitespace() self.assertEqual(m1["artist"], "TheArtist") self.assertEqual(m1["title"], "tit le1") def test_metadata_mapping_init(self): d = {'a': 'b', 'c': 2, 'd': ['x', 'y'], 'x': '', 'z': {'u', 'w'}} deleted_tags = set('c') m = Metadata(d, deleted_tags=deleted_tags, length=1234) self.assertTrue('a' in m) self.assertEqual(m.getraw('a'), ['b']) self.assertEqual(m['d'], MULTI_VALUED_JOINER.join(d['d'])) self.assertNotIn('c', m) self.assertNotIn('length', m) self.assertIn('c', m.deleted_tags) self.assertEqual(m.length, 1234) def test_metadata_mapping_init_zero(self): m = Metadata(tag1='a', tag2=0, tag3='', tag4=None) m['tag5'] = 0 m['tag1'] = '' self.assertIn('tag1', m.deleted_tags) self.assertEqual(m['tag2'], '0') self.assertNotIn('tag3', m) self.assertNotIn('tag4', m) self.assertEqual(m['tag5'], '0') def test_metadata_mapping_del(self): m = self.metadata_d1 self.assertEqual(m.getraw('a'), ['b']) self.assertNotIn('a', m.deleted_tags) self.assertNotIn('x', m.deleted_tags) self.assertRaises(KeyError, m.getraw, 'x') del m['a'] self.assertRaises(KeyError, m.getraw, 'a') self.assertIn('a', m.deleted_tags) # NOTE: historic behavior of Metadata.delete() # an attempt to delete an non-existing tag, will add it to the list # of deleted tags # so this will not raise a KeyError # as is it differs from dict or even defaultdict behavior del m['unknown'] self.assertIn('unknown', m.deleted_tags) def test_metadata_mapping_iter(self): l = set(self.metadata_d1) self.assertEqual(l, {'a', 'c', 'd'}) def test_metadata_mapping_keys(self): l = set(self.metadata_d1.keys()) self.assertEqual(l, {'a', 'c', 'd'}) def test_metadata_mapping_values(self): l = set(self.metadata_d1.values()) self.assertEqual(l, {'b', '2', 'x; y'}) def test_metadata_mapping_len(self): m = self.metadata_d1 self.assertEqual(len(m), 3) del m['x'] self.assertEqual(len(m), 3) del m['c'] self.assertEqual(len(m), 2) def _check_mapping_update(self, m): self.assertEqual(m['a'], 'b') self.assertEqual(m['c'], '3') self.assertEqual(m.getraw('d'), ['u', 'w']) self.assertEqual(m['x'], '') self.assertIn('x', m.deleted_tags) def test_metadata_mapping_update(self): # update from Metadata m = self.metadata_d2 m2 = self.metadata_d3 del m2['x'] m.update(m2) self._check_mapping_update(m) def test_metadata_mapping_update_dict(self): # update from dict m = self.metadata_d2 d2 = {'c': 3, 'd': ['u', 'w'], 'x': ''} m.update(d2) self._check_mapping_update(m) def test_metadata_mapping_update_tuple(self): # update from tuple m = self.metadata_d2 d2 = (('c', 3), ('d', ['u', 'w']), ('x', '')) m.update(d2) self._check_mapping_update(m) def test_metadata_mapping_update_dictlike(self): # update from kwargs m = self.metadata_d2 m.update(c=3, d=['u', 'w'], x='') self._check_mapping_update(m) def test_metadata_mapping_update_noparam(self): # update without parameter m = self.metadata_d2 self.assertRaises(TypeError, m.update) self.assertEqual(m['a'], 'b') def test_metadata_mapping_update_intparam(self): # update without parameter m = self.metadata_d2 self.assertRaises(TypeError, m.update, 123) def test_metadata_mapping_update_strparam(self): # update without parameter m = self.metadata_d2 self.assertRaises(ValueError, m.update, 'abc') def test_metadata_mapping_update_kw(self): m = Metadata(tag1='a', tag2='b') m.update(tag1='c') self.assertEqual(m['tag1'], 'c') self.assertEqual(m['tag2'], 'b') m.update(tag2='') self.assertIn('tag2', m.deleted_tags) def test_metadata_mapping_update_kw_del(self): m = Metadata(tag1='a', tag2='b') del m['tag1'] m2 = Metadata(tag1='c', tag2='d') del m2['tag2'] m.update(m2) self.assertEqual(m['tag1'], 'c') self.assertNotIn('tag2', m) self.assertNotIn('tag1', m.deleted_tags) self.assertIn('tag2', m.deleted_tags) def test_metadata_mapping_images(self): image1 = create_image(b'A', comment='A') image2 = create_image(b'B', comment='B') m1 = Metadata(a='b', length=1234, images=[image1]) self.assertEqual(m1.images[0], image1) self.assertEqual(len(m1), 2) # one tag, one image m1.images.append(image2) self.assertEqual(m1.images[1], image2) m1.images.pop(0) self.assertEqual(m1.images[0], image2) m2 = Metadata(a='c', length=4567, images=[image1]) m1.update(m2) self.assertEqual(m1.images[0], image1) m1.images.pop(0) self.assertEqual(len(m1), 1) # one tag, zero image self.assertFalse(m1.images) def test_metadata_mapping_iterable(self): m = Metadata(tag_tuple=('a', 0)) m['tag_set'] = {'c', 'd'} m['tag_dict'] = {'e': 1, 'f': 2} m['tag_str'] = 'gh' self.assertIn('0', m.getraw('tag_tuple')) self.assertIn('c', m.getraw('tag_set')) self.assertIn('e', m.getraw('tag_dict')) self.assertIn('gh', m.getraw('tag_str'))
def _finalize_loading(self, error): if error: self.metadata.clear() self.status = _("[could not load album %s]") % self.id del self._new_metadata del self._new_tracks self.update() return if self._requests > 0: return if not self._tracks_loaded: artists = set() totalalbumtracks = 0 absolutetracknumber = 0 va = self._new_metadata['musicbrainz_albumartistid'] == VARIOUS_ARTISTS_ID djmix_ars = {} if hasattr(self._new_metadata, "_djmix_ars"): djmix_ars = self._new_metadata._djmix_ars for medium_node in self._release_node.medium_list[0].medium: mm = Metadata() mm.copy(self._new_metadata) medium_to_metadata(medium_node, mm) discpregap = False for dj in djmix_ars.get(mm["discnumber"], []): mm.add("djmixer", dj) if "pregap" in medium_node.children: discpregap = True absolutetracknumber += 1 track = self._finalize_loading_track(medium_node.pregap[0], mm, artists, va, absolutetracknumber, discpregap) track.metadata['~pregap'] = "1" for track_node in medium_node.track_list[0].track: absolutetracknumber += 1 track = self._finalize_loading_track(track_node, mm, artists, va, absolutetracknumber, discpregap) if "data_track_list" in medium_node.children: for track_node in medium_node.data_track_list[0].track: absolutetracknumber += 1 track = self._finalize_loading_track(track_node, mm, artists, va, absolutetracknumber, discpregap) track.metadata['~datatrack'] = "1" totalalbumtracks = str(absolutetracknumber) for track in self._new_tracks: track.metadata["~totalalbumtracks"] = totalalbumtracks if len(artists) > 1: track.metadata["~multiartist"] = "1" del self._release_node self._tracks_loaded = True if not self._requests: # Prepare parser for user's script if config.setting["enable_tagger_script"]: script = config.setting["tagger_script"] if script: parser = ScriptParser() for track in self._new_tracks: # Run tagger script for each track try: parser.eval(script, track.metadata) except: self.error_append(traceback.format_exc()) # Strip leading/trailing whitespace track.metadata.strip_whitespace() # Run tagger script for the album itself try: parser.eval(script, self._new_metadata) except: self.error_append(traceback.format_exc()) self._new_metadata.strip_whitespace() for track in self.tracks: for file in list(track.linked_files): file.move(self.unmatched_files) self.metadata = self._new_metadata self.tracks = self._new_tracks del self._new_metadata del self._new_tracks self.loaded = True self.status = None self.match_files(self.unmatched_files.files) self.update() self.tagger.window.set_statusbar_message( N_('Album %(id)s loaded: %(artist)s - %(album)s'), { 'id': self.id, 'artist': self.metadata['albumartist'], 'album': self.metadata['album'] }, timeout=3000 ) for func in self._after_load_callbacks: func() self._after_load_callbacks = []
def _load(self, filename): self.log.debug("Loading file %r", filename) file = self._File(encode_filename(filename), ID3=compatid3.CompatID3) tags = file.tags or {} # upgrade custom 2.3 frames to 2.4 for old, new in self.__upgrade.items(): if old in tags and new not in tags: f = tags.pop(old) tags.add(getattr(id3, new)(encoding=f.encoding, text=f.text)) metadata = Metadata() for frame in tags.values(): frameid = frame.FrameID if frameid in self.__translate: name = self.__translate[frameid] if frameid.startswith('T'): for text in frame.text: if text: metadata.add(name, unicode(text)) elif frameid == 'COMM': for text in frame.text: if text: metadata.add('%s:%s' % (name, frame.desc), unicode(text)) else: metadata.add(name, unicode(frame)) elif frameid == "TMCL": for role, name in frame.people: if role or name: metadata.add('performer:%s' % role, name) elif frameid == "TIPL": for role, name in frame.people: if role in self.__tipl_roles and name: metadata.add(self.__tipl_roles[role], name) elif frameid == 'TXXX' and frame.desc in self.__translate_freetext: name = self.__translate_freetext[frame.desc] for text in frame.text: metadata.add(name, unicode(text)) elif frameid == 'USLT': name = 'lyrics' if frame.desc: name += frame.desc metadata.add(name, unicode(frame.text)) elif frameid == 'UFID' and frame.owner == 'http://musicbrainz.org': metadata['musicbrainz_trackid'] = unicode(frame.data) elif frameid == 'TRCK': value = frame.text[0].split('/') if len(value) > 1: metadata['tracknumber'], metadata['totaltracks'] = value[:2] else: metadata['tracknumber'] = value[0] elif frameid == 'TPOS': value = frame.text[0].split('/') if len(value) > 1: metadata['discnumber'], metadata['totaldiscs'] = value[:2] else: metadata['discnumber'] = value[0] elif frameid == 'APIC': metadata.add_image(frame.mime, frame.data) elif frameid == 'POPM': # Rating in ID3 ranges from 0 to 255, normalize this to the range 0 to 5 if frame.email == self.config.setting['rating_user_email']: rating = unicode(int(round(frame.rating / 255.0 * (self.config.setting['rating_steps'] - 1)))) metadata.add('~rating', rating) if 'date' in metadata: metadata['date'] = sanitize_date(metadata.getall('date')[0]) self._info(metadata, file) return metadata
class MetadataTest(unittest.TestCase): original = None tags = [] def setUp(self): config.setting = settings.copy() self.metadata = Metadata() self.metadata["single1"] = "single1-value" self.metadata.add_unique("single2", "single2-value") self.metadata.add_unique("single2", "single2-value") self.multi1 = ["multi1-value", "multi1-value"] self.metadata.add("multi1", self.multi1[0]) self.metadata.add("multi1", self.multi1[1]) self.multi2 = ["multi2-value1", "multi2-value2"] self.metadata["multi2"] = self.multi2 self.multi3 = ["multi3-value1", "multi3-value2"] self.metadata.set("multi3", self.multi3) self.metadata["~hidden"] = "hidden-value" def tearDown(self): pass def test_metadata_set(self): self.assertEqual(["single1-value"], dict.get(self.metadata,"single1")) self.assertEqual(["single2-value"], dict.get(self.metadata,"single2")) self.assertEqual(self.multi1, dict.get(self.metadata,"multi1")) self.assertEqual(self.multi2, dict.get(self.metadata,"multi2")) self.assertEqual(self.multi3, dict.get(self.metadata,"multi3")) self.assertEqual(["hidden-value"], dict.get(self.metadata,"~hidden")) def test_metadata_get(self): self.assertEqual("single1-value", self.metadata["single1"]) self.assertEqual("single1-value", self.metadata.get("single1")) self.assertEqual(["single1-value"], self.metadata.getall("single1")) self.assertEqual(MULTI_VALUED_JOINER.join(self.multi1), self.metadata["multi1"]) self.assertEqual(MULTI_VALUED_JOINER.join(self.multi1), self.metadata.get("multi1")) self.assertEqual(self.multi1, self.metadata.getall("multi1")) self.assertEqual("", self.metadata["nonexistent"]) self.assertEqual(None, self.metadata.get("nonexistent")) self.assertEqual([], self.metadata.getall("nonexistent")) self.assertEqual(dict.items(self.metadata), self.metadata.rawitems()) metadata_items = [(x, z) for (x, y) in dict.items(self.metadata) for z in y] self.assertEqual(metadata_items, list(self.metadata.items())) def test_metadata_delete(self): self.metadata.delete("single1") self.assertNotIn("single1", self.metadata) self.assertIn("single1", self.metadata.deleted_tags) self.metadata["single2"] = "" self.assertNotIn("single2", self.metadata) self.assertIn("single2", self.metadata.deleted_tags) def test_metadata_update(self): m = Metadata() m["old"] = "old-value" self.metadata.delete("single1") m.update(self.metadata) self.assertIn("old", m) self.assertNotIn("single1", m) self.assertIn("single1", m.deleted_tags) self.assertEqual("single2-value", m["single2"]) self.assertEqual(self.metadata.deleted_tags, m.deleted_tags) self.metadata["old"] = "old-value" for (key, value) in dict.items(self.metadata): self.assertIn(key, m) self.assertEqual(value, dict.get(m, key)) for (key, value) in dict.items(m): self.assertIn(key, self.metadata) self.assertEqual(value, dict.get(self.metadata, key)) def test_metadata_clear(self): self.metadata.clear() self.assertEqual(0, len(self.metadata)) def test_metadata_applyfunc(self): func = lambda x: x[1:] self.metadata.apply_func(func) self.assertEqual("ingle1-value", self.metadata["single1"]) self.assertEqual("ingle1-value", self.metadata.get("single1")) self.assertEqual(["ingle1-value"], self.metadata.getall("single1")) self.assertEqual(MULTI_VALUED_JOINER.join(map(func, self.multi1)), self.metadata["multi1"]) self.assertEqual(MULTI_VALUED_JOINER.join(map(func, self.multi1)), self.metadata.get("multi1")) self.assertEqual(list(map(func, self.multi1)), self.metadata.getall("multi1")) self.assertEqual("", self.metadata["nonexistent"]) self.assertEqual(None, self.metadata.get("nonexistent")) self.assertEqual([], self.metadata.getall("nonexistent")) self.assertEqual(dict.items(self.metadata), self.metadata.rawitems()) metadata_items = [(x, z) for (x, y) in dict.items(self.metadata) for z in y] self.assertEqual(metadata_items, list(self.metadata.items()))
class MetadataTest(PicardTestCase): original = None tags = [] def setUp(self): super().setUp() config.setting = settings.copy() self.metadata = Metadata() self.metadata["single1"] = "single1-value" self.metadata.add_unique("single2", "single2-value") self.metadata.add_unique("single2", "single2-value") self.multi1 = ["multi1-value", "multi1-value"] self.metadata.add("multi1", self.multi1[0]) self.metadata.add("multi1", self.multi1[1]) self.multi2 = ["multi2-value1", "multi2-value2"] self.metadata["multi2"] = self.multi2 self.multi3 = ["multi3-value1", "multi3-value2"] self.metadata.set("multi3", self.multi3) self.metadata["~hidden"] = "hidden-value" def tearDown(self): pass def test_metadata_setitem(self): self.assertEqual(["single1-value"], dict.get(self.metadata, "single1")) self.assertEqual(["single2-value"], dict.get(self.metadata, "single2")) self.assertEqual(self.multi1, dict.get(self.metadata, "multi1")) self.assertEqual(self.multi2, dict.get(self.metadata, "multi2")) self.assertEqual(self.multi3, dict.get(self.metadata, "multi3")) self.assertEqual(["hidden-value"], dict.get(self.metadata, "~hidden")) def test_metadata_get(self): self.assertEqual("single1-value", self.metadata["single1"]) self.assertEqual("single1-value", self.metadata.get("single1")) self.assertEqual(["single1-value"], self.metadata.getall("single1")) self.assertEqual(MULTI_VALUED_JOINER.join(self.multi1), self.metadata["multi1"]) self.assertEqual(MULTI_VALUED_JOINER.join(self.multi1), self.metadata.get("multi1")) self.assertEqual(self.multi1, self.metadata.getall("multi1")) self.assertEqual("", self.metadata["nonexistent"]) self.assertEqual(None, self.metadata.get("nonexistent")) self.assertEqual([], self.metadata.getall("nonexistent")) self.assertEqual(dict.items(self.metadata), self.metadata.rawitems()) metadata_items = [(x, z) for (x, y) in dict.items(self.metadata) for z in y] self.assertEqual(metadata_items, list(self.metadata.items())) def test_metadata_delete(self): self.metadata.delete("single1") self.assertNotIn("single1", self.metadata) self.assertIn("single1", self.metadata.deleted_tags) def test_metadata_implicit_delete(self): self.metadata["single2"] = "" self.assertNotIn("single2", self.metadata) self.assertIn("single2", self.metadata.deleted_tags) self.metadata["unknown"] = "" self.assertNotIn("unknown", self.metadata) self.assertNotIn("unknown", self.metadata.deleted_tags) def test_metadata_set_explicit_empty(self): self.metadata.delete("single1") self.metadata.set("single1", []) self.assertIn("single1", self.metadata) self.assertNotIn("single1", self.metadata.deleted_tags) self.assertEqual([], self.metadata.getall("single1")) def test_metadata_undelete(self): self.metadata.delete("single1") self.assertNotIn("single1", self.metadata) self.assertIn("single1", self.metadata.deleted_tags) self.metadata["single1"] = "value1" self.assertIn("single1", self.metadata) self.assertNotIn("single1", self.metadata.deleted_tags) def test_metadata_update(self): m = Metadata() m["old"] = "old-value" self.metadata.delete("single1") m.update(self.metadata) self.assertIn("old", m) self.assertNotIn("single1", m) self.assertIn("single1", m.deleted_tags) self.assertEqual("single2-value", m["single2"]) self.assertEqual(self.metadata.deleted_tags, m.deleted_tags) self.metadata["old"] = "old-value" for (key, value) in dict.items(self.metadata): self.assertIn(key, m) self.assertEqual(value, dict.get(m, key)) for (key, value) in dict.items(m): self.assertIn(key, self.metadata) self.assertEqual(value, dict.get(self.metadata, key)) def test_metadata_clear(self): self.metadata.clear() self.assertEqual(0, len(self.metadata)) def test_metadata_clear_deleted(self): self.metadata.delete("single1") self.assertIn("single1", self.metadata.deleted_tags) self.metadata.clear_deleted() self.assertNotIn("single1", self.metadata.deleted_tags) def test_metadata_applyfunc(self): def func(x): return x[1:] self.metadata.apply_func(func) self.assertEqual("ingle1-value", self.metadata["single1"]) self.assertEqual("ingle1-value", self.metadata.get("single1")) self.assertEqual(["ingle1-value"], self.metadata.getall("single1")) self.assertEqual(MULTI_VALUED_JOINER.join(map(func, self.multi1)), self.metadata["multi1"]) self.assertEqual(MULTI_VALUED_JOINER.join(map(func, self.multi1)), self.metadata.get("multi1")) self.assertEqual(list(map(func, self.multi1)), self.metadata.getall("multi1")) self.assertEqual("", self.metadata["nonexistent"]) self.assertEqual(None, self.metadata.get("nonexistent")) self.assertEqual([], self.metadata.getall("nonexistent")) self.assertEqual(dict.items(self.metadata), self.metadata.rawitems()) metadata_items = [(x, z) for (x, y) in dict.items(self.metadata) for z in y] self.assertEqual(metadata_items, list(self.metadata.items())) def test_length_score(self): results = [(20000, 0, 0.333333333333), (20000, 10000, 0.666666666667), (20000, 20000, 1.0), (20000, 30000, 0.666666666667), (20000, 40000, 0.333333333333), (20000, 50000, 0.0)] for (a, b, expected) in results: actual = Metadata.length_score(a, b) self.assertAlmostEqual(expected, actual, msg="a={a}, b={b}".format(a=a, b=b)) def test_compare_is_equal(self): m1 = Metadata() m1["title"] = "title1" m1["tracknumber"] = "2" m1.length = 360 m2 = Metadata() m2["title"] = "title1" m2["tracknumber"] = "2" m2.length = 360 self.assertEqual(m1.compare(m2), m2.compare(m1)) self.assertEqual(m1.compare(m2), 1) def test_compare_lengths(self): m1 = Metadata() m1.length = 360 m2 = Metadata() m2.length = 300 self.assertAlmostEqual(m1.compare(m2), 0.998) def test_compare_tracknumber_difference(self): m1 = Metadata() m1["tracknumber"] = "1" m2 = Metadata() m2["tracknumber"] = "2" self.assertEqual(m1.compare(m2), 0) def test_compare_deleted(self): m1 = Metadata() m1["artist"] = "TheArtist" m1["title"] = "title1" m2 = Metadata() m2["artist"] = "TheArtist" m2.delete("title") self.assertTrue(m1.compare(m2) < 1)
def _load(self, filename): log.debug("Loading file %r", filename) file = self._File(encode_filename(filename)) file.tags = file.tags or {} metadata = Metadata() for origname, values in file.tags.items(): for value in values: name = origname if name == "date" or name == "originaldate": # YYYY-00-00 => YYYY value = sanitize_date(value) elif name == 'performer' or name == 'comment': # transform "performer=Joe Barr (Piano)" to "performer:Piano=Joe Barr" name += ':' if value.endswith(')'): start = len(value) - 2 count = 1 while count > 0 and start > 0: if value[start] == ')': count += 1 elif value[start] == '(': count -= 1 start -= 1 if start > 0: name += value[start + 2:-1] value = value[:start] elif name.startswith('rating'): try: name, email = name.split(':', 1) except ValueError: email = '' if email != config.setting['rating_user_email']: continue name = '~rating' value = unicode(int(round((float(value) * (config.setting['rating_steps'] - 1))))) elif name == "fingerprint" and value.startswith("MusicMagic Fingerprint"): name = "musicip_fingerprint" value = value[22:] elif name == "tracktotal": if "totaltracks" in file.tags: continue name = "totaltracks" elif name == "disctotal": if "totaldiscs" in file.tags: continue name = "totaldiscs" elif name == "metadata_block_picture": image = mutagen.flac.Picture(base64.standard_b64decode(value)) try: coverartimage = TagCoverArtImage( file=filename, tag=name, types=types_from_id3(image.type), comment=image.desc, support_types=True, data=image.data, ) except CoverArtImageError as e: log.error('Cannot load image from %r: %s' % (filename, e)) else: metadata.append_image(coverartimage) continue elif name in self.__translate: name = self.__translate[name] metadata.add(name, value) if self._File == mutagen.flac.FLAC: for image in file.pictures: try: coverartimage = TagCoverArtImage( file=filename, tag='FLAC/PICTURE', types=types_from_id3(image.type), comment=image.desc, support_types=True, data=image.data, ) except CoverArtImageError as e: log.error('Cannot load image from %r: %s' % (filename, e)) else: metadata.append_image(coverartimage) # Read the unofficial COVERART tags, for backward compatibillity only if not "metadata_block_picture" in file.tags: try: for data in file["COVERART"]: try: coverartimage = TagCoverArtImage( file=filename, tag='COVERART', data=base64.standard_b64decode(data) ) except CoverArtImageError as e: log.error('Cannot load image from %r: %s' % (filename, e)) else: metadata.append_image(coverartimage) except KeyError: pass self._info(metadata, file) return metadata
def _finalize_loading(self, error): if error: self.metadata.clear() self.status = _("[could not load album %s]") % self.id del self._new_metadata del self._new_tracks self.update() return if self._requests > 0: return if not self._tracks_loaded: artists = set() totalalbumtracks = 0 absolutetracknumber = 0 va = self._new_metadata['musicbrainz_albumartistid'] == VARIOUS_ARTISTS_ID djmix_ars = {} if hasattr(self._new_metadata, "_djmix_ars"): djmix_ars = self._new_metadata._djmix_ars for medium_node in self._release_node['media']: mm = Metadata() mm.copy(self._new_metadata) medium_to_metadata(medium_node, mm) discpregap = False for dj in djmix_ars.get(mm["discnumber"], []): mm.add("djmixer", dj) if 'discs' in medium_node: discids = [disc.get('id') for disc in medium_node['discs']] mm['~musicbrainz_discids'] = discids mm['musicbrainz_discid'] = list(self._discids.intersection(discids)) if "pregap" in medium_node: discpregap = True absolutetracknumber += 1 track = self._finalize_loading_track(medium_node['pregap'], mm, artists, va, absolutetracknumber, discpregap) track.metadata['~pregap'] = "1" track_count = medium_node['track-count'] if track_count: tracklist_node = medium_node['tracks'] for track_node in tracklist_node: absolutetracknumber += 1 track = self._finalize_loading_track(track_node, mm, artists, va, absolutetracknumber, discpregap) if "data-tracks" in medium_node: for track_node in medium_node['data-tracks']: absolutetracknumber += 1 track = self._finalize_loading_track(track_node, mm, artists, va, absolutetracknumber, discpregap) track.metadata['~datatrack'] = "1" totalalbumtracks = str(absolutetracknumber) for track in self._new_tracks: track.metadata["~totalalbumtracks"] = totalalbumtracks if len(artists) > 1: track.metadata["~multiartist"] = "1" del self._release_node del self._release_artist_nodes self._tracks_loaded = True if not self._requests: self.enable_update_metadata_images(False) # Prepare parser for user's script for s_name, s_text in enabled_tagger_scripts_texts(): parser = ScriptParser() for track in self._new_tracks: # Run tagger script for each track try: parser.eval(s_text, track.metadata) except ScriptError: log.exception("Failed to run tagger script %s on track", s_name) track.metadata.strip_whitespace() # Run tagger script for the album itself try: parser.eval(s_text, self._new_metadata) except ScriptError: log.exception("Failed to run tagger script %s on album", s_name) self._new_metadata.strip_whitespace() for track in self.tracks: track.metadata_images_changed.connect(self.update_metadata_images) for file in list(track.linked_files): file.move(self.unmatched_files) self.metadata = self._new_metadata self.tracks = self._new_tracks del self._new_metadata del self._new_tracks self.loaded = True self.status = None self.match_files(self.unmatched_files.files) self.enable_update_metadata_images(True) self.update() self.tagger.window.set_statusbar_message( N_('Album %(id)s loaded: %(artist)s - %(album)s'), { 'id': self.id, 'artist': self.metadata['albumartist'], 'album': self.metadata['album'] }, timeout=3000 ) for func in self._after_load_callbacks: func() self._after_load_callbacks = []
class MetadataTest(PicardTestCase): original = None tags = [] def setUp(self): super().setUp() config.setting = settings.copy() self.metadata = Metadata() self.metadata["single1"] = "single1-value" self.metadata.add_unique("single2", "single2-value") self.metadata.add_unique("single2", "single2-value") self.multi1 = ["multi1-value", "multi1-value"] self.metadata.add("multi1", self.multi1[0]) self.metadata.add("multi1", self.multi1[1]) self.multi2 = ["multi2-value1", "multi2-value2"] self.metadata["multi2"] = self.multi2 self.multi3 = ["multi3-value1", "multi3-value2"] self.metadata.set("multi3", self.multi3) self.metadata["~hidden"] = "hidden-value" self.metadata_d1 = Metadata({'a': 'b', 'c': 2, 'd': ['x', 'y'], 'x': ''}) self.metadata_d2 = Metadata({'a': 'b', 'c': 2, 'd': ['x', 'y'], 'x': 'z'}) self.metadata_d3 = Metadata({'c': 3, 'd': ['u', 'w'], 'x': 'p'}) def tearDown(self): pass def test_metadata_setitem(self): self.assertEqual(["single1-value"], self.metadata.getraw("single1")) self.assertEqual(["single2-value"], self.metadata.getraw("single2")) self.assertEqual(self.multi1, self.metadata.getraw("multi1")) self.assertEqual(self.multi2, self.metadata.getraw("multi2")) self.assertEqual(self.multi3, self.metadata.getraw("multi3")) self.assertEqual(["hidden-value"], self.metadata.getraw("~hidden")) def test_metadata_get(self): self.assertEqual("single1-value", self.metadata["single1"]) self.assertEqual("single1-value", self.metadata.get("single1")) self.assertEqual(["single1-value"], self.metadata.getall("single1")) self.assertEqual(["single1-value"], self.metadata.getraw("single1")) self.assertEqual(MULTI_VALUED_JOINER.join(self.multi1), self.metadata["multi1"]) self.assertEqual(MULTI_VALUED_JOINER.join(self.multi1), self.metadata.get("multi1")) self.assertEqual(self.multi1, self.metadata.getall("multi1")) self.assertEqual(self.multi1, self.metadata.getraw("multi1")) self.assertEqual("", self.metadata["nonexistent"]) self.assertEqual(None, self.metadata.get("nonexistent")) self.assertEqual([], self.metadata.getall("nonexistent")) self.assertRaises(KeyError, self.metadata.getraw, "nonexistent") self.assertEqual(self.metadata._store.items(), self.metadata.rawitems()) metadata_items = [(x, z) for (x, y) in self.metadata.rawitems() for z in y] self.assertEqual(metadata_items, list(self.metadata.items())) def test_metadata_delete(self): self.metadata.delete("single1") self.assertNotIn("single1", self.metadata) self.assertIn("single1", self.metadata.deleted_tags) def test_metadata_implicit_delete(self): self.metadata["single2"] = "" self.assertNotIn("single2", self.metadata) self.assertIn("single2", self.metadata.deleted_tags) self.metadata["unknown"] = "" self.assertNotIn("unknown", self.metadata) self.assertNotIn("unknown", self.metadata.deleted_tags) def test_metadata_set_explicit_empty(self): self.metadata.delete("single1") self.metadata.set("single1", []) self.assertIn("single1", self.metadata) self.assertNotIn("single1", self.metadata.deleted_tags) self.assertEqual([], self.metadata.getall("single1")) def test_metadata_undelete(self): self.metadata.delete("single1") self.assertNotIn("single1", self.metadata) self.assertIn("single1", self.metadata.deleted_tags) self.metadata["single1"] = "value1" self.assertIn("single1", self.metadata) self.assertNotIn("single1", self.metadata.deleted_tags) def test_metadata_update(self): m = Metadata() m["old"] = "old-value" self.metadata.delete("single1") m.update(self.metadata) self.assertIn("old", m) self.assertNotIn("single1", m) self.assertIn("single1", m.deleted_tags) self.assertEqual("single2-value", m["single2"]) self.assertEqual(self.metadata.deleted_tags, m.deleted_tags) self.metadata["old"] = "old-value" for (key, value) in self.metadata.rawitems(): self.assertIn(key, m) self.assertEqual(value, m.getraw(key)) for (key, value) in m.rawitems(): self.assertIn(key, self.metadata) self.assertEqual(value, self.metadata.getraw(key)) def test_metadata_clear(self): self.metadata.clear() self.assertEqual(0, len(self.metadata)) def test_metadata_clear_deleted(self): self.metadata.delete("single1") self.assertIn("single1", self.metadata.deleted_tags) self.metadata.clear_deleted() self.assertNotIn("single1", self.metadata.deleted_tags) def test_metadata_applyfunc(self): def func(x): return x[1:] self.metadata.apply_func(func) self.assertEqual("ingle1-value", self.metadata["single1"]) self.assertEqual("ingle1-value", self.metadata.get("single1")) self.assertEqual(["ingle1-value"], self.metadata.getall("single1")) self.assertEqual(MULTI_VALUED_JOINER.join(map(func, self.multi1)), self.metadata["multi1"]) self.assertEqual(MULTI_VALUED_JOINER.join(map(func, self.multi1)), self.metadata.get("multi1")) self.assertEqual(list(map(func, self.multi1)), self.metadata.getall("multi1")) def test_metadata_applyfunc_preserve_tags(self): self.assertTrue(len(PRESERVED_TAGS) > 0) m = Metadata() m[PRESERVED_TAGS[0]] = 'value1' m['not_preserved'] = 'value2' def func(x): return x[1:] m.apply_func(func) self.assertEqual("value1", m[PRESERVED_TAGS[0]]) self.assertEqual("alue2", m['not_preserved']) def test_length_score(self): results = [(20000, 0, 0.333333333333), (20000, 10000, 0.666666666667), (20000, 20000, 1.0), (20000, 30000, 0.666666666667), (20000, 40000, 0.333333333333), (20000, 50000, 0.0)] for (a, b, expected) in results: actual = Metadata.length_score(a, b) self.assertAlmostEqual(expected, actual, msg="a={a}, b={b}".format(a=a, b=b)) def test_compare_is_equal(self): m1 = Metadata() m1["title"] = "title1" m1["tracknumber"] = "2" m1.length = 360 m2 = Metadata() m2["title"] = "title1" m2["tracknumber"] = "2" m2.length = 360 self.assertEqual(m1.compare(m2), m2.compare(m1)) self.assertEqual(m1.compare(m2), 1) def test_compare_lengths(self): m1 = Metadata() m1.length = 360 m2 = Metadata() m2.length = 300 self.assertAlmostEqual(m1.compare(m2), 0.998) def test_compare_tracknumber_difference(self): m1 = Metadata() m1["tracknumber"] = "1" m2 = Metadata() m2["tracknumber"] = "2" self.assertEqual(m1.compare(m2), 0) def test_compare_deleted(self): m1 = Metadata() m1["artist"] = "TheArtist" m1["title"] = "title1" m2 = Metadata() m2["artist"] = "TheArtist" m2.delete("title") self.assertTrue(m1.compare(m2) < 1) def test_strip_whitespace(self): m1 = Metadata() m1["artist"] = " TheArtist " m1["title"] = "\t\u00A0 tit le1 \r\n" m1.strip_whitespace() self.assertEqual(m1["artist"], "TheArtist") self.assertEqual(m1["title"], "tit le1") def test_metadata_mapping_init(self): d = {'a': 'b', 'c': 2, 'd': ['x', 'y'], 'x': '', 'z': {'u', 'w'}} deleted_tags = set('c') m = Metadata(d, deleted_tags=deleted_tags, length=1234) self.assertTrue('a' in m) self.assertEqual(m.getraw('a'), ['b']) self.assertEqual(m['d'], MULTI_VALUED_JOINER.join(d['d'])) self.assertNotIn('c', m) self.assertNotIn('length', m) self.assertIn('c', m.deleted_tags) self.assertEqual(m.length, 1234) def test_metadata_mapping_init_zero(self): m = Metadata(tag1='a', tag2=0, tag3='', tag4=None) m['tag5'] = 0 m['tag1'] = '' self.assertIn('tag1', m.deleted_tags) self.assertEqual(m['tag2'], '0') self.assertNotIn('tag3', m) self.assertNotIn('tag4', m) self.assertEqual(m['tag5'], '0') def test_metadata_mapping_del(self): m = self.metadata_d1 self.assertEqual(m.getraw('a'), ['b']) self.assertNotIn('a', m.deleted_tags) self.assertNotIn('x', m.deleted_tags) self.assertRaises(KeyError, m.getraw, 'x') del m['a'] self.assertRaises(KeyError, m.getraw, 'a') self.assertIn('a', m.deleted_tags) # NOTE: historic behavior of Metadata.delete() # an attempt to delete an non-existing tag, will add it to the list # of deleted tags # so this will not raise a KeyError # as is it differs from dict or even defaultdict behavior del m['unknown'] self.assertIn('unknown', m.deleted_tags) def test_metadata_mapping_iter(self): l = set(self.metadata_d1) self.assertEqual(l, {'a', 'c', 'd'}) def test_metadata_mapping_keys(self): l = set(self.metadata_d1.keys()) self.assertEqual(l, {'a', 'c', 'd'}) def test_metadata_mapping_values(self): l = set(self.metadata_d1.values()) self.assertEqual(l, {'b', '2', 'x; y'}) def test_metadata_mapping_len(self): m = self.metadata_d1 self.assertEqual(len(m), 3) del m['x'] self.assertEqual(len(m), 3) del m['c'] self.assertEqual(len(m), 2) def _check_mapping_update(self, m): self.assertEqual(m['a'], 'b') self.assertEqual(m['c'], '3') self.assertEqual(m.getraw('d'), ['u', 'w']) self.assertEqual(m['x'], '') self.assertIn('x', m.deleted_tags) def test_metadata_mapping_update(self): # update from Metadata m = self.metadata_d2 m2 = self.metadata_d3 del m2['x'] m.update(m2) self._check_mapping_update(m) def test_metadata_mapping_update_dict(self): # update from dict m = self.metadata_d2 d2 = {'c': 3, 'd': ['u', 'w'], 'x': ''} m.update(d2) self._check_mapping_update(m) def test_metadata_mapping_update_tuple(self): # update from tuple m = self.metadata_d2 d2 = (('c', 3), ('d', ['u', 'w']), ('x', '')) m.update(d2) self._check_mapping_update(m) def test_metadata_mapping_update_dictlike(self): # update from kwargs m = self.metadata_d2 m.update(c=3, d=['u', 'w'], x='') self._check_mapping_update(m) def test_metadata_mapping_update_noparam(self): # update without parameter m = self.metadata_d2 self.assertRaises(TypeError, m.update) self.assertEqual(m['a'], 'b') def test_metadata_mapping_update_intparam(self): # update without parameter m = self.metadata_d2 self.assertRaises(TypeError, m.update, 123) def test_metadata_mapping_update_strparam(self): # update without parameter m = self.metadata_d2 self.assertRaises(ValueError, m.update, 'abc') def test_metadata_mapping_update_kw(self): m = Metadata(tag1='a', tag2='b') m.update(tag1='c') self.assertEqual(m['tag1'], 'c') self.assertEqual(m['tag2'], 'b') m.update(tag2='') self.assertIn('tag2', m.deleted_tags) def test_metadata_mapping_update_kw_del(self): m = Metadata(tag1='a', tag2='b') del m['tag1'] m2 = Metadata(tag1='c', tag2='d') del m2['tag2'] m.update(m2) self.assertEqual(m['tag1'], 'c') self.assertNotIn('tag2', m) self.assertNotIn('tag1', m.deleted_tags) self.assertIn('tag2', m.deleted_tags) def test_metadata_mapping_images(self): image1 = create_image(b'A', comment='A') image2 = create_image(b'B', comment='B') m1 = Metadata(a='b', length=1234, images=[image1]) self.assertEqual(m1.images[0], image1) self.assertEqual(len(m1), 2) # one tag, one image m1.images.append(image2) self.assertEqual(m1.images[1], image2) m1.images.pop(0) self.assertEqual(m1.images[0], image2) m2 = Metadata(a='c', length=4567, images=[image1]) m1.update(m2) self.assertEqual(m1.images[0], image1) m1.images.pop(0) self.assertEqual(len(m1), 1) # one tag, zero image self.assertFalse(m1.images) def test_metadata_mapping_iterable(self): m = Metadata(tag_tuple=('a', 0)) m['tag_set'] = {'c', 'd'} m['tag_dict'] = {'e': 1, 'f': 2} m['tag_str'] = 'gh' self.assertIn('0', m.getraw('tag_tuple')) self.assertIn('c', m.getraw('tag_set')) self.assertIn('e', m.getraw('tag_dict')) self.assertIn('gh', m.getraw('tag_str'))
def _load(self, filename): log.debug("Loading file %r", filename) file = self._File(encode_filename(filename), ID3=compatid3.CompatID3) tags = file.tags or {} # upgrade custom 2.3 frames to 2.4 for old, new in self.__upgrade.items(): if old in tags and new not in tags: f = tags.pop(old) tags.add(getattr(id3, new)(encoding=f.encoding, text=f.text)) metadata = Metadata() for frame in tags.values(): frameid = frame.FrameID if frameid in self.__translate: name = self.__translate[frameid] if frameid.startswith("T"): for text in frame.text: if text: metadata.add(name, unicode(text)) elif frameid == "COMM": for text in frame.text: if text: metadata.add("%s:%s" % (name, frame.desc), unicode(text)) else: metadata.add(name, unicode(frame)) elif frameid == "TMCL": for role, name in frame.people: if role or name: metadata.add("performer:%s" % role, name) elif frameid == "TIPL": # If file is ID3v2.3, TIPL tag could contain TMCL # so we will test for TMCL values and add to TIPL if not TMCL for role, name in frame.people: if role in self._tipl_roles and name: metadata.add(self._tipl_roles[role], name) else: metadata.add("performer:%s" % role, name) elif frameid == "TXXX": name = frame.desc if name in self.__translate_freetext: name = self.__translate_freetext[name] elif (name in self.__rtranslate) != (name in self.__rtranslate_freetext): # If the desc of a TXXX frame conflicts with the name of a # Picard tag, load it into ~id3:TXXX:desc rather than desc. # # This basically performs an XOR, making sure that 'name' # is in __rtranslate or __rtranslate_freetext, but not # both. (Being in both implies we support reading it both # ways.) Currently, the only tag in both is license. name = "~id3:TXXX:" + name for text in frame.text: metadata.add(name, unicode(text)) elif frameid == "USLT": name = "lyrics" if frame.desc: name += ":%s" % frame.desc metadata.add(name, unicode(frame.text)) elif frameid == "UFID" and frame.owner == "http://musicbrainz.org": metadata["musicbrainz_recordingid"] = frame.data.decode("ascii", "ignore") elif frameid == "TRCK": value = frame.text[0].split("/") if len(value) > 1: metadata["tracknumber"], metadata["totaltracks"] = value[:2] else: metadata["tracknumber"] = value[0] elif frameid == "TPOS": value = frame.text[0].split("/") if len(value) > 1: metadata["discnumber"], metadata["totaldiscs"] = value[:2] else: metadata["discnumber"] = value[0] elif frameid == "APIC": extras = {"desc": frame.desc, "type": image_type_from_id3_num(frame.type)} metadata.add_image(frame.mime, frame.data, extras=extras) elif frameid == "POPM": # Rating in ID3 ranges from 0 to 255, normalize this to the range 0 to 5 if frame.email == config.setting["rating_user_email"]: rating = unicode(int(round(frame.rating / 255.0 * (config.setting["rating_steps"] - 1)))) metadata.add("~rating", rating) if "date" in metadata: sanitized = sanitize_date(metadata.getall("date")[0]) if sanitized: metadata["date"] = sanitized self._info(metadata, file) return metadata
def _load(self, filename): self.log.debug("Loading file %r", filename) file = self._File(encode_filename(filename), ID3=compatid3.CompatID3) tags = file.tags or {} # upgrade custom 2.3 frames to 2.4 for old, new in self.__upgrade.items(): if old in tags and new not in tags: f = tags.pop(old) tags.add(getattr(id3, new)(encoding=f.encoding, text=f.text)) metadata = Metadata() for frame in tags.values(): frameid = frame.FrameID if frameid in self.__translate: name = self.__translate[frameid] if frameid.startswith('T'): for text in frame.text: if text: metadata.add(name, unicode(text)) elif frameid == 'COMM': for text in frame.text: if text: metadata.add('%s:%s' % (name, frame.desc), unicode(text)) else: metadata.add(name, unicode(frame)) elif frameid == "TMCL": for role, name in frame.people: if role or name: metadata.add('performer:%s' % role, name) elif frameid == "TIPL": for role, name in frame.people: if role in self.__tipl_roles and name: metadata.add(self.__tipl_roles[role], name) elif frameid == 'TXXX' and frame.desc in self.__translate_freetext: name = self.__translate_freetext[frame.desc] for text in frame.text: metadata.add(name, unicode(text)) elif frameid == 'USLT': name = 'lyrics' if frame.desc: name += frame.desc metadata.add(name, unicode(frame.text)) elif frameid == 'UFID' and frame.owner == 'http://musicbrainz.org': metadata['musicbrainz_trackid'] = unicode(frame.data) elif frameid == 'TRCK': value = frame.text[0].split('/') if len(value) > 1: metadata['tracknumber'], metadata[ 'totaltracks'] = value[:2] else: metadata['tracknumber'] = value[0] elif frameid == 'TPOS': value = frame.text[0].split('/') if len(value) > 1: metadata['discnumber'], metadata['totaldiscs'] = value[:2] else: metadata['discnumber'] = value[0] elif frameid == 'APIC': metadata.add_image(frame.mime, frame.data) elif frameid == 'POPM': # Rating in ID3 ranges from 0 to 255, normalize this to the range 0 to 5 if frame.email == self.config.setting['rating_user_email']: rating = unicode( int( round(frame.rating / 255.0 * (self.config.setting['rating_steps'] - 1)))) metadata.add('~rating', rating) if 'date' in metadata: metadata['date'] = sanitize_date(metadata.getall('date')[0]) self._info(metadata, file) return metadata
def _finalize_loading(self, error): if error: self.metadata.clear() self.metadata['album'] = _("[could not load album %s]") % self.id del self._new_metadata del self._new_tracks self.update() return if self._requests > 0: return if not self._tracks_loaded: artists = set() totalalbumtracks = 0 djmix_ars = {} if hasattr(self._new_metadata, "_djmix_ars"): djmix_ars = self._new_metadata._djmix_ars for medium_node in self._release_node.medium_list[0].medium: mm = Metadata() mm.copy(self._new_metadata) medium_to_metadata(medium_node, mm) totalalbumtracks += int(mm["totaltracks"]) for dj in djmix_ars.get(mm["discnumber"], []): mm.add("djmixer", dj) for track_node in medium_node.track_list[0].track: track = Track(track_node.recording[0].id, self) self._new_tracks.append(track) # Get track metadata tm = track.metadata tm.copy(mm) track_to_metadata(track_node, track, self.config) track._customize_metadata() self._new_metadata.length += tm.length artists.add(tm["musicbrainz_artistid"]) # Run track metadata plugins try: run_track_metadata_processors(self, tm, self._release_node, track_node) except: self.log.error(traceback.format_exc()) totalalbumtracks = str(totalalbumtracks) for track in self._new_tracks: track.metadata["~totalalbumtracks"] = totalalbumtracks if len(artists) > 1: track.metadata["compilation"] = "1" del self._release_node self._tracks_loaded = True if not self._requests: # Prepare parser for user's script if self.config.setting["enable_tagger_script"]: script = self.config.setting["tagger_script"] if script: parser = ScriptParser() for track in self._new_tracks: # Run tagger script for each track try: parser.eval(script, track.metadata) except: self.log.error(traceback.format_exc()) # Strip leading/trailing whitespace track.metadata.strip_whitespace() # Run tagger script for the album itself try: parser.eval(script, self._new_metadata) except: self.log.error(traceback.format_exc()) self._new_metadata.strip_whitespace() for track in self.tracks: for file in list(track.linked_files): file.move(self.unmatched_files) self.metadata = self._new_metadata self.tracks = self._new_tracks del self._new_metadata del self._new_tracks self.loaded = True self.match_files(self.unmatched_files.files) self.update() self.tagger.window.set_statusbar_message('Album %s loaded', self.id, timeout=3000) while self._after_load_callbacks.qsize() > 0: func = self._after_load_callbacks.get() func()
def _load(self, filename): log.debug("Loading file %r", filename) file = self._get_file(encode_filename(filename)) tags = file.tags or {} # upgrade custom 2.3 frames to 2.4 for old, new in self.__upgrade.items(): if old in tags and new not in tags: f = tags.pop(old) tags.add(getattr(id3, new)(encoding=f.encoding, text=f.text)) metadata = Metadata() for frame in tags.values(): frameid = frame.FrameID if frameid in self.__translate: name = self.__translate[frameid] if frameid.startswith('T'): for text in frame.text: if text: metadata.add(name, unicode(text)) elif frameid == 'COMM': for text in frame.text: if text: metadata.add('%s:%s' % (name, frame.desc), unicode(text)) else: metadata.add(name, unicode(frame)) elif frameid == "TMCL": for role, name in frame.people: if role or name: metadata.add('performer:%s' % role, name) elif frameid == "TIPL": # If file is ID3v2.3, TIPL tag could contain TMCL # so we will test for TMCL values and add to TIPL if not TMCL for role, name in frame.people: if role in self._tipl_roles and name: metadata.add(self._tipl_roles[role], name) else: metadata.add('performer:%s' % role, name) elif frameid == 'TXXX': name = frame.desc if name in self.__translate_freetext: name = self.__translate_freetext[name] elif ((name in self.__rtranslate) != (name in self.__rtranslate_freetext)): # If the desc of a TXXX frame conflicts with the name of a # Picard tag, load it into ~id3:TXXX:desc rather than desc. # # This basically performs an XOR, making sure that 'name' # is in __rtranslate or __rtranslate_freetext, but not # both. (Being in both implies we support reading it both # ways.) Currently, the only tag in both is license. name = '~id3:TXXX:' + name for text in frame.text: metadata.add(name, unicode(text)) elif frameid == 'USLT': name = 'lyrics' if frame.desc: name += ':%s' % frame.desc metadata.add(name, unicode(frame.text)) elif frameid == 'UFID' and frame.owner == 'http://musicbrainz.org': metadata['musicbrainz_recordingid'] = frame.data.decode('ascii', 'ignore') elif frameid in self.__tag_re_parse.keys(): m = self.__tag_re_parse[frameid].search(frame.text[0]) if m: for name, value in m.groupdict().iteritems(): if value is not None: metadata[name] = value else: log.error("Invalid %s value '%s' dropped in %r", frameid, frame.text[0], filename) elif frameid == 'APIC': try: coverartimage = TagCoverArtImage( file=filename, tag=frameid, types=types_from_id3(frame.type), comment=frame.desc, support_types=True, data=frame.data, ) except CoverArtImageError as e: log.error('Cannot load image from %r: %s' % (filename, e)) else: metadata.append_image(coverartimage) elif frameid == 'POPM': # Rating in ID3 ranges from 0 to 255, normalize this to the range 0 to 5 if frame.email == config.setting['rating_user_email']: rating = unicode(int(round(frame.rating / 255.0 * (config.setting['rating_steps'] - 1)))) metadata.add('~rating', rating) if 'date' in metadata: sanitized = sanitize_date(metadata.getall('date')[0]) if sanitized: metadata['date'] = sanitized self._info(metadata, file) return metadata
def _load(self, filename): log.debug("Loading file %r", filename) file = self._File(encode_filename(filename)) file.tags = file.tags or {} metadata = Metadata() for origname, values in file.tags.items(): for value in values: name = origname if name == "date" or name == "originaldate": # YYYY-00-00 => YYYY value = sanitize_date(value) elif name == 'performer' or name == 'comment': # transform "performer=Joe Barr (Piano)" to "performer:Piano=Joe Barr" name += ':' if value.endswith(')'): start = len(value) - 2 count = 1 while count > 0 and start > 0: if value[start] == ')': count += 1 elif value[start] == '(': count -= 1 start -= 1 if start > 0: name += value[start + 2:-1] value = value[:start] elif name.startswith('rating'): try: name, email = name.split(':', 1) except ValueError: email = '' if email != config.setting['rating_user_email']: continue name = '~rating' value = string_( int( round((float(value) * (config.setting['rating_steps'] - 1))))) elif name == "fingerprint" and value.startswith( "MusicMagic Fingerprint"): name = "musicip_fingerprint" value = value[22:] elif name == "tracktotal": if "totaltracks" in file.tags: continue name = "totaltracks" elif name == "disctotal": if "totaldiscs" in file.tags: continue name = "totaldiscs" elif name == "metadata_block_picture": image = mutagen.flac.Picture( base64.standard_b64decode(value)) try: coverartimage = TagCoverArtImage( file=filename, tag=name, types=types_from_id3(image.type), comment=image.desc, support_types=True, data=image.data, ) except CoverArtImageError as e: log.error('Cannot load image from %r: %s' % (filename, e)) else: metadata.append_image(coverartimage) continue elif name in self.__translate: name = self.__translate[name] metadata.add(name, value) if self._File == mutagen.flac.FLAC: for image in file.pictures: try: coverartimage = TagCoverArtImage( file=filename, tag='FLAC/PICTURE', types=types_from_id3(image.type), comment=image.desc, support_types=True, data=image.data, ) except CoverArtImageError as e: log.error('Cannot load image from %r: %s' % (filename, e)) else: metadata.append_image(coverartimage) # Read the unofficial COVERART tags, for backward compatibillity only if "metadata_block_picture" not in file.tags: try: for data in file["COVERART"]: try: coverartimage = TagCoverArtImage( file=filename, tag='COVERART', data=base64.standard_b64decode(data)) except CoverArtImageError as e: log.error('Cannot load image from %r: %s' % (filename, e)) else: metadata.append_image(coverartimage) except KeyError: pass self._info(metadata, file) return metadata
def _load(self, filename): self.log.debug("Loading file %r", filename) file = self._File(encode_filename(filename), ID3=compatid3.CompatID3) tags = file.tags or {} # upgrade custom 2.3 frames to 2.4 for old, new in self.__upgrade.items(): if old in tags and new not in tags: f = tags.pop(old) tags.add(getattr(id3, new)(encoding=f.encoding, text=f.text)) metadata = Metadata() for frame in tags.values(): frameid = frame.FrameID if frameid in self.__translate: name = self.__translate[frameid] if frameid.startswith("T"): for text in frame.text: if text: metadata.add(name, unicode(text)) elif frameid == "COMM": for text in frame.text: if text: metadata.add("%s:%s" % (name, frame.desc), unicode(text)) else: metadata.add(name, unicode(frame)) elif frameid == "TMCL": for role, name in frame.people: if role or name: metadata.add("performer:%s" % role, name) elif frameid == "TIPL": for role, name in frame.people: if role in self.__tipl_roles and name: metadata.add(self.__tipl_roles[role], name) elif frameid == "TXXX" and frame.desc in self.__translate_freetext: name = self.__translate_freetext[frame.desc] for text in frame.text: metadata.add(name, unicode(text)) elif frameid == "USLT": name = "lyrics" if frame.desc: name += frame.desc metadata.add(name, unicode(frame.text)) elif frameid == "UFID" and frame.owner == "http://musicbrainz.org": metadata["musicbrainz_trackid"] = unicode(frame.data) elif frameid == "TRCK": value = frame.text[0].split("/") if len(value) > 1: metadata["tracknumber"], metadata["totaltracks"] = value[:2] else: metadata["tracknumber"] = value[0] elif frameid == "TPOS": value = frame.text[0].split("/") if len(value) > 1: metadata["discnumber"], metadata["totaldiscs"] = value[:2] else: metadata["discnumber"] = value[0] elif frameid == "APIC": metadata.add_image(frame.mime, frame.data) elif frameid == "POPM": # Rating in ID3 ranges from 0 to 255, normalize this to the range 0 to 5 if frame.email == self.config.setting["rating_user_email"]: rating = unicode(int(round(frame.rating / 255.0 * (self.config.setting["rating_steps"] - 1)))) metadata.add("~rating", rating) if "date" in metadata: metadata["date"] = sanitize_date(metadata.getall("date")[0]) self._info(metadata, file) return metadata
def _load(self, filename): log.debug("Loading file %r", filename) file = self._File(encode_filename(filename), ID3=compatid3.CompatID3) tags = file.tags or {} # upgrade custom 2.3 frames to 2.4 for old, new in self.__upgrade.items(): if old in tags and new not in tags: f = tags.pop(old) tags.add(getattr(id3, new)(encoding=f.encoding, text=f.text)) metadata = Metadata() for frame in tags.values(): frameid = frame.FrameID if frameid in self.__translate: name = self.__translate[frameid] if frameid.startswith('T'): for text in frame.text: if text: metadata.add(name, unicode(text)) elif frameid == 'COMM': for text in frame.text: if text: metadata.add('%s:%s' % (name, frame.desc), unicode(text)) else: metadata.add(name, unicode(frame)) elif frameid == "TMCL": for role, name in frame.people: if role or name: metadata.add('performer:%s' % role, name) elif frameid == "TIPL": # If file is ID3v2.3, TIPL tag could contain TMCL # so we will test for TMCL values and add to TIPL if not TMCL for role, name in frame.people: if role in self._tipl_roles and name: metadata.add(self._tipl_roles[role], name) else: metadata.add('performer:%s' % role, name) elif frameid == 'TXXX': name = frame.desc if name in self.__translate_freetext: name = self.__translate_freetext[name] elif ((name in self.__rtranslate) != (name in self.__rtranslate_freetext)): # If the desc of a TXXX frame conflicts with the name of a # Picard tag, load it into ~id3:TXXX:desc rather than desc. # # This basically performs an XOR, making sure that 'name' # is in __rtranslate or __rtranslate_freetext, but not # both. (Being in both implies we support reading it both # ways.) Currently, the only tag in both is license. name = '~id3:TXXX:' + name for text in frame.text: metadata.add(name, unicode(text)) elif frameid == 'USLT': name = 'lyrics' if frame.desc: name += ':%s' % frame.desc metadata.add(name, unicode(frame.text)) elif frameid == 'UFID' and frame.owner == 'http://musicbrainz.org': metadata['musicbrainz_recordingid'] = frame.data.decode('ascii', 'ignore') elif frameid == 'TRCK': value = frame.text[0].split('/') if len(value) > 1: metadata['tracknumber'], metadata['totaltracks'] = value[:2] else: metadata['tracknumber'] = value[0] elif frameid == 'TPOS': value = frame.text[0].split('/') if len(value) > 1: metadata['discnumber'], metadata['totaldiscs'] = value[:2] else: metadata['discnumber'] = value[0] elif frameid == 'APIC': extras = { 'desc': frame.desc, 'type': image_type_from_id3_num(frame.type) } metadata.add_image(frame.mime, frame.data, extras=extras) elif frameid == 'POPM': # Rating in ID3 ranges from 0 to 255, normalize this to the range 0 to 5 if frame.email == config.setting['rating_user_email']: rating = unicode(int(round(frame.rating / 255.0 * (config.setting['rating_steps'] - 1)))) metadata.add('~rating', rating) if 'date' in metadata: sanitized = sanitize_date(metadata.getall('date')[0]) if sanitized: metadata['date'] = sanitized self._info(metadata, file) return metadata
def _load(self, filename): log.debug("Loading file %r", filename) file = self._File(encode_filename(filename), ID3=compatid3.CompatID3) tags = file.tags or {} # upgrade custom 2.3 frames to 2.4 for old, new in self.__upgrade.items(): if old in tags and new not in tags: f = tags.pop(old) tags.add(getattr(id3, new)(encoding=f.encoding, text=f.text)) metadata = Metadata() for frame in tags.values(): frameid = frame.FrameID if frameid in self.__translate: name = self.__translate[frameid] if frameid.startswith('T'): for text in frame.text: if text: metadata.add(name, unicode(text)) elif frameid == 'COMM': for text in frame.text: if text: metadata.add('%s:%s' % (name, frame.desc), unicode(text)) else: metadata.add(name, unicode(frame)) elif frameid == "TMCL": for role, name in frame.people: if role or name: metadata.add('performer:%s' % role, name) elif frameid == "TIPL": # If file is ID3v2.3, TIPL tag could contain TMCL # so we will test for TMCL values and add to TIPL if not TMCL for role, name in frame.people: if role in self._tipl_roles and name: metadata.add(self._tipl_roles[role], name) else: metadata.add('performer:%s' % role, name) elif frameid == 'TXXX': name = frame.desc if name in self.__translate_freetext: name = self.__translate_freetext[name] elif ((name in self.__rtranslate) != (name in self.__rtranslate_freetext)): # If the desc of a TXXX frame conflicts with the name of a # Picard tag, load it into ~id3:TXXX:desc rather than desc. # # This basically performs an XOR, making sure that 'name' # is in __rtranslate or __rtranslate_freetext, but not # both. (Being in both implies we support reading it both # ways.) Currently, the only tag in both is license. name = '~id3:TXXX:' + name for text in frame.text: metadata.add(name, unicode(text)) elif frameid == 'USLT': name = 'lyrics' if frame.desc: name += ':%s' % frame.desc metadata.add(name, unicode(frame.text)) elif frameid == 'UFID' and frame.owner == 'http://musicbrainz.org': metadata['musicbrainz_recordingid'] = frame.data.decode( 'ascii', 'ignore') elif frameid == 'TRCK': value = frame.text[0].split('/') if len(value) > 1: metadata['tracknumber'], metadata[ 'totaltracks'] = value[:2] else: metadata['tracknumber'] = value[0] elif frameid == 'TPOS': value = frame.text[0].split('/') if len(value) > 1: metadata['discnumber'], metadata['totaldiscs'] = value[:2] else: metadata['discnumber'] = value[0] elif frameid == 'APIC': extras = { 'desc': frame.desc, 'type': image_type_from_id3_num(frame.type) } metadata.add_image(frame.mime, frame.data, extras=extras) elif frameid == 'POPM': # Rating in ID3 ranges from 0 to 255, normalize this to the range 0 to 5 if frame.email == config.setting['rating_user_email']: rating = unicode( int( round(frame.rating / 255.0 * (config.setting['rating_steps'] - 1)))) metadata.add('~rating', rating) if 'date' in metadata: sanitized = sanitize_date(metadata.getall('date')[0]) if sanitized: metadata['date'] = sanitized self._info(metadata, file) return metadata
def _finalize_loading(self, error): if error: self.metadata.clear() self.metadata['album'] = _("[could not load album %s]") % self.id del self._new_metadata del self._new_tracks self.update() return if self._requests > 0: return if not self._tracks_loaded: artists = set() totalalbumtracks = 0 djmix_ars = {} if hasattr(self._new_metadata, "_djmix_ars"): djmix_ars = self._new_metadata._djmix_ars for medium_node in self._release_node.medium_list[0].medium: mm = Metadata() mm.copy(self._new_metadata) medium_to_metadata(medium_node, mm) totalalbumtracks += int(mm["totaltracks"]) for dj in djmix_ars.get(mm["discnumber"], []): mm.add("djmixer", dj) for track_node in medium_node.track_list[0].track: track = Track(track_node.recording[0].id, self) self._new_tracks.append(track) # Get track metadata tm = track.metadata tm.copy(mm) track_to_metadata(track_node, track, self.config) track._customize_metadata() self._new_metadata.length += tm.length artists.add(tm["musicbrainz_artistid"]) # Run track metadata plugins try: run_track_metadata_processors(self, tm, self._release_node, track_node) except: self.log.error(traceback.format_exc()) totalalbumtracks = str(totalalbumtracks) for track in self._new_tracks: track.metadata["~totalalbumtracks"] = totalalbumtracks if len(artists) > 1: track.metadata["compilation"] = "1" del self._release_node self._tracks_loaded = True if not self._requests: # Prepare parser for user's script if self.config.setting["enable_tagger_script"]: script = self.config.setting["tagger_script"] if script: parser = ScriptParser() for track in self._new_tracks: # Run tagger script for each track try: parser.eval(script, track.metadata) except: self.log.error(traceback.format_exc()) # Strip leading/trailing whitespace track.metadata.strip_whitespace() # Run tagger script for the album itself try: parser.eval(script, self._new_metadata) except: self.log.error(traceback.format_exc()) self._new_metadata.strip_whitespace() for track in self.tracks: for file in list(track.linked_files): file.move(self.unmatched_files) self.metadata = self._new_metadata self.tracks = self._new_tracks del self._new_metadata del self._new_tracks self.loaded = True self.match_files(self.unmatched_files.files) self.update() self.tagger.window.set_statusbar_message(_('Album %s loaded'), self.id, timeout=3000) while self._after_load_callbacks.qsize() > 0: func = self._after_load_callbacks.get() func()
class MetadataTest(PicardTestCase): original = None tags = [] def setUp(self): super().setUp() config.setting = settings.copy() self.metadata = Metadata() self.metadata["single1"] = "single1-value" self.metadata.add_unique("single2", "single2-value") self.metadata.add_unique("single2", "single2-value") self.multi1 = ["multi1-value", "multi1-value"] self.metadata.add("multi1", self.multi1[0]) self.metadata.add("multi1", self.multi1[1]) self.multi2 = ["multi2-value1", "multi2-value2"] self.metadata["multi2"] = self.multi2 self.multi3 = ["multi3-value1", "multi3-value2"] self.metadata.set("multi3", self.multi3) self.metadata["~hidden"] = "hidden-value" self.metadata_d1 = Metadata({'a': 'b', 'c': 2, 'd': ['x', 'y'], 'x': ''}) self.metadata_d2 = Metadata({'a': 'b', 'c': 2, 'd': ['x', 'y'], 'x': 'z'}) self.metadata_d3 = Metadata({'c': 3, 'd': ['u', 'w'], 'x': 'p'}) def tearDown(self): pass def test_metadata_setitem(self): self.assertEqual(["single1-value"], self.metadata.getraw("single1")) self.assertEqual(["single2-value"], self.metadata.getraw("single2")) self.assertEqual(self.multi1, self.metadata.getraw("multi1")) self.assertEqual(self.multi2, self.metadata.getraw("multi2")) self.assertEqual(self.multi3, self.metadata.getraw("multi3")) self.assertEqual(["hidden-value"], self.metadata.getraw("~hidden")) def test_metadata_set_all_values_as_string(self): for val in (0, 2, True): str_val = str(val) self.metadata.set('val1', val) self.assertEqual([str_val], self.metadata.getraw("val1")) self.metadata['val2'] = val self.assertEqual([str_val], self.metadata.getraw("val2")) del self.metadata['val3'] self.metadata.add('val3', val) self.assertEqual([str_val], self.metadata.getraw("val3")) del self.metadata['val4'] self.metadata.add_unique('val4', val) self.assertEqual([str_val], self.metadata.getraw("val4")) def test_metadata_get(self): self.assertEqual("single1-value", self.metadata["single1"]) self.assertEqual("single1-value", self.metadata.get("single1")) self.assertEqual(["single1-value"], self.metadata.getall("single1")) self.assertEqual(["single1-value"], self.metadata.getraw("single1")) self.assertEqual(MULTI_VALUED_JOINER.join(self.multi1), self.metadata["multi1"]) self.assertEqual(MULTI_VALUED_JOINER.join(self.multi1), self.metadata.get("multi1")) self.assertEqual(self.multi1, self.metadata.getall("multi1")) self.assertEqual(self.multi1, self.metadata.getraw("multi1")) self.assertEqual("", self.metadata["nonexistent"]) self.assertEqual(None, self.metadata.get("nonexistent")) self.assertEqual([], self.metadata.getall("nonexistent")) self.assertRaises(KeyError, self.metadata.getraw, "nonexistent") self.assertEqual(self.metadata._store.items(), self.metadata.rawitems()) metadata_items = [(x, z) for (x, y) in self.metadata.rawitems() for z in y] self.assertEqual(metadata_items, list(self.metadata.items())) def test_metadata_unset(self): self.metadata.unset("single1") self.assertNotIn("single1", self.metadata) self.assertNotIn("single1", self.metadata.deleted_tags) self.assertRaises(KeyError, self.metadata.unset, 'unknown_tag') def test_metadata_delete(self): del self.metadata["single1"] self.assertNotIn("single1", self.metadata) self.assertIn("single1", self.metadata.deleted_tags) def test_metadata_legacy_delete(self): self.metadata.delete("single1") self.assertNotIn("single1", self.metadata) self.assertIn("single1", self.metadata.deleted_tags) def test_metadata_implicit_delete(self): self.metadata["single2"] = "" self.assertNotIn("single2", self.metadata) self.assertIn("single2", self.metadata.deleted_tags) self.metadata["unknown"] = "" self.assertNotIn("unknown", self.metadata) self.assertNotIn("unknown", self.metadata.deleted_tags) def test_metadata_undelete(self): self.metadata.delete("single1") self.assertNotIn("single1", self.metadata) self.assertIn("single1", self.metadata.deleted_tags) self.metadata["single1"] = "value1" self.assertIn("single1", self.metadata) self.assertNotIn("single1", self.metadata.deleted_tags) def test_normalize_tag(self): self.assertEqual('sometag', Metadata.normalize_tag('sometag')) self.assertEqual('sometag', Metadata.normalize_tag('sometag:')) self.assertEqual('sometag', Metadata.normalize_tag('sometag::')) self.assertEqual('sometag:desc', Metadata.normalize_tag('sometag:desc')) def test_metadata_tag_trailing_colon(self): self.metadata['tag:'] = 'Foo' self.assertIn('tag', self.metadata) self.assertIn('tag:', self.metadata) self.assertEqual('Foo', self.metadata['tag']) self.assertEqual('Foo', self.metadata['tag:']) del self.metadata['tag'] self.assertNotIn('tag', self.metadata) self.assertNotIn('tag:', self.metadata) def test_metadata_copy(self): m = Metadata() m["old"] = "old-value" self.metadata.delete("single1") m.copy(self.metadata) self.assertEqual(self.metadata._store, m._store) self.assertEqual(self.metadata.deleted_tags, m.deleted_tags) self.assertEqual(self.metadata.length, m.length) self.assertEqual(self.metadata.images, m.images) def test_metadata_copy_without_images(self): m = Metadata() m.copy(self.metadata, copy_images=False) self.assertEqual(self.metadata._store, m._store) self.assertEqual(self.metadata.deleted_tags, m.deleted_tags) self.assertEqual(self.metadata.length, m.length) self.assertEqual(ImageList(), m.images) def test_metadata_update(self): m = Metadata() m["old"] = "old-value" self.metadata.delete("single1") m.update(self.metadata) self.assertIn("old", m) self.assertNotIn("single1", m) self.assertIn("single1", m.deleted_tags) self.assertEqual("single2-value", m["single2"]) self.assertEqual(self.metadata.deleted_tags, m.deleted_tags) self.assertEqual(self.metadata.images, m.images) self.metadata["old"] = "old-value" self.assertEqual(self.metadata._store, m._store) def test_metadata_clear(self): self.metadata.clear() self.assertEqual(0, len(self.metadata)) def test_metadata_clear_deleted(self): self.metadata.delete("single1") self.assertIn("single1", self.metadata.deleted_tags) self.metadata.clear_deleted() self.assertNotIn("single1", self.metadata.deleted_tags) def test_metadata_applyfunc(self): def func(x): return x[1:] self.metadata.apply_func(func) self.assertEqual("ingle1-value", self.metadata["single1"]) self.assertEqual("ingle1-value", self.metadata.get("single1")) self.assertEqual(["ingle1-value"], self.metadata.getall("single1")) self.assertEqual(MULTI_VALUED_JOINER.join(map(func, self.multi1)), self.metadata["multi1"]) self.assertEqual(MULTI_VALUED_JOINER.join(map(func, self.multi1)), self.metadata.get("multi1")) self.assertEqual(list(map(func, self.multi1)), self.metadata.getall("multi1")) def test_metadata_applyfunc_preserve_tags(self): self.assertTrue(len(PRESERVED_TAGS) > 0) m = Metadata() m[PRESERVED_TAGS[0]] = 'value1' m['not_preserved'] = 'value2' def func(x): return x[1:] m.apply_func(func) self.assertEqual("value1", m[PRESERVED_TAGS[0]]) self.assertEqual("alue2", m['not_preserved']) def test_metadata_applyfunc_delete_tags(self): def func(x): return None metadata = Metadata(self.metadata) metadata.apply_func(func) self.assertEqual(0, len(metadata.rawitems())) self.assertEqual(self.metadata.keys(), metadata.deleted_tags) def test_length_score(self): results = [(20000, 0, 0.333333333333), (20000, 10000, 0.666666666667), (20000, 20000, 1.0), (20000, 30000, 0.666666666667), (20000, 40000, 0.333333333333), (20000, 50000, 0.0)] for (a, b, expected) in results: actual = Metadata.length_score(a, b) self.assertAlmostEqual(expected, actual, msg="a={a}, b={b}".format(a=a, b=b)) def test_compare_is_equal(self): m1 = Metadata() m1["title"] = "title1" m1["tracknumber"] = "2" m1.length = 360 m2 = Metadata() m2["title"] = "title1" m2["tracknumber"] = "2" m2.length = 360 self.assertEqual(m1.compare(m2), m2.compare(m1)) self.assertEqual(m1.compare(m2), 1) def test_compare_with_ignored(self): m1 = Metadata() m1["title"] = "title1" m1["tracknumber"] = "2" m1.length = 360 m2 = Metadata() m2["title"] = "title1" m2["tracknumber"] = "3" m2.length = 300 self.assertNotEqual(m1.compare(m2), 1) self.assertEqual(m1.compare(m2, ignored=['tracknumber', '~length']), 1) def test_compare_lengths(self): m1 = Metadata() m1.length = 360 m2 = Metadata() m2.length = 300 self.assertAlmostEqual(m1.compare(m2), 0.998) def test_compare_tracknumber_difference(self): m1 = Metadata() m1["tracknumber"] = "1" m2 = Metadata() m2["tracknumber"] = "2" m3 = Metadata() m3["tracknumber"] = "2" self.assertEqual(m1.compare(m2), 0) self.assertEqual(m2.compare(m3), 1) def test_compare_discnumber_difference(self): m1 = Metadata() m1["discnumber"] = "1" m2 = Metadata() m2["discnumber"] = "2" m3 = Metadata() m3["discnumber"] = "2" self.assertEqual(m1.compare(m2), 0) self.assertEqual(m2.compare(m3), 1) def test_compare_deleted(self): m1 = Metadata() m1["artist"] = "TheArtist" m1["title"] = "title1" m2 = Metadata() m2["artist"] = "TheArtist" m2.delete("title") self.assertTrue(m1.compare(m2) < 1) def test_strip_whitespace(self): m1 = Metadata() m1["artist"] = " TheArtist " m1["title"] = "\t\u00A0 tit le1 \r\n" m1["genre"] = " \t" m1.strip_whitespace() self.assertEqual(m1["artist"], "TheArtist") self.assertEqual(m1["title"], "tit le1") def test_metadata_mapping_init(self): d = {'a': 'b', 'c': 2, 'd': ['x', 'y'], 'x': '', 'z': {'u', 'w'}} deleted_tags = set('c') m = Metadata(d, deleted_tags=deleted_tags, length=1234) self.assertIn('a', m) self.assertEqual(m.getraw('a'), ['b']) self.assertEqual(m['d'], MULTI_VALUED_JOINER.join(d['d'])) self.assertNotIn('c', m) self.assertNotIn('length', m) self.assertIn('c', m.deleted_tags) self.assertEqual(m.length, 1234) def test_metadata_mapping_init_zero(self): m = Metadata(tag1='a', tag2=0, tag3='', tag4=None) m['tag5'] = 0 m['tag1'] = '' self.assertIn('tag1', m.deleted_tags) self.assertEqual(m['tag2'], '0') self.assertNotIn('tag3', m) self.assertNotIn('tag4', m) self.assertEqual(m['tag5'], '0') def test_metadata_mapping_del(self): m = self.metadata_d1 self.assertEqual(m.getraw('a'), ['b']) self.assertNotIn('a', m.deleted_tags) self.assertNotIn('x', m.deleted_tags) self.assertRaises(KeyError, m.getraw, 'x') del m['a'] self.assertRaises(KeyError, m.getraw, 'a') self.assertIn('a', m.deleted_tags) # NOTE: historic behavior of Metadata.delete() # an attempt to delete an non-existing tag, will add it to the list # of deleted tags # so this will not raise a KeyError # as is it differs from dict or even defaultdict behavior del m['unknown'] self.assertIn('unknown', m.deleted_tags) def test_metadata_mapping_iter(self): self.assertEqual(set(self.metadata_d1), {'a', 'c', 'd'}) def test_metadata_mapping_keys(self): self.assertEqual(set(self.metadata_d1.keys()), {'a', 'c', 'd'}) def test_metadata_mapping_values(self): self.assertEqual(set(self.metadata_d1.values()), {'b', '2', 'x; y'}) def test_metadata_mapping_len(self): m = self.metadata_d1 self.assertEqual(len(m), 3) del m['x'] self.assertEqual(len(m), 3) del m['c'] self.assertEqual(len(m), 2) def _check_mapping_update(self, m): self.assertEqual(m['a'], 'b') self.assertEqual(m['c'], '3') self.assertEqual(m.getraw('d'), ['u', 'w']) self.assertEqual(m['x'], '') self.assertIn('x', m.deleted_tags) def test_metadata_mapping_update(self): # update from Metadata m = self.metadata_d2 m2 = self.metadata_d3 del m2['x'] m.update(m2) self._check_mapping_update(m) def test_metadata_mapping_update_dict(self): # update from dict m = self.metadata_d2 d2 = {'c': 3, 'd': ['u', 'w'], 'x': ''} m.update(d2) self._check_mapping_update(m) def test_metadata_mapping_update_tuple(self): # update from tuple m = self.metadata_d2 d2 = (('c', 3), ('d', ['u', 'w']), ('x', '')) m.update(d2) self._check_mapping_update(m) def test_metadata_mapping_update_dictlike(self): # update from kwargs m = self.metadata_d2 m.update(c=3, d=['u', 'w'], x='') self._check_mapping_update(m) def test_metadata_mapping_update_noparam(self): # update without parameter m = self.metadata_d2 self.assertRaises(TypeError, m.update) self.assertEqual(m['a'], 'b') def test_metadata_mapping_update_intparam(self): # update without parameter m = self.metadata_d2 self.assertRaises(TypeError, m.update, 123) def test_metadata_mapping_update_strparam(self): # update without parameter m = self.metadata_d2 self.assertRaises(ValueError, m.update, 'abc') def test_metadata_mapping_update_kw(self): m = Metadata(tag1='a', tag2='b') m.update(tag1='c') self.assertEqual(m['tag1'], 'c') self.assertEqual(m['tag2'], 'b') m.update(tag2='') self.assertIn('tag2', m.deleted_tags) def test_metadata_mapping_update_kw_del(self): m = Metadata(tag1='a', tag2='b') del m['tag1'] m2 = Metadata(tag1='c', tag2='d') del m2['tag2'] m.update(m2) self.assertEqual(m['tag1'], 'c') self.assertNotIn('tag2', m) self.assertNotIn('tag1', m.deleted_tags) self.assertIn('tag2', m.deleted_tags) def test_metadata_mapping_images(self): image1 = create_image(b'A', comment='A') image2 = create_image(b'B', comment='B') m1 = Metadata(a='b', length=1234, images=[image1]) self.assertEqual(m1.images[0], image1) self.assertEqual(len(m1), 2) # one tag, one image m1.images.append(image2) self.assertEqual(m1.images[1], image2) m1.images.pop(0) self.assertEqual(m1.images[0], image2) m2 = Metadata(a='c', length=4567, images=[image1]) m1.update(m2) self.assertEqual(m1.images[0], image1) m1.images.pop(0) self.assertEqual(len(m1), 1) # one tag, zero image self.assertFalse(m1.images) def test_metadata_mapping_iterable(self): m = Metadata(tag_tuple=('a', 0)) m['tag_set'] = {'c', 'd'} m['tag_dict'] = {'e': 1, 'f': 2} m['tag_str'] = 'gh' self.assertIn('0', m.getraw('tag_tuple')) self.assertIn('c', m.getraw('tag_set')) self.assertIn('e', m.getraw('tag_dict')) self.assertIn('gh', m.getraw('tag_str')) def test_compare_to_release(self): release = load_test_json('release.json') metadata = Metadata() release_to_metadata(release, metadata) match = metadata.compare_to_release(release, Cluster.comparison_weights) self.assertEqual(1.0, match.similarity) self.assertEqual(release, match.release) def test_compare_to_release_with_score(self): release = load_test_json('release.json') metadata = Metadata() release_to_metadata(release, metadata) for score, sim in ((42, 0.42), ('42', 0.42), ('foo', 1.0), (None, 1.0)): release['score'] = score match = metadata.compare_to_release(release, Cluster.comparison_weights) self.assertEqual(sim, match.similarity) def test_weights_from_release_type_scores(self): release = load_test_json('release.json') parts = [] weights_from_release_type_scores(parts, release, {'Album': 0.75}, 666) self.assertEqual( parts[0], (0.75, 666) ) weights_from_release_type_scores(parts, release, {}, 666) self.assertEqual( parts[1], (0.5, 666) ) del release['release-group'] weights_from_release_type_scores(parts, release, {}, 777) self.assertEqual( parts[2], (0.0, 777) ) def test_preferred_countries(self): release = load_test_json('release.json') parts = [] weights_from_preferred_countries(parts, release, [], 666) self.assertFalse(parts) weights_from_preferred_countries(parts, release, ['FR'], 666) self.assertEqual(parts[0], (0.0, 666)) weights_from_preferred_countries(parts, release, ['GB'], 666) self.assertEqual(parts[1], (1.0, 666)) def test_preferred_formats(self): release = load_test_json('release.json') parts = [] weights_from_preferred_formats(parts, release, [], 777) self.assertFalse(parts) weights_from_preferred_formats(parts, release, ['Digital Media'], 777) self.assertEqual(parts[0], (0.0, 777)) weights_from_preferred_formats(parts, release, ['12" Vinyl'], 777) self.assertEqual(parts[1], (1.0, 777)) def test_compare_to_track(self): track_json = load_test_json('track.json') track = Track(track_json['id']) track_to_metadata(track_json, track) match = track.metadata.compare_to_track(track_json, File.comparison_weights) self.assertEqual(1.0, match.similarity) self.assertEqual(track_json, match.track) def test_compare_to_track_with_score(self): track_json = load_test_json('track.json') track = Track(track_json['id']) track_to_metadata(track_json, track) for score, sim in ((42, 0.42), ('42', 0.42), ('foo', 1.0), (None, 1.0)): track_json['score'] = score match = track.metadata.compare_to_track(track_json, File.comparison_weights) self.assertEqual(sim, match.similarity)
def _load_tracks(self): artists = set() all_media = [] absolutetracknumber = 0 va = self._new_metadata[ 'musicbrainz_albumartistid'] == VARIOUS_ARTISTS_ID djmix_ars = {} if hasattr(self._new_metadata, "_djmix_ars"): djmix_ars = self._new_metadata._djmix_ars for medium_node in self._release_node['media']: mm = Metadata() mm.copy(self._new_metadata) medium_to_metadata(medium_node, mm) format = medium_node.get('format') if format: all_media.append(format) for dj in djmix_ars.get(mm["discnumber"], []): mm.add("djmixer", dj) if va: mm["compilation"] = "1" else: del mm["compilation"] if 'discs' in medium_node: discids = [disc.get('id') for disc in medium_node['discs']] mm['~musicbrainz_discids'] = discids mm['musicbrainz_discid'] = list( self._discids.intersection(discids)) if "pregap" in medium_node: absolutetracknumber += 1 mm['~discpregap'] = '1' extra_metadata = { '~pregap': '1', '~absolutetracknumber': absolutetracknumber, } self._finalize_loading_track(medium_node['pregap'], mm, artists, extra_metadata) track_count = medium_node['track-count'] if track_count: tracklist_node = medium_node['tracks'] for track_node in tracklist_node: absolutetracknumber += 1 extra_metadata = { '~absolutetracknumber': absolutetracknumber, } self._finalize_loading_track(track_node, mm, artists, extra_metadata) if "data-tracks" in medium_node: for track_node in medium_node['data-tracks']: absolutetracknumber += 1 extra_metadata = { '~datatrack': '1', '~absolutetracknumber': absolutetracknumber, } self._finalize_loading_track(track_node, mm, artists, extra_metadata) totalalbumtracks = absolutetracknumber self._new_metadata['~totalalbumtracks'] = totalalbumtracks # Generate a list of unique media, but keep order of first appearance self._new_metadata['media'] = " / ".join( list(OrderedDict.fromkeys(all_media))) for track in self._new_tracks: track.metadata["~totalalbumtracks"] = totalalbumtracks if len(artists) > 1: track.metadata["~multiartist"] = "1" del self._release_node del self._release_artist_nodes self._tracks_loaded = True