1 # -*- coding: utf-8 -*-
3 Fetching similar artists from last.fm 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.simafm import SimaFM, WSHTTPError, WSNotFound, WSError
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)
41 """last.fm similar artists
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 similarities.reverse()
161 while (len(results) < dynamic
162 and len(similarities) > 0):
163 art_pop, match = similarities.pop()
164 if match < similarity:
166 results.extend(self.player.fuzzy_find_artist(art_pop))
167 results and self.log.debug('Similarity: %d%%' % match) # pylint: disable=w0106
170 def lfm_similar_artists(self, artist=None):
172 Retrieve similar artists on last.fm server.
175 curr = self.player.current.__dict__
176 name = curr.get('artist')
177 mbid = curr.get('musicbrainz_artistid', None)
178 current = Artist(name=name, mbid=mbid)
182 # initialize artists deque list to construct from DB
184 as_artists = simafm.get_similar(artist=current)
185 self.log.debug('Requesting last.fm for "{0}"'.format(current))
187 # TODO: let's propagate Artist type
188 [as_art.append((str(a), m)) for a, m in as_artists]
189 except WSHTTPError as err:
190 self.log.warning('last.fm http error: %s' % err)
191 except WSNotFound as err:
192 self.log.warning("last.fm: %s" % err)
193 except WSError as err:
194 self.log.warning('last.fm module error: %s' % err)
196 self.log.debug('Fetched %d artist(s) from last.fm' % len(as_art))
199 def get_recursive_similar_artist(self):
201 history = deque(self.history)
204 current = self.player.current
206 while depth < self.plugin_conf.getint('depth'):
207 if len(history) == 0:
209 trk = history.popleft()
210 if (trk.artist in [trk.artist for trk in extra_arts]
211 or trk.artist == current.artist):
213 extra_arts.append(trk)
215 self.log.info('EXTRA ARTS: {}'.format(
216 '/'.join([trk.artist for trk in extra_arts])))
217 for artist in extra_arts:
218 self.log.debug('Looking for artist similar to "{0.artist}" as well'.format(artist))
219 similar = self.lfm_similar_artists(artist=artist)
222 similar = sorted(similar, key=lambda sim: sim[1], reverse=True)
223 ret_extra.extend(self.get_artists_from_player(similar))
224 if current.artist in ret_extra:
225 ret_extra.remove(current.artist)
228 def get_local_similar_artists(self):
229 """Check against local player for similar artists fetched from last.fm
231 current = self.player.current
232 self.log.info('Looking for artist similar to "{0.artist}"'.format(current))
233 similar = self.lfm_similar_artists()
235 self.log.info('Got nothing from last.fm!')
237 similar = sorted(similar, key=lambda sim: sim[1], reverse=True)
238 self.log.info('First five similar artist(s): {}...'.format(
239 ' / '.join([a for a, _ in similar[0:5]])))
240 self.log.info('Looking availability in music library')
241 ret = self.get_artists_from_player(similar)
243 if len(self.history) >= 2:
244 if self.plugin_conf.getint('depth') > 1:
245 ret_extra = self.get_recursive_similar_artist()
247 ret = list(set(ret) | set(ret_extra))
249 self.log.warning('Got nothing from music library.')
250 self.log.warning('Try running in debug mode to guess why...')
252 self.log.info('Got {} artists in library'.format(len(ret)))
253 self.log.info(' / '.join(ret))
254 # Move around similars items to get in unplayed|not recently played
256 return self._get_artists_list_reorg(ret)
258 def _get_album_history(self, artist=None):
259 """Retrieve album history"""
260 duration = self.daemon_conf.getint('sima', 'history_duration')
262 for trk in self.sdb.get_history(artist=artist, duration=duration):
263 albums_list.add(trk[1])
266 def find_album(self, artists):
267 """Find albums to queue.
271 target_album_to_add = self.plugin_conf.getint('album_to_add')
272 for artist in artists:
273 self.log.info('Looking for an album to add for "%s"...' % artist)
274 albums = self.player.find_albums(artist)
275 # str conversion while Album type is not propagated
276 albums = [ str(album) for album in albums]
278 self.log.debug('Albums candidate: {0:s}'.format(' / '.join(albums)))
280 # albums yet in history for this artist
282 albums_yet_in_hist = albums & self._get_album_history(artist=artist)
283 albums_not_in_hist = list(albums - albums_yet_in_hist)
284 # Get to next artist if there are no unplayed albums
285 if not albums_not_in_hist:
286 self.log.info('No album found for "%s"' % artist)
288 album_to_queue = str()
289 random.shuffle(albums_not_in_hist)
290 for album in albums_not_in_hist:
291 tracks = self.player.find_album(artist, album)
292 # Look if one track of the album is already queued
293 # Good heuristic, at least enough to guess if the whole album is
295 if tracks[0] in self.player.queue:
296 self.log.debug('"%s" already queued, skipping!' %
299 album_to_queue = album
300 if not album_to_queue:
301 self.log.info('No album found for "%s"' % artist)
303 self.log.info('last.fm album candidate: {0} - {1}'.format(
304 artist, album_to_queue))
306 self.to_add.extend(self.player.find_album(artist, album_to_queue))
307 if nb_album_add == target_album_to_add:
311 """Get some tracks for track queue mode
313 artists = self.get_local_similar_artists()
314 nbtracks_target = self.plugin_conf.getint('track_to_add')
315 for artist in artists:
316 self.log.debug('Trying to find titles to add for "{}"'.format(
318 found = self.player.find_track(artist)
319 # find tracks not in history for artist
320 self.filter_track(found)
321 if len(self.to_add) == nbtracks_target:
324 self.log.debug('Found no tracks to queue, is your ' +
325 'history getting too large?')
327 for track in self.to_add:
328 self.log.info('last.fm candidates: {0!s}'.format(track))
331 """Get albums for album queue mode
333 artists = self.get_local_similar_artists()
334 self.find_album(artists)
337 """Get some tracks for top track queue mode
339 #artists = self.get_local_similar_artists()
342 def callback_need_track(self):
343 self._cleanup_cache()
344 if not self.player.current:
345 self.log.info('Not currently playing track, cannot queue')
348 candidates = self.to_add
350 if self.plugin_conf.get('queue_mode') != 'album':
351 random.shuffle(candidates)
354 def callback_player_database(self):
358 # vim: ai ts=4 sw=4 sts=4 expandtab