]> kaliko git repositories - mpd-sima.git/commitdiff
Add EchoNest top tracks
authorkaliko <efrim@azylum.org>
Sun, 16 Feb 2014 22:56:01 +0000 (23:56 +0100)
committerkaliko <efrim@azylum.org>
Sun, 16 Feb 2014 22:56:01 +0000 (23:56 +0100)
sima/__init__.py
sima/client.py
sima/lib/simaecho.py
sima/lib/simafm.py
sima/lib/webserv.py

index d9d4fd5bf77dd9891e7ed9e1f522a42aa87e6c10..1ceac624553e4f8261fcdc3e5d886010bc1694d3 100644 (file)
@@ -1,4 +1,6 @@
 # -*- coding: utf-8 -*-
+"""WebServices API credentials and ressources
+"""
 
 LFM = {'apikey': 'NG4xcDlxcXJwMjk4MTZycTgwM3E3b3I5MTEzb240cG8',
        'host':'ws.audioscrobbler.com',
index 2892d5e4ae82b24a6a244281359237bc97f199c4..3d10c483d326edf36b4c391f5a9010d74e52cf9e 100644 (file)
@@ -41,6 +41,7 @@ from .lib.player import Player
 from .lib.track import Track
 from .lib.meta import Album
 from .lib.simastr import SimaStr
+from .utils.leven import levenshtein_ratio
 
 
 class PlayerError(Exception):
@@ -70,6 +71,11 @@ def blacklist(artist=False, album=False, track=False):
                 if bl_getter(elem, add_not=True):
                     cls.log.info('Blacklisted: {0}'.format(elem))
                     results.remove(elem)
+                if track and cls.database.get_bl_album(elem, add_not=True):
+                    # filter album as well in track mode
+                    # (artist have already been)
+                    cls.log.info('Blacklisted: {0}'.format(elem))
+                    results.remove(elem)
             return results
         return wrapper
     return decorated
@@ -148,7 +154,7 @@ class PlayerClient(Player):
             or not hasattr(old_curr, 'id')
             or not hasattr(self.current, 'id')):
             return False
-        return (self.current.id != old_curr.id)  # pylint: disable=no-member
+        return self.current.id != old_curr.id  # pylint: disable=no-member
 
     def _flush_cache(self):
         """
@@ -169,6 +175,29 @@ class PlayerClient(Player):
             return self.find('artist', artist, 'title', title)
         return self.find('artist', artist)
 
+    @blacklist(track=True)
+    def fuzzy_find_track(self, artist, title):
+        # Retrieve all tracks from artist
+        all_tracks = self.find('artist', artist)
+        # Get all titles (filter missing titles set to 'None')
+        all_artist_titles = frozenset([tr.title for tr in all_tracks
+                                       if tr.title is not None])
+        match = get_close_matches(title, all_artist_titles, 50, 0.78)
+        if not match:
+            return []
+        for title_ in match:
+            leven = levenshtein_ratio(title.lower(), title_.lower())
+            if leven == 1:
+                pass
+            elif leven >= 0.79:  # PARAM
+                self.log.debug('title: "%s" should match "%s" (lr=%1.3f)' %
+                               (title_, title, leven))
+            else:
+                self.log.debug('title: "%s" does not match "%s" (lr=%1.3f)' %
+                               (title_, title, leven))
+                return []
+            return self.find('artist', artist, 'title', title_)
+
     @blacklist(artist=True)
     def fuzzy_find_artist(self, art):
         """
@@ -242,7 +271,6 @@ class PlayerClient(Player):
                 albums.append(Album(name=album, **kwalbart))
         for album in self.list('album', 'artist', artist):
             album_trks = [trk for trk in self.find('album', album)]
-            # TODO: add a VA filter option
             if 'Various Artists' in [tr.albumartist for tr in album_trks]:
                 self.log.debug('Discarding {0} ("Various Artists" set)'.format(album))
                 continue
@@ -250,7 +278,7 @@ class PlayerClient(Player):
             if len(set(arts)) < 2:  # TODO: better heuristic, use a ratio instead
                 if album not in albums:
                     albums.append(Album(name=album, albumartist=artist))
-            elif (album and album not in albums):
+            elif album and album not in albums:
                 self.log.debug('"{0}" probably not an album of "{1}"'.format(
                                album, artist) + '({0})'.format('/'.join(arts)))
         return albums
@@ -293,7 +321,7 @@ class PlayerClient(Player):
     def queue(self):
         plst = self.playlist
         plst.reverse()
-        return [ trk for trk in plst if int(trk.pos) > int(self.current.pos)]
+        return [trk for trk in plst if int(trk.pos) > int(self.current.pos)]
 
     @property
     def playlist(self):
index 14623031df6ab1f46d0d991572975171f0908812..8c83adaba76165cb202b99db0bb753866c6eb99a 100644 (file)
@@ -31,6 +31,7 @@ from requests import get, Request, Timeout, ConnectionError
 
 from sima import ECH
 from sima.lib.meta import Artist
+from sima.lib.track import Track
 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
@@ -78,7 +79,7 @@ class SimaEch:
                             timeout=SOCKET_TIMEOUT)
         self.__class__.ratelimit = req.headers.get('x-ratelimit-remaining', None)
         if req.status_code is not 200:
-            raise WSHTTPError(req.status_code)
+            raise WSHTTPError('{0.status_code}: {0.reason}'.format(req))
         self.current_element = req.json()
         self._controls_answer()
         if self.caching:
@@ -96,7 +97,7 @@ class SimaEch:
             raise WSNotFound('Artist not found: "{0}"'.format(self.artist))
         raise WSError(status.get('message'))
 
-    def _forge_payload(self, artist):
+    def _forge_payload(self, artist, top=False):
         """Build payload
         """
         payload = {'api_key': ECH.get('apikey')}
