]> kaliko git repositories - mpd-sima.git/blobdiff - sima/client.py
Add "Various Artist" filter in albums search
[mpd-sima.git] / sima / client.py
index fba50fdaf468c259febb6af304f85c2c4395ba99..2892d5e4ae82b24a6a244281359237bc97f199c4 100644 (file)
@@ -1,16 +1,34 @@
-# -* coding: utf-8 -*-
+# -*- coding: utf-8 -*-
+# Copyright (c) 2013, 2014 Jack Kaliko <kaliko@azylum.org>
+#
+#  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 <http://www.gnu.org/licenses/>.
+#
+#
 """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,6 +228,33 @@ class PlayerClient(Player):
             return alb_art_search
         return self.find('artist', artist, 'album', album)
 
+    @blacklist(album=True)
+    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 = []
+        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
         try:
@@ -178,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)
@@ -190,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'))
@@ -256,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):