X-Git-Url: https://git.kaliko.me/?p=python-musicpd.git;a=blobdiff_plain;f=musicpd.py;h=8c5bfc32c155e7cd1b8179ecdca217185239f591;hp=2e0f8b3194551b9562acbe45ada4457e3d39678b;hb=79fd7d84fe8acbc03b857c4ff483db9c16a0c5b1;hpb=f4753ec92d1e68e46aceda30e31cfb78d5e9dbad diff --git a/musicpd.py b/musicpd.py index 2e0f8b3..8c5bfc3 100644 --- a/musicpd.py +++ b/musicpd.py @@ -1,6 +1,8 @@ # python-musicpd: Python MPD client library +# Copyright (C) 2012-2019 kaliko +# Copyright (C) 2019 Naglis Jonaitis +# Copyright (C) 2019 Bart Van Loon # Copyright (C) 2008-2010 J. Alexander Treuman -# Copyright (C) 2012-2018 Kaliko Jack # # 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 @@ -27,7 +29,9 @@ HELLO_PREFIX = "OK MPD " ERROR_PREFIX = "ACK " SUCCESS = "OK" NEXT = "list_OK" -VERSION = '0.4.3' +VERSION = '0.4.5' +#: seconds before a tcp connection attempt times out +CONNECTION_TIMEOUT = 5 def iterator_wrapper(func): @@ -38,6 +42,7 @@ def iterator_wrapper(func): if not instance.iterate: return list(generator) instance._iterating = True + def iterator(gen): try: for item in gen: @@ -51,24 +56,31 @@ def iterator_wrapper(func): 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): @@ -96,17 +108,21 @@ class Range: except (TypeError, ValueError): raise CommandError('Not a tuple of int') + 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. + accordingly. Regarding ``MPD_HOST`` format to expose password refer + MPD client manual :manpage:`mpc (1)`. Then :py:obj:`musicpd.MPDClient.connect` will use ``host`` and ``port`` as defaults if not provided as args. @@ -119,7 +135,24 @@ class MPDClient: True >>> cli.host == environ['MPD_HOST'].split('@')[1] True - >>> # cli.connect() will use host/port as set in MPD_HOST/MPD_PORT + >>> cli.connect() # will use host/port as set in MPD_HOST/MPD_PORT + + :ivar str host: host used with the current connection + :ivar str,int port: port used with the current connection + :ivar str pwd: password detected in ``MPD_HOST`` environment variable + + .. warning:: Instance attribute host/port/pwd + + While :py:attr:`musicpd.MPDClient().host` and + :py:attr:`musicpd.MPDClient().port` keep track of current connection + host and port, :py:attr:`musicpd.MPDClient().pwd` is set once with + password extracted from environment variable. + Calling :py:meth:`musicpd.MPDClient().password()` with a new password + won't update :py:attr:`musicpd.MPDClient().pwd` value. + + Moreover, :py:attr:`musicpd.MPDClient().pwd` is only an helper attribute + exposing password extracted from ``MPD_HOST`` environment variable, it + will not be used as default value for the :py:meth:`password` method """ def __init__(self): @@ -155,7 +188,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, @@ -191,19 +224,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, @@ -220,16 +255,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 @@ -317,13 +361,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: @@ -478,6 +522,17 @@ 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._read_command_list() @@ -502,8 +557,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 @@ -521,7 +576,9 @@ class MPDClient: sock = None try: sock = socket.socket(af, socktype, proto) + sock.settimeout(CONNECTION_TIMEOUT) sock.connect(sa) + sock.settimeout(None) return sock except socket.error as socket_err: err = socket_err @@ -535,7 +592,8 @@ 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() @@ -544,32 +602,37 @@ class MPDClient: """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) + :param port: port number (defaults to 6600) + :type port: str or int 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, + * use ``MPD_HOST`` environment variable 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 + * if ``MPD_PORT`` environment variable is set, use it for port * else use ``6600`` """ if not host: host = self.host + else: + self.host = host if not port: port = self.port + else: + self.port = 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()