1 # -*- coding: utf-8 -*-
3 Fetching similar artists from echonest web services
6 # standard library import
9 from collections import deque
10 from hashlib import md5
12 # third parties components
15 from ...lib.plugin import Plugin
16 from ...lib.simaecho import SimaEch, EchoError, EchoNotFound
17 from ...lib.track import Track
18 from ...lib.meta import Artist
22 """Caching decorator"""
23 def wrapper(*args, **kwargs):
24 #pylint: disable=W0212,C0111
26 similarities = [art for art in args[1]]
27 hashedlst = md5(''.join(similarities).encode('utf-8')).hexdigest()
28 if hashedlst in cls._cache.get('asearch'):
29 cls.log.debug('cached request')
30 results = cls._cache.get('asearch').get(hashedlst)
32 results = func(*args, **kwargs)
33 cls.log.debug('caching request')
34 cls._cache.get('asearch').update({hashedlst:list(results)})
35 random.shuffle(results)
40 class EchoNest(Plugin):
41 """Echonest autoqueue plugin http://the.echonest.com/
44 def __init__(self, daemon):
45 Plugin.__init__(self, daemon)
46 self.daemon_conf = daemon.config
48 self.history = daemon.short_history
58 self.queue_mode = wrapper.get(self.plugin_conf.get('queue_mode'))
60 def _flush_cache(self):
62 Both flushes and instanciates _cache
64 name = self.__class__.__name__
65 if isinstance(self._cache, dict):
66 self.log.info('{0}: Flushing cache!'.format(name))
68 self.log.info('{0}: Initialising cache!'.format(name))
74 def _cleanup_cache(self):
75 """Avoid bloated cache
77 for _ , val in self._cache.items():
78 if isinstance(val, dict):
82 def get_history(self, artist):
83 """Constructs list of Track for already played titles for an artist.
85 duration = self.daemon_conf.getint('sima', 'history_duration')
86 tracks_from_db = self.sdb.get_history(duration=duration, artist=artist)
87 # Construct Track() objects list from database history
88 played_tracks = [Track(artist=tr[-1], album=tr[1], title=tr[2],
89 file=tr[3]) for tr in tracks_from_db]
92 def filter_track(self, tracks):
94 Extract one unplayed track from a Track object list.
96 * not already in the queue
99 artist = tracks[0].artist
100 black_list = self.player.queue + self.to_add
101 not_in_hist = list(set(tracks) - set(self.get_history(artist=artist)))
103 self.log.debug('All tracks already played for "{}"'.format(artist))
104 random.shuffle(not_in_hist)
105 #candidate = [ trk for trk in not_in_hist if trk not in black_list
106 #if not self.sdb.get_bl_track(trk, add_not=True)]
108 for trk in [_ for _ in not_in_hist if _ not in black_list]:
109 if self.sdb.get_bl_track(trk, add_not=True):
110 self.log.info('Blacklisted: {0}: '.format(trk))
112 if self.sdb.get_bl_album(trk, add_not=True):
113 self.log.info('Blacklisted album: {0}: '.format(trk))
115 # Should use albumartist heuristic as well
116 if self.plugin_conf.getboolean('single_album'):
117 if (trk.album == self.player.current.album or
118 trk.album in [tr.album for tr in self.to_add]):
119 self.log.debug('Found unplayed track ' +
120 'but from an album already queued: %s' % (trk))
122 candidate.append(trk)
124 self.log.debug('Unable to find title to add' +
125 ' for "%s".' % artist)
127 self.to_add.append(random.choice(candidate))
129 def _get_artists_list_reorg(self, alist):
131 Move around items in artists_list in order to play first not recently
134 # TODO: move to utils as a decorator
135 duration = self.daemon_conf.getint('sima', 'history_duration')
137 for trk in self.sdb.get_history(duration=duration,
139 if trk[0] not in art_in_hist:
140 art_in_hist.append(trk[0])
141 art_in_hist.reverse()
142 art_not_in_hist = [ ar for ar in alist if ar not in art_in_hist ]
143 random.shuffle(art_not_in_hist)
144 art_not_in_hist.extend(art_in_hist)
145 self.log.debug('history ordered: {}'.format(
146 ' / '.join(art_not_in_hist)))
147 return art_not_in_hist
150 def get_artists_from_player(self, similarities):
152 Look in player library for availability of similar artists in
156 while len(similarities) > 0:
157 art_pop = similarities.pop()
158 results.extend(self.player.fuzzy_find_artist(art_pop))
161 def lfm_similar_artists(self, artist=None):
163 Retrieve similar artists on echonest server.
166 curr = self.player.current.__dict__
167 name = curr.get('artist')
168 mbid = curr.get('musicbrainz_artistid', None)
169 current = Artist(name=name, mbid=mbid)
173 # initialize artists deque list to construct from DB
175 as_artists = simaech.get_similar(artist=current)
176 self.log.debug('Requesting EchoNest for "{0}"'.format(current))
178 [as_art.append(str(a)) for a in as_artists]
179 except EchoNotFound as err:
180 self.log.warning(err)
181 except EchoError as err:
182 self.log.warning('EchoNest: {0}'.format(err))
184 self.log.debug('Fetched {0} artist(s) from echonest'.format(
186 self.log.debug('x-ratelimit-remaining: {}'.format(SimaEch.ratelimit))
189 def get_recursive_similar_artist(self):
191 history = deque(self.history)
194 current = self.player.current
196 while depth < self.plugin_conf.getint('depth'):
197 if len(history) == 0:
199 trk = history.popleft()
200 if (trk.artist in [trk.artist for trk in extra_arts]
201 or trk.artist == current.artist):
203 extra_arts.append(trk)
205 self.log.info('EXTRA ARTS: {}'.format(
206 '/'.join([trk.artist for trk in extra_arts])))
207 for artist in extra_arts:
208 self.log.debug('Looking for artist similar to "{0.artist}" as well'.format(artist))
209 similar = self.lfm_similar_artists(artist=artist)
212 ret_extra.extend(self.get_artists_from_player(similar))
213 if current.artist in ret_extra:
214 ret_extra.remove(current.artist)
217 def get_local_similar_artists(self):
218 """Check against local player for similar artists fetched from last.fm
220 current = self.player.current
221 self.log.info('Looking for artist similar to "{0.artist}"'.format(current))
222 similar = list(self.lfm_similar_artists())
224 self.log.info('Got nothing from last.fm!')
226 self.log.info('First five similar artist(s): {}...'.format(
227 ' / '.join([a for a in similar[0:5]])))
228 self.log.info('Looking availability in music library')
229 ret = self.get_artists_from_player(similar)
231 if len(self.history) >= 2:
232 if self.plugin_conf.getint('depth') > 1:
233 ret_extra = self.get_recursive_similar_artist()
235 ret = list(set(ret) | set(ret_extra))
237 self.log.warning('Got nothing from music library.')
238 self.log.warning('Try running in debug mode to guess why...')
240 self.log.info('Got {} artists in library'.format(len(ret)))
241 self.log.info(' / '.join(ret))
242 # Move around similars items to get in unplayed|not recently played
244 return self._get_artists_list_reorg(ret)
246 def _get_album_history(self, artist=None):
247 """Retrieve album history"""
248 duration = self.daemon_conf.getint('sima', 'history_duration')
250 for trk in self.sdb.get_history(artist=artist, duration=duration):
251 albums_list.add(trk[1])
254 def find_album(self, artists):
255 """Find albums to queue.
259 target_album_to_add = self.plugin_conf.getint('album_to_add')
260 for artist in artists:
261 self.log.info('Looking for an album to add for "%s"...' % artist)
262 albums = self.player.find_albums(artist)
263 # str conversion while Album type is not propagated
264 albums = [ str(album) for album in albums]
266 self.log.debug('Albums candidate: {0:s}'.format(' / '.join(albums)))
268 # albums yet in history for this artist
270 albums_yet_in_hist = albums & self._get_album_history(artist=artist)
271 albums_not_in_hist = list(albums - albums_yet_in_hist)
272 # Get to next artist if there are no unplayed albums
273 if not albums_not_in_hist:
274 self.log.info('No album found for "%s"' % artist)
276 album_to_queue = str()
277 random.shuffle(albums_not_in_hist)
278 for album in albums_not_in_hist:
279 tracks = self.player.find_album(artist, album)
280 # Look if one track of the album is already queued
281 # Good heuristic, at least enough to guess if the whole album is
283 if tracks[0] in self.player.queue:
284 self.log.debug('"%s" already queued, skipping!' %
287 album_to_queue = album
288 if not album_to_queue:
289 self.log.info('No album found for "%s"' % artist)
291 self.log.info('last.fm album candidate: {0} - {1}'.format(
292 artist, album_to_queue))
294 self.to_add.extend(self.player.find_album(artist, album_to_queue))
295 if nb_album_add == target_album_to_add:
299 """Get some tracks for track queue mode
301 artists = self.get_local_similar_artists()
302 nbtracks_target = self.plugin_conf.getint('track_to_add')
303 for artist in artists:
304 self.log.debug('Trying to find titles to add for "{}"'.format(
306 found = self.player.find_track(artist)
307 # find tracks not in history for artist
308 self.filter_track(found)
309 if len(self.to_add) == nbtracks_target:
312 self.log.debug('Found no tracks to queue, is your ' +
313 'history getting too large?')
315 for track in self.to_add:
316 self.log.info('last.fm candidate: {0!s}'.format(track))
319 """Get albums for album queue mode
321 artists = self.get_local_similar_artists()
322 self.find_album(artists)
325 """Get some tracks for top track queue mode
327 #artists = self.get_local_similar_artists()
330 def callback_need_track(self):
331 self._cleanup_cache()
332 if not self.player.current:
333 self.log.info('Not currently playing track, cannot queue')
336 candidates = self.to_add
338 if self.plugin_conf.get('queue_mode') != 'album':
339 random.shuffle(candidates)
342 def callback_player_database(self):
346 # vim: ai ts=4 sw=4 sts=4 expandtab