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 [as_art.append(str(art)) for art in as_artists]
184 except EchoNotFound as err:
185 self.log.warning(err)
186 except EchoError as err:
187 self.log.warning('EchoNest: {0}'.format(err))
189 self.log.debug('Fetched {0} artist(s) from echonest'.format(
191 self.log.debug('x-ratelimit-remaining: {}'.format(SimaEch.ratelimit))
194 def get_recursive_similar_artist(self):
196 history = deque(self.history)
199 current = self.player.current
201 while depth < self.plugin_conf.getint('depth'):
202 if len(history) == 0:
204 trk = history.popleft()
205 if (trk.artist in [trk.artist for trk in extra_arts]
206 or trk.artist == current.artist):
208 extra_arts.append(trk)
210 self.log.info('EXTRA ARTS: {}'.format(
211 '/'.join([trk.artist for trk in extra_arts])))
212 for artist in extra_arts:
213 self.log.debug('Looking for artist similar to "{0.artist}" as well'.format(artist))
214 similar = self.lfm_similar_artists(artist=artist)
217 ret_extra.extend(self.get_artists_from_player(similar))
218 if current.artist in ret_extra:
219 ret_extra.remove(current.artist)
222 def get_local_similar_artists(self):
223 """Check against local player for similar artists fetched from echonest
225 current = self.player.current
226 self.log.info('Looking for artist similar to "{0.artist}"'.format(current))
227 similar = list(self.lfm_similar_artists())
229 self.log.info('Got nothing from echonest!')
231 self.log.info('First five similar artist(s): {}...'.format(
232 ' / '.join([a for a in similar[0:5]])))
233 self.log.info('Looking availability in music library')
234 ret = self.get_artists_from_player(similar)
236 if len(self.history) >= 2:
237 if self.plugin_conf.getint('depth') > 1:
238 ret_extra = self.get_recursive_similar_artist()
240 ret = list(set(ret) | set(ret_extra))
242 self.log.warning('Got nothing from music library.')
243 self.log.warning('Try running in debug mode to guess why...')
245 self.log.info('Got {} artists in library'.format(len(ret)))
246 self.log.info(' / '.join(ret))
247 # Move around similars items to get in unplayed|not recently played
249 return self._get_artists_list_reorg(ret)
251 def _get_album_history(self, artist=None):
252 """Retrieve album history"""
253 duration = self.daemon_conf.getint('sima', 'history_duration')
255 for trk in self.sdb.get_history(artist=artist, duration=duration):
256 albums_list.add(trk[1])
259 def find_album(self, artists):
260 """Find albums to queue.
264 target_album_to_add = self.plugin_conf.getint('album_to_add')
265 for artist in artists:
266 self.log.info('Looking for an album to add for "%s"...' % artist)
267 albums = self.player.find_albums(artist)
268 # str conversion while Album type is not propagated
269 albums = [ str(album) for album in albums]
271 self.log.debug('Albums candidate: {0:s}'.format(' / '.join(albums)))
273 # albums yet in history for this artist
275 albums_yet_in_hist = albums & self._get_album_history(artist=artist)
276 albums_not_in_hist = list(albums - albums_yet_in_hist)
277 # Get to next artist if there are no unplayed albums
278 if not albums_not_in_hist:
279 self.log.info('No album found for "%s"' % artist)
281 album_to_queue = str()
282 random.shuffle(albums_not_in_hist)
283 for album in albums_not_in_hist:
284 tracks = self.player.find_album(artist, album)
285 # Look if one track of the album is already queued
286 # Good heuristic, at least enough to guess if the whole album is
288 if tracks[0] in self.player.queue:
289 self.log.debug('"%s" already queued, skipping!' %
292 album_to_queue = album
293 if not album_to_queue:
294 self.log.info('No album found for "%s"' % artist)
296 self.log.info('last.fm album candidate: {0} - {1}'.format(
297 artist, album_to_queue))
299 self.to_add.extend(self.player.find_album(artist, album_to_queue))
300 if nb_album_add == target_album_to_add:
304 """Get some tracks for track queue mode
306 artists = self.get_local_similar_artists()
307 nbtracks_target = self.plugin_conf.getint('track_to_add')
308 for artist in artists:
309 self.log.debug('Trying to find titles to add for "{}"'.format(
311 found = self.player.find_track(artist)
312 # find tracks not in history for artist
313 self.filter_track(found)
314 if len(self.to_add) == nbtracks_target:
317 self.log.debug('Found no tracks to queue, is your ' +
318 'history getting too large?')
320 for track in self.to_add:
321 self.log.info('echonest candidates: {0!s}'.format(track))
324 """Get albums for album queue mode
326 artists = self.get_local_similar_artists()
327 self.find_album(artists)
330 """Get some tracks for top track queue mode
332 #artists = self.get_local_similar_artists()
335 def callback_need_track(self):
336 self._cleanup_cache()
337 if not self.player.current:
338 self.log.info('Not currently playing track, cannot queue')
341 candidates = self.to_add
343 if self.plugin_conf.get('queue_mode') != 'album':
344 random.shuffle(candidates)
347 def callback_player_database(self):
351 # vim: ai ts=4 sw=4 sts=4 expandtab