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
155 dynamic = self.plugin_conf.getint('dynamic')
158 similarity = self.plugin_conf.getint('similarity')
160 while (len(results) < dynamic
161 and len(similarities) > 0):
162 art_pop = similarities.pop()
163 results.extend(self.player.fuzzy_find_artist(art_pop))
166 def lfm_similar_artists(self, artist=None):
168 Retrieve similar artists on echonest server.
171 curr = self.player.current.__dict__
172 name = curr.get('artist')
173 mbid = curr.get('musicbrainz_artistid', None)
174 current = Artist(name=name, mbid=mbid)
178 # initialize artists deque list to construct from DB
180 as_artists = simaech.get_similar(artist=current)
181 self.log.debug('Requesting EchoNest for "{0}"'.format(current))
183 # TODO: let's propagate Artist type
184 [as_art.append(str(art)) for art in as_artists]
185 except EchoNotFound as err:
186 self.log.warning(err)
187 except EchoError as err:
188 self.log.warning('EchoNest: {0}'.format(err))
190 self.log.debug('Fetched {0} artist(s) from echonest'.format(
192 self.log.debug('x-ratelimit-remaining: {}'.format(SimaEch.ratelimit))
195 def get_recursive_similar_artist(self):
197 history = deque(self.history)
200 current = self.player.current
202 while depth < self.plugin_conf.getint('depth'):
203 if len(history) == 0:
205 trk = history.popleft()
206 if (trk.artist in [trk.artist for trk in extra_arts]
207 or trk.artist == current.artist):
209 extra_arts.append(trk)
211 self.log.info('EXTRA ARTS: {}'.format(
212 '/'.join([trk.artist for trk in extra_arts])))
213 for artist in extra_arts:
214 self.log.debug('Looking for artist similar to "{0.artist}" as well'.format(artist))
215 similar = self.lfm_similar_artists(artist=artist)
218 ret_extra.extend(self.get_artists_from_player(similar))
219 if current.artist in ret_extra:
220 ret_extra.remove(current.artist)
223 def get_local_similar_artists(self):
224 """Check against local player for similar artists fetched from echonest
226 current = self.player.current
227 self.log.info('Looking for artist similar to "{0.artist}"'.format(current))
228 similar = list(self.lfm_similar_artists())
230 self.log.info('Got nothing from echonest!')
232 self.log.info('First five similar artist(s): {}...'.format(
233 ' / '.join([a for a in similar[0:5]])))
234 self.log.info('Looking availability in music library')
235 ret = self.get_artists_from_player(similar)
237 if len(self.history) >= 2:
238 if self.plugin_conf.getint('depth') > 1:
239 ret_extra = self.get_recursive_similar_artist()
241 ret = list(set(ret) | set(ret_extra))
243 self.log.warning('Got nothing from music library.')
244 self.log.warning('Try running in debug mode to guess why...')
246 self.log.info('Got {} artists in library'.format(len(ret)))
247 self.log.info(' / '.join(ret))
248 # Move around similars items to get in unplayed|not recently played
250 return self._get_artists_list_reorg(ret)
252 def _get_album_history(self, artist=None):
253 """Retrieve album history"""
254 duration = self.daemon_conf.getint('sima', 'history_duration')
256 for trk in self.sdb.get_history(artist=artist, duration=duration):
257 albums_list.add(trk[1])
260 def find_album(self, artists):
261 """Find albums to queue.
265 target_album_to_add = self.plugin_conf.getint('album_to_add')
266 for artist in artists:
267 self.log.info('Looking for an album to add for "%s"...' % artist)
268 albums = self.player.find_albums(artist)
269 # str conversion while Album type is not propagated
270 albums = [ str(album) for album in albums]
272 self.log.debug('Albums candidate: {0:s}'.format(' / '.join(albums)))
274 # albums yet in history for this artist
276 albums_yet_in_hist = albums & self._get_album_history(artist=artist)
277 albums_not_in_hist = list(albums - albums_yet_in_hist)
278 # Get to next artist if there are no unplayed albums
279 if not albums_not_in_hist:
280 self.log.info('No album found for "%s"' % artist)
282 album_to_queue = str()
283 random.shuffle(albums_not_in_hist)
284 for album in albums_not_in_hist:
285 tracks = self.player.find_album(artist, album)
286 # Look if one track of the album is already queued
287 # Good heuristic, at least enough to guess if the whole album is
289 if tracks[0] in self.player.queue:
290 self.log.debug('"%s" already queued, skipping!' %
293 album_to_queue = album
294 if not album_to_queue:
295 self.log.info('No album found for "%s"' % artist)
297 self.log.info('echonest album candidates: {0} - {1}'.format(
298 artist, album_to_queue))
300 self.to_add.extend(self.player.find_album(artist, album_to_queue))
301 if nb_album_add == target_album_to_add:
305 """Get some tracks for track queue mode
307 artists = self.get_local_similar_artists()
308 nbtracks_target = self.plugin_conf.getint('track_to_add')
309 for artist in artists:
310 self.log.debug('Trying to find titles to add for "{}"'.format(
312 found = self.player.find_track(artist)
313 # find tracks not in history for artist
314 self.filter_track(found)
315 if len(self.to_add) == nbtracks_target:
318 self.log.debug('Found no tracks to queue, is your ' +
319 'history getting too large?')
321 for track in self.to_add:
322 self.log.info('echonest candidates: {0!s}'.format(track))
325 """Get albums for album queue mode
327 artists = self.get_local_similar_artists()
328 self.find_album(artists)
331 """Get some tracks for top track queue mode
333 #artists = self.get_local_similar_artists()
336 def callback_need_track(self):
337 self._cleanup_cache()
338 if not self.player.current:
339 self.log.info('Not currently playing track, cannot queue')
342 candidates = self.to_add
344 if self.plugin_conf.get('queue_mode') != 'album':
345 random.shuffle(candidates)
348 def callback_player_database(self):
352 # vim: ai ts=4 sw=4 sts=4 expandtab