class Session:
    """Main API to *libmunin* and caching layer."""
    def __init__(self, name, mask, config=DEFAULT_CONFIG):
        """Create a new session:

        :param name: The name of the session. Used to load it again from disk.
        :param mask: The mask. See :term:`Mask`
        :param config: A dictionary with config values. See :class:`DefaultConfig` for available keys.
        """
        self._config = config
        self._name = name

        # Publicly readable attribute.
        self.mapping = {}

        # Make access to the mask more efficient
        self._mask = copy(mask)
        self._attribute_list = sorted(mask)
        self._listidx_to_key = {k: i for i, k in enumerate(self._attribute_list)}

        # Lookup tables for those attributes (fast access is crucial here)
        def make_index(idx, default_func):
            index = {}
            for key, descr in self._mask.items():
                if descr[idx] is not None:
                    index[key] = descr[idx]
                else:
                    index[key] = default_func(key)

            return index

        # Import this locally, since we might get circular import otherway:
        from munin.distance import DistanceFunction
        from munin.provider import Provider

        # Build indices and set default values:
        self._key_to_providers = make_index(0,
                lambda key: Provider()
        )
        self._key_to_distfuncs = make_index(1,
                lambda key: DistanceFunction(self._key_to_providers[key])
        )
        self._key_to_weighting = make_index(2,
                lambda key: 1.0
        )

        # Sum of the individual weights, pre-calculated once.
        self._weight_sum = sum((descr[2] for descr in mask.values()))

        # Create the associated database.
        self._database = Database(self)

        # Filtering related:
        self._filtering_enabled = config['recom_history_sieving']
        self._recom_history = RecommendationHistory(
            penalty_map=config['recom_history_penalty']
        )

        # Publicly readable attribute.
        self.mapping = bidict()

    def __getitem__(self, idx):
        """Return the song with a certain uid.

        :raises KeyError: On invalid uid.
        :returns: a song object which has the same uid attribute.
        """
        return self.database[idx]

    def __iter__(self):
        """Iterate over all songs in the database

        This is a proxy to Database.__iter__
        """
        return iter(self.database)

    def __len__(self):
        """Get the number of songs currently in the database"""
        return len(self.database)

    @property
    def name(self):
        'Return the name you passed to the session'
        return self._name

    @property
    def database(self):
        'yield the associated :class:`munin.database.Database`'
        return self._database

    @property
    def config(self):
        'Return the config dictionary passed to ``__init__``'
        return self._config

    ###############################
    #  Attribute Mask Attributes  #
    ###############################

    # Make this only gettable, so we can distribute the reference
    # around all session objects.
    @property
    def mask(self):
        'Returns a copy of the mask (as passed in)'
        return copy(self._mask)

    @property
    def mask_length(self):
        'Returns the length of the attribte mask (number of keys)'
        return len(self._mask)

    def key_at_index(self, idx):
        'Retrieve the key of the mask at index ``idx``'
        return self._attribute_list[idx]

    def index_for_key(self, key):
        'Retrieve the index for the key given by ``key``'
        return self._listidx_to_key[key]

    def provider_for_key(self, key):
        'Get the provider for the key in ``key``'
        return self._key_to_providers[key]

    def distance_function_for_key(self, key):
        'Get the :class:`munin.distance.DistanceFunction` for ``key``'
        return self._key_to_distfuncs[key]

    def weight_for_key(self, key):
        'Get the weighting (*float*) for ``key``'
        return self._key_to_weighting[key]

    def _weight(self, dist_dict):
        'This is in Session for performance reasons'
        dist_sum = 0.0

        for key, (_, _, weight) in self._mask.items():
            try:
                dist_sum += dist_dict[key] * weight
            except KeyError:
                dist_sum += weight

        return dist_sum / self._weight_sum

    ############################
    #  Caching Implementation  #
    ############################

    @staticmethod
    def from_archive_path(full_path):
        """Load a cached session from a file on the disk.

        Example usage: ::

            >>> Session.from_archive_path('/tmp/test.gz')
            <Session object at 0x2343424>

        .. note::

            If you prefer to save the sessions in XDG_CACHE_HOME anyway,
            just use :func:`Session.from_name`.

        :param full_path: a path to a packed session.
        :type full_path: str
        :returns: A cached session.
        :rtype: :class:`Session`
        """
        base_path, _ = os.path.splitext(full_path)
        try:
            with tarfile.open(full_path, 'r:*') as tar:
                tar.extractall(base_path)

            with open(os.path.join(base_path, 'session.pickle'), 'rb') as handle:
                return pickle.load(handle)
        except OSError as err:
            LOGGER.debug('Could not load session: ' + str(err))
            return None

    @staticmethod
    def from_name(session_name):
        """Like :func:`from_archive_path`, but be clever and load it
        from *${XDG_CACHE_HOME}/libmunin/<session_name>/session.pickle*

        :param session_name: The name of a session.
        :type session_name: str
        :returns: A cached session.
        :rtype: :class:`Session`
        """
        return Session.from_archive_path(get_cache_path(session_name) + '.gz')

    def save(self, path=None):
        """Save the session (and all caches) to disk.

        :param path: Where to save the session in. If none XDG_CACHE_HOME is used.
        """
        path = os.path.join(path, self.name) if path else get_cache_path(self.name)
        if os.path.isfile(path):
            os.remove(path)
        if os.path.isdir(path):
            rmtree(path, ignore_errors=True)
        os.mkdir(path)

        # class VerbosePickler (pickle._Pickler):
        #     def save(self, obj):
        #         print('pickling object  {0} of type {1}'.format(obj, type(obj)))
        #         pickle._Pickler.save(self, obj)

        with open(os.path.join(path, 'session.pickle'), 'wb') as handle:
            # VerbosePickler(handle).dump(self)
            pickle.dump(self, handle)

        with tarfile.open(path + '.gz', 'w:gz') as tar:
            tar.add(path, arcname='')

        shutil.rmtree(path)

    ###########################################################################
    #                             Recommendations                             #
    ###########################################################################

    @property
    def sieving(self):
        """
        Return True if sieving is enabled.
        """
        return self._filtering_enabled

    @sieving.setter
    def sieving(self, mode):
        """Enable the sieving, i.e. filter recommendation that were given in
        similar form recently.

        .. seealso::

            :class:`munin.history.RecommendationHistory` for more information
            on what is used in the background here.

        :param bool mode: The mode to set.
        """
        self._filtering_enabled = mode

    def _recom_sieve(self, iterator, number):
        """Filter songs that are now allowed by history, if sieving is enabled.

        This also feeds the recommendation history.

        :param iterator: Any recommendation iterator that yields songs.
                         You should pass an infinite iterator here.
        :param number: The number to finally yield
        :return: iterator that yields the sieved songs.
        """
        enabled = self._filtering_enabled
        if not enabled:
            iterator = islice(iterator, number) if number else iterator
            for recom in iterator:
                self._recom_history.feed(recom)
                yield recom
        else:
            for recom in islice(self._recom_sieve_enabled(iterator), number):
                yield recom

    def _recom_sieve_enabled(self, iterator):
            recom_set = set()
            while True:
                iterator, starter = tee(iterator)

                for recom in filter(lambda r: r not in recom_set, starter):
                    if self._recom_history.allowed(recom):
                        # we found a candidate!
                        break
                else:
                    # exhausted.
                    break

                self._recom_history.feed(recom)
                recom_set.add(recom)

                yield recom

    def recommend_from_attributes(self, subset, number=20, max_seeds=10, max_numeric_offset=None):
        """Find n recommendations solely from intelligent guessing.

        This will try to find a good rule, that indicates a user's
        favourite song, and will call :func:`recommendations_from_seed` on it.
        If no rules are known, the most played song will be chosen.
        If there is none, a random song is picked.

        The first song in the recommendations yielded is the seed song.

        .. seealso: :func:`recommendations_from_seed`

        :param dict subset: Attribute-Value mapping that seed songs must have.
        :param max_seeds: Maximum songs with the subset to get as a base.
        :param max_numeric_offset: Same as with :func:`find_matching_attributes`
        """
        return self._recom_sieve(munin.graph.recommendations_from_attributes(
            subset,
            self.database,
            self.rule_index,
            max_seeds,
            max_numeric_offset
        ), number)

    def recommend_from_seed(self, song, number=20):
        """Recommend songs based on a certain attribute.

        For example you can search by a certain genre by calling it like this: ::

            >>> recommend_from_attributes({'genre', 'death metal'}, ...)

        The value passed must match fully, no fuzzy matching is performed.

        :returns: Recommendations like the others or None if no suitable song found.
        """
        song = song_or_uid(self.database, song)
        return self._recom_sieve(munin.graph.recommendations_from_seed(
            self.database,
            self.rule_index,
            song
        ), number)

    def recommend_from_heuristic(self, number=20):
        """Give 'n' recommendations based on 'song'.

        - Will lookup rules for song.
        - If no rules found, a breadth first search starting with song is performed.
        - Otherwise, breadth first from songs mentioned in the rules are done.

        The first song in the recommendations yielded is the seed song.

        :param graph: The graph to breadth first search on.
        :type graph: :class:`igraph.Graph`
        :param rule_index: Rule database.
        :type rule_index: :class:`munin.history.RuleIndex`
        :param song: Song to base recommendations on.
        :type song: :class:`munin.song.Song`
        :param n: Deliver so many recommendations (at max.)
        :returns: An iterator that yields recommend songs.
        """
        return self._recom_sieve(munin.graph.recommendations_from_heuristic(
            self.database,
            self.rule_index
        ), number)

    def explain_recommendation(self, seed_song, recommendation, max_reasons=3):
        """Explain the recommendation you got.

        **Usage Example:**

            >>> explain_recommendation(seed_song, recommendation)
            (~0.4, [
                ('genre', 0.1),    # Very similar common attribute
                ('moodbar', 0.2),  # Quite similar
                ('lyrics', 0.5)    # Well, that's okay.
            ])

        :param seed_song: The seed song used.
                          For ``_heuristic`` and ``_attribute`` this is the first song.
        :param recommendation: The recommendation you want to have explained.
        :param max_reasons: How many reasons to yield at a maximum.
        :retruns: Tuple of the total distance to each other and a list of pairs
                  that consist of (attribute_name: subdistance_float)
        """
        seed_song = song_or_uid(self.database, seed_song)
        recommend = song_or_uid(self.database, recommendation)
        return munin.graph.explain_recommendation(
            self,
            seed_song,
            recommend,
            max_reasons
        )

    ###########################################################################
    #                          Proxy Methods                                  #
    ###########################################################################

    def feed_history(self, song):
        """Feed a single song to the history.

        If the feeded song is not yet in the database,
        it will be added automatically.

        :param song: The song to feed in the history.
        """
        self.database.feed_history(song_or_uid(self.database, song))

    def add(self, value_mapping):
        """Add a song with the values in the ``value_mapping``.

        This function should be always called like this to trigger a rebuild:

        .. code-block:: python

            >>> with session.transaction():
            ...     session.add({'genre': 'death metal', ...})

        :param dict value_mapping: A mapping :term:`Attribute` : Value.
        :raises KeyError: If an unknown :term:`Attribute` was used.
        :returns: The *UID* of the newly added song.
        """
        return self.database.add(value_mapping)

    def modify(self, song, sub_value_mapping):
        """Modify an existing and known song by the attributes mentioned
        in ``sub_value_mapping``.

        This should be run during the ``fix_graph`` contextmanager.

        Usage example:

        .. code-block:: python

            >>> with session.fix_graph():
            ...     session.modify(some_song, {'rating': 5})

        One usecase is shown in the example, the adjustment of the rating.

        .. note::

            This is **not a cheap function**. The song is removed and all
            distances to it are recalculated under the hood.

        .. seealso::

            :meth:`munin.session.Session.insert`

        .. warning::

            Attention! The returned uid is the same as before, but the underlying
            song is different. Do not compare by reference!

        :param song: The song to modify, either an uid or a :class:`munin.song.Song`
        :param sub_value_mapping: A mapping of attributes in values.
        :returns: ``song.uid``
        """
        song = song_or_uid(self.database, song)
        return self.database.modify(song, sub_value_mapping)

    def insert(self, value_mapping):
        """Insert a song without triggering a rebuild.

        This function should be always called like this to trigger a cleanup of the graph:

        .. code-block:: python

            >>> with session.fix_graph():
            ...     session.insert({'genre': 'death metal', ...})

        The rest is the same as with :meth:`add`.
        """
        return self.database.insert(value_mapping)

    def remove(self, song):
        """Remove a single song (or *UID*) from the Graph.

        This function will try to close the hole. If :meth:`insert`
        is called afterwards the *UID* of the deleted song will be re-used.

        :returns: The *UID* of the deleted song.
        """
        song = song_or_uid(self.database, song)
        return self.database.remove(song.uid)

    def playcount(self, song):
        """Get the playcount of a song.

        If no playcount is known for it, 0 will be returned.

        :returns: Number of times this song was played.
        :rtype: int
        """
        song = song_or_uid(self.database, song)
        return self.database.playcount(song)

    def playcounts(self, n=0):
        """Get all playcounts, or the most common.

        :param n: The number of most  common plays to select. Might be less.
        :returns: A list of tuples if n > 0, or a Mapping.
        """
        return self.database.playcounts(n)

    def find_matching_attributes(self, subset, max_numeric_offset=None):
        """Search the database for a subset of the attributes/values in subset.

        Example:

        .. code-block:: python

            >>> find_matching_attributes({'genre': 'metal', 'artist': 'Debauchery'})
            # yields songs from Debauchery with that have the exact genre 'metal'

        Numeric example:

        .. code-block:: python

            >>> find_matching_attributes({'rating': 5}, max_numeric_offset=1)
            # yield songs with rating 4-5

        .. warning::

            If ``max_numeric_offset`` is used, you may only use attributes
            that support the *__sub__* operator, otherwise a TypeError will be raised.

        :raises KeyError: if an unknown key is specified in subset.
        :raises TypeError: if max_numeric_offset is not None and some values do
                           not support substraction.
        :param subset: The subset of attributes the songs must have.
        :param max_numeric_offset:
        :returns: A lazy iterator over the matching songs.
        """
        return self.database.find_matching_attributes(subset, max_numeric_offset)

    @contextmanager
    def transaction(self):
        'Convienience method: Excecute block and call :func:`rebuild` afterwards.'
        with self.fix_graph():
            try:
                yield
            finally:
                self.database.rebuild()

    @contextmanager
    def fix_graph(self):
        """Fix the previosuly rebuild graph.

        This means checking if unsorted distances can be found (which should not happend)
        and checking if unidirectional edges can be found (which get deleted).

        You should this contextmanager when calling :meth:`insert` or :meth:`remove`.
        """
        try:
            yield
        finally:
            self.database.fix_graph()

    @property
    def rule_index(self):
        return self.database._rule_index

    @property
    def listen_history(self):
        return self.database._listen_history

    @property
    def recom_history(self):
        return self._recom_history def pairup(provider, distance_function, weight):
    """Convienience function for easy mask building.

    Every distance function needs to know the provider that processed the value.
    This is needed to implement the compress functionality. In order to stop you
    from writing code like this:

        >>> prov = Provider()
        >>> dfunc = DistanceFunction(prov)
        >>> # Somehwere down:
        >>> {'artist': (prov, dfunc, 0.5)}

    You can just write:

        >>> {'artist': pairup(Provider(), DistanceFunction(), 0.5)}

    This function will set the provider in the DistanceFunction for you.
    """
    if distance_function is not None:
        distance_function._provider = provider
    return (provider, distance_function, weight) class GenreTreeProvider(Provider):
    'Normalize a genre by matching it agains precalculated Tree of sub genres'
    def __init__(self, quality='all', **kwargs):
        """Creates a GenreTreeProvider with a certain quality.

        A GenreTreeProvider will try to normalize a genre by using a Tree of
        705 single genres that will be matched with the input genre in a fast way.

        The result will be a list of Paths. A Path is a tuple of indices, representing
        a possible way through the Tree. For debugging purpose you can use
        GenreTreeProvider.resolve_path() on the path to get the full genre back.

        The Quality levels are:

            - ``all``: Try to find all possible paths through the Tree, sorted
               by the first index (which is useful for comparing.)
            - ``single``: Simply take the first possible path found. Fastest.
            - ``best_two``: Like list, but also uses the reverse word list in a
              second try. Might give better results than 'single' at the cost
              of some speed.

        Default is ``all``.

        This provider is reversible.

        :param quality: One of ``all``, ``best_two``  ``single`` [*default:* ``all``]
        :type quality: String
        """
        Provider.__init__(self, **kwargs)
        self._root = load_genre_tree(get_cache_path('genre_tree.dump'))
        self._build_func = {
            'all': build_genre_path_all,
            'best_two': build_genre_path_best_of_two,
            'single': build_genre_path_single
        }.get(quality, build_genre_path_all)

    def do_process(self, input_value):
        'Subclassed from Provider, will be called for you on the input.'
        result = []
        for sub_genre in prepare_genre_list(input_value):
            words = prepare_single_genre(sub_genre)
            result += self._build_func(self._root, words)
        return tuple(result) or None

    def reverse(self, output_values):
        """Translate the paths in output_values back to genre strings.

        :returns:  A list of genre strings.
        :rtype: [str]
        """
        results = []
        for output_value in output_values:
            val = (' '.join(reversed(self.resolve_path(p))) for p in output_value)
            results.append(tuple(val))
        return tuple(results)

    def resolve_path(self, path):
        """Resolve a path like: ::

            (197, 1, 0)

        to: ::

            ["metal", "death", "brutal"]

        To get back the actual genre, do this: ::

            >>> provider = GenreTreeProvider()
            >>> ' '.join(reversed(provider.resolve_path((197, 1, 0))))
            "brutal death metal"

        or just use :func:`reverse`.

        :param path: The path to resolve.
        :type path: tuple of ints
        :returns: A list of subgenres ordered by specialization.
        :rtype: list of strings
        """
        return self._root.resolve_path(path) class KeywordsProvider(Provider):
    """Splits an arbitary text into a list of keywordsets.

    Each keywordset contains one more words that are considered to be repeated
    often or that are distinct in the text.

    **Takes:** An arbitary text, or a one-element tuple with a string.
    **Gives:** A list of keywordsets, similar to the WordlistProvider.
    """
    def do_process(self, text):
        if isinstance(text, tuple):
            text = text[0]

        language_code, keywords_map = extract_keywords(text)
        if language_code is None or not keywords_map:
            return None

        return (
            language_code,
            [set(keys) for keys, rating in keywords_map.items() if rating >= 2.0]
        ) class StemProvider(Provider):
    """Stem the input values (either a single word or a list of words)

    Uses the porter stemmer algorithm.
    """
    def __init__(self, language='english', **kwargs):
        """
        See here for a full list of languages:

            http://nltk.org/_modules/nltk/stem/snowball.html

        .. note::

            This does not depend on nltk, it depends on the ``pystemmer`` package.

        :param language: language to use during stemming, defaults to english.
        """
        Provider.__init__(self, **kwargs)
        self._stemmer = Stemmer(language)

    def do_process(self, input_value):
        if isinstance(input_value, str):
            return self._stemmer.stemWord(input_value)
        else:
            return self._stemmer.stemWords(input_value) class MoodbarAudioFileProvider(MoodbarMoodFileProvider):
    """Moodbar Provider for audio files.

    Takes a path to an arbitary audio file.
    Will look for audio_file_path + '.mood' before computing it.
    Resulting mood file will be stored in the same path.
    """
    def do_process(self, audio_file_path):
        mood_file_path = audio_file_path + '.mood'
        if not os.path.exists(mood_file_path):
            if compute_moodbar_for_file(audio_file_path, mood_file_path):
                return None
        return MoodbarMoodFileProvider.do_process(self, mood_file_path) class DateProvider(Provider):
    """Try to parse an arbitary date string into a year."""
    def do_process(self, input_value):
        if isinstance(input_value, tuple):
            input_value = input_value[0]
        try:
            return (int(input_value), )
        except ValueError:
            try:
                datetime = magicdate.magicdate(input_value)
                return (datetime.year, )
            except:
                return None class BPMCachedProvider(BPMProvider):
    """Same as :class:`BPMProvider`, but adds a caching layer.

    A .bpm file with the calculated value will be stored along the audio file,
    and the same place will be checked before actually calculating it.
    """
    def __init__(self, cache_invalid=False, **kwargs):
        """
        :param cache_invalid: Also cache invalid results of failed calculations?
        """
        Provider.__init__(self, **kwargs)
        self._cache_invalid = cache_invalid

    def do_process(self, audio_path):
        try:
            cache_path = audio_path + '.bpm'
            print(cache_path)
            if os.access(cache_path, os.R_OK):
                with open(cache_path, 'r') as handle:
                    content = handle.read()
                    if content:
                        LOGGER.debug('bpm for {} was cached.'.format(audio_path))
                        return (float(content), )
                    else:
                        return None

            LOGGER.debug('calculating bpm for {}'.format(audio_path))
            bpm = BPMProvider.do_process(self, audio_path)
            if self._cache_invalid or bpm is not None:
                with open(cache_path, 'w') as handle:
                    if bpm is not None:
                        handle.write(str(bpm[0]))
            return bpm
        except OSError:
            pass

        return () def normalize_unicode_glyphs(string):
