1 # -*- coding: utf-8 -*-
2 # Copyright (c) 2009-2015 Jack Kaliko <kaliko@azylum.org>
4 # This file is part of sima
6 # sima is free software: you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation, either version 3 of the License, or
9 # (at your option) any later version.
11 # sima is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
16 # You should have received a copy of the GNU General Public License
17 # along with sima. If not, see <http://www.gnu.org/licenses/>.
21 Fetching similar artists from last.fm web services
24 # standard library import
27 from collections import deque
28 from hashlib import md5
30 # third parties components
33 from .plugin import Plugin
34 from .track import Track
35 from .meta import Artist, MetaContainer
36 from ..utils.utils import WSError, WSNotFound
39 """Caching decorator"""
40 def wrapper(*args, **kwargs):
41 #pylint: disable=W0212,C0111
43 similarities = [art.name for art in args[1]]
44 hashedlst = md5(''.join(similarities).encode('utf-8')).hexdigest()
45 if hashedlst in cls._cache.get('asearch'):
46 cls.log.debug('cached request')
47 results = cls._cache.get('asearch').get(hashedlst)
49 results = func(*args, **kwargs)
50 cls.log.debug('caching request')
51 cls._cache.get('asearch').update({hashedlst:list(results)})
52 random.shuffle(results)
57 class WebService(Plugin):
58 """similar artists webservice
60 # pylint: disable=bad-builtin
62 def __init__(self, daemon):
63 Plugin.__init__(self, daemon)
64 self.daemon_conf = daemon.config
66 self.history = daemon.short_history
71 wrapper = {'track': self._track,
73 'album': self._album,}
74 self.queue_mode = wrapper.get(self.plugin_conf.get('queue_mode'))
77 def _flush_cache(self):
79 Both flushes and instanciates _cache
81 name = self.__class__.__name__
82 if isinstance(self._cache, dict):
83 self.log.info('{0}: Flushing cache!'.format(name))
85 self.log.info('{0}: Initialising cache!'.format(name))
86 self._cache = {'asearch': dict(),
89 def _cleanup_cache(self):
90 """Avoid bloated cache
92 for _, val in self._cache.items():
93 if isinstance(val, dict):
97 def get_history(self, artist):
98 """Constructs list of Track for already played titles for an artist.
100 duration = self.daemon_conf.getint('sima', 'history_duration')
101 tracks_from_db = self.sdb.get_history(duration=duration, artist=artist)
102 # Construct Track() objects list from database history
103 played_tracks = [Track(artist=tr[-1], album=tr[1], title=tr[2],
104 file=tr[3]) for tr in tracks_from_db]
107 def filter_track(self, tracks):
109 Extract one unplayed track from a Track object list.
111 * not already in the queue
114 artist = tracks[0].artist
115 # In random play mode use complete playlist to filter
116 if self.player.playmode.get('random'):
117 black_list = self.player.playlist + self.to_add
119 black_list = self.player.queue + self.to_add
120 not_in_hist = list(set(tracks) - set(self.get_history(artist=artist)))
121 if self.plugin_conf.get('queue_mode') != 'top' and not not_in_hist:
122 self.log.debug('All tracks already played for "%s"', artist)
123 random.shuffle(not_in_hist)
125 for trk in [_ for _ in not_in_hist if _ not in black_list]:
126 # Should use albumartist heuristic as well
127 if self.plugin_conf.getboolean('single_album'): # pylint: disable=no-member
128 if (trk.album == self.player.current.album or
129 trk.album in [tr.album for tr in black_list]):
130 self.log.debug('Found unplayed track ' +
131 'but from an album already queued: %s', trk)
133 candidate.append(trk)
136 self.to_add.append(random.choice(candidate))
139 def _get_artists_list_reorg(self, alist):
141 Move around items in artists_list in order to play first not recently
145 duration = self.daemon_conf.getint('sima', 'history_duration')
146 for art in self.sdb.get_artists_history(alist, duration=duration):
149 reorg = [art for art in alist if art not in hist]
154 def get_artists_from_player(self, similarities):
156 Look in player library for availability of similar artists in
159 dynamic = self.plugin_conf.getint('max_art') # pylint: disable=no-member
163 similarities.reverse()
164 while (len(results) < dynamic
165 and len(similarities) > 0):
166 art_pop = similarities.pop()
167 res = self.player.search_artist(art_pop)
172 def ws_similar_artists(self, artist):
174 Retrieve similar artists from WebServive.
176 # initialize artists deque list to construct from DB
178 as_artists = self.ws.get_similar(artist=artist)
179 self.log.debug('Requesting {} for {!r}'.format(self.ws.name, artist))
181 [as_art.append(art) for art in as_artists]
182 except WSNotFound as err:
183 self.log.warning('{}: {}'.format(self.ws.name, err))
185 self.log.debug('Trying without MusicBrainzID')
187 return self.ws_similar_artists(Artist(name=artist.name))
188 except WSNotFound as err:
189 self.log.debug('{}: {}'.format(self.ws.name, err))
190 except WSError as err:
191 self.log.warning('{}: {}'.format(self.ws.name, err))
193 self.log.debug('Fetched {} artist(s)'.format(len(as_art)))
196 def get_recursive_similar_artist(self):
197 """Check against local player for similar artists (recursive w/ history)
199 if not self.player.playlist:
201 history = list(self.history)
202 # In random play mode use complete playlist to filter
203 if self.player.playmode.get('random'):
204 history = self.player.playlist + history
206 history = self.player.queue + history
207 history = deque(history)
208 last_trk = history.popleft() # remove
212 while depth < self.plugin_conf.getint('depth'): # pylint: disable=no-member
213 if len(history) == 0:
215 trk = history.popleft()
216 if (trk.Artist in extra_arts
217 or trk.Artist == last_trk.Artist):
219 extra_arts.append(trk.Artist)
221 self.log.debug('EXTRA ARTS: %s', '/'.join(map(str, extra_arts)))
222 for artist in extra_arts:
223 self.log.debug('Looking for artist similar '
224 'to "{}" as well'.format(artist))
225 similar = self.ws_similar_artists(artist=artist)
228 ret_extra.extend(self.get_artists_from_player(similar))
230 if last_trk.Artist in ret_extra:
231 ret_extra.remove(last_trk.Artist)
233 self.log.debug('similar artist(s) found: %s',
234 ' / '.join(map(str, MetaContainer(ret_extra))))
237 def get_local_similar_artists(self):
238 """Check against local player for similar artists
240 if not self.player.playlist:
242 tolookfor = self.player.playlist[-1].Artist
243 self.log.info('Looking for artist similar to "{}"'.format(tolookfor))
244 self.log.debug(repr(tolookfor))
245 similar = self.ws_similar_artists(tolookfor)
247 self.log.info('Got nothing from {0}!'.format(self.ws.name))
249 self.log.info('First five similar artist(s): %s...',
250 ' / '.join(map(str, list(similar)[:5])))
251 self.log.info('Looking availability in music library')
252 ret = MetaContainer(self.get_artists_from_player(similar))
254 self.log.debug('regular found in library: %s',
255 ' / '.join(map(str, ret)))
257 self.log.debug('Got nothing similar from library!')
259 if len(self.history) >= 2:
260 if self.plugin_conf.getint('depth') > 1: # pylint: disable=no-member
261 ret_extra = self.get_recursive_similar_artist()
263 # get them reorg to pick up best element
264 ret_extra = self._get_artists_list_reorg(ret_extra)
265 # tries to pickup less artist from extra art
267 ret_extra = MetaContainer(ret_extra)
269 ret_extra = MetaContainer(ret_extra[:max(4, len(ret))//2])
271 self.log.debug('extra found in library: %s',
272 ' / '.join(map(str, ret_extra)))
273 ret = ret | ret_extra
275 self.log.warning('Got nothing from music library.')
277 # In random play mode use complete playlist to filter
278 if self.player.playmode.get('random'):
279 queued_artists = MetaContainer([trk.Artist for trk in self.player.playlist])
281 queued_artists = MetaContainer([trk.Artist for trk in self.player.queue])
282 self.log.trace('Already queued: {}'.format(queued_artists))
283 self.log.trace('Candidate: {}'.format(ret))
284 if ret & queued_artists:
285 self.log.debug('Removing already queued artists: '
286 '{0}'.format('/'.join(map(str, ret & queued_artists))))
287 ret = ret - queued_artists
288 if self.player.current and self.player.current.Artist in ret:
289 self.log.debug('Removing current artist: {0}'.format(self.player.current.Artist))
290 ret = ret - MetaContainer([self.player.current.Artist])
291 # Move around similars items to get in unplayed|not recently played
293 self.log.info('Got {} artists in library'.format(len(ret)))
294 candidates = self._get_artists_list_reorg(list(ret))
296 self.log.info(' / '.join(map(str, candidates)))
299 def _get_album_history(self, artist=None):
300 """Retrieve album history"""
301 duration = self.daemon_conf.getint('sima', 'history_duration')
303 for trk in self.sdb.get_history(artist=artist.name, duration=duration):
304 albums_list.add(trk[1])
307 def find_album(self, artists):
308 """Find albums to queue.
312 target_album_to_add = self.plugin_conf.getint('album_to_add') # pylint: disable=no-member
313 for artist in artists:
314 self.log.info('Looking for an album to add for "%s"...' % artist)
315 albums = self.player.search_albums(artist)
316 # str conversion while Album type is not propagated
317 albums = [str(album) for album in albums]
319 self.log.debug('Albums candidate: %s', ' / '.join(albums))
321 # albums yet in history for this artist
323 albums_yet_in_hist = albums & self._get_album_history(artist=artist)
324 albums_not_in_hist = list(albums - albums_yet_in_hist)
325 # Get to next artist if there are no unplayed albums
326 if not albums_not_in_hist:
327 self.log.info('No unplayed album found for "%s"' % artist)
329 album_to_queue = str()
330 random.shuffle(albums_not_in_hist)
331 for album in albums_not_in_hist:
332 # Controls the album found is not already queued
333 if album in {t.album for t in self.player.queue}:
334 self.log.debug('"%s" already queued, skipping!', album)
336 # In random play mode use complete playlist to filter
337 if self.player.playmode.get('random'):
338 if album in {t.album for t in self.player.playlist}:
339 self.log.debug('"%s" already in playlist, skipping!', album)
341 album_to_queue = album
342 if not album_to_queue:
343 self.log.info('No album found for "%s"', artist)
345 self.log.info('%s album candidate: %s - %s', self.ws.name, artist, album_to_queue)
347 self.to_add.extend(self.player.find_album(artist, album_to_queue))
348 if nb_album_add == target_album_to_add:
351 def find_top(self, artists):
353 find top tracks for artists in artists list.
356 nbtracks_target = self.plugin_conf.getint('track_to_add') # pylint: disable=no-member
357 for artist in artists:
358 if len(self.to_add) == nbtracks_target:
360 self.log.info('Looking for a top track for {0}'.format(artist))
363 titles = [t for t in self.ws.get_toptrack(artist)]
364 except WSError as err:
365 self.log.warning('%s: %s', self.ws.name, err)
367 found = self.player.fuzzy_find_track(artist, trk.title)
368 random.shuffle(found)
370 self.log.debug('%s', found[0])
371 if self.filter_track(found):
375 """Get some tracks for track queue mode
377 artists = self.get_local_similar_artists()
378 nbtracks_target = self.plugin_conf.getint('track_to_add') # pylint: disable=no-member
379 for artist in artists:
380 self.log.debug('Trying to find titles to add for "%r"', artist)
381 found = self.player.find_track(artist)
382 random.shuffle(found)
384 self.log.debug('Found nothing to queue for {0}'.format(artist))
386 # find tracks not in history for artist
387 self.filter_track(found)
388 if len(self.to_add) == nbtracks_target:
391 self.log.debug('Found no tracks to queue!')
393 for track in self.to_add:
394 self.log.info('{1} candidates: {0!s}'.format(track, self.ws.name))
397 """Get albums for album queue mode
399 artists = self.get_local_similar_artists()
400 self.find_album(artists)
403 """Get some tracks for top track queue mode
405 artists = self.get_local_similar_artists()
406 self.find_top(artists)
407 for track in self.to_add:
408 self.log.info('{1} candidates: {0!s}'.format(track, self.ws.name))
410 def callback_need_track(self):
411 self._cleanup_cache()
412 if len(self.player.playlist) == 0:
413 self.log.info('No last track, cannot queue')
415 if not self.player.playlist[-1].artist:
416 self.log.warning('No artist set for the last track in queue')
417 self.log.debug(repr(self.player.current))
420 msg = ' '.join(['{0}: {1:>3d}'.format(k, v) for
421 k, v in sorted(self.ws.stats.items())])
422 self.log.debug('http stats: ' + msg)
423 candidates = self.to_add
425 if self.plugin_conf.get('queue_mode') != 'album':
426 random.shuffle(candidates)
429 def callback_player_database(self):
433 # vim: ai ts=4 sw=4 sts=4 expandtab