X-Git-Url: https://git.kaliko.me/?a=blobdiff_plain;f=sima%2Fclient.py;h=2892d5e4ae82b24a6a244281359237bc97f199c4;hb=7a910ce647cc646dc7ca8adbc69e1f0e725761fc;hp=80c16e530875a2b6fd68fcff717568bbb339e96d;hpb=ab9952e0258426133f6bb0fe6395ec7464dfdd33;p=mpd-sima.git diff --git a/sima/client.py b/sima/client.py index 80c16e5..2892d5e 100644 --- a/sima/client.py +++ b/sima/client.py @@ -1,16 +1,34 @@ -# -* 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 """ # pylint: disable=C0111 -# standart library import - +# standard library import from difflib import get_close_matches +from itertools import dropwhile from select import select -# third parties componants +# third parties components try: from musicpd import (MPDClient, MPDError, CommandError) except ImportError as err: @@ -21,6 +39,7 @@ except ImportError as err: # local import from .lib.player import Player from .lib.track import Track +from .lib.meta import Album from .lib.simastr import SimaStr @@ -32,8 +51,31 @@ class PlayerCommandError(PlayerError): PlayerUnHandledError = MPDError # pylint: disable=C0103 + +def blacklist(artist=False, album=False, track=False): + #pylint: disable=C0111,W0212 + field = (artist, album, track) + def decorated(func): + def wrapper(*args, **kwargs): + cls = args[0] + boolgen = (bl for bl in field) + bl_fun = (cls.database.get_bl_artist, + cls.database.get_bl_album, + cls.database.get_bl_track,) + #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: + if bl_getter(elem, add_not=True): + cls.log.info('Blacklisted: {0}'.format(elem)) + results.remove(elem) + return results + return wrapper + return decorated + class PlayerClient(Player): - """MPC Client + """MPD Client From python-musicpd: _fetch_nothing … _fetch_item single str @@ -45,14 +87,17 @@ 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) + def __init__(self, host="localhost", port="6600", password=None): 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 @@ -63,7 +108,7 @@ class PlayerClient(Player): 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: @@ -105,13 +150,27 @@ class PlayerClient(Player): 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(self, art): + @blacklist(artist=True) + def fuzzy_find_artist(self, art): """ Controls presence of artist in music library. Crosschecking artist names with SimaStr objects / difflib / levenshtein @@ -123,14 +182,13 @@ class PlayerClient(Player): """ 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: + 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, all_artists, 50, 0.73) + match = get_close_matches(artist.orig, self.artists, 50, 0.73) if not match: return [] self.log.debug('found close match for "%s": %s' % @@ -170,15 +228,32 @@ class PlayerClient(Player): return alb_art_search return self.find('artist', artist, 'album', album) + @blacklist(album=True) def find_albums(self, artist): """ - Special wrapper around album search: - Album lookup is made through AlbumArtist/Album instead of Artist/Album + 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 """ - alb_art_search = self.list('album', 'albumartist', artist,) - if alb_art_search: - return alb_art_search - return self.list('album', 'artist', artist) + albums = [] + kwalbart = {'albumartist':artist, 'artist':artist} + for album in self.list('album', 'albumartist', artist): + if album not in albums: + albums.append(Album(name=album, **kwalbart)) + for album in self.list('album', 'artist', artist): + album_trks = [trk for trk in self.find('album', album)] + # TODO: add a VA filter option + 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): + self.log.debug('"{0}" probably not an album of "{1}"'.format( + album, artist) + '({0})'.format('/'.join(arts))) + return albums def monitor(self): curr = self.current @@ -188,6 +263,8 @@ class PlayerClient(Player): 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) @@ -200,6 +277,10 @@ class PlayerClient(Player): 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')) @@ -266,10 +347,12 @@ class PlayerClient(Player): raise PlayerError('Could connect to "%s", ' 'but command "%s" not available' % (host, nddcmd)) + self._flush_cache() 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):