]> kaliko git repositories - mpd-sima.git/commitdiff
MPD Client refactoring
authorkaliko <kaliko@azylum.org>
Sun, 10 May 2020 11:26:06 +0000 (13:26 +0200)
committerkaliko <kaliko@azylum.org>
Sun, 10 May 2020 11:26:06 +0000 (13:26 +0200)
doc/Changelog
sima/core.py
sima/info.py
sima/lib/player.py [deleted file]
sima/lib/webserv.py
sima/mpdclient.py [moved from sima/client.py with 52% similarity]
sima/plugins/core/uniq.py
sima/plugins/internal/crop.py
sima/plugins/internal/random.py

index f0126f8db91253ac67b136f0aaec0570d05c3628..b2774c1036daed9fe6bad6c63b948323dfab89cd 100644 (file)
@@ -1,5 +1,6 @@
-MPD_sima v0.15.4 UNRELEASED
+MPD_sima v0.16.0 UNRELEASED
 
 
+ * Major MPD client refactoring
  * Refactored random plugin
  * Fixed bug in MPD client reconnection
 
  * Refactored random plugin
  * Fixed bug in MPD client reconnection
 
index ead1ba8e9563d1e2ad61465854b508e192c596e3..62d123ae9e1afa626a144568b57319501e87e048 100644 (file)
@@ -25,8 +25,8 @@ import time
 from collections import deque
 from logging import getLogger
 
 from collections import deque
 from logging import getLogger
 
-from .client import PlayerClient
-from .client import PlayerError, PlayerUnHandledError
+from .mpdclient import MPD as PlayerClient
+from .mpdclient import PlayerError, MPDError
 from .lib.simadb import SimaDB
 from .lib.daemon import Daemon
 from .utils.utils import SigHup
 from .lib.simadb import SimaDB
 from .lib.daemon import Daemon
 from .utils.utils import SigHup
@@ -46,16 +46,9 @@ class Sima(Daemon):
         self.log = getLogger('sima')
         self._plugins = list()
         self._core_plugins = list()
         self.log = getLogger('sima')
         self._plugins = list()
         self._core_plugins = list()
-        self.player = self.__get_player()  # Player client
+        self.player = PlayerClient(self)  # Player client
         self.short_history = deque(maxlen=60)
 
         self.short_history = deque(maxlen=60)
 
-    def __get_player(self):
-        """Instanciate the player"""
-        host = self.config.get('MPD', 'host')
-        port = self.config.get('MPD', 'port')
-        pswd = self.config.get('MPD', 'password', fallback=None)
-        return PlayerClient(host, port, pswd)
-
     def add_history(self):
         """Handle local, in memory, short history"""
         self.short_history.appendleft(self.player.current)
     def add_history(self):
         """Handle local, in memory, short history"""
         self.short_history.appendleft(self.player.current)
@@ -132,7 +125,7 @@ class Sima(Daemon):
             except PlayerError as err:
                 self.log.debug(err)
                 continue
             except PlayerError as err:
                 self.log.debug(err)
                 continue
-            except PlayerUnHandledError as err:
+            except MPDError as err:
                 #TODO: unhandled Player exceptions
                 self.log.warning('Unhandled player exception: %s', err)
             self.log.info('Got reconnected')
                 #TODO: unhandled Player exceptions
                 self.log.warning('Unhandled player exception: %s', err)
             self.log.info('Got reconnected')
@@ -164,22 +157,16 @@ class Sima(Daemon):
         """
         """
         try:
         """
         """
         try:
-            self.log.info('Connecting MPD: {0}:{1}'.format(*self.player._mpd))
+            self.log.info('Connecting MPD: %(host)s:%(port)s', self.config['MPD'])
             self.player.connect()
             self.foreach_plugin('start')
             self.player.connect()
             self.foreach_plugin('start')
-        except (PlayerError, PlayerUnHandledError) as err:
+        except (PlayerError, MPDError) as err:
             self.log.warning('Player: %s', err)
             self.reconnect_player()
         while 42:
             try:
                 self.loop()
             self.log.warning('Player: %s', err)
             self.reconnect_player()
         while 42:
             try:
                 self.loop()
-            except PlayerUnHandledError as err:
-                #TODO: unhandled Player exceptions
-                self.log.warning('Unhandled player exception: %s', err)
-                del self.player
-                self.player = self.__get_player()
-                time.sleep(5)
-            except PlayerError as err:
+            except (PlayerError, MPDError) as err:
                 self.log.warning('Player error: %s', err)
                 self.reconnect_player()
                 del self.changed
                 self.log.warning('Player error: %s', err)
                 self.reconnect_player()
                 del self.changed
@@ -187,7 +174,7 @@ class Sima(Daemon):
     def loop(self):
         """Dispatching callbacks to plugins
         """
     def loop(self):
         """Dispatching callbacks to plugins
         """