@@ -110,6 +111,15 @@ class SimaEch:
             payload.update(name=artist.name)
         payload.update(bucket='id:musicbrainz')
         payload.update(results=100)
+        if top:
+            if artist.mbid:
+                aid = payload.pop('id')
+                payload.update(artist_id=aid)
+            else:
+                name = payload.pop('name')
+                payload.update(artist=name)
+            payload.update(results=100)
+            payload.update(sort='song_hotttnesss-desc')
         return payload
 
     def get_similar(self, artist=None):
@@ -129,6 +139,24 @@ class SimaEch:
                                           ).lstrip('musicbrainz:artist:')
             yield Artist(mbid=mbid, name=art.get('name'))
 
+    def get_toptrack(self, artist=None):
+        """Fetch artist top tracks
+        """
+        payload = self._forge_payload(artist, top=True)
+        # Construct URL
+        self._ressource = '{0}/song/search'.format(SimaEch.root_url)
+        self._fetch(payload)
+        titles = list()
+        artist = {
+                'artist': artist.name,
+                'musicbrainz_artistid': artist.mbid,
+                }
+        for song in self.current_element.get('response').get('songs'):
+            title = song.get('title')
+            if title not in titles:
+                titles.append(title)
+                yield Track(title=title, **artist )
+
 
 # VIM MODLINE
 # vim: ai ts=4 sw=4 sts=4 expandtab
index 3210fdcdf7a4beb8dca96755e0c0470b849f92d7..5e6ad106e8e969f2c0f03d7e34a118a78d3f72ab 100644 (file)
@@ -77,7 +77,7 @@ class SimaFM:
                             timeout=SOCKET_TIMEOUT)
         #self.__class__.ratelimit = req.headers.get('x-ratelimit-remaining', None)
         if req.status_code is not 200:
-            raise WSHTTPError(req.status_code)
+            raise WSHTTPError('{0.status_code}: {0.reason}'.format(req))
         self.current_element = req.json()
         self._controls_answer()
         if self.caching:
index 291c4c4e3cccdb2c097f73ef04a14be908dd7ed8..b805b916396740adb5213cbf77accf1258088635 100644 (file)
@@ -117,19 +117,11 @@ class WebService(Plugin):
         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:
+        if self.plugin_conf.get('queue_mode') != 'top' and 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
@@ -139,10 +131,9 @@ class WebService(Plugin):
                     continue
             candidate.append(trk)
         if not candidate:
-            self.log.debug('Unable to find title to add' +
-                           ' for "%s".' % artist)
-            return None
+            return False
         self.to_add.append(random.choice(candidate))
+        return True
 
     def _get_artists_list_reorg(self, alist):
         """
@@ -160,7 +151,7 @@ class WebService(Plugin):
         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(
+        self.log.info('{}'.format(
                        ' / '.join(art_not_in_hist)))
         return art_not_in_hist
 
@@ -181,7 +172,7 @@ class WebService(Plugin):
             results.extend(self.player.fuzzy_find_artist(art_pop))
         return results
 
-    def lfm_similar_artists(self, artist=None):
+    def ws_similar_artists(self, artist=None):
         """
         Retrieve similar artists from WebServive.
         """
@@ -230,7 +221,7 @@ class WebService(Plugin):
             self.log.debug(
                     'Looking for artist similar to "{0.artist}" as well'.format(
                         artist))
-            similar = self.lfm_similar_artists(artist=artist)
+            similar = self.ws_similar_artists(artist=artist)
             if not similar:
                 return ret_extra
             ret_extra.extend(self.get_artists_from_player(similar))
@@ -243,7 +234,7 @@ class WebService(Plugin):
         """
         current = self.player.current
         self.log.info('Looking for artist similar to "{0.artist}"'.format(current))
-        similar = self.lfm_similar_artists()
+        similar = self.ws_similar_artists()
         if not similar:
             self.log.info('Got nothing from {0}!'.format(self.ws.name))
             return []
@@ -262,7 +253,6 @@ class WebService(Plugin):
             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)
@@ -320,6 +310,32 @@ class WebService(Plugin):
             if nb_album_add == target_album_to_add:
                 return True
 
+    def find_top(self, artists):
+        """
+        find top tracks for artists in artists list.
+        """
+        self.to_add = list()
+        nbtracks_target = self.plugin_conf.getint('track_to_add')
+        webserv = self.ws()
+        for artist in artists:
+            artist = Artist(name=artist)
+            if len(self.to_add) == nbtracks_target:
+                return True
+            self.log.info('Looking for a top track for {0}'.format(artist))
+            titles = deque()
+            try:
+                titles = [t for t in webserv.get_toptrack(artist)]
+            except WSError as err:
+                self.log.warning('{0}: {1}'.format(self.ws.name, err))
+            if self.ws.ratelimit:
+                self.log.info('{0.name} ratelimit: {0.ratelimit}'.format(self.ws))
+            for trk in titles:
+                found = self.player.fuzzy_find_track(artist.name, trk.title)
+                if found:
+                    self.log.debug('{0}'.format(found[0]))
+                    if self.filter_track(found):
+                        break
+
     def _track(self):
         """Get some tracks for track queue mode
         """
@@ -349,8 +365,11 @@ class WebService(Plugin):
     def _top(self):
         """Get some tracks for top track queue mode
         """
-        #artists = self.get_local_similar_artists()
-        pass
+        artists = self.get_local_similar_artists()
+        nbtracks_target = self.plugin_conf.getint('track_to_add')
+        self.find_top(artists)
+        for track in self.to_add:
+            self.log.info('{1} candidates: {0!s}'.format(track, self.ws.name))
 
     def callback_need_track(self):
         self._cleanup_cache()