X-Git-Url: http://git.kaliko.me/?p=python-musicpd.git;a=blobdiff_plain;f=musicpd.py;h=aae85a5f5348e897760cfe4248668a8b58047c68;hp=5b4c3888ebae5f2343135819abd92f762634ac53;hb=a1d6fc6df18bca9a06232a77deecf241f48de92c;hpb=50cb8c23bcd8767244d68e24374e2978a85da420 diff --git a/musicpd.py b/musicpd.py index 5b4c388..aae85a5 100644 --- a/musicpd.py +++ b/musicpd.py @@ -1,6 +1,7 @@ # python-musicpd: Python MPD client library # Copyright (C) 2008-2010 J. Alexander Treuman -# Copyright (C) 2012-2014 Kaliko Jack +# Copyright (C) 2012-2019 Kaliko Jack +# Copyright (C) 2019 Naglis Jonaitis # # python-musicpd is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as published by @@ -15,39 +16,68 @@ # You should have received a copy of the GNU Lesser General Public License # along with python-musicpd. If not, see . -# pylint: disable=C0111 +# pylint: disable=missing-docstring import socket +import os + +from functools import wraps HELLO_PREFIX = "OK MPD " ERROR_PREFIX = "ACK " SUCCESS = "OK" NEXT = "list_OK" -VERSION = '0.4.2' +VERSION = '0.4.4' + + +def iterator_wrapper(func): + """Decorator handling iterate option""" + @wraps(func) + def decorated_function(instance, *args, **kwargs): + generator = func(instance, *args, **kwargs) + if not instance.iterate: + return list(generator) + instance._iterating = True + + def iterator(gen): + try: + for item in gen: + yield item + finally: + instance._iterating = False + return iterator(generator) + return decorated_function class MPDError(Exception): pass + class ConnectionError(MPDError): pass + class ProtocolError(MPDError): pass + class CommandError(MPDError): pass + class CommandListError(MPDError): pass + class PendingCommandError(MPDError): pass + class IteratingError(MPDError): pass + class Range: def __init__(self, tpl): @@ -77,13 +107,34 @@ class Range: class _NotConnected: + def __getattr__(self, attr): return self._dummy def _dummy(*args): raise ConnectionError("Not connected") + class MPDClient: + + """MPDClient instance will look for ``MPD_HOST``/``MPD_PORT``/``XDG_RUNTIME_DIR`` environment + variables and set instance attribute ``host``, ``port`` and ``pwd`` + accordingly. + + Then :py:obj:`musicpd.MPDClient.connect` will use ``host`` and ``port`` as defaults if not provided as args. + + Cf. :py:obj:`musicpd.MPDClient.connect` for details. + + >>> from os import environ + >>> environ['MPD_HOST'] = 'pass@mpdhost' + >>> cli = musicpd.MPDClient() + >>> cli.pwd == environ['MPD_HOST'].split('@')[0] + True + >>> cli.host == environ['MPD_HOST'].split('@')[1] + True + >>> # cli.connect() will use host/port as set in MPD_HOST/MPD_PORT + """ + def __init__(self): self.iterate = False self._reset() @@ -117,7 +168,7 @@ class MPDClient: "seekid": self._fetch_nothing, "seekcur": self._fetch_nothing, "stop": self._fetch_nothing, - # Playlist Commands + # Queue Commands "add": self._fetch_nothing, "addid": self._fetch_item, "clear": self._fetch_nothing, @@ -153,19 +204,21 @@ class MPDClient: "rm": self._fetch_nothing, "save": self._fetch_nothing, # Database Commands + "albumart": self._fetch_composite, "count": self._fetch_object, "find": self._fetch_songs, "findadd": self._fetch_nothing, "list": self._fetch_list, "listall": self._fetch_database, "listallinfo": self._fetch_database, + "listfiles": self._fetch_database, "lsinfo": self._fetch_database, + "readcomments": self._fetch_object, "search": self._fetch_songs, "searchadd": self._fetch_nothing, "searchaddpl": self._fetch_nothing, "update": self._fetch_item, "rescan": self._fetch_item, - "readcomments": self._fetch_object, # Mounts and neighbors "mount": self._fetch_nothing, "unmount": self._fetch_nothing, @@ -182,16 +235,25 @@ class MPDClient: "kill": None, "password": self._fetch_nothing, "ping": self._fetch_nothing, + "tagtypes": self._fetch_list, + "tagtypes disable": self._fetch_nothing, + "tagtypes enable": self._fetch_nothing, + "tagtypes clear": self._fetch_nothing, + "tagtypes all": self._fetch_nothing, + # Partition Commands + "partition": self._fetch_nothing, + "listpartitions": self._fetch_list, + "newpartition": self._fetch_nothing, # Audio Output Commands "disableoutput": self._fetch_nothing, "enableoutput": self._fetch_nothing, "toggleoutput": self._fetch_nothing, "outputs": self._fetch_outputs, + "outputset": self._fetch_nothing, # Reflection Commands "config": self._fetch_object, "commands": self._fetch_list, "notcommands": self._fetch_list, - "tagtypes": self._fetch_list, "urlhandlers": self._fetch_list, "decoders": self._fetch_plugins, # Client to Client @@ -201,6 +263,32 @@ class MPDClient: "readmessages": self._fetch_messages, "sendmessage": self._fetch_nothing, } + self._get_envvars() + + def _get_envvars(self): + """ + Retrieve MPD env. var. to overrides "localhost:6600" + Use MPD_HOST/MPD_PORT if set + else use MPD_HOST=${XDG_RUNTIME_DIR:-/run/}/mpd/socket if file exists + """ + self.host = 'localhost' + self.pwd = None + self.port = os.environ.get('MPD_PORT', '6600') + mpd_host_env = os.environ.get('MPD_HOST') + if mpd_host_env: + # If password is set: + # mpd_host_env = ['pass', 'host'] because MPD_HOST=pass@host + mpd_host_env = mpd_host_env.split('@') + mpd_host_env.reverse() + self.host = mpd_host_env[0] + if len(mpd_host_env) > 1 and mpd_host_env[1]: + self.pwd = mpd_host_env[1] + else: + # Is socket there + xdg_runtime_dir = os.environ.get('XDG_RUNTIME_DIR', '/run') + rundir = os.path.join(xdg_runtime_dir, 'mpd/socket') + if os.path.exists(rundir): + self.host = rundir def __getattr__(self, attr): if attr == 'send_noidle': # have send_noidle to cancel idle as well as noidle @@ -253,13 +341,13 @@ class MPDClient: raise IteratingError("Cannot execute '%s' while iterating" % command) if self._pending: - raise PendingCommandError("Cannot execute '%s' with " - "pending commands" % command) + raise PendingCommandError( + "Cannot execute '%s' with pending commands" % command) retval = self._commands[command] if self._command_list is not None: if not callable(retval): - raise CommandListError("'%s' not allowed in command list" % - command) + raise CommandListError( + "'%s' not allowed in command list" % command) self._write_command(command, args) self._command_list.append(retval) else: @@ -358,19 +446,6 @@ class MPDClient: self._command_list = None self._fetch_nothing() - def _iterator_wrapper(self, iterator): - try: - for item in iterator: - yield item - finally: - self._iterating = False - - def _wrap_iterator(self, iterator): - if not self.iterate: - return list(iterator) - self._iterating = True - return self._iterator_wrapper(iterator) - def _fetch_nothing(self): line = self._read_line() if line is not None: @@ -382,11 +457,13 @@ class MPDClient: return return pairs[0][1] + @iterator_wrapper def _fetch_list(self): - return self._wrap_iterator(self._read_list()) + return self._read_list() + @iterator_wrapper def _fetch_playlist(self): - return self._wrap_iterator(self._read_playlist()) + return self._read_playlist() def _fetch_object(self): objs = list(self._read_objects()) @@ -394,8 +471,9 @@ class MPDClient: return {} return objs[0] + @iterator_wrapper def _fetch_objects(self, delimiters): - return self._wrap_iterator(self._read_objects(delimiters)) + return self._read_objects(delimiters) def _fetch_changes(self): return self._fetch_objects(["cpos"]) @@ -424,8 +502,20 @@ class MPDClient: def _fetch_neighbors(self): return self._fetch_objects(["neighbor"]) + def _fetch_composite(self): + obj = {} + for key, value in self._read_pairs(): + key = key.lower() + obj[key] = value + if key == 'binary': + break + by = self._read_line() + obj['data'] = by.encode(errors='surrogateescape') + return obj + + @iterator_wrapper def _fetch_command_list(self): - return self._wrap_iterator(self._read_command_list()) + return self._read_command_list() def _hello(self): line = self._rfile.readline() @@ -437,7 +527,6 @@ class MPDClient: self.mpd_version = line[len(HELLO_PREFIX):].strip() def _reset(self): - # pylint: disable=w0201 self.mpd_version = None self._iterating = False self._pending = [] @@ -448,8 +537,8 @@ class MPDClient: def _connect_unix(self, path): if not hasattr(socket, "AF_UNIX"): - raise ConnectionError("Unix domain sockets not supported " - "on this platform") + raise ConnectionError( + "Unix domain sockets not supported on this platform") sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) sock.connect(path) return sock @@ -481,19 +570,42 @@ class MPDClient: def noidle(self): # noidle's special case if not self._pending or self._pending[0] != 'idle': - raise CommandError('cannot send noidle if send_idle was not called') + raise CommandError( + 'cannot send noidle if send_idle was not called') del self._pending[0] self._write_command("noidle") return self._fetch_list() - def connect(self, host, port): + def connect(self, host=None, port=None): + """Connects the MPD server + + :param str host: hostname, IP or FQDN (defaults to `localhost` or socket, see below for details) + :param str port: port number (defaults to 6600) + + The connect method honors MPD_HOST/MPD_PORT environment variables. + + .. note:: Default host/port + + If host evaluate to :py:obj:`False` + * use ``MPD_HOST`` env. var. if set, extract password if present, + * else looks for a existing file in ``${XDG_RUNTIME_DIR:-/run/}/mpd/socket`` + * else set host to ``localhost`` + + If port evaluate to :py:obj:`False` + * if ``MPD_PORT`` env. var. is set, use it for port + * else use ``6600`` + """ + if not host: + host = self.host + if not port: + port = self.port if self._sock is not None: raise ConnectionError("Already connected") if host.startswith("/"): self._sock = self._connect_unix(host) else: self._sock = self._connect_tcp(host, port) - self._rfile = self._sock.makefile("r", encoding='utf-8') + self._rfile = self._sock.makefile("r", encoding='utf-8', errors='surrogateescape') self._wfile = self._sock.makefile("w", encoding='utf-8') try: self._hello() @@ -502,6 +614,10 @@ class MPDClient: raise def disconnect(self): + """Closes the MPD connection. + The client closes the actual socket, it does not use the + 'close' request from MPD protocol (as suggested in documentation). + """ if hasattr(self._rfile, 'close'): self._rfile.close() if hasattr(self._wfile, 'close'): @@ -538,5 +654,4 @@ class MPDClient: def escape(text): return text.replace("\\", "\\\\").replace('"', '\\"') - # vim: set expandtab shiftwidth=4 softtabstop=4 textwidth=79: