1 # -*- coding: utf-8 -*-
2 # Copyright (c) 2009-2020 kaliko <kaliko@azylum.org>
3 # Copyright (c) 2019 sacha <sachahony@gmail.com>
5 # This file is part of sima
7 # sima is free software: you can redistribute it and/or modify
8 # it under the terms of the GNU General Public License as published by
9 # the Free Software Foundation, either version 3 of the License, or
10 # (at your option) any later version.
12 # sima is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU General Public License for more details.
17 # You should have received a copy of the GNU General Public License
18 # along with sima. If not, see <http://www.gnu.org/licenses/>.
22 Fetching similar artists from last.fm web services
25 # standard library import
28 from collections import deque
29 from hashlib import md5
31 # third parties components
34 from .plugin import AdvancedPlugin
35 from .meta import Artist, MetaContainer
36 from ..utils.utils import WSError, WSNotFound, WSTimeout
40 """Caching decorator"""
41 def wrapper(*args, **kwargs):
42 #pylint: disable=W0212,C0111
44 similarities = [art.name for art in args[1]]
45 hashedlst = md5(''.join(similarities).encode('utf-8')).hexdigest()
46 if hashedlst in cls._cache.get('asearch'):
47 cls.log.debug('cached request')
48 results = cls._cache.get('asearch').get(hashedlst)
50 results = func(*args, **kwargs)
51 cls.log.debug('caching request')
52 cls._cache.get('asearch').update({hashedlst: list(results)})
53 random.shuffle(results)
58 class WebService(AdvancedPlugin):
59 """similar artists webservice
62 def __init__(self, daemon):
63 super().__init__(daemon)
64 self.history = daemon.short_history
69 wrapper = {'track': self._track,
72 self.queue_mode = wrapper.get(self.plugin_conf.get('queue_mode'))
76 def _flush_cache(self):
78 Both flushes and instanciates _cache
80 name = self.__class__.__name__
81 if isinstance(self._cache, dict):
82 self.log.info('%s: Flushing cache!', name)
84 self.log.info('%s: Initialising cache!', name)
85 self._cache = {'asearch': dict(),
88 def _cleanup_cache(self):
89 """Avoid bloated cache
91 for _, val in self._cache.items():
92 if isinstance(val, dict):
97 def get_artists_from_player(self, similarities):
99 Look in player library for availability of similar artists in
102 dynamic = self.plugin_conf.getint('max_art')
106 similarities.reverse()
107 while (len(results) < dynamic and similarities):
108 art_pop = similarities.pop()
109 res = self.player.search_artist(art_pop)
114 def ws_similar_artists(self, artist):
116 Retrieve similar artists from WebServive.
118 # initialize artists deque list to construct from DB
120 as_artists = self.ws.get_similar(artist=artist)
121 self.log.debug('Requesting %s for %r', self.ws.name, artist)
123 [as_art.append(art) for art in as_artists]
124 except WSNotFound as err:
125 self.log.warning('%s: %s', self.ws.name, err)
127 self.log.debug('Trying without MusicBrainzID')
129 return self.ws_similar_artists(Artist(name=artist.name))
130 except WSNotFound as err:
131 self.log.debug('%s: %s', self.ws.name, err)
132 except WSTimeout as err:
133 self.log.warning('%s: %s', self.ws.name, err)
134 if self.ws_retry < 3:
136 self.log.warning('%s: retrying', self.ws.name)
137 as_art = self.ws_similar_artists(artist)
139 self.log.warning('%s: stop retrying', self.ws.name)
141 except WSError as err:
142 self.log.warning('%s: %s', self.ws.name, err)
144 self.log.debug('Fetched %d artist(s)', len(as_art))
147 def get_recursive_similar_artist(self):
148 """Check against local player for similar artists (recursive w/ history)
150 if not self.player.playlist:
152 history = list(self.history)
153 # In random play mode use complete playlist to filter
154 if self.player.playmode.get('random'):
155 history = self.player.playlist + history
157 history = self.player.queue + history
158 history = deque(history)
159 last_trk = history.popleft() # remove
163 while depth < self.plugin_conf.getint('depth'):
166 trk = history.popleft()
167 if (trk.Artist in extra_arts
168 or trk.Artist == last_trk.Artist):
170 extra_arts.append(trk.Artist)
172 self.log.debug('EXTRA ARTS: %s', '/'.join(map(str, extra_arts)))
173 for artist in extra_arts:
174 self.log.debug('Looking for artist similar '
175 'to "%s" as well', artist)
176 similar = self.ws_similar_artists(artist=artist)
179 ret_extra.extend(self.get_artists_from_player(similar))
181 if last_trk.Artist in ret_extra:
182 ret_extra.remove(last_trk.Artist)
184 self.log.debug('similar artist(s) found: %s',
185 ' / '.join(map(str, MetaContainer(ret_extra))))
188 def get_local_similar_artists(self):
189 """Check against local player for similar artists
191 if not self.player.playlist:
193 tolookfor = self.player.playlist[-1].Artist
194 self.log.info('Looking for artist similar to "%s"', tolookfor)
195 self.log.debug('%r', tolookfor)
196 similar = self.ws_similar_artists(tolookfor)
198 self.log.info('Got nothing from %s!', self.ws.name)
200 self.log.info('First five similar artist(s): %s...',
201 ' / '.join(map(str, list(similar)[:5])))
202 self.log.info('Looking availability in music library')
203 ret = MetaContainer(self.get_artists_from_player(similar))
205 self.log.debug('regular found in library: %s',
206 ' / '.join(map(str, ret)))
208 self.log.debug('Got nothing similar from library!')
210 if len(self.history) >= 2:
211 if self.plugin_conf.getint('depth') > 1:
212 ret_extra = self.get_recursive_similar_artist()
214 # get them reorg to pick up best element
215 ret_extra = self.get_reorg_artists_list(ret_extra)
216 # tries to pickup less artist from extra art
218 ret_extra = MetaContainer(ret_extra[:max(4, len(ret))//2])
220 self.log.debug('extra found in library: %s',
221 ' / '.join(map(str, ret_extra)))
222 ret = ret | ret_extra
224 self.log.warning('Got nothing from music library.')
226 # In random play mode use complete playlist to filter
227 if self.player.playmode.get('random'):
228 queued_artists = MetaContainer([trk.Artist for trk
229 in self.player.playlist])
231 queued_artists = MetaContainer([trk.Artist for trk
232 in self.player.queue])
233 self.log.trace('Already queued: %s', queued_artists)
234 self.log.trace('Candidate: %s', ret)
235 if ret & queued_artists:
236 self.log.debug('Removing already queued artists: '
237 '%s', '/'.join(map(str, ret & queued_artists)))
238 ret = ret - queued_artists
239 current = self.player.current
240 if current and current.Artist in ret:
241 self.log.debug('Removing current artist: %s', current.Artist)
242 ret = ret - MetaContainer([current.Artist])
243 # Move around similars items to get in unplayed|not recently played
245 self.log.info('Got %d artists in library', len(ret))
246 candidates = self.get_reorg_artists_list(ret)
248 self.log.info(' / '.join(map(str, candidates)))
251 def _get_album_history(self, artist):
252 """Retrieve album history"""
254 for trk in self.get_history(artist=artist.name):
257 albums_list.add(trk.album)
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 album = self.album_candidate(artist, unplayed=True)
271 candidates = self.player.find_tracks(album)
272 if self.plugin_conf.getboolean('shuffle_album'):
273 random.shuffle(candidates)
274 # this allows to select a maximum number of track from the album
275 # a value of 0 (default) means keep all
276 nbtracks = self.plugin_conf.getint('track_to_add_from_album')
278 candidates = candidates[0:nbtracks]
279 self.to_add.extend(candidates)
280 if nb_album_add == target_album_to_add:
283 def find_top(self, artists):
285 find top tracks for artists in artists list.
288 nbtracks_target = self.plugin_conf.getint('track_to_add')
289 for artist in artists:
290 if len(self.to_add) == nbtracks_target:
292 self.log.info('Looking for a top track for %s', artist)
295 titles = [t for t in self.ws.get_toptrack(artist)]
296 except WSError as err:
297 self.log.warning('%s: %s', self.ws.name, err)
300 found = self.player.search_track(artist, trk.title)
302 random.shuffle(found)
303 top_trk = self.filter_track(found)
305 self.to_add.append(top_trk)
309 """Get some tracks for track queue mode
311 artists = self.get_local_similar_artists()
312 nbtracks_target = self.plugin_conf.getint('track_to_add')
313 for artist in artists:
314 self.log.debug('Trying to find titles to add for "%r"', artist)
315 found = self.player.find_tracks(artist)
317 self.log.debug('Found nothing to queue for %s', artist)
319 random.shuffle(found)
320 # find tracks not in history for artist
321 track_candidate = self.filter_track(found)
323 self.to_add.append(track_candidate)
324 if len(self.to_add) == nbtracks_target:
327 self.log.debug('Found no tracks to queue!')
329 for track in self.to_add:
330 self.log.info('%s candidates: %s', track, self.ws.name)
333 """Get albums for album queue mode
335 artists = self.get_local_similar_artists()
336 self.find_album(artists)
339 """Get some tracks for top track queue mode
341 artists = self.get_local_similar_artists()
342 self.find_top(artists)
343 for track in self.to_add:
344 self.log.info('%s candidates: %s', self.ws.name, track)
346 def callback_need_track(self):
347 self._cleanup_cache()
348 if not self.player.playlist:
349 self.log.info('No last track, cannot queue')
351 if not self.player.playlist[-1].artist:
352 self.log.warning('No artist set for the last track in queue')
353 self.log.debug(repr(self.player.current))
356 msg = ' '.join(['{0}: {1:>3d}'.format(k, v) for
357 k, v in sorted(self.ws.stats.items())])
358 self.log.debug('http stats: ' + msg)
359 candidates = self.to_add
361 if self.plugin_conf.get('queue_mode') != 'album':
362 random.shuffle(candidates)
365 def callback_player_database(self):
369 # vim: ai ts=4 sw=4 sts=4 expandtab