X-Git-Url: https://git.kaliko.me/?a=blobdiff_plain;f=sima%2Fplugins%2Finternal%2Flastfm.py;h=125f0090291655dc60656998b5a541a36869e6cd;hb=1c01e4a7aa37eeef825c6918fc90b154a7f4ccc7;hp=28bb3a42a075bef56ac740105443ef1e2465e353;hpb=47ec78a3de8e2280849ec3ab70df3371514bf0ac;p=mpd-sima.git diff --git a/sima/plugins/internal/lastfm.py b/sima/plugins/internal/lastfm.py index 28bb3a4..125f009 100644 --- a/sima/plugins/internal/lastfm.py +++ b/sima/plugins/internal/lastfm.py @@ -1,352 +1,51 @@ # -*- coding: utf-8 -*- +# Copyright (c) 2013, 2014 Jack Kaliko +# +# This file is part of sima +# +# sima is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# sima is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with sima. If not, see . +# +# """ Fetching similar artists from last.fm web services """ # standard library import -import random - -from collections import deque -from hashlib import md5 +from os.path import join # third parties components # local import -from ...lib.plugin import Plugin -from ...lib.simafm import SimaFM, XmlFMHTTPError, XmlFMNotFound, XmlFMError -from ...lib.track import Track - - -def cache(func): - """Caching decorator""" - def wrapper(*args, **kwargs): - #pylint: disable=W0212,C0111 - cls = args[0] - similarities = [art + str(match) for art, match in args[1]] - hashedlst = md5(''.join(similarities).encode('utf-8')).hexdigest() - if hashedlst in cls._cache.get('asearch'): - cls.log.debug('cached request') - results = cls._cache.get('asearch').get(hashedlst) - else: - results = func(*args, **kwargs) - cls.log.debug('caching request') - cls._cache.get('asearch').update({hashedlst:list(results)}) - random.shuffle(results) - return results - return wrapper +from ...lib.simafm import SimaFM +from ...lib.webserv import WebService +from ...lib.cache import FileCache -class Lastfm(Plugin): +class Lastfm(WebService): """last.fm similar artists """ def __init__(self, daemon): - Plugin.__init__(self, daemon) - self.daemon_conf = daemon.config - self.sdb = daemon.sdb - self.history = daemon.short_history - ## - self.to_add = list() - self._cache = None - self._flush_cache() - wrapper = { - 'track': self._track, - 'top': self._top, - 'album': self._album, - } - self.queue_mode = wrapper.get(self.plugin_conf.get('queue_mode')) - - def _flush_cache(self): - """ - Both flushes and instanciates _cache - """ - if isinstance(self._cache, dict): - self.log.info('Lastfm: Flushing cache!') - else: - self.log.info('Lastfm: Initialising cache!') - self._cache = { - 'asearch': dict(), - 'tsearch': dict(), - } - - def _cleanup_cache(self): - """Avoid bloated cache - """ - for _ , val in self._cache.items(): - if isinstance(val, dict): - while len(val) > 150: - val.popitem() - - def get_history(self, artist): - """Constructs list of Track for already played titles for an artist. - """ - duration = self.daemon_conf.getint('sima', 'history_duration') - tracks_from_db = self.sdb.get_history(duration=duration, artist=artist) - # Construct Track() objects list from database history - played_tracks = [Track(artist=tr[-1], album=tr[1], title=tr[2], - file=tr[3]) for tr in tracks_from_db] - return played_tracks - - def filter_track(self, tracks): - """ - Extract one unplayed track from a Track object list. - * not in history - * not already in the queue - * not blacklisted - """ - artist = tracks[0].artist - black_list = self.player.queue + self.to_add - not_in_hist = list(set(tracks) - set(self.get_history(artist=artist))) - if not not_in_hist: - self.log.debug('All tracks already played for "{}"'.format(artist)) - random.shuffle(not_in_hist) - #candidate = [ trk for trk in not_in_hist if trk not in black_list - #if not self.sdb.get_bl_track(trk, add_not=True)] - candidate = [] - for trk in [_ for _ in not_in_hist if _ not in black_list]: - if self.sdb.get_bl_track(trk, add_not=True): - self.log.info('Blacklisted: {0}: '.format(trk)) - continue - if self.sdb.get_bl_album(trk, add_not=True): - self.log.info('Blacklisted album: {0}: '.format(trk)) - continue - # Should use albumartist heuristic as well - if self.plugin_conf.getboolean('single_album'): - if (trk.album == self.player.current.album or - trk.album in [tr.album for tr in self.to_add]): - self.log.debug('Found unplayed track ' + - 'but from an album already queued: %s' % (trk)) - continue - candidate.append(trk) - if not candidate: - self.log.debug('Unable to find title to add' + - ' for "%s".' % artist) - return None - self.to_add.append(random.choice(candidate)) - - def _get_artists_list_reorg(self, alist): - """ - Move around items in artists_list in order to play first not recently - played artists - """ - # TODO: move to utils as a decorator - duration = self.daemon_conf.getint('sima', 'history_duration') - art_in_hist = list() - for trk in self.sdb.get_history(duration=duration, - artists=alist): - if trk[0] not in art_in_hist: - art_in_hist.append(trk[0]) - art_in_hist.reverse() - art_not_in_hist = [ ar for ar in alist if ar not in art_in_hist ] - random.shuffle(art_not_in_hist) - art_not_in_hist.extend(art_in_hist) - self.log.debug('history ordered: {}'.format( - ' / '.join(art_not_in_hist))) - return art_not_in_hist - - @cache - def get_artists_from_player(self, similarities): - """ - Look in player library for availability of similar artists in - similarities - """ - dynamic = self.plugin_conf.getint('dynamic') - if dynamic <= 0: - dynamic = 100 - similarity = self.plugin_conf.getint('similarity') - results = list() - similarities.reverse() - while (len(results) < dynamic - and len(similarities) > 0): - art_pop, match = similarities.pop() - if match < similarity: - break - results.extend(self.player.fuzzy_find_artist(art_pop)) - results and self.log.debug('Similarity: %d%%' % match) # pylint: disable=w0106 - return results - - def lfm_similar_artists(self, artist=None): - """ - Retrieve similar artists on last.fm server. - """ - if artist is None: - current = self.player.current - else: - current = artist - simafm = SimaFM() - # initialize artists deque list to construct from DB - as_art = deque() - as_artists = simafm.get_similar(artist=current.artist) - self.log.debug('Requesting last.fm for "{0.artist}"'.format(current)) - try: - [as_art.append((a, m)) for a, m in as_artists] - except XmlFMHTTPError as err: - self.log.warning('last.fm http error: %s' % err) - except XmlFMNotFound as err: - self.log.warning("last.fm: %s" % err) - except XmlFMError as err: - self.log.warning('last.fm module error: %s' % err) - if as_art: - self.log.debug('Fetched %d artist(s) from last.fm' % len(as_art)) - return as_art - - def get_recursive_similar_artist(self): - ret_extra = list() - history = deque(self.history) - history.popleft() - depth = 0 - current = self.player.current - extra_arts = list() - while depth < self.plugin_conf.getint('depth'): - if len(history) == 0: - break - trk = history.popleft() - if (trk.artist in [trk.artist for trk in extra_arts] - or trk.artist == current.artist): - continue - extra_arts.append(trk) - depth += 1 - self.log.info('EXTRA ARTS: {}'.format( - '/'.join([trk.artist for trk in extra_arts]))) - for artist in extra_arts: - self.log.debug('Looking for artist similar to "{0.artist}" as well'.format(artist)) - similar = self.lfm_similar_artists(artist=artist) - if not similar: - return ret_extra - similar = sorted(similar, key=lambda sim: sim[1], reverse=True) - ret_extra.extend(self.get_artists_from_player(similar)) - if current.artist in ret_extra: - ret_extra.remove(current.artist) - return ret_extra - - def get_local_similar_artists(self): - """Check against local player for similar artists fetched from last.fm - """ - current = self.player.current - self.log.info('Looking for artist similar to "{0.artist}"'.format(current)) - similar = self.lfm_similar_artists() - if not similar: - self.log.info('Got nothing from last.fm!') - return [] - similar = sorted(similar, key=lambda sim: sim[1], reverse=True) - self.log.info('First five similar artist(s): {}...'.format( - ' / '.join([a for a, m in similar[0:5]]))) - self.log.info('Looking availability in music library') - ret = self.get_artists_from_player(similar) - ret_extra = None - if len(self.history) >= 2: - if self.plugin_conf.getint('depth') > 1: - ret_extra = self.get_recursive_similar_artist() - if ret_extra: - ret = list(set(ret) | set(ret_extra)) - if not ret: - self.log.warning('Got nothing from music library.') - self.log.warning('Try running in debug mode to guess why...') - return [] - self.log.info('Got {} artists in library'.format(len(ret))) - self.log.info(' / '.join(ret)) - # Move around similars items to get in unplayed|not recently played - # artist first. - return self._get_artists_list_reorg(ret) - - def _get_album_history(self, artist=None): - """Retrieve album history""" - duration = self.daemon_conf.getint('sima', 'history_duration') - albums_list = set() - for trk in self.sdb.get_history(artist=artist, duration=duration): - albums_list.add(trk[1]) - return albums_list - - def find_album(self, artists): - """Find albums to queue. - """ - self.to_add = list() - nb_album_add = 0 - target_album_to_add = self.plugin_conf.getint('album_to_add') - for artist in artists: - self.log.info('Looking for an album to add for "%s"...' % artist) - albums = self.player.find_albums(artist) - # str conversion while Album type is not propagated - albums = [ str(album) for album in albums] - if albums: - self.log.debug('Albums candidate: {0:s}'.format(' / '.join(albums))) - else: continue - # albums yet in history for this artist - albums = set(albums) - albums_yet_in_hist = albums & self._get_album_history(artist=artist) - albums_not_in_hist = list(albums - albums_yet_in_hist) - # Get to next artist if there are no unplayed albums - if not albums_not_in_hist: - self.log.info('No album found for "%s"' % artist) - continue - album_to_queue = str() - random.shuffle(albums_not_in_hist) - for album in albums_not_in_hist: - tracks = self.player.find_album(artist, album) - # Look if one track of the album is already queued - # Good heuristic, at least enough to guess if the whole album is - # already queued. - if tracks[0] in self.player.queue: - self.log.debug('"%s" already queued, skipping!' % - tracks[0].album) - continue - album_to_queue = album - if not album_to_queue: - self.log.info('No album found for "%s"' % artist) - continue - self.log.info('last.fm album candidate: {0} - {1}'.format( - artist, album_to_queue)) - nb_album_add += 1 - self.to_add.extend(self.player.find_album(artist, album_to_queue)) - if nb_album_add == target_album_to_add: - return True - - def _track(self): - """Get some tracks for track queue mode - """ - artists = self.get_local_similar_artists() - nbtracks_target = self.plugin_conf.getint('track_to_add') - for artist in artists: - self.log.debug('Trying to find titles to add for "{}"'.format( - artist)) - found = self.player.find_track(artist) - # find tracks not in history for artist - self.filter_track(found) - if len(self.to_add) == nbtracks_target: - break - if not self.to_add: - self.log.debug('Found no tracks to queue, is your ' + - 'history getting too large?') - return None - for track in self.to_add: - self.log.info('last.fm candidate: {0!s}'.format(track)) - - def _album(self): - """Get albums for album queue mode - """ - artists = self.get_local_similar_artists() - self.find_album(artists) - - def _top(self): - """Get some tracks for top track queue mode - """ - #artists = self.get_local_similar_artists() - pass - - def callback_need_track(self): - self._cleanup_cache() - if not self.player.current: - self.log.info('Not currently playing track, cannot queue') - return None - self.queue_mode() - candidates = self.to_add - self.to_add = list() - if self.plugin_conf.get('queue_mode') != 'album': - random.shuffle(candidates) - return candidates + WebService.__init__(self, daemon) + # Set persitent cache + vardir = daemon.config['sima']['var_dir'] + persitent_cache = daemon.config.getboolean('lastfm', 'cache') + if persitent_cache: + self.log.debug('Persistant cache enabled in {}'.format(join(vardir, 'http', 'LastFM'))) + SimaFM.cache = FileCache(join(vardir, 'http', 'LastFM')) + self.ws = SimaFM() - def callback_player_database(self): - self._flush_cache() # VIM MODLINE # vim: ai ts=4 sw=4 sts=4 expandtab