-        # hanging here untill a monitored event is raised in the player
+        # hanging here until a monitored event is raised in the player
         if getattr(self, 'changed', False): # first iteration exception
             self.changed = self.player.monitor()
         else:  # first iteration goes through else
         if getattr(self, 'changed', False): # first iteration exception
             self.changed = self.player.monitor()
         else:  # first iteration goes through else
index aecdc0f45482e3350f108cc596cd5b490748e871..947d1e579991a3a81095f71b75f6df5dfd9855f3 100644 (file)
@@ -12,7 +12,7 @@ short.
 """
 
 
 """
 
 
-__version__ = '0.15.4'
+__version__ = '0.16.0'
 __author__ = 'kaliko jack'
 __email__ = 'kaliko@azylum.org'
 __url__ = 'git://git.kaliko.me/sima.git'
 __author__ = 'kaliko jack'
 __email__ = 'kaliko@azylum.org'
 __url__ = 'git://git.kaliko.me/sima.git'
diff --git a/sima/lib/player.py b/sima/lib/player.py
deleted file mode 100644 (file)
index 1d1f693..0000000
+++ /dev/null
@@ -1,189 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright (c) 2009-2014 Jack Kaliko <jack@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/>.
-#
-#
-
-# TODO:
-# Add decorator to filter through history?
-
-# standard library import
-import logging
-from itertools import dropwhile
-
-# local import
-
-def blacklist(artist=False, album=False, track=False):
-    #pylint: disable=C0111,W0212
-    field = (album, track)
-    def decorated(func):
-        def wrapper(*args, **kwargs):
-            if not args[0].database:
-                return func(*args, **kwargs)
-            cls = args[0]
-            boolgen = (bl for bl in field)
-            bl_fun = (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 = list()
-            for elem in func(*args, **kwargs):
-                if bl_getter(elem, add_not=True):
-                    #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 Player(object):
-    """Player interface to inherit from.
-
-    When querying player music library for tracks, Player instance *must* return
-    Track objects (usually a list of them)
-
-    Player instance should expose the following immutable attributes:
-        * artists
-        * state
-        * current
-        * queue
-        * playlist
-    """
-
-    def __init__(self):
-        super().__init__()
-        self.log = logging.getLogger('sima')
-
-    def monitor(self):
-        """Monitor player for change
-        Returns :
-            * database  player media library has changed
-            * playlist  playlist modified
-            * options   player options changed: repeat mode, etc…
-            * player    player state changed: paused, stopped, skip track…
-        """
-        raise NotImplementedError
-
-    def clean(self):
-        """Any cleanup necessary"""
-        pass
-
-    def remove(self, position=0):
-        """Removes the oldest element of the playlist (index 0)
-        """
-        raise NotImplementedError
-
-    def find_track(self, artist, title=None):
-        """
-        Find tracks for a specific artist or filtering with a track title
-            >>> player.find_track(Artist('The Beatles'))
-            >>> player.find_track(Artist('Nirvana'), title='Smells Like Teen Spirit')
-
-        Returns a list of Track objects
-        """
-        raise NotImplementedError
-
-    def find_album(self, artist, album):
-        """
-        Find tracks by track's album name
-            >>> player.find_album('Nirvana', 'Nevermind')
-
-        Returns a list of Track objects
-        """
-        raise NotImplementedError
-
-    def search_albums(self, artist):
-        """
-        Find albums by artist's name
-            >>> art = Artist(name='Nirvana')
-            >>> player.search_albums(art)
-
-        Returns a list of string objects
-        """
-        raise NotImplementedError
-
-    def search_artist(self, artist):
-        """
-        Search artists based on a fuzzy search in the media library
-            >>> art = Artist(name='the beatles', mbid=<UUID4>) # mbid optional
-            >>> bea = player.search_artist(art)
-            >>> print(bea.names)
-            >>> ['The Beatles', 'Beatles', 'the beatles']
-
-        Returns an Artist object
-        """
-        raise NotImplementedError
-
-    def disconnect(self):
-        """Closing client connection with the Player
-        """
-        raise NotImplementedError
-
-    def connect(self):
-        """Connect client to the Player
-        """
-        raise NotImplementedError
-
-    @property
-    def artists(self):
-        #pylint: disable=C0111
-        raise NotImplementedError
-
-    @property
-    def state(self):
-        """Returns (play|stop|pause)"""
-        #pylint: disable=C0111
-        raise NotImplementedError
-
-    @property
-    def playmode(self):
-        """Returns a mapping
-        {
-            'repeat': boolean,
-            'random': boolean,
-            'single': boolean,
-            'consume': boolean,
-            …
-            }
-        """
-        #pylint: disable=C0111
-        raise NotImplementedError
-
-    @property
-    def current(self):
-        #pylint: disable=C0111
-        raise NotImplementedError
-
-    @property
-    def queue(self):
-        #pylint: disable=C0111
-        raise NotImplementedError
-
-    @property
-    def playlist(self):
-        #pylint: disable=C0111
-        raise NotImplementedError
-
-# VIM MODLINE
-# vim: ai ts=4 sw=4 sts=4 expandtab
index 9800b92f32beca530b79502d13f65f5d45fceef0..0304dd639c1cd9866abb64026720e49a8ef3aec8 100644 (file)
@@ -33,7 +33,7 @@ from hashlib import md5
 # local import
 from .plugin import Plugin
 from .track import Track
 # local import
 from .plugin import Plugin
 from .track import Track
-from .meta import Artist, MetaContainer
+from .meta import Artist, Album, MetaContainer
 from ..utils.utils import WSError, WSNotFound
 
 def cache(func):
 from ..utils.utils import WSError, WSNotFound
 
 def cache(func):
@@ -345,7 +345,8 @@ class WebService(Plugin):
                 continue
             self.log.info('%s album candidate: %s - %s', self.ws.name, artist, album_to_queue)
             nb_album_add += 1
                 continue
             self.log.info('%s album candidate: %s - %s', self.ws.name, artist, album_to_queue)
             nb_album_add += 1
-            candidates = self.player.find_album(artist, album_to_queue)
+            candidates = self.player.find_tracks(Album(name=album_to_queue,
+                                                       artist=artist))
             if self.plugin_conf.getboolean('shuffle_album'):
                 random.shuffle(candidates)
             # this allows to select a maximum number of track from the album
             if self.plugin_conf.getboolean('shuffle_album'):
                 random.shuffle(candidates)
             # this allows to select a maximum number of track from the album
@@ -373,7 +374,7 @@ class WebService(Plugin):
             except WSError as err:
                 self.log.warning('%s: %s', self.ws.name, err)
             for trk in titles:
             except WSError as err:
                 self.log.warning('%s: %s', self.ws.name, err)
             for trk in titles:
-                found = self.player.fuzzy_find_track(artist, trk.title)
+                found = self.player.search_track(artist, trk.title)
                 random.shuffle(found)
                 if found:
                     self.log.debug('%s', found[0])
                 random.shuffle(found)
                 if found:
                     self.log.debug('%s', found[0])
similarity index 52%
rename from sima/client.py
rename to sima/mpdclient.py
index d10a3fe24d844c327bab2848a1d35ef86ae86789..d13c8cb417e44662e37bed1aef7a3a56db3fb9a1 100644 (file)
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
 # -*- coding: utf-8 -*-
-# Copyright (c) 2013, 2014 Jack Kaliko <kaliko@azylum.org>
+# Copyright (c) 2009-2020 kaliko <kaliko@azylum.org>
 #
 #  This file is part of sima
 #
 #
 #  This file is part of sima
 #
 #
 #  You should have received a copy of the GNU General Public License
 #  along with sima.  If not, see <http://www.gnu.org/licenses/>.
 #
 #  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
 
 # standard library import
 from difflib import get_close_matches
 
 # standard library import
 from difflib import get_close_matches
-from select import select
+from functools import wraps
+from itertools import dropwhile
+
+# external module
+from musicpd import MPDClient, MPDError
 
 
-# third parties components
-try:
-    from musicpd import (MPDClient, MPDError, CommandError)
-except ImportError as err:
-    from sys import exit as sexit
-    print('ERROR: missing python-musicpd?\n{0}'.format(err))
-    sexit(1)
 
 # local import
 
 # local import
-from .lib.simastr import SimaStr
-from .lib.player import Player, blacklist
+from .lib.meta import Artist, Album
 from .lib.track import Track
 from .lib.track import Track
-from .lib.meta import Album, Artist
+from .lib.simastr import SimaStr
 from .utils.leven import levenshtein_ratio
 
 
 class PlayerError(Exception):
     """Fatal error in poller."""
 
 from .utils.leven import levenshtein_ratio
 
 
 class PlayerError(Exception):
     """Fatal error in poller."""
 
-class PlayerCommandError(PlayerError):
-    """Command error"""
-
-PlayerUnHandledError = MPDError  # pylint: disable=C0103
 
 
+# Some decorators
 def bl_artist(func):
     def wrapper(*args, **kwargs):
         cls = args[0]
 def bl_artist(func):
     def wrapper(*args, **kwargs):
         cls = args[0]
-        if not args[0].database:
+        if not cls.database:
             return func(*args, **kwargs)
         result = func(*args, **kwargs)
         if not result:
             return func(*args, **kwargs)
         result = func(*args, **kwargs)
         if not result:
@@ -73,83 +59,141 @@ def bl_artist(func):
         return resp
     return wrapper
 
         return resp
     return wrapper
 
+def tracks_wrapper(func):
+    @wraps(func)
+    def wrapper(*args, **kwargs):
+        ret = func(*args, **kwargs)
+        if isinstance(ret, dict):
+            return Track(**ret)
+        elif isinstance(ret, list):
+            return [Track(**t) for t in ret]
+    return wrapper
+# / decorators
+
+def blacklist(artist=False, album=False, track=False):
+    #pylint: disable=C0111,W0212
+    field = (album, track)
+    def decorated(func):
+        def wrapper(*args, **kwargs):
+            if not args[0].database:
+                return func(*args, **kwargs)
+            cls = args[0]
+            boolgen = (bl for bl in field)
+            bl_fun = (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 = list()
+            for elem in func(*args, **kwargs):
+                if bl_getter(elem, add_not=True):
+                    #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):
-    """MPD Client
-    From python-musicpd:
-        _fetch_nothing  …
-        _fetch_item     single str
-        _fetch_object   single dict
-        _fetch_list     list of str
-        _fetch_playlist list of str
-        _fetch_changes  list of dict
-        _fetch_database list of dict
-        _fetch_songs    list of dict, especially tracks
-        _fetch_plugins,
-    TODO: handle exception in command not going through _client_wrapper() (ie.
-          remove…)
+class MPD(MPDClient):
     """
     """
-    database = None  # sima database (history, blacklist)
+    Player instance inheriting from MPDClient (python-musicpd).
 
 
-    def __init__(self, host="localhost", port="6600", password=None):
+    Some methods are overridden to format objects as sima.lib.Track for
+    instance, other are calling parent class directly through super().
+    cf. MPD.__getattr__
+
+    .. note::
+
+        * find methods are looking for exact match of the object provided attributes in MPD music library
+        * search methods are looking for exact match + fuzzy match.
+    """
+    needed_cmds = ['status', 'stats', 'add', 'find',
+                   'search', 'currentsong', 'ping']
+    database = None
+
+    def __init__(self, daemon):
         super().__init__()
         super().__init__()
-        self._comm = self._args = None
-        self._mpd = host, port, password
-        self._client = MPDClient()
-        self._client.iterate = True
+        self.use_mbid = True
+        self.daemon = daemon
+        self.log = daemon.log
+        self.config = self.daemon.config['MPD']
         self._cache = None
 
         self._cache = None
 
-    def __getattr__(self, attr):
-        command = attr
-        wrapper = self._execute
-        return lambda *args: wrapper(command, args)
-
-    def _execute(self, command, args):
-        self._write_command(command, args)
-        return self._client_wrapper()
+    # ######### Overriding MPDClient ###########
+    def __getattr__(self, cmd):
+        """Wrapper around MPDClient calls for abstract overriding"""
+        track_wrapped = {
+                         'currentsong',
+                         'find',
+                         'playlistinfo',
+                         }
+        if cmd in track_wrapped:
+            return tracks_wrapper(super().__getattr__(cmd))
+        return super().__getattr__(cmd)
 
 
-    def _write_command(self, command, args=None):
-        self._comm = command
-        self._args = list()
-        for arg in args:
-            self._args.append(arg)
+    def disconnect(self):
+        """Overriding explicitly MPDClient.disconnect()"""
+        if self._sock:
+            super().disconnect()
 
 
-    def _client_wrapper(self):
-        func = self._client.__getattr__(self._comm)
+    def connect(self):
+        """Overriding explicitly MPDClient.connect()"""
+        # host, port, password
+        host = self.config.get('host')
+        port = self.config.get('port')
+        password = self.config.get('password', fallback=None)
+        self.disconnect()
         try:
         try:
-            ans = func(*self._args)
-        # WARNING: MPDError is an ancestor class of # CommandError
-        except CommandError as err:
-            raise PlayerCommandError('MPD command error: %s' % err)
-        except (MPDError, IOError) as err:
-            raise PlayerError(err)
-        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']
-        if self._comm in tracks_listing + track_obj:
-            if isinstance(ans, list):
-                return [Track(**track) for track in ans]
-            elif isinstance(ans, dict):
-                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
+            super().connect(host, port)
+        # Catch socket errors
+        except IOError as err:
+            raise PlayerError('Could not connect to "%s:%s": %s' %
+                              (host, port, err.strerror))
+        # Catch all other possible errors
+        # ConnectionError and ProtocolError are always fatal.  Others may not
+        # be, but we don't know how to handle them here, so treat them as if
+        # they are instead of ignoring them.
+        except MPDError as err:
+            raise PlayerError('Could not connect to "%s:%s": %s' %
+                              (host, port, err))
+        if password:
+            try:
+                self.password(password)
+            except (MPDError, IOError) as err:
+                raise PlayerError("Could not connect to '%s': %s", (host, err))
+        # Controls we have sufficient rights
+        available_cmd = self.commands()
+        for cmd in MPD.needed_cmds:
+            if cmd not in available_cmd:
+                self.disconnect()
+                raise PlayerError('Could connect to "%s", '
+                                  'but command "%s" not available' %
+                                  (host, cmd))
+        # Controls use of MusicBrainzIdentifier
+        # TODO: Use config instead of Artist object attibute?
+        if self.use_mbid:
+            tt = self.tagtypes()
+            if 'MUSICBRAINZ_ARTISTID' not in tt:
+                self.log.warning('Use of MusicBrainzIdentifier is set but MPD is '
+                                 'not providing related metadata')
+                self.log.info(tt)
+                self.log.warning('Disabling MusicBrainzIdentifier')
+                self.use_mbid = False
+            else:
+                self.log.debug('Available metadata: %s', tt)  # pylint: disable=no-member
+        else:
+            self.log.warning('Use of MusicBrainzIdentifier disabled!')
+            self.log.info('Consider using MusicBrainzIdentifier for your music library')
+        self._reset_cache()
+    # ######### / Overriding MPDClient #########
 
 
-    def _flush_cache(self):
+    def _reset_cache(self):
         """
         Both flushes and instantiates _cache
         """
         """
         Both flushes and instantiates _cache
         """
@@ -158,42 +202,160 @@ class PlayerClient(Player):
         else:
             self.log.info('Player: Initialising cache!')
         self._cache = {'artists': frozenset(),
         else:
             self.log.info('Player: Initialising cache!')
         self._cache = {'artists': frozenset(),
-                       'nombid_artists': frozenset(),}
-        self._cache['artists'] = frozenset(filter(None, self._execute('list', ['artist'])))
+                       'nombid_artists': frozenset()}
+        self._cache['artists'] = frozenset(filter(None, self.list('artist')))
         if Artist.use_mbid:
         if Artist.use_mbid:
-            self._cache['nombid_artists'] = frozenset(filter(None, self._execute('list', ['artist', 'musicbrainz_artistid', ''])))
+            self._cache['nombid_artists'] = frozenset(filter(None, self.list('artist', 'musicbrainz_artistid', '')))
 
 
-    @blacklist(track=True)
-    def find_track(self, artist, title=None):
+    def _skipped_track(self, previous):
+        if (self.state == 'stop'
+                or not hasattr(previous, 'id')
+                or not hasattr(self.current, 'id')):
+            return False
+        return self.current.id != previous.id  # pylint: disable=no-member
+
+    def monitor(self):
+        """OLD socket Idler
+        Monitor player for change
+        Returns a list a events among:
+
+            * database  player media library has changed
+            * playlist  playlist modified
+            * options   player options changed: repeat mode, etc…
+            * player    player state changed: paused, stopped, skip track…
+            * skipped   current track skipped
+        """
+        curr = self.current
+        try:
+            ret = self.idle('database', 'playlist', 'player', 'options')
+        except (MPDError, IOError) as err:
+            raise PlayerError("Couldn't init idle: %s" % err)
+        if self._skipped_track(curr):
+            ret.append('skipped')
+        if 'database' in ret:
+            self._flush_cache()
+        return ret
+
+    def clean(self):
+        """Clean blocking event (idle) and pending commands
+        """
+        if 'idle' in self._pending:
+            self.noidle()
+        elif self._pending:
+            self.log.warning('pending commands: %s', self._pending)
+
+    def add(self, payload):
+        """Overriding MPD's add method to accept Track objects"""
+        if isinstance(payload, Track):
+            super().__getattr__('add')(payload.file)
+        elif isinstance(payload, list):
+            for tr in payload:  # TODO: use send command here
+                self.add(tr)
+        else:
+            self.log.error('Cannot add %s', payload)
+
+    # ######### Properties #####################
+    @property
+    def current(self):
+        return self.currentsong()
+
+    @property
+    def playlist(self):
+        """
+        Override deprecated MPD playlist command
+        """
+        return self.playlistinfo()
+
+    @property
+    def playmode(self):
+        plm = {'repeat': None, 'single': None,
+               'random': None, 'consume': None, }
+        for key, val in self.status().items():
+            if key in plm.keys():
+                plm.update({key: bool(int(val))})
+        return plm
+
+    @property
+    def queue(self):
+        plst = self.playlist
+        curr_position = int(self.current.pos)
+        plst.reverse()
+        return [trk for trk in plst if int(trk.pos) > curr_position]
+
+    @property
+    def state(self):
+        """Returns (play|stop|pause)"""
+        return str(self.status().get('state'))
+    # ######### / Properties ###################
+
+# #### find_tracks ####
+    def find_album(self, artist, album_name):
+        self.log.warning('update call to find_album→find_tracks(<Album object>)')
+        return self.find_tracks(Album(name=album_name, artist=artist))
+
+    def find_track(self, *args, **kwargs):
+        self.log.warning('update call to find_track→find_tracks')
+        return self.find_tracks(*args, **kwargs)
+
+    def find_tracks(self, what):
+        """Find tracks for a specific artist or album
+            >>> player.find_tracks(Artist('Nirvana'))
+            >>> player.find_tracks(Album('In Utero', artist=(Artist('Nirvana'))
+
+        :param Artist,Album what: Artist or Album to fetch track from
+
+        Returns a list of :py:obj:Track objects
+        """
+        if isinstance(what, Artist):
+            return self._find_art(what)
+        elif isinstance(what, Album):
+            return self._find_alb(what)
+        elif isinstance(what, str):
+            return self.find_tracks(Artist(name=what))
+
+    def _find_art(self, artist):
         tracks = set()
         if artist.mbid:
         tracks = set()
         if artist.mbid:
-            if title:
-                tracks |= set(self.find('musicbrainz_artistid', artist.mbid,
-                                        'title', title))
-            else:
-                tracks |= set(self.find('musicbrainz_artistid', artist.mbid))
+            tracks |= set(self.find('musicbrainz_artistid', artist.mbid))
         for name in artist.names:
         for name in artist.names:
-            if title:
-                tracks |= set(self.find('artist', name, 'title', title))
-            else:
-                tracks |= set(self.find('artist', name))
+            tracks |= set(self.find('artist', name))
         return list(tracks)
 
         return list(tracks)
 
+    def _find_alb(self, album):
+        albums = set()
+        if album.mbid and self.use_mbid:
+            filt = f'(MUSICBRAINZ_ALBUMID == {album.mbid})'
+            albums |= set(self.find(filt))
+        # Now look for album with no MusicBrainzIdentifier
+        if album.artist.mbid and self.use_mbid:  # Use album artist MBID if possible
+            filt = f"((MUSICBRAINZ_ALBUMARTISTID == '{album.artist.mbid}') AND (album == '{album!s}'))"
+            albums |= set(self.find(filt))
+        if not albums:  # Falls back to albumartist/album name
+            filt = f"((albumartist == '{album.artist!s}') AND (album == '{album!s}'))"
+            albums |= set(self.find(filt))
+        if not albums:  # Falls back to artist/album name
+            filt = f"((artist == '{album.artist!s}') AND (album == '{album!s}'))"
+            albums |= set(self.find(filt))
+        return list(albums)
+# #### / find_tracks ##
+
+# #### Search Methods #####
     @bl_artist
     def search_artist(self, artist):
         """
         Search artists based on a fuzzy search in the media library
             >>> art = Artist(name='the beatles', mbid=<UUID4>) # mbid optional
     @bl_artist
     def search_artist(self, artist):
         """
         Search artists based on a fuzzy search in the media library
             >>> art = Artist(name='the beatles', mbid=<UUID4>) # mbid optional
-            >>> bea = player.search_artist(art)
+            >>> bea = player.search_artist(art)c
             >>> print(bea.names)
             >>> ['The Beatles', 'Beatles', 'the beatles']
 
         Returns an Artist object
             >>> print(bea.names)
             >>> ['The Beatles', 'Beatles', 'the beatles']
 
         Returns an Artist object
+        TODO: Re-use find method here!!!
         """
         found = False
         if artist.mbid:
             # look for exact search w/ musicbrainz_artistid
         """
         found = False
         if artist.mbid:
             # look for exact search w/ musicbrainz_artistid
-            exact_m = self._execute('list', ['artist', 'musicbrainz_artistid', artist.mbid])
+            exact_m = self.list('artist', 'musicbrainz_artistid', artist.mbid)
             if exact_m:
                 _ = [artist.add_alias(name) for name in exact_m]
                 found = True
             if exact_m:
                 _ = [artist.add_alias(name) for name in exact_m]
                 found = True
@@ -206,7 +368,7 @@ class PlayerClient(Player):
             artists = self._cache.get('artists')
         match = get_close_matches(artist.name, artists, 50, 0.73)
         if not match and not found:
             artists = self._cache.get('artists')
         match = get_close_matches(artist.name, artists, 50, 0.73)
         if not match and not found:
-            return
+            return None
         if len(match) > 1:
             self.log.debug('found close match for "%s": %s', artist, '/'.join(match))
         # Does not perform fuzzy matching on short and single word strings
         if len(match) > 1:
             self.log.debug('found close match for "%s": %s', artist, '/'.join(match))
         # Does not perform fuzzy matching on short and single word strings
@@ -218,7 +380,7 @@ class PlayerClient(Player):
                     artist.add_alias(close_art)
                     return artist
                 else:
                     artist.add_alias(close_art)
                     return artist
                 else:
-                    return
+                    return None
         for fuzz_art in match:
             # Regular lowered string comparison
             if artist.name.lower() == fuzz_art.lower():
         for fuzz_art in match:
             # Regular lowered string comparison
             if artist.name.lower() == fuzz_art.lower():
@@ -238,9 +400,12 @@ class PlayerClient(Player):
                 self.log.debug('Found: %s', '/'.join(list(artist.names)[:4]))
             return artist
 
                 self.log.debug('Found: %s', '/'.join(list(artist.names)[:4]))
             return artist
 
-    def fuzzy_find_track(self, artist, title):
+    @blacklist(track=True)
+    def search_track(self, artist, title):
+        """Fuzzy search of title by an artist
+        """
         # Retrieve all tracks from artist
         # Retrieve all tracks from artist
-        all_tracks = self.find_track(artist, title)
+        all_tracks = self.find_tracks(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])
         # 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])
@@ -260,14 +425,6 @@ class PlayerClient(Player):
                 return []
             return self.find('artist', artist, 'title', mtitle)
 
                 return []
             return self.find('artist', artist, 'title', mtitle)
 
-    def find_album(self, artist, album):
-        """
-        Special wrapper around album search:
-        Album lookup is made through AlbumArtist/Album instead of Artist/Album
-        MPD falls back to Artist if AlbumArtist is not found  (cf. documentation)
-        """
-        return self.find('albumartist', artist, 'album', album)
-
     @blacklist(album=True)
     def search_albums(self, artist):
         """
     @blacklist(album=True)
     def search_albums(self, artist):
         """
@@ -283,7 +440,7 @@ class PlayerClient(Player):
         for name in artist.names:
             if artist.aliases:
                 self.log.debug('Searching album for aliase: "%s"', name)
         for name in artist.names:
             if artist.aliases:
                 self.log.debug('Searching album for aliase: "%s"', name)
-            kwalbart = {'albumartist':name, 'artist':name}
+            kwalbart = {'albumartist': name, 'artist': name}
             for album in self.list('album', 'albumartist', name):
                 if album and album not in albums:
                     albums.append(Album(name=album, **kwalbart))
             for album in self.list('album', 'albumartist', name):
                 if album and album not in albums:
                     albums.append(Album(name=album, **kwalbart))
@@ -292,7 +449,7 @@ class PlayerClient(Player):
                 if 'Various Artists' in [tr.albumartist for tr in album_trks]:
                     self.log.debug('Discarding %s ("Various Artists" set)', album)
                     continue
                 if 'Various Artists' in [tr.albumartist for tr in album_trks]:
                     self.log.debug('Discarding %s ("Various Artists" set)', album)
                     continue
-                arts = set([trk.artist for trk in album_trks])
+                arts = {trk.artist for trk in album_trks}
                 # Avoid selecting album where artist is credited for a single
                 # track of the album
                 if len(set(arts)) < 2:  # TODO: better heuristic, use a ratio instead
                 # Avoid selecting album where artist is credited for a single
                 # track of the album
                 if len(set(arts)) < 2:  # TODO: better heuristic, use a ratio instead
@@ -302,140 +459,7 @@ class PlayerClient(Player):
                     self.log.debug('"{0}" probably not an album of "{1}"'.format(
                         album, artist) + '({0})'.format('/'.join(arts)))
         return 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:
-            self.send_idle('database', 'playlist', 'player', 'options')
-            select([self._client], [], [], 60)
-            ret = self.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 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: %s', self._client._pending)
-
-    def remove(self, position=0):
-        self.delete(position)
-
-    def add(self, track):
-        """Overriding MPD's add method to accept add signature with a Track
-        object"""
-        self._execute('add', [track.file])
-
-    @property
-    def artists(self):
-        return self._cache.get('artists')
-
-    @property
-    def state(self):
-        return str(self.status().get('state'))
-
-    @property
-    def playmode(self):
-        plm = {'repeat': None,
-               'single': None,
-               'random': None,
-               'consume': None,
-              }
-        for key, val in self.status().items():
-            if key in plm.keys():
-                plm.update({key:bool(int(val))})
-        return plm
-
-    @property
-    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
-        """
-        return self.playlistinfo()
-
-    def connect(self):
-        host, port, password = self._mpd
-        self.disconnect()
-        try:
-            self._client.connect(host, port)
-
-        # Catch socket errors
-        except IOError as err:
-            raise PlayerError('Could not connect to "%s:%s": %s' %
-                              (host, port, err.strerror))
-
-        # Catch all other possible errors
-        # ConnectionError and ProtocolError are always fatal.  Others may not
-        # be, but we don't know how to handle them here, so treat them as if
-        # they are instead of ignoring them.
-        except MPDError as err:
-            raise PlayerError('Could not connect to "%s:%s": %s' %
-                              (host, port, err))
-
-        if password:
-            try:
-                self._client.password(password)
-            except (MPDError, IOError) as err:
-                raise PlayerError("Could not connect to '%s': %s", (host, err))
-        # Controls we have sufficient rights
-        needed_cmds = ['status', 'stats', 'add', 'find', \
-                       'search', 'currentsong', 'ping']
-
-        available_cmd = self._client.commands()
-        for nddcmd in needed_cmds:
-            if nddcmd not in available_cmd:
-                self.disconnect()
-                raise PlayerError('Could connect to "%s", '
-                                  'but command "%s" not available' %
-                                  (host, nddcmd))
-
-        #  Controls use of MusicBrainzIdentifier
-        if Artist.use_mbid:
-            if 'MUSICBRAINZ_ARTISTID' not in self._client.tagtypes():
-                self.log.warning('Use of MusicBrainzIdentifier is set but MPD is '
-                                 'not providing related metadata')
-                self.log.info(self._client.tagtypes())
-                self.log.warning('Disabling MusicBrainzIdentifier')
-                Artist.use_mbid = False
-            else:
-                self.log.trace('Available metadata: %s', self._client.tagtypes())  # pylint: disable=no-member
-        else:
-            self.log.warning('Use of MusicBrainzIdentifier disabled!')
-            self.log.info('Consider using MusicBrainzIdentifier for your music library')
-        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):
-            pass
-        try:
-            self._client.disconnect()
-        # Disconnecting failed, so use a new client object instead
-        # This should never happen.  If it does, something is seriously broken,
-        # and the client object shouldn't be trusted to be re-used.
-        except (MPDError, IOError):
-            self._client = MPDClient()
+# #### / Search Methods ###
 
 # VIM MODLINE
 # vim: ai ts=4 sw=4 sts=4 expandtab
 
 # VIM MODLINE
 # vim: ai ts=4 sw=4 sts=4 expandtab
index db2caffe41172faf8a27d8750da36f721e3f0800..5a74170fbda2454228490a4001f5d3b41c2242ca 100644 (file)
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
 # -*- coding: utf-8 -*-
-# Copyright (c) 2014 Jack Kaliko <kaliko@azylum.org>
+# Copyright (c) 2014, 2020 kaliko <kaliko@azylum.org>
 #
 #  This file is part of sima
 #
 #
 #  This file is part of sima
 #
@@ -30,7 +30,7 @@ from socket import getfqdn
 # third parties components
 
 # local import
 # third parties components
 
 # local import
-from ...client import PlayerError
+from ...mpdclient import PlayerError
 from ...lib.plugin import Plugin
 
 
 from ...lib.plugin import Plugin
 
 
index af6b3175d5203829568e44d27bf309a87a7938fc..ac8dd46e1098194ef9d15ec776e7327f946e7598 100644 (file)
@@ -31,7 +31,6 @@ from ...lib.plugin import Plugin
 class Crop(Plugin):
     """
     Crop playlist on next track
 class Crop(Plugin):
     """
     Crop playlist on next track
-    kinda MPD's consume
     """
     def __init__(self, daemon):
         super().__init__(daemon)
     """
     def __init__(self, daemon):
         super().__init__(daemon)
@@ -63,7 +62,7 @@ class Crop(Plugin):
         if player.currentsong().pos > self.target:
             self.log.debug('cropping playlist')
         while player.currentsong().pos > self.target:
         if player.currentsong().pos > self.target:
             self.log.debug('cropping playlist')
         while player.currentsong().pos > self.target:
-            player.remove()
+            player.delete(0)
 
 
 # VIM MODLINE
 
 
 # VIM MODLINE
index 4d9bb805124cef4c23388c7763334d36f0659003..bf7ce8b42f40f749447323d1d31482c62ad47b1c 100644 (file)
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
 # -*- coding: utf-8 -*-
-# Copyright (c) 2013, 2014, 2015 Jack Kaliko <kaliko@azylum.org>
+# Copyright (c) 2013, 2014, 2015, 2020 kaliko <kaliko@azylum.org>
 #
 #  This file is part of sima
 #
 #
 #  This file is part of sima
 #
@@ -78,13 +78,13 @@ class Random(Plugin):
         self.candidates = []
         trks = []
         target = self.plugin_conf.getint('track_to_add')
         self.candidates = []
         trks = []
         target = self.plugin_conf.getint('track_to_add')
-        artists = list(self.player.artists)
+        artists = self.player.list('artist')
         random.shuffle(artists)
         for art in artists:
             if self.filtered_artist(art):
                 continue
             self.log.debug('Random art: {}'.format(art))
         random.shuffle(artists)
         for art in artists:
             if self.filtered_artist(art):
                 continue
             self.log.debug('Random art: {}'.format(art))
-            trks = self.player.find_track(Artist(art))
+            trks = self.player.find_tracks(Artist(art))
             if trks:
                 trk = random.choice(trks)
                 self.candidates.append(trk)
             if trks:
                 trk = random.choice(trks)
                 self.candidates.append(trk)