X-Git-Url: http://git.kaliko.me/?a=blobdiff_plain;f=sima%2Fclient.py;h=9724357c3c521acc87689a6143e91910903f33d9;hb=8ce90ac30d1daa9bcf2ced0aa06c8c59302f71ca;hp=e19a5aafdabbdce12ad18e747eb5d8c16e5f83ea;hpb=1cc879f39941fc302f9a841a532c9f749797cca4;p=mpd-sima.git diff --git a/sima/client.py b/sima/client.py index e19a5aa..9724357 100644 --- a/sima/client.py +++ b/sima/client.py @@ -3,11 +3,13 @@ This client is built above python-musicpd a fork of python-mpd """ +# pylint: disable=C0111 -# standart library import +# standard library import +from difflib import get_close_matches from select import select -# third parties componants +# third parties components try: from musicpd import (MPDClient, MPDError, CommandError) except ImportError as err: @@ -18,6 +20,7 @@ except ImportError as err: # local import from .lib.player import Player from .lib.track import Track +from .lib.simastr import SimaStr class PlayerError(Exception): @@ -26,6 +29,8 @@ class PlayerError(Exception): class PlayerCommandError(PlayerError): """Command error""" +PlayerUnHandledError = MPDError # pylint: disable=C0103 + class PlayerClient(Player): """MPC Client @@ -43,22 +48,27 @@ 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 + self._cache = None def __getattr__(self, attr): command = attr wrapper = self._execute return lambda *args: wrapper(command, args) + def __del__(self): + """Avoid hanging sockets""" + self.disconnect() + def _execute(self, command, args): self._write_command(command, args) return self._client_wrapper() - def _write_command(self, command, args=[]): + def _write_command(self, command, args=None): self._comm = command self._args = list() for arg in args: @@ -76,13 +86,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): @@ -91,12 +103,80 @@ class PlayerClient(Player): return Track(**ans) return ans + def __skipped_track(self, old_curr): + if (self.state == 'stop' + or not hasattr(old_curr, 'id') + or not hasattr(self.current, 'id')): + return False + return (self.current.id != old_curr.id) # pylint: disable=no-member + + def _flush_cache(self): + """ + Both flushes and instantiates _cache + """ + if isinstance(self._cache, dict): + self.log.info('Player: Flushing cache!') + else: + self.log.info('Player: Initialising cache!') + self._cache = { + 'artists': None, + } + self._cache['artists'] = frozenset(self._client.list('artist')) + def find_track(self, artist, title=None): #return getattr(self, 'find')('artist', artist, 'title', title) if title: return self.find('artist', artist, 'title', title) return self.find('artist', artist) + def fuzzy_find_artist(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) + + # Check against the actual string in artist list + if artist.orig in self.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, self.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 + # 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)) + else: + self.log.debug('FZZZ: "%s" does not match "%s"' % + (fuzz_art, artist)) + return matching_artists + def find_album(self, artist, album): """ Special wrapper around album search: @@ -107,25 +187,49 @@ class PlayerClient(Player): return alb_art_search return self.find('artist', artist, 'album', album) - def monitor(self): - try: - self._client.send_idle('database', 'playlist', 'player', 'options') - select([self._client], [], [], 60) - return self._client.fetch_idle() - except (MPDError, IOError) as err: - raise PlayerError("Couldn't init idle: %s" % err) + def find_albums(self, artist): + """ + Fetch all albums for "AlbumArtist" == artist + Filter albums returned for "artist" == artist since MPD returns any + album containing at least a single track for artist + """ + albums = set() + albums.update(self.list('album', 'albumartist', artist)) + for album in self.list('album', 'artist', artist): + arts = set([trk.artist for trk in self.find('album', album)]) + if len(arts) < 2: + albums.add(album) + else: + self.log.debug('"{0}" probably not an album of "{1}"'.format( + album, artist) + '({0})'.format('/'.join(arts))) + return albums - def idle(self): + def monitor(self): + curr = self.current try: self._client.send_idle('database', 'playlist', 'player', 'options') select([self._client], [], [], 60) - return self._client.fetch_idle() + ret = self._client.fetch_idle() + if self.__skipped_track(curr): + ret.append('skipped') + if 'database' in ret: + self._flush_cache() + return ret except (MPDError, IOError) as err: raise PlayerError("Couldn't init idle: %s" % err) def remove(self, position=0): self._client.delete(position) + def add(self, track): + """Overriding MPD's add method to accept add signature with a Track + object""" + self._client.add(track.file) + + @property + def artists(self): + return self._cache.get('artists') + @property def state(self): return str(self._client.status().get('state')) @@ -134,6 +238,13 @@ class PlayerClient(Player): def current(self): return self.currentsong() + @property + def queue(self): + plst = self.playlist + plst.reverse() + return [ trk for trk in plst if int(trk.pos) > int(self.current.pos)] + + @property def playlist(self): """ Override deprecated MPD playlist command @@ -141,14 +252,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 @@ -156,24 +268,24 @@ 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)) - # Controls we have sufficient rights for MPD_sima + (host, err)) + # Controls we have sufficient rights needed_cmds = ['status', 'stats', 'add', 'find', \ 'search', 'currentsong', 'ping'] @@ -183,7 +295,8 @@ class PlayerClient(Player): self.disconnect() raise PlayerError('Could connect to "%s", ' 'but command "%s" not available' % - (self._host, nddcmd)) + (host, nddcmd)) + self._flush_cache() def disconnect(self): # Try to tell MPD we're closing the connection first @@ -192,7 +305,6 @@ class PlayerClient(Player): # If that fails, don't worry, just ignore it and disconnect except (MPDError, IOError): pass - try: self._client.disconnect() # Disconnecting failed, so use a new client object instead