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 for art in as_artists:
179 if len(as_art) > self.plugin_conf.getint('artists'):
181 as_art.append(str(art))
182 except EchoNotFound as err:
183 self.log.warning(err)
184 except EchoError as err:
185 self.log.warning('EchoNest: {0}'.format(err))
187 self.log.debug('Fetched {0} artist(s) from echonest'.format(
189 self.log.debug('x-ratelimit-remaining: {}'.format(SimaEch.ratelimit))
192 def get_recursive_similar_artist(self):
194 history = deque(self.history)
197 current = self.player.current
199 while depth < self.plugin_conf.getint('depth'):
200 if len(history) == 0:
202 trk = history.popleft()
203 if (trk.artist in [trk.artist for trk in extra_arts]
204 or trk.artist == current.artist):
206 extra_arts.append(trk)
208 self.log.info('EXTRA ARTS: {}'.format(
209 '/'.join([trk.artist for trk in extra_arts])))
210 for artist in extra_arts:
211 self.log.debug('Looking for artist similar to "{0.artist}" as well'.format(artist))
212 similar = self.lfm_similar_artists(artist=artist)
215 ret_extra.extend(self.get_artists_from_player(similar))
216 if current.artist in ret_extra:
217 ret_extra.remove(current.artist)
220 def get_local_similar_artists(self):
221 """Check against local player for similar artists fetched from last.fm
223 current = self.player.current
224 self.log.info('Looking for artist similar to "{0.artist}"'.format(current))
225 similar = list(self.lfm_similar_artists())
227 self.log.info('Got nothing from last.fm!')
229 self.log.info('First five similar artist(s): {}...'.format(
230 ' / '.join([a for a in similar[0:5]])))
231 self.log.info('Looking availability in music library')
232 ret = self.get_artists_from_player(similar)
234 if len(self.history) >= 2:
235 if self.plugin_conf.getint('depth') > 1:
236 ret_extra = self.get_recursive_similar_artist()
238 ret = list(set(ret) | set(ret_extra))
240 self.log.warning('Got nothing from music library.')
241 self.log.warning('Try running in debug mode to guess why...')
243 self.log.info('Got {} artists in library'.format(len(ret)))
244 self.log.info(' / '.join(ret))
245 # Move around similars items to get in unplayed|not recently played
247 return self._get_artists_list_reorg(ret)
249 def _get_album_history(self, artist=None):
250 """Retrieve album history"""
251 duration = self.daemon_conf.getint('sima', 'history_duration')
253 for trk in self.sdb.get_history(artist=artist, duration=duration):
254 albums_list.add(trk[1])
257 def find_album(self, artists):
258 """Find albums to queue.
262 target_album_to_add = self.plugin_conf.getint('album_to_add')
263 for artist in artists:
264 self.log.info('Looking for an album to add for "%s"...' % artist)
265 albums = self.player.find_albums(artist)
266 # str conversion while Album type is not propagated
267 albums = [ str(album) for album in albums]
269 self.log.debug('Albums candidate: {0:s}'.format(' / '.join(albums)))
271 # albums yet in history for this artist
273 albums_yet_in_hist = albums & self._get_album_history(artist=artist)
274 albums_not_in_hist = list(albums - albums_yet_in_hist)
275 # Get to next artist if there are no unplayed albums
276 if not albums_not_in_hist:
277 self.log.info('No album found for "%s"' % artist)
279 album_to_queue = str()
280 random.shuffle(albums_not_in_hist)
281 for album in albums_not_in_hist:
282 tracks = self.player.find_album(artist, album)
283 # Look if one track of the album is already queued
284 # Good heuristic, at least enough to guess if the whole album is
286 if tracks[0] in self.player.queue:
287 self.log.debug('"%s" already queued, skipping!' %
290 album_to_queue = album
291 if not album_to_queue:
292 self.log.info('No album found for "%s"' % artist)
294 self.log.info('last.fm album candidate: {0} - {1}'.format(
295 artist, album_to_queue))
297 self.to_add.extend(self.player.find_album(artist, album_to_queue))
298 if nb_album_add == target_album_to_add:
302 """Get some tracks for track queue mode
304 artists = self.get_local_similar_artists()
305 nbtracks_target = self.plugin_conf.getint('track_to_add')
306 for artist in artists:
307 self.log.debug('Trying to find titles to add for "{}"'.format(
309 found = self.player.find_track(artist)
310 # find tracks not in history for artist
311 self.filter_track(found)
312 if len(self.to_add) == nbtracks_target:
315 self.log.debug('Found no tracks to queue, is your ' +
316 'history getting too large?')
318 for track in self.to_add:
319 self.log.info('echonest candidates: {0!s}'.format(track))
322 """Get albums for album queue mode
324 artists = self.get_local_similar_artists()
325 self.find_album(artists)
328 """Get some tracks for top track queue mode
330 #artists = self.get_local_similar_artists()
333 def callback_need_track(self):
334 self._cleanup_cache()
335 if not self.player.current:
336 self.log.info('Not currently playing track, cannot queue')
339 candidates = self.to_add
341 if self.plugin_conf.get('queue_mode') != 'album':
342 random.shuffle(candidates)
345 def callback_player_database(self):
349 # vim: ai ts=4 sw=4 sts=4 expandtab