def strip_stopwords(words):
    def do_process(self, input_string):
    def __init__(self, **kwargs):
    def do_process(self, input_string):
    def __init__(self, **kwargs):
    def do_process(self, input_string):
            def test_splitting(self):
            def test_splitting(self):
class NormalizeProvider(Provider):
class ArtistNormalizeProvider(Provider):
class AlbumNormalizeProvider(Provider):
        class TestArtistNormalizeProvider(unittest.TestCase):
        class TestAlbumNormalizeProvider(unittest.TestCase): class BPMDistance(DistanceFunction):
    """Distance Function that compares two Beats Per Minute Lists."""
    def do_compute(self, lefts, rights):
        max_n = 200
        left, right = max(0, lefts[0] - 50), max(0, rights[0] - 50)
        return abs(max(left, max_n) - max(right, max_n)) / max_n class DateDistance(DistanceFunction):
    """Compare the distance of two years, map them inbetween [0.0, 1.0]
    """
    def __init__(self, **kwargs):
        """
        As minimal year 1970 is assumed, as maximal year the current year.
        """
        DistanceFunction.__init__(self, **kwargs)
        self._min, self._now = 1970, date.today().year
        self._max_diff = (self._now - self._min) or 1

    def do_compute(self, lefts, rights):
        left, right = lefts[0], rights[0]
        diff = abs(left - right) / self._max_diff
        return max(0.0, min(1.0, diff)) class GenreTreeAvgDistance(DistanceFunction):
    """
    Like :class:`munin.distance.genre.GenreTreeDistance`,
    but use Average Linkage instead of Single Linkage.

    This is recommended if you have long genre descriptions, like
    produced by :class:`munin.provider.genre.DiscogsGenreProvider`.
    """
    def do_compute(self, lefts, rights):
        if not lefts or not rights:
            return 1.0

        dists = 0
        lefts, rights = sorted([lefts, rights], key=len, reverse=True)

        for left in lefts:
            dists += min(compare_single_path(left, right) for right in rights)

        return dists / len(lefts) class RatingDistance(DistanceFunction):
    """Instance a new RatingDistance.
    """
    # Default to "5 stars"
    def __init__(self, no_rating=0, min_rating=1, max_rating=5, **kwargs):
        """
        :param no_rating: The rating that unrated songs will have, e.g. 0 stars.
        :param min_rating: The minimal rating you will have e.g. 1 stars.
        :param max_rating: The maximal rating you will have e.g. 5 stars.
        """
        DistanceFunction.__init__(self, **kwargs)

        self._min_rating = min_rating
        self._max_rating = max_rating
        self._no_rating = no_rating

    def do_compute(self, lefts, rights):
        l_rating = min(lefts[0], self._max_rating)
        r_rating = min(rights[0], self._max_rating)

        if l_rating == self._no_rating or r_rating == self._no_rating:
            if l_rating == r_rating:
                return 0.0
            else:
                return 0.5

        diff = abs((l_rating - self._min_rating) - (r_rating - self._min_rating))
        return diff / (self._max_rating - self._min_rating) class MoodbarDistance(DistanceFunction):
    """Distance Function that compares two MoodbarDescriptions."""
    def do_compute(self, lefts, rights):
        """Compute the distance between two moodbar desc

        Only the first element in the individual lists is used.

        :param lefts: Packed left moodbar description.
        :param rights: Packed right moodbar description.
        """
        if not (lefts and rights):
            return 1.0

        distance = 1.0
        ld, rd = lefts[0], rights[0]
        distf = lambda v1, v2, m: 1.0 - sqrt(min(1.0, abs(v1 - v2) / m))

        for left_chan, right_chan in zip(ld.channels, rd.channels):
            hist_diff = sum(abs(a - b) for a, b in zip(left_chan.histogram, right_chan.histogram))
            distance -= 0.045 * (1.0 - hist_diff / (5 * 255))
            distance -= 0.045 * distf(left_chan.diffsum, right_chan.diffsum, 50)

        distance -= 0.025 * distf(ld.average_max, rd.average_max, 255)
        distance -= 0.025 * distf(ld.average_min, rd.average_min, 255)
        distance -= 0.050 * distf(ld.blackness, rd.blackness, 50)

        lkeys, rkeys = ld.dominant_colors.keys(), rd.dominant_colors.keys()
        if lkeys and rkeys:
            distance -= 0.63 * (len(lkeys & rkeys) / max(len(lkeys), len(rkeys)))

        # Good distances are in the range 0.3 - 0.5 usually
        # Therefore we handle lower values better.
        # http://www.arndt-bruenner.de/mathe/scripts/regr.htm
        # was used to calculate a polynom that scales these values.
        # This function goes through the following points:
        # 0      0
        # 0,25   0,125
        # 0,5    0,5
        # 0,75   0,875
        # 1      1
        #
        # The function is not 100% correct, so we clamp possible precision
        # errors into [0, 1]
        d2 = distance * distance
        d3 = d2 * distance
        return min(1.0, max(0, -2.666 * d3 + 4 * d2 - 0.3333 * distance)) class KeywordsDistance(DistanceFunction):
    """
    The distance between two keywords is computes as follows:

        :math:`distance(A, B) = \\min(\\frac{\\vert a\\cup b\\vert}{\\max \\vert a \\vert, \\vert b \\vert} \\forall a, b \\in A \\times B)`

    *edit*: Nope. Changed.

    where *A* and *B* are the list of keywords to compare, and *a* and *b* are
    the individual keywordsets.
    """
    def do_compute(self, lefts, rights):
        if not lefts or not rights:
            return 1.0

        left_lang, lefts = lefts
        right_lang, rights = rights

        if right_lang != left_lang:
            return 1.0

        min_distance = 1.0
        for kwa, kwb in product(lefts, rights):
            # common = sum(1 for _ in takewhile(lambda t: t[0] == t[1], zip(kwa, kwb)))
            distance = 1.0 - len(kwa & kwb) / max(4, max(len(kwa), len(kwb)))
            min_distance = min(distance, min_distance)
            if float_cmp(distance, 0.0):
                break

        return 0.67 * min_distance + 0.33 * (not right_lang == left_lang) class LevenshteinDistance(DistanceFunction):
    """Compute the damerau-levenshtein distance of two words.

    **Takes:** two lists of length 1.
    """
    def do_compute(self, lefts, rights):
        lev = normalized_damerau_levenshtein_distance
        dist_sum = 0

        smaller, larger = sorted((lefts, rights), key=len)
        for word_a in larger:
            dist_sum += min(lev(word_b, word_a) for word_b in smaller)

        return dist_sum / len(larger) def check_for_moodbar():
    """Check if all required tools are installed for this Provider.

        - ``moodbar`` (``moodbar`` executable)

    :returns: True if binary was found.
    """
    return bool(shutil.which('moodbar')) def check_for_bpmtools():
    """Check if all required tools are installed for this Provider.

        - ``bpm-tools`` (``bpm`` executable)
        - ``sox`` (``sox`` executable)

    :returns: True if both binaries were found.
    """
    return shutil.which('bpm') and shutil.which('sox') def check_for_plyr():
    """Returns True if the plyr lyrics provider is available"""
    return HAS_PLYR
