Пример #1
0
    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)
Пример #2
0
    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)
Пример #3
0
    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)
Пример #4
0
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())
Пример #5
0
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))
Пример #6
0
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))
Пример #7
0
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)
Пример #8
0
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())
Пример #9
0
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))
Пример #10
0
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())
Пример #11
0
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)