def test_eq(self): list1 = ImageList() list2 = ImageList() list3 = ImageList() list1.append(self.images['a']) list1.append(self.images['b']) list2.append(self.images['b']) list2.append(self.images['a']) list3.append(self.images['a']) list3.append(self.images['c']) self.assertEqual(list1, list2) self.assertNotEqual(list1, list3)
def test_eq(self): list1 = ImageList() list2 = ImageList() list3 = ImageList() list1.append(self.images['a']) list1.append(self.images['b']) list2.append(self.images['b']) list2.append(self.images['a']) list3.append(self.images['a']) list3.append(self.images['c']) self.assertTrue(list1 == list2) self.assertFalse(list1 == list3)
class Metadata(dict): """List of metadata items with dict-like access.""" __weights = [ ('title', 22), ('artist', 6), ('album', 12), ('tracknumber', 6), ('totaltracks', 5), ] multi_valued_joiner = MULTI_VALUED_JOINER def __init__(self): super().__init__() self.images = ImageList() self.has_common_images = True self.deleted_tags = set() self.length = 0 def __bool__(self): return bool(len(self) or len(self.images)) def append_image(self, coverartimage): self.images.append(coverartimage) def set_front_image(self, coverartimage): # First remove all front images self.images[:] = [img for img in self.images if not img.is_front_image()] self.images.append(coverartimage) @property def images_to_be_saved_to_tags(self): if not config.setting["save_images_to_tags"]: return () images = [img for img in self.images if img.can_be_saved_to_tags] if config.setting["embed_only_one_front_image"]: front_image = self.get_single_front_image(images) if front_image: return front_image return images def get_single_front_image(self, images=None): if not images: images = self.images for img in images: if img.is_front_image(): return [img] return [] def remove_image(self, index): self.images.pop(index) @staticmethod def length_score(a, b): return (1.0 - min(abs(a - b), LENGTH_SCORE_THRES_MS) / float(LENGTH_SCORE_THRES_MS)) def compare(self, other): parts = [] if self.length and other.length: score = self.length_score(self.length, other.length) parts.append((score, 8)) for name, weight in self.__weights: a = self[name] b = other[name] if a and b: if name in ('tracknumber', 'totaltracks'): try: ia = int(a) ib = int(b) except ValueError: ia = a ib = b score = 1.0 - (int(ia != ib)) else: score = similarity2(a, b) parts.append((score, weight)) elif (a and name in other.deleted_tags or b and name in self.deleted_tags): parts.append((0, weight)) return linear_combination_of_weights(parts) def compare_to_release(self, release, weights): """ Compare metadata to a MusicBrainz release. Produces a probability as a linear combination of weights that the metadata matches a certain album. """ parts = self.compare_to_release_parts(release, weights) return (linear_combination_of_weights(parts), release) def compare_to_release_parts(self, release, weights): parts = [] if "album" in self: b = release['title'] parts.append((similarity2(self["album"], b), weights["album"])) if "albumartist" in self and "albumartist" in weights: a = self["albumartist"] b = artist_credit_from_node(release['artist-credit'])[0] parts.append((similarity2(a, b), weights["albumartist"])) try: a = int(self["totaltracks"]) except (ValueError, KeyError): pass else: try: if "title" in weights: b = release['media'][0]['track-count'] else: b = release['track-count'] except KeyError: b = 0 score = 0.0 if a > b else 0.3 if a < b else 1.0 parts.append((score, weights["totaltracks"])) preferred_countries = config.setting["preferred_release_countries"] preferred_formats = config.setting["preferred_release_formats"] total_countries = len(preferred_countries) if total_countries: score = 0.0 if "country" in release: try: i = preferred_countries.index(release['country']) score = float(total_countries - i) / float(total_countries) except ValueError: pass parts.append((score, weights["releasecountry"])) total_formats = len(preferred_formats) if total_formats and 'media' in release: score = 0.0 subtotal = 0 for medium in release['media']: if "format" in medium: try: i = preferred_formats.index(medium['format']) score += float(total_formats - i) / float(total_formats) except ValueError: pass subtotal += 1 if subtotal > 0: score /= subtotal parts.append((score, weights["format"])) if "releasetype" in weights: # This section generates a score that determines how likely this release will be selected in a lookup. # The score goes from 0 to 1 with 1 being the most likely to be chosen and 0 the least likely # This score is based on the preferences of release-types found in this release # This algorithm works by taking the scores of the primary type (and secondary if found) and averages them # If no types are found, it is set to the score of the 'Other' type or 0.5 if 'Other' doesnt exist type_scores = dict(config.setting["release_type_scores"]) score = 0.0 other_score = type_scores.get('Other', 0.5) if 'release-group' in release and 'primary-type' in release['release-group']: types_found = [release['release-group']['primary-type']] if 'secondary-types' in release['release-group']: types_found += release['release-group']['secondary-types'] for release_type in types_found: score += type_scores.get(release_type, other_score) score /= len(types_found) parts.append((score, weights["releasetype"])) rg = QObject.tagger.get_release_group_by_id(release['release-group']['id']) if release['id'] in rg.loaded_albums: parts.append((1.0, 6)) return parts def compare_to_track(self, track, weights): parts = [] if 'title' in self: a = self['title'] b = track.get('title', '') parts.append((similarity2(a, b), weights["title"])) if 'artist' in self: a = self['artist'] artist_credits = track.get('artist-credit', []) b = artist_credit_from_node(artist_credits)[0] parts.append((similarity2(a, b), weights["artist"])) a = self.length if a > 0 and 'length' in track: b = track['length'] score = self.length_score(a, b) parts.append((score, weights["length"])) releases = [] if "releases" in track: releases = track['releases'] if not releases: sim = linear_combination_of_weights(parts) return (sim, None, None, track) result = (-1,) for release in releases: release_parts = self.compare_to_release_parts(release, weights) sim = linear_combination_of_weights(parts + release_parts) if sim > result[0]: rg = release['release-group'] if "release-group" in release else None result = (sim, rg, release, track) return result def copy(self, other): self.clear() self.update(other) def update(self, other): for key in other.keys(): self.set(key, other.getall(key)[:]) if other.images: self.images = other.images[:] if other.length: self.length = other.length self.deleted_tags.update(other.deleted_tags) # Remove deleted tags from UI on save for tag in other.deleted_tags: self.pop(tag, None) def clear(self): super().clear() self.images = ImageList() self.length = 0 self.clear_deleted() def clear_deleted(self): self.deleted_tags = set() def getall(self, name): return super().get(name, []) def get(self, name, default=None): values = super().get(name, None) if values: return self.multi_valued_joiner.join(values) else: return default def __getitem__(self, name): return self.get(name, '') def set(self, name, values): super().__setitem__(name, values) if name in self.deleted_tags: self.deleted_tags.remove(name) def __setitem__(self, name, values): if not isinstance(values, list): values = [values] values = [str(value) for value in values if value] if len(values): self.set(name, values) elif name in self: self.delete(name) def add(self, name, value): if value or value == 0: self.setdefault(name, []).append(value) if name in self.deleted_tags: self.deleted_tags.remove(name) def add_unique(self, name, value): if value not in self.getall(name): self.add(name, value) def delete(self, name): if name in self: self.pop(name, None) self.deleted_tags.add(name) def items(self): for name, values in super().items(): for value in values: yield name, value def rawitems(self): """Returns the metadata items. >>> m.rawitems() [("key1", ["value1", "value2"]), ("key2", ["value3"])] """ return dict.items(self) def apply_func(self, func): for key, values in self.rawitems(): if key not in PRESERVED_TAGS: super().__setitem__(key, [func(value) for value in values]) def strip_whitespace(self): """Strip leading/trailing whitespace. >>> m = Metadata() >>> m["foo"] = " bar " >>> m["foo"] " bar " >>> m.strip_whitespace() >>> m["foo"] "bar" """ self.apply_func(lambda s: s.strip())
class Metadata(MutableMapping): """List of metadata items with dict-like access.""" __weights = [ ('title', 22), ('artist', 6), ('album', 12), ('tracknumber', 6), ('totaltracks', 5), ('discnumber', 5), ('totaldiscs', 4), ] __date_match_factors = { 'exact': 1.00, 'year': 0.95, 'close_year': 0.85, 'exists_vs_null': 0.65, 'no_release_date': 0.25, 'differed': 0.0 } multi_valued_joiner = MULTI_VALUED_JOINER def __init__(self, *args, deleted_tags=None, images=None, length=None, **kwargs): self._store = dict() self.deleted_tags = set() self.length = 0 self.images = ImageList() self.has_common_images = True if args or kwargs: self.update(*args, **kwargs) if images is not None: for image in images: self.images.append(image) if deleted_tags is not None: for tag in deleted_tags: del self[tag] if length is not None: self.length = int(length) def __bool__(self): return bool(len(self)) def __len__(self): return len(self._store) + len(self.images) @staticmethod def length_score(a, b): return (1.0 - min(abs(a - b), LENGTH_SCORE_THRES_MS) / float(LENGTH_SCORE_THRES_MS)) def compare(self, other, ignored=None): parts = [] if ignored is None: ignored = [] if self.length and other.length and '~length' not in ignored: score = self.length_score(self.length, other.length) parts.append((score, 8)) for name, weight in self.__weights: if name in ignored: continue a = self[name] b = other[name] if a and b: if name in ('tracknumber', 'totaltracks', 'discnumber', 'totaldiscs'): try: ia = int(a) ib = int(b) except ValueError: ia = a ib = b score = 1.0 - (int(ia != ib)) else: score = similarity2(a, b) parts.append((score, weight)) elif (a and name in other.deleted_tags or b and name in self.deleted_tags): parts.append((0, weight)) return linear_combination_of_weights(parts) def compare_to_release(self, release, weights): """ Compare metadata to a MusicBrainz release. Produces a probability as a linear combination of weights that the metadata matches a certain album. """ parts = self.compare_to_release_parts(release, weights) sim = linear_combination_of_weights(parts) * get_score(release) return SimMatchRelease(similarity=sim, release=release) def compare_to_release_parts(self, release, weights): parts = [] if "album" in self: b = release['title'] parts.append((similarity2(self["album"], b), weights["album"])) if "albumartist" in self and "albumartist" in weights: a = self["albumartist"] b = artist_credit_from_node(release['artist-credit'])[0] parts.append((similarity2(a, b), weights["albumartist"])) try: a = int(self["totaltracks"]) b = release['track-count'] score = 0.0 if a > b else 0.3 if a < b else 1.0 parts.append((score, weights["totaltracks"])) except (ValueError, KeyError): pass # Date Logic date_match_factor = 0.0 if "date" in release and release['date'] != '': release_date = release['date'] if "date" in self: metadata_date = self['date'] if release_date == metadata_date: # release has a date and it matches what our metadata had exactly. date_match_factor = self.__date_match_factors['exact'] else: release_year = extract_year_from_date(release_date) if release_year is not None: metadata_year = extract_year_from_date(metadata_date) if metadata_year is not None: if release_year == metadata_year: # release has a date and it matches what our metadata had for year exactly. date_match_factor = self.__date_match_factors['year'] elif abs(release_year - metadata_year) <= 2: # release has a date and it matches what our metadata had closely (year +/- 2). date_match_factor = self.__date_match_factors['close_year'] else: # release has a date but it does not match ours (all else equal, # its better to have an unknown date than a wrong date, since # the unknown could actually be correct) date_match_factor = self.__date_match_factors['differed'] else: # release has a date but we don't have one (all else equal, we prefer # tracks that have non-blank date values) date_match_factor = self.__date_match_factors['exists_vs_null'] else: # release has a no date (all else equal, we don't prefer this # release since its date is missing) date_match_factor = self.__date_match_factors['no_release_date'] parts.append((date_match_factor, weights['date'])) config = get_config() weights_from_preferred_countries(parts, release, config.setting["preferred_release_countries"], weights["releasecountry"]) weights_from_preferred_formats(parts, release, config.setting["preferred_release_formats"], weights["format"]) if "releasetype" in weights: weights_from_release_type_scores(parts, release, config.setting["release_type_scores"], weights["releasetype"]) rg = QObject.tagger.get_release_group_by_id(release['release-group']['id']) if release['id'] in rg.loaded_albums: parts.append((1.0, 6)) return parts def compare_to_track(self, track, weights): parts = [] if 'title' in self: a = self['title'] b = track.get('title', '') parts.append((similarity2(a, b), weights["title"])) if 'artist' in self: a = self['artist'] artist_credits = track.get('artist-credit', []) b = artist_credit_from_node(artist_credits)[0] parts.append((similarity2(a, b), weights["artist"])) a = self.length if a > 0 and 'length' in track: b = track['length'] score = self.length_score(a, b) parts.append((score, weights["length"])) releases = [] if "releases" in track: releases = track['releases'] search_score = get_score(track) if not releases: sim = linear_combination_of_weights(parts) * search_score return SimMatchTrack(similarity=sim, releasegroup=None, release=None, track=track) if 'isvideo' in weights: metadata_is_video = self['~video'] == '1' track_is_video = track.get('video', False) score = 1 if metadata_is_video == track_is_video else 0 parts.append((score, weights['isvideo'])) result = SimMatchTrack(similarity=-1, releasegroup=None, release=None, track=None) for release in releases: release_parts = self.compare_to_release_parts(release, weights) sim = linear_combination_of_weights(parts + release_parts) * search_score if sim > result.similarity: rg = release['release-group'] if "release-group" in release else None result = SimMatchTrack(similarity=sim, releasegroup=rg, release=release, track=track) return result def copy(self, other, copy_images=True): self.clear() self._update_from_metadata(other, copy_images) def update(self, *args, **kwargs): one_arg = len(args) == 1 if one_arg and (isinstance(args[0], self.__class__) or isinstance(args[0], MultiMetadataProxy)): self._update_from_metadata(args[0]) elif one_arg and isinstance(args[0], MutableMapping): # update from MutableMapping (ie. dict) for k, v in args[0].items(): self[k] = v elif args or kwargs: # update from a dict-like constructor parameters for k, v in dict(*args, **kwargs).items(): self[k] = v else: # no argument, raise TypeError to mimic dict.update() raise TypeError("descriptor 'update' of '%s' object needs an argument" % self.__class__.__name__) def diff(self, other): """Returns a new Metadata object with only the tags that changed in self compared to other""" m = Metadata() for tag, values in self.rawitems(): other_values = other.getall(tag) if other_values != values: m[tag] = values m.deleted_tags = self.deleted_tags - other.deleted_tags return m def _update_from_metadata(self, other, copy_images=True): for k, v in other.rawitems(): self.set(k, v[:]) for tag in other.deleted_tags: del self[tag] if copy_images and other.images: self.images = other.images.copy() if other.length: self.length = other.length def clear(self): self._store.clear() self.images = ImageList() self.length = 0 self.clear_deleted() def clear_deleted(self): self.deleted_tags = set() @staticmethod def normalize_tag(name): return name.rstrip(':') def getall(self, name): return self._store.get(self.normalize_tag(name), []) def getraw(self, name): return self._store[self.normalize_tag(name)] def get(self, key, default=None): values = self._store.get(self.normalize_tag(key), None) if values: return self.multi_valued_joiner.join(values) else: return default def __getitem__(self, name): return self.get(name, '') def set(self, name, values): name = self.normalize_tag(name) if isinstance(values, str) or not isinstance(values, Iterable): values = [values] values = [str(value) for value in values if value or value == 0] if values: self._store[name] = values self.deleted_tags.discard(name) elif name in self._store: del self[name] def __setitem__(self, name, values): self.set(name, values) def __contains__(self, name): return self._store.__contains__(self.normalize_tag(name)) def __delitem__(self, name): name = self.normalize_tag(name) try: del self._store[name] except KeyError: pass finally: self.deleted_tags.add(name) def add(self, name, value): if value or value == 0: name = self.normalize_tag(name) self._store.setdefault(name, []).append(str(value)) self.deleted_tags.discard(name) def add_unique(self, name, value): name = self.normalize_tag(name) if value not in self.getall(name): self.add(name, value) def delete(self, name): """Deprecated: use del directly""" del self[self.normalize_tag(name)] def unset(self, name): """Removes a tag from the metadata, but does not mark it for deletion. Args: name: name of the tag to unset """ name = self.normalize_tag(name) try: del self._store[name] except KeyError: pass def __iter__(self): return iter(self._store) def items(self): for name, values in self._store.items(): for value in values: yield name, value def rawitems(self): """Returns the metadata items. >>> m.rawitems() [("key1", ["value1", "value2"]), ("key2", ["value3"])] """ return self._store.items() def apply_func(self, func): for name, values in list(self.rawitems()): if name not in PRESERVED_TAGS: self[name] = [func(value) for value in values] def strip_whitespace(self): """Strip leading/trailing whitespace. >>> m = Metadata() >>> m["foo"] = " bar " >>> m["foo"] " bar " >>> m.strip_whitespace() >>> m["foo"] "bar" """ self.apply_func(str.strip) def __repr__(self): return "%s(%r, deleted_tags=%r, length=%r, images=%r)" % (self.__class__.__name__, self._store, self.deleted_tags, self.length, self.images) def __str__(self): return ("store: %r\ndeleted: %r\nimages: %r\nlength: %r" % (self._store, self.deleted_tags, [str(img) for img in self.images], self.length))
class Metadata(MutableMapping): """List of metadata items with dict-like access.""" __weights = [ ('title', 22), ('artist', 6), ('album', 12), ('tracknumber', 6), ('totaltracks', 5), ] multi_valued_joiner = MULTI_VALUED_JOINER def __init__(self, *args, deleted_tags=None, images=None, length=None, **kwargs): self._store = dict() self.deleted_tags = set() self.length = 0 self.images = ImageList() self.has_common_images = True d = dict(*args, **kwargs) for k, v in d.items(): self[k] = v if images is not None: for image in images: self.images.append(image) if deleted_tags is not None: for tag in deleted_tags: del self[tag] if length is not None: self.length = int(length) def __bool__(self): return bool(len(self)) def __len__(self): return len(self._store) + len(self.images) @staticmethod def length_score(a, b): return (1.0 - min(abs(a - b), LENGTH_SCORE_THRES_MS) / float(LENGTH_SCORE_THRES_MS)) def compare(self, other, ignored=None): parts = [] if ignored is None: ignored = [] if self.length and other.length and '~length' not in ignored: score = self.length_score(self.length, other.length) parts.append((score, 8)) for name, weight in self.__weights: if name in ignored: continue a = self[name] b = other[name] if a and b: if name in ('tracknumber', 'totaltracks'): try: ia = int(a) ib = int(b) except ValueError: ia = a ib = b score = 1.0 - (int(ia != ib)) else: score = similarity2(a, b) parts.append((score, weight)) elif (a and name in other.deleted_tags or b and name in self.deleted_tags): parts.append((0, weight)) return linear_combination_of_weights(parts) def compare_to_release(self, release, weights): """ Compare metadata to a MusicBrainz release. Produces a probability as a linear combination of weights that the metadata matches a certain album. """ parts = self.compare_to_release_parts(release, weights) sim = linear_combination_of_weights(parts) * get_score(release) return SimMatchRelease(similarity=sim, release=release) def compare_to_release_parts(self, release, weights): parts = [] if "album" in self: b = release['title'] parts.append((similarity2(self["album"], b), weights["album"])) if "albumartist" in self and "albumartist" in weights: a = self["albumartist"] b = artist_credit_from_node(release['artist-credit'])[0] parts.append((similarity2(a, b), weights["albumartist"])) try: a = int(self["totaltracks"]) b = release['track-count'] score = 0.0 if a > b else 0.3 if a < b else 1.0 parts.append((score, weights["totaltracks"])) except (ValueError, KeyError): pass preferred_countries = config.setting["preferred_release_countries"] preferred_formats = config.setting["preferred_release_formats"] total_countries = len(preferred_countries) if total_countries: score = 0.0 if "country" in release: try: i = preferred_countries.index(release['country']) score = float(total_countries - i) / float(total_countries) except ValueError: pass parts.append((score, weights["releasecountry"])) total_formats = len(preferred_formats) if total_formats and 'media' in release: score = 0.0 subtotal = 0 for medium in release['media']: if "format" in medium: try: i = preferred_formats.index(medium['format']) score += float(total_formats - i) / float(total_formats) except ValueError: pass subtotal += 1 if subtotal > 0: score /= subtotal parts.append((score, weights["format"])) if "releasetype" in weights: # This section generates a score that determines how likely this release will be selected in a lookup. # The score goes from 0 to 1 with 1 being the most likely to be chosen and 0 the least likely # This score is based on the preferences of release-types found in this release # This algorithm works by taking the scores of the primary type (and secondary if found) and averages them # If no types are found, it is set to the score of the 'Other' type or 0.5 if 'Other' doesnt exist type_scores = dict(config.setting["release_type_scores"]) score = 0.0 other_score = type_scores.get('Other', 0.5) if 'release-group' in release and 'primary-type' in release[ 'release-group']: types_found = [release['release-group']['primary-type']] if 'secondary-types' in release['release-group']: types_found += release['release-group']['secondary-types'] for release_type in types_found: score += type_scores.get(release_type, other_score) score /= len(types_found) parts.append((score, weights["releasetype"])) rg = QObject.tagger.get_release_group_by_id( release['release-group']['id']) if release['id'] in rg.loaded_albums: parts.append((1.0, 6)) return parts def compare_to_track(self, track, weights): parts = [] if 'title' in self: a = self['title'] b = track.get('title', '') parts.append((similarity2(a, b), weights["title"])) if 'artist' in self: a = self['artist'] artist_credits = track.get('artist-credit', []) b = artist_credit_from_node(artist_credits)[0] parts.append((similarity2(a, b), weights["artist"])) a = self.length if a > 0 and 'length' in track: b = track['length'] score = self.length_score(a, b) parts.append((score, weights["length"])) releases = [] if "releases" in track: releases = track['releases'] search_score = get_score(track) if not releases: sim = linear_combination_of_weights(parts) * search_score return SimMatchTrack(similarity=sim, releasegroup=None, release=None, track=track) if 'isvideo' in weights: metadata_is_video = self['~video'] == '1' track_is_video = track.get('video', False) score = 1 if metadata_is_video == track_is_video else 0 parts.append((score, weights['isvideo'])) result = SimMatchTrack(similarity=-1, releasegroup=None, release=None, track=None) for release in releases: release_parts = self.compare_to_release_parts(release, weights) sim = linear_combination_of_weights(parts + release_parts) * search_score if sim > result.similarity: rg = release[ 'release-group'] if "release-group" in release else None result = SimMatchTrack(similarity=sim, releasegroup=rg, release=release, track=track) return result def copy(self, other, copy_images=True): self.clear() self._update_from_metadata(other, copy_images) def update(self, *args, **kwargs): one_arg = len(args) == 1 if one_arg and isinstance(args[0], self.__class__): self._update_from_metadata(args[0]) elif one_arg and isinstance(args[0], MutableMapping): # update from MutableMapping (ie. dict) for k, v in args[0].items(): self[k] = v elif args or kwargs: # update from a dict-like constructor parameters for k, v in dict(*args, **kwargs).items(): self[k] = v else: # no argument, raise TypeError to mimic dict.update() raise TypeError( "descriptor 'update' of '%s' object needs an argument" % self.__class__.__name__) def _update_from_metadata(self, other, copy_images=True): for k, v in other.rawitems(): self.set(k, v[:]) for tag in other.deleted_tags: del self[tag] if copy_images and other.images: self.images = other.images.copy() if other.length: self.length = other.length def clear(self): self._store.clear() self.images = ImageList() self.length = 0 self.clear_deleted() def clear_deleted(self): self.deleted_tags = set() def getall(self, name): return self._store.get(name, []) def getraw(self, name): return self._store[name] def get(self, key, default=None): values = self._store.get(key, None) if values: return self.multi_valued_joiner.join(values) else: return default def __getitem__(self, name): return self.get(name, '') def set(self, name, values): if isinstance(values, str) or not isinstance(values, Iterable): values = [values] values = [str(value) for value in values if value or value == 0] if values: self._store[name] = values self.deleted_tags.discard(name) elif name in self._store: del self[name] def __setitem__(self, name, values): self.set(name, values) def __contains__(self, name): return self._store.__contains__(name) def __delitem__(self, name): try: del self._store[name] except KeyError: pass finally: self.deleted_tags.add(name) def add(self, name, value): if value or value == 0: self._store.setdefault(name, []).append(str(value)) self.deleted_tags.discard(name) def add_unique(self, name, value): if value not in self.getall(name): self.add(name, value) def delete(self, name): """Deprecated: use del directly""" del self[name] def __iter__(self): return iter(self._store) def items(self): for name, values in self._store.items(): for value in values: yield name, value def rawitems(self): """Returns the metadata items. >>> m.rawitems() [("key1", ["value1", "value2"]), ("key2", ["value3"])] """ return self._store.items() def apply_func(self, func): for name, values in list(self.rawitems()): if name not in PRESERVED_TAGS: self[name] = [func(value) for value in values] def strip_whitespace(self): """Strip leading/trailing whitespace. >>> m = Metadata() >>> m["foo"] = " bar " >>> m["foo"] " bar " >>> m.strip_whitespace() >>> m["foo"] "bar" """ self.apply_func(str.strip) def __repr__(self): return "%s(%r, deleted_tags=%r, length=%r, images=%r)" % ( self.__class__.__name__, self._store, self.deleted_tags, self.length, self.images) def __str__(self): return ("store: %r\ndeleted: %r\nimages: %r\nlength: %r" % (self._store, self.deleted_tags, [str(img) for img in self.images], self.length))
class ImageListTest(PicardTestCase): def setUp(self): super().setUp() self.imagelist = ImageList() def create_image(name, types): return CoverArtImage( url='file://file' + name, data=create_fake_png(name.encode('utf-8')), types=types, support_types=True, support_multi_types=True ) self.images = { 'a': create_image('a', ["booklet"]), 'b': create_image('b', ["booklet", "front"]), 'c': create_image('c', ["front", "booklet"]), } def test_append(self): self.imagelist.append(self.images['a']) self.assertEqual(self.imagelist[0], self.images['a']) def test_eq(self): list1 = ImageList() list2 = ImageList() list3 = ImageList() list1.append(self.images['a']) list1.append(self.images['b']) list2.append(self.images['b']) list2.append(self.images['a']) list3.append(self.images['a']) list3.append(self.images['c']) self.assertEqual(list1, list2) self.assertNotEqual(list1, list3) def test_get_front_image(self): self.imagelist.append(self.images['a']) self.imagelist.append(self.images['b']) self.assertEqual(self.imagelist.get_front_image(), self.images['b']) def test_to_be_saved_to_tags(self): def to_be_saved(settings): return self.imagelist.to_be_saved_to_tags(settings=settings) settings = { "save_images_to_tags": True, "embed_only_one_front_image": False, } # save all but no images self.assertEqual(list(to_be_saved(settings)), []) # save all, only one non-front image in the list self.imagelist.append(self.images['a']) self.assertEqual(list(to_be_saved(settings)), [self.images['a']]) # save all, 2 images, one of them is a front image (b) self.imagelist.append(self.images['b']) self.assertEqual(list(to_be_saved(settings)), [self.images['a'], self.images['b']]) # save only one front, 2 images, one of them is a front image (b) settings["embed_only_one_front_image"] = True self.assertEqual(list(to_be_saved(settings)), [self.images['b']]) # save only one front, 3 images, two of them have front type (b & c) self.imagelist.append(self.images['c']) self.assertEqual(list(to_be_saved(settings)), [self.images['b']]) # 3 images, but do not save settings["save_images_to_tags"] = False self.assertEqual(list(to_be_saved(settings)), []) # settings is missing a setting del settings["save_images_to_tags"] with self.assertRaises(KeyError): image = next(to_be_saved(settings)) def test_strip_front_images(self): self.imagelist.append(self.images['a']) self.imagelist.append(self.images['b']) self.imagelist.append(self.images['c']) # strip front images from list, only a isn't self.assertEqual(len(self.imagelist), 3) self.imagelist.strip_front_images() self.assertNotIn(self.images['b'], self.imagelist) self.assertNotIn(self.images['c'], self.imagelist) self.assertIn(self.images['a'], self.imagelist) self.assertEqual(len(self.imagelist), 1) def test_imagelist_insert(self): imagelist = ImageList() imagelist.insert(0, 'a') self.assertEqual(imagelist[0], 'a') imagelist.insert(0, 'b') self.assertEqual(imagelist[0], 'b') self.assertEqual(imagelist[1], 'a') def test_imagelist_clear(self): imagelist = ImageList(['a', 'b']) self.assertEqual(len(imagelist), 2) imagelist.clear() self.assertEqual(len(imagelist), 0) def test_imagelist_copy(self): imagelist1 = ImageList(['a', 'b']) imagelist2 = imagelist1.copy() imagelist3 = imagelist1 imagelist1[0] = 'c' self.assertEqual(imagelist2[0], 'a') self.assertEqual(imagelist3[0], 'c') def test_imagelist_del(self): imagelist = ImageList(['a', 'b']) del imagelist[0] self.assertEqual(imagelist[0], 'b') self.assertEqual(len(imagelist), 1)
class Metadata(dict): """List of metadata items with dict-like access.""" __weights = [ ('title', 22), ('artist', 6), ('album', 12), ('tracknumber', 6), ('totaltracks', 5), ] multi_valued_joiner = MULTI_VALUED_JOINER def __init__(self): super(Metadata, self).__init__() self.images = ImageList() self.deleted_tags = set() self.length = 0 def __bool__(self): return bool(len(self) or len(self.images)) def append_image(self, coverartimage): self.images.append(coverartimage) def set_front_image(self, coverartimage): # First remove all front images self.images[:] = [img for img in self.images if not img.is_front_image()] self.images.append(coverartimage) @property def images_to_be_saved_to_tags(self): if not config.setting["save_images_to_tags"]: return () images = [img for img in self.images if img.can_be_saved_to_tags] if config.setting["embed_only_one_front_image"]: front_image = self.get_single_front_image(images) if front_image: return front_image return images def get_single_front_image(self, images=None): if not images: images = self.images for img in images: if img.is_front_image(): return [img] return [] def remove_image(self, index): self.images.pop(index) def compare(self, other): parts = [] if self.length and other.length: score = 1.0 - min(abs(self.length - other.length), 30000) / 30000.0 parts.append((score, 8)) for name, weight in self.__weights: a = self[name] b = other[name] if a and b: if name in ('tracknumber', 'totaltracks'): try: ia = int(a) ib = int(b) except ValueError: ia = a ib = b score = 1.0 - (int(ia != ib)) else: score = similarity2(a, b) parts.append((score, weight)) return linear_combination_of_weights(parts) def compare_to_release(self, release, weights): """ Compare metadata to a MusicBrainz release. Produces a probability as a linear combination of weights that the metadata matches a certain album. """ parts = self.compare_to_release_parts(release, weights) return (linear_combination_of_weights(parts), release) def compare_to_release_parts(self, release, weights): parts = [] if "album" in self: b = release['title'] parts.append((similarity2(self["album"], b), weights["album"])) if "albumartist" in self and "albumartist" in weights: a = self["albumartist"] b = artist_credit_from_node(release['artist-credit'])[0] parts.append((similarity2(a, b), weights["albumartist"])) try: a = int(self["totaltracks"]) except (ValueError, KeyError): pass else: if "title" in weights: b = release['media'][0]['track-count'] else: b = release['track-count'] score = 0.0 if a > b else 0.3 if a < b else 1.0 parts.append((score, weights["totaltracks"])) preferred_countries = config.setting["preferred_release_countries"] preferred_formats = config.setting["preferred_release_formats"] total_countries = len(preferred_countries) if total_countries: score = 0.0 if "country" in release: try: i = preferred_countries.index(release['country']) score = float(total_countries - i) / float(total_countries) except ValueError: pass parts.append((score, weights["releasecountry"])) total_formats = len(preferred_formats) if total_formats: score = 0.0 subtotal = 0 for medium in release['media']: if "format" in medium: try: i = preferred_formats.index(medium['format']) score += float(total_formats - i) / float(total_formats) except ValueError: pass subtotal += 1 if subtotal > 0: score /= subtotal parts.append((score, weights["format"])) if "releasetype" in weights: type_scores = dict(config.setting["release_type_scores"]) if 'release-group' in release and 'primary-type' in release['release-group']: release_type = release['release-group']['primary-type'] score = type_scores.get(release_type, type_scores.get('Other', 0.5)) else: score = 0.0 parts.append((score, weights["releasetype"])) rg = QObject.tagger.get_release_group_by_id(release['release-group']['id']) if release['id'] in rg.loaded_albums: parts.append((1.0, 6)) return parts def compare_to_track(self, track, weights): parts = [] if 'title' in self: a = self['title'] b = track['title'] parts.append((similarity2(a, b), weights["title"])) if 'artist' in self: a = self['artist'] b = artist_credit_from_node(track['artist-credit'])[0] parts.append((similarity2(a, b), weights["artist"])) a = self.length if a > 0 and 'length' in track: b = track['length'] score = 1.0 - min(abs(a - b), 30000) / 30000.0 parts.append((score, weights["length"])) releases = [] if "releases" in track: releases = track['releases'] if not releases: sim = linear_combination_of_weights(parts) return (sim, None, None, track) result = (-1,) for release in releases: release_parts = self.compare_to_release_parts(release, weights) sim = linear_combination_of_weights(parts + release_parts) if sim > result[0]: rg = release['release-group'] if "release-group" in release else None result = (sim, rg, release, track) return result def copy(self, other): self.clear() self.update(other) def update(self, other): for key in other.keys(): self.set(key, other.getall(key)[:]) if other.images: self.images = other.images[:] if other.length: self.length = other.length self.deleted_tags.update(other.deleted_tags) # Remove deleted tags from UI on save for tag in other.deleted_tags: self.pop(tag, None) def clear(self): dict.clear(self) self.images = ImageList() self.length = 0 self.deleted_tags = set() def getall(self, name): return dict.get(self, name, []) def get(self, name, default=None): values = dict.get(self, name, None) if values: return self.multi_valued_joiner.join(values) else: return default def __getitem__(self, name): return self.get(name, '') def set(self, name, values): dict.__setitem__(self, name, values) if name in self.deleted_tags: self.deleted_tags.remove(name) def __setitem__(self, name, values): if not isinstance(values, list): values = [values] values = [string_(value) for value in values if value] if len(values): self.set(name, values) else: self.delete(name) def add(self, name, value): if value or value == 0: self.setdefault(name, []).append(value) if name in self.deleted_tags: self.deleted_tags.remove(name) def add_unique(self, name, value): if value not in self.getall(name): self.add(name, value) def delete(self, name): if name in self: self.pop(name, None) self.deleted_tags.add(name) def items(self): for name, values in dict.items(self): for value in values: yield name, value def rawitems(self): """Returns the metadata items. >>> m.rawitems() [("key1", ["value1", "value2"]), ("key2", ["value3"])] """ return dict.items(self) def apply_func(self, func): for key, values in self.rawitems(): if key not in PRESERVED_TAGS: self[key] = [func(value) for value in values] def strip_whitespace(self): """Strip leading/trailing whitespace. >>> m = Metadata() >>> m["foo"] = " bar " >>> m["foo"] " bar " >>> m.strip_whitespace() >>> m["foo"] "bar" """ self.apply_func(lambda s: s.strip())
class Metadata(MutableMapping): """List of metadata items with dict-like access.""" __weights = [ ('title', 22), ('artist', 6), ('album', 12), ('tracknumber', 6), ('totaltracks', 5), ] multi_valued_joiner = MULTI_VALUED_JOINER def __init__(self, *args, deleted_tags=None, images=None, length=None, **kwargs): self._store = dict() self.deleted_tags = set() self.length = 0 self.images = ImageList() self.has_common_images = True d = dict(*args, **kwargs) for k, v in d.items(): self[k] = v if images is not None: for image in images: self.images.append(image) if deleted_tags is not None: for tag in deleted_tags: del self[tag] if length is not None: self.length = int(length) def __bool__(self): return bool(len(self)) def __len__(self): return len(self._store) + len(self.images) @staticmethod def length_score(a, b): return (1.0 - min(abs(a - b), LENGTH_SCORE_THRES_MS) / float(LENGTH_SCORE_THRES_MS)) def compare(self, other): parts = [] if self.length and other.length: score = self.length_score(self.length, other.length) parts.append((score, 8)) for name, weight in self.__weights: a = self[name] b = other[name] if a and b: if name in ('tracknumber', 'totaltracks'): try: ia = int(a) ib = int(b) except ValueError: ia = a ib = b score = 1.0 - (int(ia != ib)) else: score = similarity2(a, b) parts.append((score, weight)) elif (a and name in other.deleted_tags or b and name in self.deleted_tags): parts.append((0, weight)) return linear_combination_of_weights(parts) def compare_to_release(self, release, weights): """ Compare metadata to a MusicBrainz release. Produces a probability as a linear combination of weights that the metadata matches a certain album. """ parts = self.compare_to_release_parts(release, weights) sim = linear_combination_of_weights(parts) if 'score' in release: sim *= release['score'] / 100 return SimMatchRelease(similarity=sim, release=release) def compare_to_release_parts(self, release, weights): parts = [] if "album" in self: b = release['title'] parts.append((similarity2(self["album"], b), weights["album"])) if "albumartist" in self and "albumartist" in weights: a = self["albumartist"] b = artist_credit_from_node(release['artist-credit'])[0] parts.append((similarity2(a, b), weights["albumartist"])) try: a = int(self["totaltracks"]) except (ValueError, KeyError): pass else: try: if "title" in weights: b = release['media'][0]['track-count'] else: b = release['track-count'] except KeyError: b = 0 score = 0.0 if a > b else 0.3 if a < b else 1.0 parts.append((score, weights["totaltracks"])) preferred_countries = config.setting["preferred_release_countries"] preferred_formats = config.setting["preferred_release_formats"] total_countries = len(preferred_countries) if total_countries: score = 0.0 if "country" in release: try: i = preferred_countries.index(release['country']) score = float(total_countries - i) / float(total_countries) except ValueError: pass parts.append((score, weights["releasecountry"])) total_formats = len(preferred_formats) if total_formats and 'media' in release: score = 0.0 subtotal = 0 for medium in release['media']: if "format" in medium: try: i = preferred_formats.index(medium['format']) score += float(total_formats - i) / float(total_formats) except ValueError: pass subtotal += 1 if subtotal > 0: score /= subtotal parts.append((score, weights["format"])) if "releasetype" in weights: # This section generates a score that determines how likely this release will be selected in a lookup. # The score goes from 0 to 1 with 1 being the most likely to be chosen and 0 the least likely # This score is based on the preferences of release-types found in this release # This algorithm works by taking the scores of the primary type (and secondary if found) and averages them # If no types are found, it is set to the score of the 'Other' type or 0.5 if 'Other' doesnt exist type_scores = dict(config.setting["release_type_scores"]) score = 0.0 other_score = type_scores.get('Other', 0.5) if 'release-group' in release and 'primary-type' in release['release-group']: types_found = [release['release-group']['primary-type']] if 'secondary-types' in release['release-group']: types_found += release['release-group']['secondary-types'] for release_type in types_found: score += type_scores.get(release_type, other_score) score /= len(types_found) parts.append((score, weights["releasetype"])) rg = QObject.tagger.get_release_group_by_id(release['release-group']['id']) if release['id'] in rg.loaded_albums: parts.append((1.0, 6)) return parts def compare_to_track(self, track, weights): parts = [] if 'title' in self: a = self['title'] b = track.get('title', '') parts.append((similarity2(a, b), weights["title"])) if 'artist' in self: a = self['artist'] artist_credits = track.get('artist-credit', []) b = artist_credit_from_node(artist_credits)[0] parts.append((similarity2(a, b), weights["artist"])) a = self.length if a > 0 and 'length' in track: b = track['length'] score = self.length_score(a, b) parts.append((score, weights["length"])) releases = [] if "releases" in track: releases = track['releases'] if not releases: sim = linear_combination_of_weights(parts) return SimMatchTrack(similarity=sim, releasegroup=None, release=None, track=track) result = SimMatchTrack(similarity=-1, releasegroup=None, release=None, track=None) for release in releases: release_parts = self.compare_to_release_parts(release, weights) sim = linear_combination_of_weights(parts + release_parts) if 'score' in track: sim *= track['score'] / 100 if sim > result.similarity: rg = release['release-group'] if "release-group" in release else None result = SimMatchTrack(similarity=sim, releasegroup=rg, release=release, track=track) return result def copy(self, other): self.clear() self.update(other) def update(self, *args, **kwargs): one_arg = len(args) == 1 if one_arg and isinstance(args[0], self.__class__): # update from Metadata object other = args[0] for k, v in other.rawitems(): self.set(k, v[:]) for tag in other.deleted_tags: del self[tag] if other.images: self.images = other.images.copy() if other.length: self.length = other.length elif one_arg and isinstance(args[0], MutableMapping): # update from MutableMapping (ie. dict) for k, v in args[0].items(): self[k] = v elif args or kwargs: # update from a dict-like constructor parameters for k, v in dict(*args, **kwargs).items(): self[k] = v else: # no argument, raise TypeError to mimic dict.update() raise TypeError("descriptor 'update' of '%s' object needs an argument" % self.__class__.__name__) def clear(self): self._store.clear() self.images = ImageList() self.length = 0 self.clear_deleted() def clear_deleted(self): self.deleted_tags = set() def getall(self, name): return self._store.get(name, []) def getraw(self, name): return self._store[name] def get(self, key, default=None): values = self._store.get(key, None) if values: return self.multi_valued_joiner.join(values) else: return default def __contains__(self, name): return self._store.__contains__(name) def __getitem__(self, name): return self.get(name, '') def set(self, name, values): self._store[name] = values self.deleted_tags.discard(name) def __setitem__(self, name, values): if isinstance(values, str) or not isinstance(values, Iterable): values = [values] values = [str(value) for value in values if value or value == 0] if values: self.set(name, values) elif name in self._store: del self[name] def __delitem__(self, name): try: del self._store[name] except KeyError: pass finally: self.deleted_tags.add(name) def add(self, name, value): if value or value == 0: self._store.setdefault(name, []).append(value) self.deleted_tags.discard(name) def add_unique(self, name, value): if value not in self.getall(name): self.add(name, value) def delete(self, name): """Deprecated: use del directly""" del self[name] def __iter__(self): return iter(self._store) def items(self): for name, values in self._store.items(): for value in values: yield name, value def rawitems(self): """Returns the metadata items. >>> m.rawitems() [("key1", ["value1", "value2"]), ("key2", ["value3"])] """ return self._store.items() def apply_func(self, func): for name, values in self.rawitems(): if name not in PRESERVED_TAGS: self[name] = [func(value) for value in values] def strip_whitespace(self): """Strip leading/trailing whitespace. >>> m = Metadata() >>> m["foo"] = " bar " >>> m["foo"] " bar " >>> m.strip_whitespace() >>> m["foo"] "bar" """ self.apply_func(str.strip) def __repr__(self): return "%s(%r, deleted_tags=%r, length=%r, images=%r)" % (self.__class__.__name__, self._store, self.deleted_tags, self.length, self.images) def __str__(self): return ("store: %r\ndeleted: %r\nimages: %r\nlength: %r" % (self._store, self.deleted_tags, [str(img) for img in self.images], self.length))
class Metadata(dict): """List of metadata items with dict-like access.""" __weights = [ ('title', 22), ('artist', 6), ('album', 12), ('tracknumber', 6), ('totaltracks', 5), ] multi_valued_joiner = MULTI_VALUED_JOINER def __init__(self): super(Metadata, self).__init__() self.images = ImageList() self.deleted_tags = set() self.length = 0 def __bool__(self): return bool(len(self) or len(self.images)) def append_image(self, coverartimage): self.images.append(coverartimage) def set_front_image(self, coverartimage): # First remove all front images self.images[:] = [ img for img in self.images if not img.is_front_image() ] self.images.append(coverartimage) @property def images_to_be_saved_to_tags(self): if not config.setting["save_images_to_tags"]: return () images = [img for img in self.images if img.can_be_saved_to_tags] if config.setting["embed_only_one_front_image"]: front_image = self.get_single_front_image(images) if front_image: return front_image return images def get_single_front_image(self, images=None): if not images: images = self.images for img in images: if img.is_front_image(): return [img] return [] def remove_image(self, index): self.images.pop(index) def compare(self, other): parts = [] if self.length and other.length: score = 1.0 - min(abs(self.length - other.length), 30000) / 30000.0 parts.append((score, 8)) for name, weight in self.__weights: a = self[name] b = other[name] if a and b: if name in ('tracknumber', 'totaltracks'): try: ia = int(a) ib = int(b) except ValueError: ia = a ib = b score = 1.0 - (int(ia != ib)) else: score = similarity2(a, b) parts.append((score, weight)) return linear_combination_of_weights(parts) def compare_to_release(self, release, weights): """ Compare metadata to a MusicBrainz release. Produces a probability as a linear combination of weights that the metadata matches a certain album. """ parts = self.compare_to_release_parts(release, weights) return (linear_combination_of_weights(parts), release) def compare_to_release_parts(self, release, weights): parts = [] if "album" in self: b = release.title[0].text parts.append((similarity2(self["album"], b), weights["album"])) if "albumartist" in self and "albumartist" in weights: a = self["albumartist"] b = artist_credit_from_node(release.artist_credit[0])[0] parts.append((similarity2(a, b), weights["albumartist"])) if "totaltracks" in self: try: a = int(self["totaltracks"]) except ValueError: pass else: if "title" in weights: b = int( release.medium_list[0].medium[0].track_list[0].count) else: b = int(release.medium_list[0].track_count[0].text) score = 0.0 if a > b else 0.3 if a < b else 1.0 parts.append((score, weights["totaltracks"])) preferred_countries = config.setting["preferred_release_countries"] preferred_formats = config.setting["preferred_release_formats"] total_countries = len(preferred_countries) if total_countries: score = 0.0 if "country" in release.children: try: i = preferred_countries.index(release.country[0].text) score = float(total_countries - i) / float(total_countries) except ValueError: pass parts.append((score, weights["releasecountry"])) total_formats = len(preferred_formats) if total_formats: score = 0.0 subtotal = 0 for medium in release.medium_list[0].medium: if "format" in medium.children: try: i = preferred_formats.index(medium.format[0].text) score += float(total_formats - i) / float(total_formats) except ValueError: pass subtotal += 1 if subtotal > 0: score /= subtotal parts.append((score, weights["format"])) if "releasetype" in weights: type_scores = dict(config.setting["release_type_scores"]) if 'release_group' in release.children and 'type' in release.release_group[ 0].attribs: release_type = release.release_group[0].type score = type_scores.get(release_type, type_scores.get('Other', 0.5)) else: score = 0.0 parts.append((score, weights["releasetype"])) rg = QObject.tagger.get_release_group_by_id( release.release_group[0].id) if release.id in rg.loaded_albums: parts.append((1.0, 6)) return parts def compare_to_track(self, track, weights): parts = [] if 'title' in self: a = self['title'] b = track.title[0].text parts.append((similarity2(a, b), weights["title"])) if 'artist' in self: a = self['artist'] b = artist_credit_from_node(track.artist_credit[0])[0] parts.append((similarity2(a, b), weights["artist"])) a = self.length if a > 0 and 'length' in track.children: b = int(track.length[0].text) score = 1.0 - min(abs(a - b), 30000) / 30000.0 parts.append((score, weights["length"])) releases = [] if "release_list" in track.children and "release" in track.release_list[ 0].children: releases = track.release_list[0].release if not releases: sim = linear_combination_of_weights(parts) return (sim, None, None, track) result = (-1, ) for release in releases: release_parts = self.compare_to_release_parts(release, weights) sim = linear_combination_of_weights(parts + release_parts) if sim > result[0]: rg = release.release_group[ 0] if "release_group" in release.children else None result = (sim, rg, release, track) return result def copy(self, other): self.clear() self.update(other) def update(self, other): for key in other.keys(): self.set(key, other.getall(key)[:]) if other.images: self.images = other.images[:] if other.length: self.length = other.length self.deleted_tags.update(other.deleted_tags) # Remove deleted tags from UI on save for tag in other.deleted_tags: self.pop(tag, None) def clear(self): dict.clear(self) self.images = ImageList() self.length = 0 self.deleted_tags = set() def getall(self, name): return dict.get(self, name, []) def get(self, name, default=None): values = dict.get(self, name, None) if values: return self.multi_valued_joiner.join(values) else: return default def __getitem__(self, name): return self.get(name, u'') def set(self, name, values): dict.__setitem__(self, name, values) if name in self.deleted_tags: self.deleted_tags.remove(name) def __setitem__(self, name, values): if not isinstance(values, list): values = [values] values = [string_(value) for value in values if value] if len(values): self.set(name, values) else: self.delete(name) def add(self, name, value): if value or value == 0: self.setdefault(name, []).append(value) if name in self.deleted_tags: self.deleted_tags.remove(name) def add_unique(self, name, value): if value not in self.getall(name): self.add(name, value) def delete(self, name): if name in self: self.pop(name, None) self.deleted_tags.add(name) def items(self): for name, values in dict.items(self): for value in values: yield name, value def rawitems(self): """Returns the metadata items. >>> m.rawitems() [("key1", ["value1", "value2"]), ("key2", ["value3"])] """ return dict.items(self) def apply_func(self, func): for key, values in self.rawitems(): if not key.startswith("~"): self[key] = [func(value) for value in values] def strip_whitespace(self): """Strip leading/trailing whitespace. >>> m = Metadata() >>> m["foo"] = " bar " >>> m["foo"] " bar " >>> m.strip_whitespace() >>> m["foo"] "bar" """ self.apply_func(lambda s: s.strip())
class ImageListTest(PicardTestCase): def setUp(self): super().setUp() self.imagelist = ImageList() def create_image(name, types): return CoverArtImage( url='file://file' + name, data=create_fake_png(name.encode('utf-8')), types=types, support_types=True, support_multi_types=True ) self.images = { 'a': create_image('a', ["booklet"]), 'b': create_image('b', ["booklet", "front"]), 'c': create_image('c', ["front", "booklet"]), } def test_append(self): self.imagelist.append(self.images['a']) self.assertEqual(self.imagelist[0], self.images['a']) def test_eq(self): list1 = ImageList() list2 = ImageList() list3 = ImageList() list1.append(self.images['a']) list1.append(self.images['b']) list2.append(self.images['b']) list2.append(self.images['a']) list3.append(self.images['a']) list3.append(self.images['c']) self.assertTrue(list1 == list2) self.assertFalse(list1 == list3) def test_get_front_image(self): self.imagelist.append(self.images['a']) self.imagelist.append(self.images['b']) self.assertEqual(self.imagelist.get_front_image(), self.images['b']) def test_to_be_saved_to_tags(self): def to_be_saved(settings): return self.imagelist.to_be_saved_to_tags(settings=settings) settings = { "save_images_to_tags": True, "embed_only_one_front_image": False, } # save all but no images self.assertEqual(list(to_be_saved(settings)), []) # save all, only one non-front image in the list self.imagelist.append(self.images['a']) self.assertEqual(list(to_be_saved(settings)), [self.images['a']]) # save all, 2 images, one of them is a front image (b) self.imagelist.append(self.images['b']) self.assertEqual(list(to_be_saved(settings)), [self.images['a'], self.images['b']]) # save only one front, 2 images, one of them is a front image (b) settings["embed_only_one_front_image"] = True self.assertEqual(list(to_be_saved(settings)), [self.images['b']]) # save only one front, 3 images, two of them have front type (b & c) self.imagelist.append(self.images['c']) self.assertEqual(list(to_be_saved(settings)), [self.images['b']]) # 3 images, but do not save settings["save_images_to_tags"] = False self.assertEqual(list(to_be_saved(settings)), []) # settings is missing a setting del settings["save_images_to_tags"] with self.assertRaises(KeyError): image = next(to_be_saved(settings)) def test_strip_front_images(self): self.imagelist.append(self.images['a']) self.imagelist.append(self.images['b']) self.imagelist.append(self.images['c']) # strip front images from list, only a isn't self.assertEqual(len(self.imagelist), 3) self.imagelist.strip_front_images() self.assertNotIn(self.images['b'], self.imagelist) self.assertNotIn(self.images['c'], self.imagelist) self.assertIn(self.images['a'], self.imagelist) self.assertEqual(len(self.imagelist), 1) def test_imagelist_insert(self): l = ImageList() l.insert(0, 'a') self.assertEqual(l[0], 'a') l.insert(0, 'b') self.assertEqual(l[0], 'b') self.assertEqual(l[1], 'a') def test_imagelist_clear(self): l = ImageList(['a', 'b']) self.assertEqual(len(l), 2) l.clear() self.assertEqual(len(l), 0) def test_imagelist_copy(self): l = ['a', 'b'] l1 = ImageList(l) l2 = l1.copy() l3 = l1 l1[0] = 'c' self.assertEqual(l2[0], 'a') self.assertEqual(l3[0], 'c') def test_imagelist_del(self): l = ImageList(['a', 'b']) del l[0] self.assertEqual(l[0], 'b') self.assertEqual(len(l), 1)