]> kaliko git repositories - mpd-sima.git/commitdiff
Refactored lastfm/echonest webservices
authorkaliko <efrim@azylum.org>
Sun, 9 Feb 2014 16:56:32 +0000 (17:56 +0100)
committerkaliko <efrim@azylum.org>
Sun, 9 Feb 2014 16:56:32 +0000 (17:56 +0100)
lastfm/echonest now inherit from the same WebService object
removed similarity option, renamed "dynamic" as "max_art"

README
sima/lib/simaecho.py
sima/lib/simafm.py
sima/lib/webservice.py [new file with mode: 0644]
sima/plugins/internal/echonest.py
sima/plugins/internal/lastfm.py
sima/utils/config.py
sima/utils/utils.py

diff --git a/README b/README
index 373ef5d5750abfef99fb6011a4d42d0f99e38e12..00360ca00f39d344118935c41c497503dc7a4eed 100644 (file)
--- a/README
+++ b/README
@@ -1,7 +1,9 @@
 Design for python >= 3.3
 
-Requires python-musicpd:
+Requires: python-musicpd >= 0.4.0 [0],
+          requests >= 2.2.0 [1]
 
-               http://media.kaliko.me/src/musicpd/
+       [0] http://media.kaliko.me/src/musicpd/
+       [1] http://docs.python-requests.org/
 
 
index 716a43eb169094bf7e8d0c67963484f4197a436d..3e6f28147d29d6669b3178929d9ff6b65411b627 100644 (file)
@@ -34,6 +34,7 @@ from requests import get, Request, Timeout, ConnectionError
 
 from sima import ECH
 from sima.lib.meta import Artist
+from sima.utils.utils import WSError, WSNotFound, WSTimeout, WSHTTPError
 from sima.utils.utils import getws, Throttle, Cache, purge_cache
 if len(ECH.get('apikey')) == 23:  # simple hack allowing imp.reload
     getws(ECH)
@@ -43,18 +44,6 @@ WAIT_BETWEEN_REQUESTS = timedelta(0, 1)
 SOCKET_TIMEOUT = 4
 
 
-class EchoError(Exception):
-    pass
-
-class EchoNotFound(EchoError):
-    pass
-
-class EchoTimeout(EchoError):
-    pass
-
-class EchoHTTPError(EchoError):
-    pass
-
 class SimaEch():
     """
     """
@@ -62,6 +51,7 @@ class SimaEch():
     cache = {}
     timestamp = datetime.utcnow()
     ratelimit = None
+    name = 'EchoNest'
 
     def __init__(self, cache=True):
         self.artist = None
@@ -79,10 +69,10 @@ class SimaEch():
         try:
             self._fetch_ech(payload)
         except Timeout:
