X-Git-Url: http://git.kaliko.me/?a=blobdiff_plain;f=sima%2Fclient.py;h=7c0e5a7c365758d1cced6d0b412b88486a6abbc8;hb=0264d2f9cea7c5e60ac71234ee4f7de78b338850;hp=b3c9591e28b7d949885d05ff68feac0a95a7c28f;hpb=02644ed407ef89aa5fadee5344f62216572c7588;p=mpd-sima.git diff --git a/sima/client.py b/sima/client.py index b3c9591..7c0e5a7 100644 --- a/sima/client.py +++ b/sima/client.py @@ -1,4 +1,22 @@ -# -* coding: utf-8 -*- +# -*- 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 . +# +# """MPD client for Sima This client is built above python-musicpd a fork of python-mpd @@ -21,8 +39,9 @@ except ImportError as err: # local import from .lib.player import Player from .lib.track import Track -from .lib.album import Album +from .lib.meta import Album from .lib.simastr import SimaStr +from .utils.leven import levenshtein_ratio class PlayerError(Exception): @@ -47,17 +66,23 @@ def blacklist(artist=False, album=False, track=False): #bl_getter = next(fn for fn, bl in zip(bl_fun, boolgen) if bl is True) bl_getter = next(dropwhile(lambda _: not next(boolgen), bl_fun)) #cls.log.debug('using {0} as bl filter'.format(bl_getter.__name__)) - results = func(*args, **kwargs) - for elem in results: + results = list() + for elem in func(*args, **kwargs): if bl_getter(elem, add_not=True): - cls.log.info('Blacklisted: {0}'.format(elem)) - results.remove(elem) + cls.log.debug('Blacklisted "{0}"'.format(elem)) + continue + if track and cls.database.get_bl_album(elem, add_not=True): + # filter album as well in track mode + # (artist have already been) + cls.log.debug('Blacklisted alb. "{0.album}"'.format(elem)) + continue + results.append(elem) return results return wrapper return decorated class PlayerClient(Player): - """MPC Client + """MPD Client From python-musicpd: _fetch_nothing … _fetch_item single str @@ -69,9 +94,9 @@ class PlayerClient(Player): _fetch_songs list of dict, especially tracks _fetch_plugins, TODO: handle exception in command not going through _client_wrapper() (ie. - find_aa, remove…) + remove…) """ - database = None # sima database (history, blaclist) + database = None # sima database (history, blacklist) def __init__(self, host="localhost", port="6600", password=None): super().__init__() @@ -130,7 +155,7 @@ class PlayerClient(Player): 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 + return self.current.id != old_curr.id # pylint: disable=no-member def _flush_cache(self): """ @@ -145,12 +170,36 @@ class PlayerClient(Player): } self._cache['artists'] = frozenset(self._client.list('artist')) + @blacklist(track=True) 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) + @blacklist(track=True) + def fuzzy_find_track(self, artist, title): + # Retrieve all tracks from artist + all_tracks = self.find('artist', artist) + # Get all titles (filter missing titles set to 'None') + all_artist_titles = frozenset([tr.title for tr in all_tracks + if tr.title is not None]) + match = get_close_matches(title, all_artist_titles, 50, 0.78) + if not match: + return [] + for title_ in match: + leven = levenshtein_ratio(title.lower(), title_.lower()) + if leven == 1: + pass + elif leven >= 0.79: # PARAM + self.log.debug('title: "%s" should match "%s" (lr=%1.3f)' % + (title_, title, leven)) + else: + self.log.debug('title: "%s" does not match "%s" (lr=%1.3f)' % + (title_, title, leven)) + return [] + return self.find('artist', artist, 'title', title_) + @blacklist(artist=True) def fuzzy_find_artist(self, art): """ @@ -168,7 +217,7 @@ class PlayerClient(Player): # 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] + return [artist.orig] # Then proceed with fuzzy matching if got nothing match = get_close_matches(artist.orig, self.artists, 50, 0.73) if not match: @@ -223,11 +272,15 @@ class PlayerClient(Player): if album not in albums: albums.append(Album(name=album, **kwalbart)) for album in self.list('album', 'artist', artist): - arts = set([trk.artist for trk in self.find('album', album)]) - if len(arts) < 2: # TODO: better heuristic, use a ratio instead + album_trks = [trk for trk in self.find('album', album)] + if 'Various Artists' in [tr.albumartist for tr in album_trks]: + self.log.debug('Discarding {0} ("Various Artists" set)'.format(album)) + continue + arts = set([trk.artist for trk in album_trks]) + if len(set(arts)) < 2: # TODO: better heuristic, use a ratio instead if album not in albums: albums.append(Album(name=album, albumartist=artist)) - elif (album and album not in albums): + elif album and album not in albums: self.log.debug('"{0}" probably not an album of "{1}"'.format( album, artist) + '({0})'.format('/'.join(arts))) return albums @@ -235,9 +288,9 @@ class PlayerClient(Player): def monitor(self): curr = self.current try: - self._client.send_idle('database', 'playlist', 'player', 'options') + self.send_idle('database', 'playlist', 'player', 'options') select([self._client], [], [], 60) - ret = self._client.fetch_idle() + ret = self.fetch_idle() if self.__skipped_track(curr): ret.append('skipped') if 'database' in ret: @@ -246,8 +299,16 @@ class PlayerClient(Player): except (MPDError, IOError) as err: raise PlayerError("Couldn't init idle: %s" % err) + def clean(self): + """Clean blocking event (idle) and pending commands + """ + if 'idle' in self._client._pending: + self._client.noidle() + elif self._client._pending: + self.log.warning('pending commands: {}'.format(self._client._pending)) + def remove(self, position=0): - self._client.delete(position) + self.delete(position) def add(self, track): """Overriding MPD's add method to accept add signature with a Track @@ -260,7 +321,7 @@ class PlayerClient(Player): @property def state(self): - return str(self._client.status().get('state')) + return str(self.status().get('state')) @property def current(self): @@ -270,7 +331,7 @@ class PlayerClient(Player): def queue(self): plst = self.playlist plst.reverse() - return [ trk for trk in plst if int(trk.pos) > int(self.current.pos)] + return [trk for trk in plst if int(trk.pos) > int(self.current.pos)] @property def playlist(self): @@ -329,6 +390,7 @@ class PlayerClient(Player): def disconnect(self): # Try to tell MPD we're closing the connection first try: + self._client.noidle() self._client.close() # If that fails, don't worry, just ignore it and disconnect except (MPDError, IOError):