From c660efb577c11bde6229d37550bf197fa6bae3e4 Mon Sep 17 00:00:00 2001 From: kaliko Date: Sat, 12 Oct 2013 14:57:52 +0200 Subject: [PATCH] Move fuzzy_find method in the player. Exposes daemon/player in plugins --- sima/client.py | 92 ++++++++++++++++++++++++++++------ sima/lib/player.py | 19 +++++-- sima/lib/plugin.py | 1 + sima/plugins/addhist.py | 1 - sima/plugins/lastfm.py | 68 ++----------------------- sima/plugins/mpd.py | 2 + sima/plugins/randomfallback.py | 1 - 7 files changed, 102 insertions(+), 82 deletions(-) diff --git a/sima/client.py b/sima/client.py index 9d61005..929f662 100644 --- a/sima/client.py +++ b/sima/client.py @@ -3,8 +3,11 @@ This client is built above python-musicpd a fork of python-mpd """ +# pylint: disable=C0111 # standart library import + +from difflib import get_close_matches from select import select # third parties componants @@ -18,6 +21,8 @@ except ImportError as err: # local import from .lib.player import Player from .lib.track import Track +from .lib.simastr import SimaStr +from .utils.leven import levenshtein_ratio class PlayerError(Exception): @@ -26,7 +31,7 @@ class PlayerError(Exception): class PlayerCommandError(PlayerError): """Command error""" -PlayerUnHandledError = MPDError +PlayerUnHandledError = MPDError # pylint: disable=C0103 class PlayerClient(Player): """MPC Client @@ -44,9 +49,9 @@ class PlayerClient(Player): find_aa, remove…) """ def __init__(self, host="localhost", port="6600", password=None): - self._host = host - self._port = port - self._password = password + super().__init__() + self._comm = self._args = None + self._mpd = host, port, password self._client = MPDClient() self._client.iterate = True @@ -77,13 +82,15 @@ class PlayerClient(Player): return self._track_format(ans) def _track_format(self, ans): + """ + unicode_obj = ["idle", "listplaylist", "list", "sticker list", + "commands", "notcommands", "tagtypes", "urlhandlers",] + """ # TODO: ain't working for "sticker find" and "sticker list" tracks_listing = ["playlistfind", "playlistid", "playlistinfo", "playlistsearch", "plchanges", "listplaylistinfo", "find", "search", "sticker find",] track_obj = ['currentsong'] - unicode_obj = ["idle", "listplaylist", "list", "sticker list", - "commands", "notcommands", "tagtypes", "urlhandlers",] if self._comm in tracks_listing + track_obj: # pylint: disable=w0142 if isinstance(ans, list): @@ -105,6 +112,62 @@ class PlayerClient(Player): return self.find('artist', artist, 'title', title) return self.find('artist', artist) + def fuzzy_find(self, art): + """ + Controls presence of artist in music library. + Crosschecking artist names with SimaStr objects / difflib / levenshtein + + TODO: proceed crosschecking even when an artist matched !!! + Not because we found "The Doors" as "The Doors" that there is no + remaining entries as "Doors" :/ + not straight forward, need probably heavy refactoring. + """ + matching_artists = list() + artist = SimaStr(art) + all_artists = self.list('artist') + + # Check against the actual string in artist list + if artist.orig in all_artists: + self.log.debug('found exact match for "%s"' % artist) + return [artist] + # Then proceed with fuzzy matching if got nothing + match = get_close_matches(artist.orig, all_artists, 50, 0.73) + if not match: + return [] + self.log.debug('found close match for "%s": %s' % + (artist, '/'.join(match))) + # Does not perform fuzzy matching on short and single word strings + # Only lowercased comparison + if ' ' not in artist.orig and len(artist) < 8: + for fuzz_art in match: + # Regular string comparison SimaStr().lower is regular string + if artist.lower() == fuzz_art.lower(): + matching_artists.append(fuzz_art) + self.log.debug('"%s" matches "%s".' % (fuzz_art, artist)) + return matching_artists + for fuzz_art in match: + # Regular string comparison SimaStr().lower is regular string + if artist.lower() == fuzz_art.lower(): + matching_artists.append(fuzz_art) + self.log.debug('"%s" matches "%s".' % (fuzz_art, artist)) + return matching_artists + # Proceed with levenshtein and SimaStr + leven = levenshtein_ratio(artist.stripped.lower(), + SimaStr(fuzz_art).stripped.lower()) + # SimaStr string __eq__, not regular string comparison here + if artist == fuzz_art: + matching_artists.append(fuzz_art) + self.log.info('"%s" quite probably matches "%s" (SimaStr)' % + (fuzz_art, artist)) + elif leven >= 0.82: # PARAM + matching_artists.append(fuzz_art) + self.log.debug('FZZZ: "%s" should match "%s" (lr=%1.3f)' % + (fuzz_art, artist, leven)) + else: + self.log.debug('FZZZ: "%s" does not match "%s" (lr=%1.3f)' % + (fuzz_art, artist, leven)) + return matching_artists + def find_album(self, artist, album): """ Special wrapper around album search: @@ -157,14 +220,15 @@ class PlayerClient(Player): return self.playlistinfo() def connect(self): + host, port, password = self._mpd self.disconnect() try: - self._client.connect(self._host, self._port) + self._client.connect(host, port) # Catch socket errors except IOError as err: raise PlayerError('Could not connect to "%s:%s": %s' % - (self._host, self._port, err.strerror)) + (host, port, err.strerror)) # Catch all other possible errors # ConnectionError and ProtocolError are always fatal. Others may not @@ -172,23 +236,23 @@ class PlayerClient(Player): # they are instead of ignoring them. except MPDError as err: raise PlayerError('Could not connect to "%s:%s": %s' % - (self._host, self._port, err)) + (host, port, err)) - if self._password: + if password: try: - self._client.password(self._password) + self._client.password(password) # Catch errors with the password command (e.g., wrong password) except CommandError as err: raise PlayerError("Could not connect to '%s': " "password command failed: %s" % - (self._host, err)) + (host, err)) # Catch all other possible errors except (MPDError, IOError) as err: raise PlayerError("Could not connect to '%s': " "error with password command: %s" % - (self._host, err)) + (host, err)) # Controls we have sufficient rights needed_cmds = ['status', 'stats', 'add', 'find', \ 'search', 'currentsong', 'ping'] @@ -199,7 +263,7 @@ class PlayerClient(Player): self.disconnect() raise PlayerError('Could connect to "%s", ' 'but command "%s" not available' % - (self._host, nddcmd)) + (host, nddcmd)) def disconnect(self): # Try to tell MPD we're closing the connection first diff --git a/sima/lib/player.py b/sima/lib/player.py index 9f2910d..726b0b6 100644 --- a/sima/lib/player.py +++ b/sima/lib/player.py @@ -3,6 +3,9 @@ # TODO: # Add decorator to filter through history? +# standart library import +import logging + # local import #from sima.lib.track import Track @@ -16,8 +19,8 @@ class Player(object): """ def __init__(self): - self.state = {} - self.current = {} + super().__init__() + self.log = logging.getLogger('sima') def monitor(self): """Monitor player for change @@ -52,6 +55,17 @@ class Player(object): Returns a list of Track objects """ + def fuzzy_find(self, artist): + """ + Find artists based on a fuzzy search in the media library + >>> bea = player.fuzzy_find('beatles') + >>> print(bea) + >>> ['The Beatles'] + + Returns a list of strings (artist names) + """ + raise NotImplementedError + def disconnect(self): """Closing client connection with the Player """ @@ -64,4 +78,3 @@ class Player(object): # VIM MODLINE # vim: ai ts=4 sw=4 sts=4 expandtab - diff --git a/sima/lib/plugin.py b/sima/lib/plugin.py index d44abaf..cea4448 100644 --- a/sima/lib/plugin.py +++ b/sima/lib/plugin.py @@ -25,6 +25,7 @@ class Plugin(): def __init__(self, daemon): self.log = daemon.log self.__daemon = daemon + self.player = daemon.player self.plugin_conf = None self.__get_config() diff --git a/sima/plugins/addhist.py b/sima/plugins/addhist.py index 70c3f17..e9a36e8 100644 --- a/sima/plugins/addhist.py +++ b/sima/plugins/addhist.py @@ -16,7 +16,6 @@ class History(Plugin): def __init__(self, daemon): Plugin.__init__(self, daemon) self.sdb = daemon.sdb - self.player = daemon.player def shutdown(self): self.log.info('Cleaning database') diff --git a/sima/plugins/lastfm.py b/sima/plugins/lastfm.py index 7f2397a..0bbddbd 100644 --- a/sima/plugins/lastfm.py +++ b/sima/plugins/lastfm.py @@ -7,16 +7,13 @@ Fetching similar artists from last.fm web services import random from collections import deque -from difflib import get_close_matches from hashlib import md5 # third parties componants # local import -from ..utils.leven import levenshtein_ratio from ..lib.plugin import Plugin from ..lib.simafm import SimaFM, XmlFMHTTPError, XmlFMNotFound, XmlFMError -from ..lib.simastr import SimaStr from ..lib.track import Track @@ -46,7 +43,6 @@ class Lastfm(Plugin): Plugin.__init__(self, daemon) self.daemon_conf = daemon.config self.sdb = daemon.sdb - self.player = daemon.player self.history = daemon.short_history ## self.to_add = list() @@ -77,6 +73,7 @@ class Lastfm(Plugin): def _cleanup_cache(self): """Avoid bloated cache """ + # TODO: call cleanup once its dict instance are used somewhere XXX for _ , val in self._cache.items(): if isinstance(val, dict): while len(val) > 150: @@ -116,6 +113,7 @@ class Lastfm(Plugin): 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, @@ -130,62 +128,6 @@ class Lastfm(Plugin): ' / '.join(art_not_in_hist))) return art_not_in_hist - def _cross_check_artist(self, art): - """ - Controls presence of artists in liste in music library. - Crosschecking artist names with SimaStr objects / difflib / levenshtein - - TODO: proceed crosschecking even when an artist matched !!! - Not because we found "The Doors" as "The Doors" that there is no - remaining entries as "Doors" :/ - not straight forward, need probably heavy refactoring. - """ - matching_artists = list() - artist = SimaStr(art) - all_artists = self._cache.get('artists') - - # Check against the actual string in artist list - if artist.orig in all_artists: - self.log.debug('found exact match for "%s"' % artist) - return [artist] - # Then proceed with fuzzy matching if got nothing - match = get_close_matches(artist.orig, all_artists, 50, 0.73) - if not match: - return [] - self.log.debug('found close match for "%s": %s' % - (artist, '/'.join(match))) - # Does not perform fuzzy matching on short and single word strings - # Only lowercased comparison - if ' ' not in artist.orig and len(artist) < 8: - for fuzz_art in match: - # Regular string comparison SimaStr().lower is regular string - if artist.lower() == fuzz_art.lower(): - matching_artists.append(fuzz_art) - self.log.debug('"%s" matches "%s".' % (fuzz_art, artist)) - return matching_artists - for fuzz_art in match: - # Regular string comparison SimaStr().lower is regular string - if artist.lower() == fuzz_art.lower(): - matching_artists.append(fuzz_art) - self.log.debug('"%s" matches "%s".' % (fuzz_art, artist)) - return matching_artists - # Proceed with levenshtein and SimaStr - leven = levenshtein_ratio(artist.stripped.lower(), - SimaStr(fuzz_art).stripped.lower()) - # SimaStr string __eq__, not regular string comparison here - if artist == fuzz_art: - matching_artists.append(fuzz_art) - self.log.info('"%s" quite probably matches "%s" (SimaStr)' % - (fuzz_art, artist)) - elif leven >= 0.82: # PARAM - matching_artists.append(fuzz_art) - self.log.debug('FZZZ: "%s" should match "%s" (lr=%1.3f)' % - (fuzz_art, artist, leven)) - else: - self.log.debug('FZZZ: "%s" does not match "%s" (lr=%1.3f)' % - (fuzz_art, artist, leven)) - return matching_artists - @cache def get_artists_from_player(self, similarities): """ @@ -203,8 +145,8 @@ class Lastfm(Plugin): art_pop, match = similarities.pop() if match < similarity: break - results.extend(self._cross_check_artist(art_pop)) - results and self.log.debug('Similarity: %d%%' % match) + results.extend(self.player.fuzzy_find(art_pop)) + results and self.log.debug('Similarity: %d%%' % match) # pylint: disable=w0106 return results def lfm_similar_artists(self, artist=None): @@ -322,7 +264,7 @@ class Lastfm(Plugin): def callback_need_track(self): self._cleanup_cache() if not self.player.current: - self.log.info('No currently playing track, cannot queue') + self.log.info('Not currently playing track, cannot queue') return None self.queue_mode() candidates = self.to_add diff --git a/sima/plugins/mpd.py b/sima/plugins/mpd.py index 26a07e5..5ad156f 100644 --- a/sima/plugins/mpd.py +++ b/sima/plugins/mpd.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- """ + Deal with MPD options ‑ idle and repeat mode """ # standard library import @@ -31,6 +32,7 @@ class MpdOptions(Plugin): self.log.info('MPD "repeat" mode activated.') self.daemon.enabled = False else: + self.log.debug('enabling queuing (leaving single|repeat mode)') self.daemon.enabled = True def shutdown(self): diff --git a/sima/plugins/randomfallback.py b/sima/plugins/randomfallback.py index 37d57af..4951c80 100644 --- a/sima/plugins/randomfallback.py +++ b/sima/plugins/randomfallback.py @@ -17,7 +17,6 @@ class RandomFallBack(Plugin): def __init__(self, daemon): Plugin.__init__(self, daemon) - self.player = daemon.player self.daemon = daemon ## self.to_add = list() -- 2.39.5