-            raise EchoTimeout('Failed to reach server within {0}s'.format(
+            raise WSTimeout('Failed to reach server within {0}s'.format(
                                SOCKET_TIMEOUT))
         except ConnectionError as err:
-            raise EchoError(err)
+            raise WSError(err)
 
     @Throttle(WAIT_BETWEEN_REQUESTS)
     def _fetch_ech(self, payload):
@@ -91,7 +81,7 @@ class SimaEch():
                             timeout=SOCKET_TIMEOUT)
         self.__class__.ratelimit = req.headers.get('x-ratelimit-remaining', None)
         if req.status_code is not 200:
-            raise EchoHTTPError(req.status_code)
+            raise WSHTTPError(req.status_code)
         self.current_element = req.json()
         self._controls_answer()
         if self.caching:
@@ -106,8 +96,8 @@ class SimaEch():
         if code is 0:
             return True
         if code is 5:
-            raise EchoNotFound('Artist not found: "{0}"'.format(self.artist))
-        raise EchoError(status.get('message'))
+            raise WSNotFound('Artist not found: "{0}"'.format(self.artist))
+        raise WSError(status.get('message'))
 
     def _forge_payload(self, artist):
         """
index d01567ea05435ea03c938fba99e64dabaccd3342..04ee37b915386413d6ccd24d00865df8c72b42fd 100644 (file)
@@ -31,6 +31,7 @@ from requests import get, Request, Timeout, ConnectionError
 
 from sima import LFM
 from sima.lib.meta import Artist
+from sima.utils.utils import WSError, WSNotFound, WSTimeout, WSHTTPError
 from sima.utils.utils import getws, Throttle, Cache, purge_cache
 if len(LFM.get('apikey')) == 43:  # simple hack allowing imp.reload
     getws(LFM)
@@ -40,27 +41,14 @@ WAIT_BETWEEN_REQUESTS = timedelta(0, 1)
 SOCKET_TIMEOUT = 4
 
 
-class WSError(Exception):
-    pass
-
-class WSNotFound(WSError):
-    pass
-
-class WSTimeout(WSError):
-    pass
-
-class WSHTTPError(WSError):
-    pass
-
-
-
 class SimaFM():
     """
     """
     root_url = 'http://{host}/{version}/'.format(**LFM)
     cache = {}
     timestamp = datetime.utcnow()
-    #ratelimit = None
+    name = 'Last.fm'
+    ratelimit = None
 
     def __init__(self, cache=True):
         self.artist = None
@@ -137,9 +125,7 @@ class SimaFM():
         # Construct URL
         self._fetch(payload)
         for art in self.current_element.get('similarartists').get('artist'):
-            match = 100 * float(art.get('match'))
-            yield Artist(mbid=art.get('mbid', None),
-                         name=art.get('name')), match
+            yield Artist(name=art.get('name'), mbid=art.get('mbid', None))
 
 
 # VIM MODLINE
diff --git a/sima/lib/webservice.py b/sima/lib/webservice.py
new file mode 100644 (file)
index 0000000..0937bde
--- /dev/null
@@ -0,0 +1,354 @@
+# -*- coding: utf-8 -*-
+"""
+Fetching similar artists from last.fm web services
+"""
+
+# standard library import
+import random
+
+from collections import deque
+from hashlib import md5
+
+# third parties components
+
+# local import
+from .plugin import Plugin
+from .track import Track
+from .meta import Artist
+from ..utils.utils import WSError
+
+def cache(func):
+    """Caching decorator"""
+    def wrapper(*args, **kwargs):
+        #pylint: disable=W0212,C0111
+        cls = args[0]
+        similarities = [art for art in args[1]]
+        hashedlst = md5(''.join(similarities).encode('utf-8')).hexdigest()
+        if hashedlst in cls._cache.get('asearch'):
+            cls.log.debug('cached request')
+            results = cls._cache.get('asearch').get(hashedlst)
+        else:
+            results = func(*args, **kwargs)
+            cls.log.debug('caching request')
+            cls._cache.get('asearch').update({hashedlst:list(results)})
+        random.shuffle(results)
+        return results
+    return wrapper
+
+
+class WebService(Plugin):
+    """similar artists webservice
+    """
+
+    def __init__(self, daemon):
+        Plugin.__init__(self, daemon)
+        self.daemon_conf = daemon.config
+        self.sdb = daemon.sdb
+        self.history = daemon.short_history
+        ##
+        self.to_add = list()
+        self._cache = None
+        self._flush_cache()
+        wrapper = {
+                'track': self._track,
+                'top': self._top,
+                'album': self._album,
+                }
+        self.queue_mode = wrapper.get(self.plugin_conf.get('queue_mode'))
+        self.ws = None
+
+    def _flush_cache(self):
+        """
+        Both flushes and instanciates _cache
+        """
+        name = self.__class__.__name__
+        if isinstance(self._cache, dict):
+            self.log.info('{0}: Flushing cache!'.format(name))
+        else:
+            self.log.info('{0}: Initialising cache!'.format(name))
+        self._cache = {
+                'asearch': dict(),
+                'tsearch': dict(),
+                }
+
+    def _cleanup_cache(self):
+        """Avoid bloated cache
+        """
+        for _ , val in self._cache.items():
+            if isinstance(val, dict):
+                while len(val) > 150:
+                    val.popitem()
+
+    def get_history(self, artist):
+        """Constructs list of Track for already played titles for an artist.
+        """
+        duration = self.daemon_conf.getint('sima', 'history_duration')
+        tracks_from_db = self.sdb.get_history(duration=duration, artist=artist)
+        # Construct Track() objects list from database history
+        played_tracks = [Track(artist=tr[-1], album=tr[1], title=tr[2],
+                               file=tr[3]) for tr in tracks_from_db]
+        return played_tracks
+
+    def filter_track(self, tracks):
+        """
+        Extract one unplayed track from a Track object list.
+            * not in history
+            * not already in the queue
+            * not blacklisted
+        """
+        artist = tracks[0].artist
+        black_list = self.player.queue + self.to_add
+        not_in_hist = list(set(tracks) - set(self.get_history(artist=artist)))
+        if not not_in_hist:
+            self.log.debug('All tracks already played for "{}"'.format(artist))
+        random.shuffle(not_in_hist)
+        #candidate = [ trk for trk in not_in_hist if trk not in black_list
+                      #if not self.sdb.get_bl_track(trk, add_not=True)]
+        candidate = []
+        for trk in [_ for _ in not_in_hist if _ not in black_list]:
+            if self.sdb.get_bl_track(trk, add_not=True):
+                self.log.info('Blacklisted: {0}: '.format(trk))
+                continue
+            if self.sdb.get_bl_album(trk, add_not=True):
+                self.log.info('Blacklisted album: {0}: '.format(trk))
+                continue
+            # Should use albumartist heuristic as well
+            if self.plugin_conf.getboolean('single_album'):
+                if (trk.album == self.player.current.album or
+                    trk.album in [tr.album for tr in self.to_add]):
+                    self.log.debug('Found unplayed track ' +
+                               'but from an album already queued: %s' % (trk))
+                    continue
+            candidate.append(trk)
+        if not candidate:
+            self.log.debug('Unable to find title to add' +
+                           ' for "%s".' % artist)
+            return None
+        self.to_add.append(random.choice(candidate))
+
+    def _get_artists_list_reorg(self, alist):
+        """
+        Move around items in artists_list in order to play first not recently
+        played artists
+        """
+        # TODO: move to utils as a decorator
+        duration = self.daemon_conf.getint('sima', 'history_duration')
+        art_in_hist = list()
+        for trk in self.sdb.get_history(duration=duration,
+                                        artists=alist):
+            if trk[0] not in art_in_hist:
+                art_in_hist.append(trk[0])
+        art_in_hist.reverse()
+        art_not_in_hist = [ ar for ar in alist if ar not in art_in_hist ]
+        random.shuffle(art_not_in_hist)
+        art_not_in_hist.extend(art_in_hist)
+        self.log.debug('history ordered: {}'.format(
+                       ' / '.join(art_not_in_hist)))
+        return art_not_in_hist
+
+    @cache
+    def get_artists_from_player(self, similarities):
+        """
+        Look in player library for availability of similar artists in
+        similarities
+        """
+        dynamic = self.plugin_conf.getint('max_art')
+        if dynamic <= 0:
+            dynamic = 100
+        results = list()
+        similarities.reverse()
+        while (len(results) < dynamic
+            and len(similarities) > 0):
+            art_pop = similarities.pop()
+            results.extend(self.player.fuzzy_find_artist(art_pop))
+        return results
+
+    def lfm_similar_artists(self, artist=None):
+        """
+        Retrieve similar artists from WebServive.
+        """
+        if artist is None:
+            curr = self.player.current.__dict__
+            name = curr.get('artist')
+            mbid = curr.get('musicbrainz_artistid', None)
+            current = Artist(name=name, mbid=mbid)
+        else:
+            current = artist
+        # initialize artists deque list to construct from DB
+        as_art = deque()
+        as_artists = self.ws().get_similar(artist=current)
+        self.log.debug('Requesting {1} for "{0}"'.format(current,
+                        self.ws.name))
+        try:
+            # TODO: let's propagate Artist type
+            [as_art.append(str(art)) for art in as_artists]
+        except WSError as err:
+            self.log.warning('{0}: {1}'.format(self.ws.name, err))
+        if as_art:
+            self.log.debug('Fetched {0} artist(s)'.format(len(as_art)))
+        if self.ws.ratelimit:
+            self.log.info('{0.name} ratelimit: {0.ratelimit}'.format(self.ws))
+        return as_art
+
+    def get_recursive_similar_artist(self):
+        ret_extra = list()
+        history = deque(self.history)
+        history.popleft()
+        depth = 0
+        current = self.player.current
+        extra_arts = list()
+        while depth < self.plugin_conf.getint('depth'):
+            if len(history) == 0:
+                break
+            trk = history.popleft()
+            if (trk.artist in [trk.artist for trk in extra_arts]
+                or trk.artist == current.artist):
+                continue
+            extra_arts.append(trk)
+            depth += 1
+        self.log.info('EXTRA ARTS: {}'.format(
+            '/'.join([trk.artist for trk in extra_arts])))
+        for artist in extra_arts:
+            self.log.debug('Looking for artist similar to "{0.artist}" as well'.format(artist))
+            similar = self.lfm_similar_artists(artist=artist)
+            if not similar:
+                return ret_extra
+            ret_extra.extend(self.get_artists_from_player(similar))
+            if current.artist in ret_extra:
+                ret_extra.remove(current.artist)
+        return ret_extra
+
+    def get_local_similar_artists(self):
+        """Check against local player for similar artists
+        """
+        current = self.player.current
+        self.log.info('Looking for artist similar to "{0.artist}"'.format(current))
+        similar = self.lfm_similar_artists()
+        if not similar:
+            self.log.info('Got nothing from {0}!'.format(self.ws.name))
+            return []
+        self.log.info('First five similar artist(s): {}...'.format(
+                      ' / '.join([a for a in list(similar)[0:5]])))
+        self.log.info('Looking availability in music library')
+        ret = self.get_artists_from_player(similar)
+        ret_extra = None
+        if len(self.history) >= 2:
+            if self.plugin_conf.getint('depth') > 1:
+                ret_extra = self.get_recursive_similar_artist()
+        if ret_extra:
+            ret = list(set(ret) | set(ret_extra))
+        if not ret:
+            self.log.warning('Got nothing from music library.')
+            self.log.warning('Try running in debug mode to guess why...')
+            return []
+        self.log.info('Got {} artists in library'.format(len(ret)))
+        self.log.info(' / '.join(ret))
+        # Move around similars items to get in unplayed|not recently played
+        # artist first.
+        return self._get_artists_list_reorg(ret)
+
+    def _get_album_history(self, artist=None):
+        """Retrieve album history"""
+        duration = self.daemon_conf.getint('sima', 'history_duration')
+        albums_list = set()
+        for trk in self.sdb.get_history(artist=artist, duration=duration):
+            albums_list.add(trk[1])
+        return albums_list
+
+    def find_album(self, artists):
+        """Find albums to queue.
+        """
+        self.to_add = list()
+        nb_album_add = 0
+        target_album_to_add = self.plugin_conf.getint('album_to_add')
+        for artist in artists:
+            self.log.info('Looking for an album to add for "%s"...' % artist)
+            albums = self.player.find_albums(artist)
+            # str conversion while Album type is not propagated
+            albums = [ str(album) for album in albums]
+            if albums:
+                self.log.debug('Albums candidate: {0:s}'.format(' / '.join(albums)))
+            else: continue
+            # albums yet in history for this artist
+            albums = set(albums)
+            albums_yet_in_hist = albums & self._get_album_history(artist=artist)
+            albums_not_in_hist = list(albums - albums_yet_in_hist)
+            # Get to next artist if there are no unplayed albums
+            if not albums_not_in_hist:
+                self.log.info('No album found for "%s"' % artist)
+                continue
+            album_to_queue = str()
+            random.shuffle(albums_not_in_hist)
+            for album in albums_not_in_hist:
+                tracks = self.player.find_album(artist, album)
+                # Look if one track of the album is already queued
+                # Good heuristic, at least enough to guess if the whole album is
+                # already queued.
+                if tracks[0] in self.player.queue:
+                    self.log.debug('"%s" already queued, skipping!' %
+                            tracks[0].album)
+                    continue
+                album_to_queue = album
+            if not album_to_queue:
+                self.log.info('No album found for "%s"' % artist)
+                continue
+            self.log.info('{2} album candidate: {0} - {1}'.format(
+                           artist, album_to_queue, self.ws.name))
+            nb_album_add += 1
+            self.to_add.extend(self.player.find_album(artist, album_to_queue))
+            if nb_album_add == target_album_to_add:
+                return True
+
+    def _track(self):
+        """Get some tracks for track queue mode
+        """
+        artists = self.get_local_similar_artists()
+        nbtracks_target = self.plugin_conf.getint('track_to_add')
+        for artist in artists:
+            self.log.debug('Trying to find titles to add for "{}"'.format(
+                           artist))
+            found = self.player.find_track(artist)
+            # find tracks not in history for artist
+            self.filter_track(found)
+            if len(self.to_add) == nbtracks_target:
+                break
+        if not self.to_add:
+            self.log.debug('Found no tracks to queue, is your ' +
+                            'history getting too large?')
+            return None
+        for track in self.to_add:
+            self.log.info('{1} candidates: {0!s}'.format(track, self.ws.name))
+
+    def _album(self):
+        """Get albums for album queue mode
+        """
+        artists = self.get_local_similar_artists()
+        self.find_album(artists)
+
+    def _top(self):
+        """Get some tracks for top track queue mode
+        """
+        #artists = self.get_local_similar_artists()
+        pass
+
+    def callback_need_track(self):
+        self._cleanup_cache()
+        if not self.player.current:
+            self.log.info('No current track, cannot queue')
+            return None
+        if not self.player.current.artist:
+            self.log.warning('No artist set for the current track')
+            self.log.debug(repr(self.player.current))
+            return None
+        self.queue_mode()
+        candidates = self.to_add
+        self.to_add = list()
+        if self.plugin_conf.get('queue_mode') != 'album':
+            random.shuffle(candidates)
+        return candidates
+
+    def callback_player_database(self):
+        self._flush_cache()
+
+# VIM MODLINE
+# vim: ai ts=4 sw=4 sts=4 expandtab
index a5c1ede7bad1bc5ff60b7cde988b0b92c8786878..74dd2b8ae983f85551d2f27fa7ecc76b045173ca 100644 (file)
@@ -4,350 +4,21 @@ Fetching similar artists from echonest web services
 """
 
 # standard library import
-import random
-
-from collections import deque
-from hashlib import md5
 
 # third parties components
 
 # local import
-from ...lib.plugin import Plugin
-from ...lib.simaecho import SimaEch, EchoError
-from ...lib.track import Track
-from ...lib.meta import Artist
-
-
-def cache(func):
-    """Caching decorator"""
-    def wrapper(*args, **kwargs):
-        #pylint: disable=W0212,C0111
-        cls = args[0]
-        similarities = [art for art in args[1]]
-        hashedlst = md5(''.join(similarities).encode('utf-8')).hexdigest()
-        if hashedlst in cls._cache.get('asearch'):
-            cls.log.debug('cached request')
-            results = cls._cache.get('asearch').get(hashedlst)
-        else:
-            results = func(*args, **kwargs)
-            cls.log.debug('caching request')
-            cls._cache.get('asearch').update({hashedlst:list(results)})
-        random.shuffle(results)
-        return results
-    return wrapper
+from ...lib.simaecho import SimaEch
+from ...lib.webservice import WebService
 
 
-class EchoNest(Plugin):
-    """Echonest autoqueue plugin http://the.echonest.com/
+class EchoNest(WebService):
+    """last.fm similar artists
     """
 
     def __init__(self, daemon):
-        Plugin.__init__(self, daemon)
-        self.daemon_conf = daemon.config
-        self.sdb = daemon.sdb
-        self.history = daemon.short_history
-        ##
-        self.to_add = list()
-        self._cache = None
-        self._flush_cache()
-        wrapper = {
-                'track': self._track,
-                'top': self._top,
-                'album': self._album,
-                }
-        self.queue_mode = wrapper.get(self.plugin_conf.get('queue_mode'))
-
-    def _flush_cache(self):
-        """
-        Both flushes and instanciates _cache
-        """
-        name = self.__class__.__name__
-        if isinstance(self._cache, dict):
-            self.log.info('{0}: Flushing cache!'.format(name))
-        else:
-            self.log.info('{0}: Initialising cache!'.format(name))
-        self._cache = {
-                'asearch': dict(),
-                'tsearch': dict(),
-                }
-
-    def _cleanup_cache(self):
-        """Avoid bloated cache
-        """
-        for _ , val in self._cache.items():
-            if isinstance(val, dict):
-                while len(val) > 150:
-                    val.popitem()
-
-    def get_history(self, artist):
-        """Constructs list of Track for already played titles for an artist.
-        """
-        duration = self.daemon_conf.getint('sima', 'history_duration')
-        tracks_from_db = self.sdb.get_history(duration=duration, artist=artist)
-        # Construct Track() objects list from database history
-        played_tracks = [Track(artist=tr[-1], album=tr[1], title=tr[2],
-                               file=tr[3]) for tr in tracks_from_db]
-        return played_tracks
-
-    def filter_track(self, tracks):
-        """
-        Extract one unplayed track from a Track object list.
-            * not in history
-            * not already in the queue
-            * not blacklisted
-        """
-        artist = tracks[0].artist
-        black_list = self.player.queue + self.to_add
-        not_in_hist = list(set(tracks) - set(self.get_history(artist=artist)))
-        if not not_in_hist:
-            self.log.debug('All tracks already played for "{}"'.format(artist))
-        random.shuffle(not_in_hist)
-        #candidate = [ trk for trk in not_in_hist if trk not in black_list
-                      #if not self.sdb.get_bl_track(trk, add_not=True)]
-        candidate = []
-        for trk in [_ for _ in not_in_hist if _ not in black_list]:
-            if self.sdb.get_bl_track(trk, add_not=True):
-                self.log.info('Blacklisted: {0}: '.format(trk))
-                continue
-            if self.sdb.get_bl_album(trk, add_not=True):
-                self.log.info('Blacklisted album: {0}: '.format(trk))
-                continue
-            # Should use albumartist heuristic as well
-            if self.plugin_conf.getboolean('single_album'):
-                if (trk.album == self.player.current.album or
-                    trk.album in [tr.album for tr in self.to_add]):
-                    self.log.debug('Found unplayed track ' +
-                               'but from an album already queued: %s' % (trk))
-                    continue
-            candidate.append(trk)
-        if not candidate:
-            self.log.debug('Unable to find title to add' +
-                           ' for "%s".' % artist)
-            return None
-        self.to_add.append(random.choice(candidate))
-
-    def _get_artists_list_reorg(self, alist):
-        """
-        Move around items in artists_list in order to play first not recently
-        played artists
-        """
-        # TODO: move to utils as a decorator
-        duration = self.daemon_conf.getint('sima', 'history_duration')
-        art_in_hist = list()
-        for trk in self.sdb.get_history(duration=duration,
-                                        artists=alist):
-            if trk[0] not in art_in_hist:
-                art_in_hist.append(trk[0])
-        art_in_hist.reverse()
-        art_not_in_hist = [ ar for ar in alist if ar not in art_in_hist ]
-        random.shuffle(art_not_in_hist)
-        art_not_in_hist.extend(art_in_hist)
-        self.log.debug('history ordered: {}'.format(
-                       ' / '.join(art_not_in_hist)))
-        return art_not_in_hist
-
-    @cache
-    def get_artists_from_player(self, similarities):
-        """
-        Look in player library for availability of similar artists in
-        similarities
-        """
-        dynamic = self.plugin_conf.getint('dynamic')
-        if dynamic <= 0:
-            dynamic = 100
-        similarity = self.plugin_conf.getint('similarity')
-        results = list()
-        while (len(results) < dynamic
-            and len(similarities) > 0):
-            art_pop = similarities.pop()
-            results.extend(self.player.fuzzy_find_artist(art_pop))
-        return results
-
-    def lfm_similar_artists(self, artist=None):
-        """
-        Retrieve similar artists on echonest server.
-        """
-        if artist is None:
-            curr = self.player.current.__dict__
-            name = curr.get('artist')
-            mbid = curr.get('musicbrainz_artistid', None)
-            current = Artist(name=name, mbid=mbid)
-        else:
-            current = artist
-        simaech = SimaEch()
-        # initialize artists deque list to construct from DB
-        as_art = deque()
-        as_artists = simaech.get_similar(artist=current)
-        self.log.debug('Requesting EchoNest for "{0}"'.format(current))
-        try:
-            # TODO: let's propagate Artist type
-            [as_art.append(str(art)) for art in as_artists]
-        except EchoError as err:
-            self.log.warning('EchoNest: {0}'.format(err))
-        if as_art:
-            self.log.debug('Fetched {0} artist(s)'.format(len(as_art)))
-        self.log.debug('x-ratelimit-remaining: {}'.format(SimaEch.ratelimit))
-        return as_art
-
-    def get_recursive_similar_artist(self):
-        ret_extra = list()
-        history = deque(self.history)
-        history.popleft()
-        depth = 0
-        current = self.player.current
-        extra_arts = list()
-        while depth < self.plugin_conf.getint('depth'):
-            if len(history) == 0:
-                break
-            trk = history.popleft()
-            if (trk.artist in [trk.artist for trk in extra_arts]
-                or trk.artist == current.artist):
-                continue
-            extra_arts.append(trk)
-            depth += 1
-        self.log.info('EXTRA ARTS: {}'.format(
-            '/'.join([trk.artist for trk in extra_arts])))
-        for artist in extra_arts:
-            self.log.debug('Looking for artist similar to "{0.artist}" as well'.format(artist))
-            similar = self.lfm_similar_artists(artist=artist)
-            if not similar:
-                return ret_extra
-            ret_extra.extend(self.get_artists_from_player(similar))
-            if current.artist in ret_extra:
-                ret_extra.remove(current.artist)
-        return ret_extra
-
-    def get_local_similar_artists(self):
-        """Check against local player for similar artists fetched from echonest
-        """
-        current = self.player.current
-        self.log.info('Looking for artist similar to "{0.artist}"'.format(current))
-        similar = list(self.lfm_similar_artists())
-        if not similar:
-            self.log.info('Got nothing from echonest!')
-            return []
-        self.log.info('First five similar artist(s): {}...'.format(
-                      ' / '.join([a for a in similar[0:5]])))
-        self.log.info('Looking availability in music library')
-        ret = self.get_artists_from_player(similar)
-        ret_extra = None
-        if len(self.history) >= 2:
-            if self.plugin_conf.getint('depth') > 1:
-                ret_extra = self.get_recursive_similar_artist()
-        if ret_extra:
-            ret = list(set(ret) | set(ret_extra))
-        if not ret:
-            self.log.warning('Got nothing from music library.')
-            self.log.warning('Try running in debug mode to guess why...')
-            return []
-        self.log.info('Got {} artists in library'.format(len(ret)))
-        self.log.info(' / '.join(ret))
-        # Move around similars items to get in unplayed|not recently played
-        # artist first.
-        return self._get_artists_list_reorg(ret)
-
-    def _get_album_history(self, artist=None):
-        """Retrieve album history"""
-        duration = self.daemon_conf.getint('sima', 'history_duration')
-        albums_list = set()
-        for trk in self.sdb.get_history(artist=artist, duration=duration):
-            albums_list.add(trk[1])
-        return albums_list
-
-    def find_album(self, artists):
-        """Find albums to queue.
-        """
-        self.to_add = list()
-        nb_album_add = 0
-        target_album_to_add = self.plugin_conf.getint('album_to_add')
-        for artist in artists:
-            self.log.info('Looking for an album to add for "%s"...' % artist)
-            albums = self.player.find_albums(artist)
-            # str conversion while Album type is not propagated
-            albums = [ str(album) for album in albums]
-            if albums:
-                self.log.debug('Albums candidate: {0:s}'.format(' / '.join(albums)))
-            else: continue
-            # albums yet in history for this artist
-            albums = set(albums)
-            albums_yet_in_hist = albums & self._get_album_history(artist=artist)
-            albums_not_in_hist = list(albums - albums_yet_in_hist)
-            # Get to next artist if there are no unplayed albums
-            if not albums_not_in_hist:
-                self.log.info('No album found for "%s"' % artist)
-                continue
-            album_to_queue = str()
-            random.shuffle(albums_not_in_hist)
-            for album in albums_not_in_hist:
-                tracks = self.player.find_album(artist, album)
-                # Look if one track of the album is already queued
-                # Good heuristic, at least enough to guess if the whole album is
-                # already queued.
-                if tracks[0] in self.player.queue:
-                    self.log.debug('"%s" already queued, skipping!' %
-                            tracks[0].album)
-                    continue
-                album_to_queue = album
-            if not album_to_queue:
-                self.log.info('No album found for "%s"' % artist)
-                continue
-            self.log.info('echonest album candidates: {0} - {1}'.format(
-                           artist, album_to_queue))
-            nb_album_add += 1
-            self.to_add.extend(self.player.find_album(artist, album_to_queue))
-            if nb_album_add == target_album_to_add:
-                return True
-
-    def _track(self):
-        """Get some tracks for track queue mode
-        """
-        artists = self.get_local_similar_artists()
-        nbtracks_target = self.plugin_conf.getint('track_to_add')
-        for artist in artists:
-            self.log.debug('Trying to find titles to add for "{}"'.format(
-                           artist))
-            found = self.player.find_track(artist)
-            # find tracks not in history for artist
-            self.filter_track(found)
-            if len(self.to_add) == nbtracks_target:
-                break
-        if not self.to_add:
-            self.log.debug('Found no tracks to queue, is your ' +
-                            'history getting too large?')
-            return None
-        for track in self.to_add:
-            self.log.info('echonest candidates: {0!s}'.format(track))
-
-    def _album(self):
-        """Get albums for album queue mode
-        """
-        artists = self.get_local_similar_artists()
-        self.find_album(artists)
-
-    def _top(self):
-        """Get some tracks for top track queue mode
-        """
-        #artists = self.get_local_similar_artists()
-        pass
-
-    def callback_need_track(self):
-        self._cleanup_cache()
-        if not self.player.current:
-            self.log.info('No current track, cannot queue')
-            return None
-        if not self.player.current.artist:
-            self.log.warning('No artist set for the current track')
-            self.log.debug(repr(self.player.current))
-            return None
-        self.queue_mode()
-        candidates = self.to_add
-        self.to_add = list()
-        if self.plugin_conf.get('queue_mode') != 'album':
-            random.shuffle(candidates)
-        return candidates
-
-    def callback_player_database(self):
-        self._flush_cache()
+        WebService.__init__(self, daemon)
+        self.ws = SimaEch
 
 # VIM MODLINE
 # vim: ai ts=4 sw=4 sts=4 expandtab
index 9ca859f6a724629ba73be2d39c1e8b27a99e1e27..b4f56c2337e318e71162f9ab64bc180b40c81784 100644 (file)
@@ -4,355 +4,22 @@ Fetching similar artists from last.fm web services
 """
 
 # standard library import
-import random
-
-from collections import deque
-from hashlib import md5
 
 # third parties components
 
 # local import
-from ...lib.plugin import Plugin
-from ...lib.simafm import SimaFM, WSError
-from ...lib.track import Track
-from ...lib.meta import Artist
-
-
-def cache(func):
-    """Caching decorator"""
-    def wrapper(*args, **kwargs):
-        #pylint: disable=W0212,C0111
-        cls = args[0]
-        similarities = [art for art, _ in args[1]]
-        hashedlst = md5(''.join(similarities).encode('utf-8')).hexdigest()
-        if hashedlst in cls._cache.get('asearch'):
-            cls.log.debug('cached request')
-            results = cls._cache.get('asearch').get(hashedlst)
-        else:
-            results = func(*args, **kwargs)
-            cls.log.debug('caching request')
-            cls._cache.get('asearch').update({hashedlst:list(results)})
-        random.shuffle(results)
-        return results
-    return wrapper
+from ...lib.simafm import SimaFM
+from ...lib.webservice import WebService
 
 
-class Lastfm(Plugin):
+class Lastfm(WebService):
     """last.fm similar artists
     """
 
     def __init__(self, daemon):
-        Plugin.__init__(self, daemon)
-        self.daemon_conf = daemon.config
-        self.sdb = daemon.sdb
-        self.history = daemon.short_history
-        ##
-        self.to_add = list()
-        self._cache = None
-        self._flush_cache()
-        wrapper = {
-                'track': self._track,
-                'top': self._top,
-                'album': self._album,
-                }
-        self.queue_mode = wrapper.get(self.plugin_conf.get('queue_mode'))
-
-    def _flush_cache(self):
-        """
-        Both flushes and instanciates _cache
-        """
-        name = self.__class__.__name__
-        if isinstance(self._cache, dict):
-            self.log.info('{0}: Flushing cache!'.format(name))
-        else:
-            self.log.info('{0}: Initialising cache!'.format(name))
-        self._cache = {
-                'asearch': dict(),
-                'tsearch': dict(),
-                }
-
-    def _cleanup_cache(self):
-        """Avoid bloated cache
-        """
-        for _ , val in self._cache.items():
-            if isinstance(val, dict):
-                while len(val) > 150:
-                    val.popitem()
-
-    def get_history(self, artist):
-        """Constructs list of Track for already played titles for an artist.
-        """
-        duration = self.daemon_conf.getint('sima', 'history_duration')
-        tracks_from_db = self.sdb.get_history(duration=duration, artist=artist)
-        # Construct Track() objects list from database history
-        played_tracks = [Track(artist=tr[-1], album=tr[1], title=tr[2],
-                               file=tr[3]) for tr in tracks_from_db]
-        return played_tracks
-
-    def filter_track(self, tracks):
-        """
-        Extract one unplayed track from a Track object list.
-            * not in history
-            * not already in the queue
-            * not blacklisted
-        """
-        artist = tracks[0].artist
-        black_list = self.player.queue + self.to_add
-        not_in_hist = list(set(tracks) - set(self.get_history(artist=artist)))
-        if not not_in_hist:
-            self.log.debug('All tracks already played for "{}"'.format(artist))
-        random.shuffle(not_in_hist)
-        #candidate = [ trk for trk in not_in_hist if trk not in black_list
-                      #if not self.sdb.get_bl_track(trk, add_not=True)]
-        candidate = []
-        for trk in [_ for _ in not_in_hist if _ not in black_list]:
-            if self.sdb.get_bl_track(trk, add_not=True):
-                self.log.info('Blacklisted: {0}: '.format(trk))
-                continue
-            if self.sdb.get_bl_album(trk, add_not=True):
-                self.log.info('Blacklisted album: {0}: '.format(trk))
-                continue
-            # Should use albumartist heuristic as well
-            if self.plugin_conf.getboolean('single_album'):
-                if (trk.album == self.player.current.album or
-                    trk.album in [tr.album for tr in self.to_add]):
-                    self.log.debug('Found unplayed track ' +
-                               'but from an album already queued: %s' % (trk))
-                    continue
-            candidate.append(trk)
-        if not candidate:
-            self.log.debug('Unable to find title to add' +
-                           ' for "%s".' % artist)
-            return None
-        self.to_add.append(random.choice(candidate))
-
-    def _get_artists_list_reorg(self, alist):
-        """
-        Move around items in artists_list in order to play first not recently
-        played artists
-        """
-        # TODO: move to utils as a decorator
-        duration = self.daemon_conf.getint('sima', 'history_duration')
-        art_in_hist = list()
-        for trk in self.sdb.get_history(duration=duration,
-                                        artists=alist):
-            if trk[0] not in art_in_hist:
-                art_in_hist.append(trk[0])
-        art_in_hist.reverse()
-        art_not_in_hist = [ ar for ar in alist if ar not in art_in_hist ]
-        random.shuffle(art_not_in_hist)
-        art_not_in_hist.extend(art_in_hist)
-        self.log.debug('history ordered: {}'.format(
-                       ' / '.join(art_not_in_hist)))
-        return art_not_in_hist
-
-    @cache
-    def get_artists_from_player(self, similarities):
-        """
-        Look in player library for availability of similar artists in
-        similarities
-        """
-        dynamic = self.plugin_conf.getint('dynamic')
-        if dynamic <= 0:
-            dynamic = 100
-        similarity = self.plugin_conf.getint('similarity')
-        results = list()
-        similarities.reverse()
-        while (len(results) < dynamic
-            and len(similarities) > 0):
-            art_pop, match = similarities.pop()
-            if match < similarity:
-                break
-            results.extend(self.player.fuzzy_find_artist(art_pop))
-        results and self.log.debug('Similarity: %d%%' % match) # pylint: disable=w0106
-        return results
-
-    def lfm_similar_artists(self, artist=None):
-        """
-        Retrieve similar artists on last.fm server.
-        """
-        if artist is None:
-            curr = self.player.current.__dict__
-            name = curr.get('artist')
-            mbid = curr.get('musicbrainz_artistid', None)
-            current = Artist(name=name, mbid=mbid)
-        else:
-            current = artist
-        simafm = SimaFM()
-        # initialize artists deque list to construct from DB
-        as_art = deque()
-        as_artists = simafm.get_similar(artist=current)
-        self.log.debug('Requesting last.fm for "{0}"'.format(current))
-        try:
-            # TODO: let's propagate Artist type
-            [as_art.append((str(a), m)) for a, m in as_artists]
-        except WSError as err:
-            self.log.warning('Last.fm: {0}'.format(err))
-        if as_art:
-            self.log.debug('Fetched {0} artist(s)'.format(len(as_art)))
-        return as_art
-
-    def get_recursive_similar_artist(self):
-        ret_extra = list()
-        history = deque(self.history)
-        history.popleft()
-        depth = 0
-        current = self.player.current
-        extra_arts = list()
-        while depth < self.plugin_conf.getint('depth'):
-            if len(history) == 0:
-                break
-            trk = history.popleft()
-            if (trk.artist in [trk.artist for trk in extra_arts]
-                or trk.artist == current.artist):
-                continue
-            extra_arts.append(trk)
-            depth += 1
-        self.log.info('EXTRA ARTS: {}'.format(
-            '/'.join([trk.artist for trk in extra_arts])))
-        for artist in extra_arts:
-            self.log.debug('Looking for artist similar to "{0.artist}" as well'.format(artist))
-            similar = self.lfm_similar_artists(artist=artist)
-            if not similar:
-                return ret_extra
-            similar = sorted(similar, key=lambda sim: sim[1], reverse=True)
-            ret_extra.extend(self.get_artists_from_player(similar))
-            if current.artist in ret_extra:
-                ret_extra.remove(current.artist)
-        return ret_extra
-
-    def get_local_similar_artists(self):
-        """Check against local player for similar artists fetched from last.fm
-        """
-        current = self.player.current
-        self.log.info('Looking for artist similar to "{0.artist}"'.format(current))
-        similar = self.lfm_similar_artists()
-        if not similar:
-            self.log.info('Got nothing from last.fm!')
-            return []
-        similar = sorted(similar, key=lambda sim: sim[1], reverse=True)
-        self.log.info('First five similar artist(s): {}...'.format(
-                      ' / '.join([a for a, _ in similar[0:5]])))
-        self.log.info('Looking availability in music library')
-        ret = self.get_artists_from_player(similar)
-        ret_extra = None
-        if len(self.history) >= 2:
-            if self.plugin_conf.getint('depth') > 1:
-                ret_extra = self.get_recursive_similar_artist()
-        if ret_extra:
-            ret = list(set(ret) | set(ret_extra))
-        if not ret:
-            self.log.warning('Got nothing from music library.')
-            self.log.warning('Try running in debug mode to guess why...')
-            return []
-        self.log.info('Got {} artists in library'.format(len(ret)))
-        self.log.info(' / '.join(ret))
-        # Move around similars items to get in unplayed|not recently played
-        # artist first.
-        return self._get_artists_list_reorg(ret)
-
-    def _get_album_history(self, artist=None):
-        """Retrieve album history"""
-        duration = self.daemon_conf.getint('sima', 'history_duration')
-        albums_list = set()
-        for trk in self.sdb.get_history(artist=artist, duration=duration):
-            albums_list.add(trk[1])
-        return albums_list
-
-    def find_album(self, artists):
-        """Find albums to queue.
-        """
-        self.to_add = list()
-        nb_album_add = 0
-        target_album_to_add = self.plugin_conf.getint('album_to_add')
-        for artist in artists:
-            self.log.info('Looking for an album to add for "%s"...' % artist)
-            albums = self.player.find_albums(artist)
-            # str conversion while Album type is not propagated
-            albums = [ str(album) for album in albums]
-            if albums:
-                self.log.debug('Albums candidate: {0:s}'.format(' / '.join(albums)))
-            else: continue
-            # albums yet in history for this artist
-            albums = set(albums)
-            albums_yet_in_hist = albums & self._get_album_history(artist=artist)
-            albums_not_in_hist = list(albums - albums_yet_in_hist)
-            # Get to next artist if there are no unplayed albums
-            if not albums_not_in_hist:
-                self.log.info('No album found for "%s"' % artist)
-                continue
-            album_to_queue = str()
-            random.shuffle(albums_not_in_hist)
-            for album in albums_not_in_hist:
-                tracks = self.player.find_album(artist, album)
-                # Look if one track of the album is already queued
-                # Good heuristic, at least enough to guess if the whole album is
-                # already queued.
-                if tracks[0] in self.player.queue:
-                    self.log.debug('"%s" already queued, skipping!' %
-                            tracks[0].album)
-                    continue
-                album_to_queue = album
-            if not album_to_queue:
-                self.log.info('No album found for "%s"' % artist)
-                continue
-            self.log.info('last.fm album candidate: {0} - {1}'.format(
-                           artist, album_to_queue))
-            nb_album_add += 1
-            self.to_add.extend(self.player.find_album(artist, album_to_queue))
-            if nb_album_add == target_album_to_add:
-                return True
-
-    def _track(self):
-        """Get some tracks for track queue mode
-        """
-        artists = self.get_local_similar_artists()
-        nbtracks_target = self.plugin_conf.getint('track_to_add')
-        for artist in artists:
-            self.log.debug('Trying to find titles to add for "{}"'.format(
-                           artist))
-            found = self.player.find_track(artist)
-            # find tracks not in history for artist
-            self.filter_track(found)
-            if len(self.to_add) == nbtracks_target:
-                break
-        if not self.to_add:
-            self.log.debug('Found no tracks to queue, is your ' +
-                            'history getting too large?')
-            return None
-        for track in self.to_add:
-            self.log.info('last.fm candidates: {0!s}'.format(track))
-
-    def _album(self):
-        """Get albums for album queue mode
-        """
-        artists = self.get_local_similar_artists()
-        self.find_album(artists)
-
-    def _top(self):
-        """Get some tracks for top track queue mode
-        """
-        #artists = self.get_local_similar_artists()
-        pass
-
-    def callback_need_track(self):
-        self._cleanup_cache()
-        if not self.player.current:
-            self.log.info('No current track, cannot queue')
-            return None
-        if not self.player.current.artist:
-            self.log.warning('No artist set for the current track')
-            self.log.debug(repr(self.player.current))
-            return None
-        self.queue_mode()
-        candidates = self.to_add
-        self.to_add = list()
-        if self.plugin_conf.get('queue_mode') != 'album':
-            random.shuffle(candidates)
-        return candidates
+        WebService.__init__(self, daemon)
+        self.ws = SimaFM
 
-    def callback_player_database(self):
-        self._flush_cache()
 
 # VIM MODLINE
 # vim: ai ts=4 sw=4 sts=4 expandtab
index dbc29d691b40b93c93dddeead3ddb02062048568..99fb3227b7b69b18f085d513dcefcb99e608c67d 100644 (file)
@@ -62,16 +62,15 @@ DEFAULT_CONF = {
             },
         'echonest': {
             'queue_mode': "track", #TODO control values
-            'dynamic': "15",
+            'max_art': "15",
             'single_album': "false",
             'track_to_add': "1",
             'album_to_add': "1",
             'depth': "1",
             },
         'lastfm': {
-            'dynamic': "10",
-            'similarity': "15",
             'queue_mode': "track", #TODO control values
+            'max_art': "10",
             'single_album': "false",
             'track_to_add': "1",
             'album_to_add': "1",
index 93bac25e78084dd94b92708fc7c83b5c91daefd8..d0edcd4ab6f8ca671143df6fbfe8e0e97ac44cc0 100644 (file)
@@ -185,5 +185,21 @@ class Cache():
     def get(self):
         return self.elem
 
+
+# http client exceptions (for webservices)
+
+class WSError(Exception):
+    pass
+
+class WSNotFound(WSError):
+    pass
+
+class WSTimeout(WSError):
+    pass
+
+class WSHTTPError(WSError):
+    pass
+
+
 # VIM MODLINE
 # vim: ai ts=4 sw=4 sts=4 expandtab