import logging
from munin.session import Session
from munin.helper import pairup
from munin.provider import \
    ArtistNormalizeProvider, \
    AlbumNormalizeProvider, \
    TitleNormalizeProvider, \
    MoodbarAudioFileProvider, \
    GenreTreeProvider, \
    BPMCachedProvider, \
    StemProvider, \
    KeywordsProvider, \
    DateProvider
from munin.distance import \
    MoodbarDistance, \
    GenreTreeAvgDistance, \
    BPMDistance, \
    KeywordsDistance, \
    RatingDistance, \
    LevenshteinDistance, \
    DateDistance
from munin.provider.moodbar import check_for_moodbar
from munin.provider.bpm import check_for_bpmtools
from munin.provider.keywords import check_for_plyr
#!/usr/bin/env python
# encoding: utf-8

"""
A "easy" version of the Session object where you do not have to set the mask yourself.

Instead a number of preconfigured number of attributes are selected that
are know to work well together.
"""

LOGGER = logging.getLogger(__name__)






# Checking if the attribute shall be used:


class EasySession(Session):
    @staticmethod
    def from_name(name='EasySession'):
        return Session.from_name(name)

    def __init__(self, name='EasySession', disabled_attrs=None):

        mask = {
            'artist': pairup(
                ArtistNormalizeProvider(compress=True),
                None,
                0.5,
            ),
            'album': pairup(
                AlbumNormalizeProvider(compress=True),
                None,
                0.5,
            ),
            'title': pairup(
                TitleNormalizeProvider(compress=False) | StemProvider(),
                LevenshteinDistance(),
                1,
            ),
            'date': pairup(
                DateProvider(),
                DateDistance(),
                2
            ),
            'bpm': pairup(
                BPMCachedProvider(),
                BPMDistance(),
                3
            ),
            'lyrics': pairup(
                KeywordsProvider(),
                KeywordsDistance(),
                3
            ),
            'rating': pairup(
                None,

                RatingDistance(),