X-Git-Url: http://git.kaliko.me/?p=python-musicpd.git;a=blobdiff_plain;f=musicpd.py;h=008d9d8d4838539fad90031ce538d166971fe5b7;hp=3aa4cb071ab8c5b21049b6adaa1547492fb5de98;hb=e645adfa14a04168a4d8a892e88569205f868c84;hpb=9e3563b9b6ef05e97e54d90d1149409a3669f0a5 diff --git a/musicpd.py b/musicpd.py index 3aa4cb0..008d9d8 100644 --- a/musicpd.py +++ b/musicpd.py @@ -1,7 +1,8 @@ # python-musicpd: Python MPD client library -# Copyright (C) 2008-2010 J. Alexander Treuman -# Copyright (C) 2012-2019 Kaliko Jack +# Copyright (C) 2012-2020 kaliko # Copyright (C) 2019 Naglis Jonaitis +# Copyright (C) 2019 Bart Van Loon +# Copyright (C) 2008-2010 J. Alexander Treuman # # 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 @@ -23,12 +24,13 @@ import os from functools import wraps - HELLO_PREFIX = "OK MPD " ERROR_PREFIX = "ACK " SUCCESS = "OK" NEXT = "list_OK" -VERSION = '0.4.4' +VERSION = '0.4.5' +#: seconds before a tcp connection attempt times out +CONNECTION_TIMEOUT = 5 def iterator_wrapper(func): @@ -116,10 +118,10 @@ class _NotConnected: 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. @@ -132,7 +134,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): @@ -204,16 +223,17 @@ class MPDClient: "rm": self._fetch_nothing, "save": self._fetch_nothing, # Database Commands - #"albumart": self._fetch_object, + "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_songs, + "listfiles": self._fetch_database, "lsinfo": self._fetch_database, "readcomments": self._fetch_object, + "readpicture": self._fetch_composite, "search": self._fetch_songs, "searchadd": self._fetch_nothing, "searchaddpl": self._fetch_nothing, @@ -371,8 +391,22 @@ class MPDClient: parts.append('"%s"' % escape(str(arg))) self._write_line(" ".join(parts)) - def _read_line(self): - line = self._rfile.readline() + def _read_binary(self, amount): + chunk = bytearray() + while amount > 0: + result = self._rbfile.read(amount) + if len(result) == 0: + self.disconnect() + raise ConnectionError("Connection lost while reading binary content") + chunk.extend(result) + amount -= len(result) + return bytes(chunk) + + def _read_line(self, binary=False): + if binary: + line = self._rbfile.readline().decode('utf-8') + else: + line = self._rfile.readline() if not line.endswith("\n"): self.disconnect() raise ConnectionError("Connection lost while reading line") @@ -389,8 +423,8 @@ class MPDClient: return return line - def _read_pair(self, separator): - line = self._read_line() + def _read_pair(self, separator, binary=False): + line = self._read_line(binary=binary) if line is None: return pair = line.split(separator, 1) @@ -398,11 +432,11 @@ class MPDClient: raise ProtocolError("Could not parse pair: '%s'" % line) return pair - def _read_pairs(self, separator=": "): - pair = self._read_pair(separator) + def _read_pairs(self, separator=": ", binary=False): + pair = self._read_pair(separator, binary=binary) while pair: yield pair - pair = self._read_pair(separator) + pair = self._read_pair(separator, binary=binary) def _read_list(self): seen = None @@ -502,6 +536,31 @@ class MPDClient: def _fetch_neighbors(self): return self._fetch_objects(["neighbor"]) + def _fetch_composite(self): + obj = {} + for key, value in self._read_pairs(binary=True): + key = key.lower() + obj[key] = value + if key == 'binary': + break + if not obj: + # If the song file was recognized, but there is no picture, the + # response is successful, but is otherwise empty. + return obj + amount = int(obj['binary']) + try: + obj['data'] = self._read_binary(amount) + except IOError as err: + raise ConnectionError('Error reading binary content: %s' % err) + if len(obj['data']) != amount: + raise ConnectionError('Error reading binary content: ' + 'Expects %sB, got %s' % (amount, len(obj['data']))) + # Fetches trailing new line + self._read_line(binary=True) + # Fetches SUCCESS code + self._read_line(binary=True) + return obj + @iterator_wrapper def _fetch_command_list(self): return self._read_command_list() @@ -522,6 +581,7 @@ class MPDClient: self._command_list = None self._sock = None self._rfile = _NotConnected() + self._rbfile = _NotConnected() self._wfile = _NotConnected() def _connect_unix(self, path): @@ -545,7 +605,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 @@ -569,32 +631,38 @@ 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._rbfile = self._sock.makefile("rb") self._wfile = self._sock.makefile("w", encoding='utf-8') try: self._hello() @@ -609,6 +677,8 @@ class MPDClient: """ if hasattr(self._rfile, 'close'): self._rfile.close() + if hasattr(self._rbfile, 'close'): + self._rbfile.close() if hasattr(self._wfile, 'close'): self._wfile.close() if hasattr(self._sock, 'close'):