From 751e07b3e43a4812c29f1c89f03e58023278b634 Mon Sep 17 00:00:00 2001 From: kaliko Date: Sun, 10 May 2020 13:26:06 +0200 Subject: [PATCH] MPD Client refactoring --- doc/Changelog | 3 +- sima/core.py | 29 +- sima/info.py | 2 +- sima/lib/player.py | 189 ----------- sima/lib/webserv.py | 7 +- sima/{client.py => mpdclient.py} | 534 ++++++++++++++++--------------- sima/plugins/core/uniq.py | 4 +- sima/plugins/internal/crop.py | 3 +- sima/plugins/internal/random.py | 6 +- 9 files changed, 300 insertions(+), 477 deletions(-) delete mode 100644 sima/lib/player.py rename sima/{client.py => mpdclient.py} (52%) diff --git a/doc/Changelog b/doc/Changelog index f0126f8..b2774c1 100644 --- a/doc/Changelog +++ b/doc/Changelog @@ -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 diff --git a/sima/core.py b/sima/core.py index ead1ba8..62d123a 100644 --- a/sima/core.py +++ b/sima/core.py @@ -25,8 +25,8 @@ import time 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 @@ -46,16 +46,9 @@ class Sima(Daemon): 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) - 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) @@ -132,7 +125,7 @@ class Sima(Daemon): 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') @@ -164,22 +157,16 @@ class Sima(Daemon): """ """ 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') - except (PlayerError, PlayerUnHandledError) as err: + except (PlayerError, MPDError) as err: 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 @@ -187,7 +174,7 @@ class Sima(Daemon): 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 diff --git a/sima/info.py b/sima/info.py index aecdc0f..947d1e5 100644 --- a/sima/info.py +++ b/sima/info.py @@ -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' diff --git a/sima/lib/player.py b/sima/lib/player.py deleted file mode 100644 index 1d1f693..0000000 --- a/sima/lib/player.py +++ /dev/null @@ -1,189 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) 2009-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 . -# -# - -# 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=) # 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 diff --git a/sima/lib/webserv.py b/sima/lib/webserv.py index 9800b92..0304dd6 100644 --- a/sima/lib/webserv.py +++ b/sima/lib/webserv.py @@ -33,7 +33,7 @@ from hashlib import md5 # 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): @@ -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 - 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 @@ -373,7 +374,7 @@ class WebService(Plugin): 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]) diff --git a/sima/client.py b/sima/mpdclient.py similarity index 52% rename from sima/client.py rename to sima/mpdclient.py index d10a3fe..d13c8cb 100644 --- a/sima/client.py +++ b/sima/mpdclient.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2013, 2014 Jack Kaliko +# Copyright (c) 2009-2020 kaliko # # This file is part of sima # @@ -15,46 +15,32 @@ # # 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 # 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 -from .lib.simastr import SimaStr -from .lib.player import Player, blacklist +from .lib.meta import Artist, Album 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.""" -class PlayerCommandError(PlayerError): - """Command error""" - -PlayerUnHandledError = MPDError # pylint: disable=C0103 +# Some decorators 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: @@ -73,83 +59,141 @@ def bl_artist(func): 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__() - 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 - 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: - 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 """ @@ -158,42 +202,160 @@ class PlayerClient(Player): 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: - 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()') + 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: - 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: - 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) + 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=) # 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 + TODO: Re-use find method here!!! """ 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 @@ -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: - 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 @@ -218,7 +380,7 @@ class PlayerClient(Player): 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(): @@ -238,9 +400,12 @@ class PlayerClient(Player): 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 - 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]) @@ -260,14 +425,6 @@ class PlayerClient(Player): 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): """ @@ -283,7 +440,7 @@ class PlayerClient(Player): 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)) @@ -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 - 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 @@ -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 - - 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 diff --git a/sima/plugins/core/uniq.py b/sima/plugins/core/uniq.py index db2caff..5a74170 100644 --- a/sima/plugins/core/uniq.py +++ b/sima/plugins/core/uniq.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2014 Jack Kaliko +# Copyright (c) 2014, 2020 kaliko # # This file is part of sima # @@ -30,7 +30,7 @@ from socket import getfqdn # third parties components # local import -from ...client import PlayerError +from ...mpdclient import PlayerError from ...lib.plugin import Plugin diff --git a/sima/plugins/internal/crop.py b/sima/plugins/internal/crop.py index af6b317..ac8dd46 100644 --- a/sima/plugins/internal/crop.py +++ b/sima/plugins/internal/crop.py @@ -31,7 +31,6 @@ from ...lib.plugin import Plugin class Crop(Plugin): """ Crop playlist on next track - kinda MPD's consume """ 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: - player.remove() + player.delete(0) # VIM MODLINE diff --git a/sima/plugins/internal/random.py b/sima/plugins/internal/random.py index 4d9bb80..bf7ce8b 100644 --- a/sima/plugins/internal/random.py +++ b/sima/plugins/internal/random.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2013, 2014, 2015 Jack Kaliko +# Copyright (c) 2013, 2014, 2015, 2020 kaliko # # This file is part of sima # @@ -78,13 +78,13 @@ class Random(Plugin): 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)) - trks = self.player.find_track(Artist(art)) + trks = self.player.find_tracks(Artist(art)) if trks: trk = random.choice(trks) self.candidates.append(trk) -- 2.39.2