]> kaliko git repositories - python-musicpd.git/blob - musicpd.py
Some documentation refactoring, improvements
[python-musicpd.git] / musicpd.py
1 # -*- coding: utf-8 -*-
2 # SPDX-FileCopyrightText: 2012-2023  kaliko <kaliko@azylum.org>
3 # SPDX-FileCopyrightText: 2021       Wonko der Verständige <wonko@hanstool.org>
4 # SPDX-FileCopyrightText: 2019       Naglis Jonaitis <naglis@mailbox.org>
5 # SPDX-FileCopyrightText: 2019       Bart Van Loon <bbb@bbbart.be>
6 # SPDX-FileCopyrightText: 2008-2010  J. Alexander Treuman <jat@spatialrift.net>
7 # SPDX-License-Identifier: LGPL-3.0-or-later
8 """Python Music Player Daemon client library"""
9
10
11 import socket
12 import os
13
14 from functools import wraps
15
16 HELLO_PREFIX = "OK MPD "
17 ERROR_PREFIX = "ACK "
18 SUCCESS = "OK"
19 NEXT = "list_OK"
20 #: Module version
21 VERSION = '0.9.0b0'
22 #: Seconds before a connection attempt times out
23 #: (overriden by :envvar:`MPD_TIMEOUT` env. var.)
24 CONNECTION_TIMEOUT = 30
25 #: Socket timeout in second (Default is None for no timeout)
26 SOCKET_TIMEOUT = None
27
28
29 def iterator_wrapper(func):
30     """Decorator handling iterate option"""
31     @wraps(func)
32     def decorated_function(instance, *args, **kwargs):
33         generator = func(instance, *args, **kwargs)
34         if not instance.iterate:
35             return list(generator)
36         instance._iterating = True
37
38         def iterator(gen):
39             try:
40                 for item in gen:
41                     yield item
42             finally:
43                 instance._iterating = False
44         return iterator(generator)
45     return decorated_function
46
47
48 class MPDError(Exception):
49     """Main musicpd Exception"""
50
51
52 class ConnectionError(MPDError):
53     """Fatal Connection Error, cannot recover from it."""
54
55
56 class ProtocolError(MPDError):
57     """Fatal Protocol Error, cannot recover from it"""
58
59
60 class CommandError(MPDError):
61     """Malformed command, socket should be fine, can reuse it"""
62
63
64 class CommandListError(MPDError):
65     """"""
66
67
68 class PendingCommandError(MPDError):
69     """"""
70
71
72 class IteratingError(MPDError):
73     """"""
74
75
76 class Range:
77
78     def __init__(self, tpl):
79         self.tpl = tpl
80         self.lower = ''
81         self.upper = ''
82         self._check()
83
84     def __str__(self):
85         return f'{self.lower}:{self.upper}'
86
87     def __repr__(self):
88         return f'Range({self.tpl})'
89
90     def _check_element(self, item):
91         if item is None or item == '':
92             return ''
93         try:
94             return str(int(item))
95         except (TypeError, ValueError) as err:
96             raise CommandError(f'Not an integer: "{item}"') from err
97         return item
98
99     def _check(self):
100         if not isinstance(self.tpl, tuple):
101             raise CommandError('Wrong type, provide a tuple')
102         if len(self.tpl) == 0:
103             return
104         if len(self.tpl) == 1:
105             self.lower = self._check_element(self.tpl[0])
106             return
107         if len(self.tpl) != 2:
108             raise CommandError('Range wrong size (0, 1 or 2 allowed)')
109         self.lower = self._check_element(self.tpl[0])
110         self.upper = self._check_element(self.tpl[1])
111         if self.lower == '' and self.upper != '':
112             raise CommandError(f'Integer expected to start the range: {self.tpl}')
113         if self.upper.isdigit() and self.lower.isdigit():
114             if int(self.lower) > int(self.upper):
115                 raise CommandError(f'Wrong range: {self.lower} > {self.upper}')
116
117
118 class _NotConnected:
119
120     def __getattr__(self, attr):
121         return self._dummy
122
123     def _dummy(self, *args):
124         raise ConnectionError("Not connected")
125
126
127 class MPDClient:
128     """MPDClient instance will look for :envvar:`MPD_HOST`/:envvar:`MPD_PORT`/:envvar:`XDG_RUNTIME_DIR` environment
129     variables and set instance attribute :py:attr:`host`, :py:attr:`port` and :py:obj:`pwd`
130     accordingly.
131
132     Then :py:obj:`musicpd.MPDClient.connect` will use :py:obj:`host` and
133     :py:obj:`port` as defaults if not provided as args.
134
135     Regarding :envvar:`MPD_HOST` format to expose password refer this module
136     documentation or MPD client manual :manpage:`mpc (1)`.
137
138     >>> from os import environ
139     >>> environ['MPD_HOST'] = 'pass@mpdhost'
140     >>> cli = musicpd.MPDClient()
141     >>> cli.pwd == environ['MPD_HOST'].split('@')[0]
142     True
143     >>> cli.host == environ['MPD_HOST'].split('@')[1]
144     True
145     >>> cli.connect() # will use host/port as set in MPD_HOST/MPD_PORT
146
147     .. note::
148
149       default host:
150        * use :envvar:`MPD_HOST` environment variable if set, extract password if present,
151        * else use :envvar:`XDG_RUNTIME_DIR` to looks for an existing file in ``${XDG_RUNTIME_DIR:-/run/}/mpd/socket``
152        * else set host to ``localhost``
153
154       default port:
155        * use :envvar:`MPD_PORT` environment variable is set
156        * else use ``6600``
157
158     .. warning:: **Instance attribute host/port/pwd**
159
160       While :py:attr:`musicpd.MPDClient.host` and
161       :py:attr:`musicpd.MPDClient.port` keep track of current connection
162       host and port, :py:attr:`musicpd.MPDClient.pwd` is set once with
163       password extracted from environment variable.
164       Calling :py:meth:`password` methode with a new password
165       won't update :py:attr:`musicpd.MPDClient.pwd` value.
166
167       Moreover, :py:attr:`musicpd.MPDClient.pwd` is only an helper attribute
168       exposing password extracted from :envvar:`MPD_HOST` environment variable, it
169       will not be used as default value for the :py:meth:`password` method
170     """
171
172     def __init__(self):
173         self.iterate = False
174         #: Socket timeout value in seconds
175         self._socket_timeout = SOCKET_TIMEOUT
176         #: Current connection timeout value, defaults to
177         #: :py:obj:`CONNECTION_TIMEOUT` or env. var. ``MPD_TIMEOUT`` if provided
178         self.mpd_timeout = None
179         self.mpd_version = ''
180         """Protocol version as exposed by the server as a :py:obj:`str`
181
182         .. note:: This is the version of the protocol spoken, not the real version of the daemon."""
183         self._reset()
184         self._commands = {
185             # Status Commands
186             "clearerror":         self._fetch_nothing,
187             "currentsong":        self._fetch_object,
188             "idle":               self._fetch_list,
189             #"noidle":             None,
190             "status":             self._fetch_object,
191             "stats":              self._fetch_object,
192             # Playback Option Commands
193             "consume":            self._fetch_nothing,
194             "crossfade":          self._fetch_nothing,
195             "mixrampdb":          self._fetch_nothing,
196             "mixrampdelay":       self._fetch_nothing,
197             "random":             self._fetch_nothing,
198             "repeat":             self._fetch_nothing,
199             "setvol":             self._fetch_nothing,
200             "getvol":             self._fetch_object,
201             "single":             self._fetch_nothing,
202             "replay_gain_mode":   self._fetch_nothing,
203             "replay_gain_status": self._fetch_item,
204             "volume":             self._fetch_nothing,
205             # Playback Control Commands
206             "next":               self._fetch_nothing,
207             "pause":              self._fetch_nothing,
208             "play":               self._fetch_nothing,
209             "playid":             self._fetch_nothing,
210             "previous":           self._fetch_nothing,
211             "seek":               self._fetch_nothing,
212             "seekid":             self._fetch_nothing,
213             "seekcur":            self._fetch_nothing,
214             "stop":               self._fetch_nothing,
215             # Queue Commands
216             "add":                self._fetch_nothing,
217             "addid":              self._fetch_item,
218             "clear":              self._fetch_nothing,
219             "delete":             self._fetch_nothing,
220             "deleteid":           self._fetch_nothing,
221             "move":               self._fetch_nothing,
222             "moveid":             self._fetch_nothing,
223             "playlist":           self._fetch_playlist,
224             "playlistfind":       self._fetch_songs,
225             "playlistid":         self._fetch_songs,
226             "playlistinfo":       self._fetch_songs,
227             "playlistsearch":     self._fetch_songs,
228             "plchanges":          self._fetch_songs,
229             "plchangesposid":     self._fetch_changes,
230             "prio":               self._fetch_nothing,
231             "prioid":             self._fetch_nothing,
232             "rangeid":            self._fetch_nothing,
233             "shuffle":            self._fetch_nothing,
234             "swap":               self._fetch_nothing,
235             "swapid":             self._fetch_nothing,
236             "addtagid":           self._fetch_nothing,
237             "cleartagid":         self._fetch_nothing,
238             # Stored Playlist Commands
239             "listplaylist":       self._fetch_list,
240             "listplaylistinfo":   self._fetch_songs,
241             "listplaylists":      self._fetch_playlists,
242             "load":               self._fetch_nothing,
243             "playlistadd":        self._fetch_nothing,
244             "playlistclear":      self._fetch_nothing,
245             "playlistdelete":     self._fetch_nothing,
246             "playlistmove":       self._fetch_nothing,
247             "rename":             self._fetch_nothing,
248             "rm":                 self._fetch_nothing,
249             "save":               self._fetch_nothing,
250             # Database Commands
251             "albumart":           self._fetch_composite,
252             "count":              self._fetch_object,
253             "getfingerprint":     self._fetch_object,
254             "find":               self._fetch_songs,
255             "findadd":            self._fetch_nothing,
256             "list":               self._fetch_list,
257             "listall":            self._fetch_database,
258             "listallinfo":        self._fetch_database,
259             "listfiles":          self._fetch_database,
260             "lsinfo":             self._fetch_database,
261             "readcomments":       self._fetch_object,
262             "readpicture":        self._fetch_composite,
263             "search":             self._fetch_songs,
264             "searchadd":          self._fetch_nothing,
265             "searchaddpl":        self._fetch_nothing,
266             "update":             self._fetch_item,
267             "rescan":             self._fetch_item,
268             # Mounts and neighbors
269             "mount":              self._fetch_nothing,
270             "unmount":            self._fetch_nothing,
271             "listmounts":         self._fetch_mounts,
272             "listneighbors":      self._fetch_neighbors,
273             # Sticker Commands
274             "sticker get":        self._fetch_item,
275             "sticker set":        self._fetch_nothing,
276             "sticker delete":     self._fetch_nothing,
277             "sticker list":       self._fetch_list,
278             "sticker find":       self._fetch_songs,
279             # Connection Commands
280             "close":              None,
281             "kill":               None,
282             "password":           self._fetch_nothing,
283             "ping":               self._fetch_nothing,
284             "binarylimit":        self._fetch_nothing,
285             "tagtypes":           self._fetch_list,
286             "tagtypes disable":   self._fetch_nothing,
287             "tagtypes enable":    self._fetch_nothing,
288             "tagtypes clear":     self._fetch_nothing,
289             "tagtypes all":       self._fetch_nothing,
290             # Partition Commands
291             "partition":          self._fetch_nothing,
292             "listpartitions":     self._fetch_list,
293             "newpartition":       self._fetch_nothing,
294             "delpartition":       self._fetch_nothing,
295             "moveoutput":         self._fetch_nothing,
296             # Audio Output Commands
297             "disableoutput":      self._fetch_nothing,
298             "enableoutput":       self._fetch_nothing,
299             "toggleoutput":       self._fetch_nothing,
300             "outputs":            self._fetch_outputs,
301             "outputset":          self._fetch_nothing,
302             # Reflection Commands
303             "config":             self._fetch_object,
304             "commands":           self._fetch_list,
305             "notcommands":        self._fetch_list,
306             "urlhandlers":        self._fetch_list,
307             "decoders":           self._fetch_plugins,
308             # Client to Client
309             "subscribe":          self._fetch_nothing,
310             "unsubscribe":        self._fetch_nothing,
311             "channels":           self._fetch_list,
312             "readmessages":       self._fetch_messages,
313             "sendmessage":        self._fetch_nothing,
314         }
315         #: host used with the current connection (:py:obj:`str`)
316         self.host = None
317         #: password detected in :envvar:`MPD_HOST` environment variable (:py:obj:`str`)
318         self.pwd = None
319         #: port used with the current connection (:py:obj:`int`, :py:obj:`str`)
320         self.port = None
321         self._get_envvars()
322
323     def _get_envvars(self):
324         """
325         Retrieve MPD env. var. to overrides default "localhost:6600"
326         """
327         # Set some defaults
328         self.host = 'localhost'
329         self.port = os.getenv('MPD_PORT', '6600')
330         _host = os.getenv('MPD_HOST', '')
331         if _host:
332             # If password is set: MPD_HOST=pass@host
333             if '@' in _host:
334                 mpd_host_env = _host.split('@', 1)
335                 if mpd_host_env[0]:
336                     # A password is actually set
337                     self.pwd = mpd_host_env[0]
338                     if mpd_host_env[1]:
339                         self.host = mpd_host_env[1]
340                 elif mpd_host_env[1]:
341                     # No password set but leading @ is an abstract socket
342                     self.host = '@'+mpd_host_env[1]
343             else:
344                 # MPD_HOST is a plain host
345                 self.host = _host
346         else:
347             # Is socket there
348             xdg_runtime_dir = os.getenv('XDG_RUNTIME_DIR', '/run')
349             rundir = os.path.join(xdg_runtime_dir, 'mpd/socket')
350             if os.path.exists(rundir):
351                 self.host = rundir
352         _mpd_timeout = os.getenv('MPD_TIMEOUT', '')
353         if _mpd_timeout.isdigit():
354             self.mpd_timeout = int(_mpd_timeout)
355         else:  # Use CONNECTION_TIMEOUT as default even if MPD_TIMEOUT carries gargage
356             self.mpd_timeout = CONNECTION_TIMEOUT
357
358     def __getattr__(self, attr):
359         if attr == 'send_noidle':  # have send_noidle to cancel idle as well as noidle
360             return self.noidle
361         if attr.startswith("send_"):
362             command = attr.replace("send_", "", 1)
363             wrapper = self._send
364         elif attr.startswith("fetch_"):
365             command = attr.replace("fetch_", "", 1)
366             wrapper = self._fetch
367         else:
368             command = attr
369             wrapper = self._execute
370         if command not in self._commands:
371             command = command.replace("_", " ")
372             if command not in self._commands:
373                 cls = self.__class__.__name__
374                 raise AttributeError(f"'{cls}' object has no attribute '{attr}'")
375         return lambda *args: wrapper(command, args)
376
377     def _send(self, command, args):
378         if self._command_list is not None:
379             raise CommandListError("Cannot use send_%s in a command list" %
380                                    command.replace(" ", "_"))
381         self._write_command(command, args)
382         retval = self._commands[command]
383         if retval is not None:
384             self._pending.append(command)
385
386     def _fetch(self, command, args=None):  # pylint: disable=unused-argument
387         cmd_fmt = command.replace(" ", "_")
388         if self._command_list is not None:
389             raise CommandListError(f"Cannot use fetch_{cmd_fmt} in a command list")
390         if self._iterating:
391             raise IteratingError(f"Cannot use fetch_{cmd_fmt} while iterating")
392         if not self._pending:
393             raise PendingCommandError("No pending commands to fetch")
394         if self._pending[0] != command:
395             raise PendingCommandError(f"'{command}' is not the currently pending command")
396         del self._pending[0]
397         retval = self._commands[command]
398         if callable(retval):
399             return retval()
400         return retval
401
402     def _execute(self, command, args):  # pylint: disable=unused-argument
403         if self._iterating:
404             raise IteratingError(f"Cannot execute '{command}' while iterating")
405         if self._pending:
406             raise PendingCommandError(f"Cannot execute '{command}' with pending commands")
407         retval = self._commands[command]
408         if self._command_list is not None:
409             if not callable(retval):
410                 raise CommandListError(f"'{command}' not allowed in command list")
411             self._write_command(command, args)
412             self._command_list.append(retval)
413         else:
414             self._write_command(command, args)
415             if callable(retval):
416                 return retval()
417             return retval
418         return None
419
420     def _write_line(self, line):
421         self._wfile.write(f"{line!s}\n")
422         self._wfile.flush()
423
424     def _write_command(self, command, args=None):
425         if args is None:
426             args = []
427         parts = [command]
428         for arg in args:
429             if isinstance(arg, tuple):
430                 parts.append('{0!s}'.format(Range(arg)))
431             else:
432                 parts.append('"%s"' % escape(str(arg)))
433         if '\n' in ' '.join(parts):
434             raise CommandError('new line found in the command!')
435         self._write_line(" ".join(parts))
436
437     def _read_binary(self, amount):
438         chunk = bytearray()
439         while amount > 0:
440             result = self._rbfile.read(amount)
441             if len(result) == 0:
442                 self.disconnect()
443                 raise ConnectionError("Connection lost while reading binary content")
444             chunk.extend(result)
445             amount -= len(result)
446         return bytes(chunk)
447
448     def _read_line(self, binary=False):
449         if binary:
450             line = self._rbfile.readline().decode('utf-8')
451         else:
452             line = self._rfile.readline()
453         if not line.endswith("\n"):
454             self.disconnect()
455             raise ConnectionError("Connection lost while reading line")
456         line = line.rstrip("\n")
457         if line.startswith(ERROR_PREFIX):
458             error = line[len(ERROR_PREFIX):].strip()
459             raise CommandError(error)
460         if self._command_list is not None:
461             if line == NEXT:
462                 return None
463             if line == SUCCESS:
464                 raise ProtocolError(f"Got unexpected '{SUCCESS}'")
465         elif line == SUCCESS:
466             return None
467         return line
468
469     def _read_pair(self, separator, binary=False):
470         line = self._read_line(binary=binary)
471         if line is None:
472             return None
473         pair = line.split(separator, 1)
474         if len(pair) < 2:
475             raise ProtocolError(f"Could not parse pair: '{line}'")
476         return pair
477
478     def _read_pairs(self, separator=": ", binary=False):
479         pair = self._read_pair(separator, binary=binary)
480         while pair:
481             yield pair
482             pair = self._read_pair(separator, binary=binary)
483
484     def _read_list(self):
485         seen = None
486         for key, value in self._read_pairs():
487             if key != seen:
488                 if seen is not None:
489                     raise ProtocolError(f"Expected key '{seen}', got '{key}'")
490                 seen = key
491             yield value
492
493     def _read_playlist(self):
494         for _, value in self._read_pairs(":"):
495             yield value
496
497     def _read_objects(self, delimiters=None):
498         obj = {}
499         if delimiters is None:
500             delimiters = []
501         for key, value in self._read_pairs():
502             key = key.lower()
503             if obj:
504                 if key in delimiters:
505                     yield obj
506                     obj = {}
507                 elif key in obj:
508                     if not isinstance(obj[key], list):
509                         obj[key] = [obj[key], value]
510                     else:
511                         obj[key].append(value)
512                     continue
513             obj[key] = value
514         if obj:
515             yield obj
516
517     def _read_command_list(self):
518         try:
519             for retval in self._command_list:
520                 yield retval()
521         finally:
522             self._command_list = None
523         self._fetch_nothing()
524
525     def _fetch_nothing(self):
526         line = self._read_line()
527         if line is not None:
528             raise ProtocolError(f"Got unexpected return value: '{line}'")
529
530     def _fetch_item(self):
531         pairs = list(self._read_pairs())
532         if len(pairs) != 1:
533             return None
534         return pairs[0][1]
535
536     @iterator_wrapper
537     def _fetch_list(self):
538         return self._read_list()
539
540     @iterator_wrapper
541     def _fetch_playlist(self):
542         return self._read_playlist()
543
544     def _fetch_object(self):
545         objs = list(self._read_objects())
546         if not objs:
547             return {}
548         return objs[0]
549
550     @iterator_wrapper
551     def _fetch_objects(self, delimiters):
552         return self._read_objects(delimiters)
553
554     def _fetch_changes(self):
555         return self._fetch_objects(["cpos"])
556
557     def _fetch_songs(self):
558         return self._fetch_objects(["file"])
559
560     def _fetch_playlists(self):
561         return self._fetch_objects(["playlist"])
562
563     def _fetch_database(self):
564         return self._fetch_objects(["file", "directory", "playlist"])
565
566     def _fetch_outputs(self):
567         return self._fetch_objects(["outputid"])
568
569     def _fetch_plugins(self):
570         return self._fetch_objects(["plugin"])
571
572     def _fetch_messages(self):
573         return self._fetch_objects(["channel"])
574
575     def _fetch_mounts(self):
576         return self._fetch_objects(["mount"])
577
578     def _fetch_neighbors(self):
579         return self._fetch_objects(["neighbor"])
580
581     def _fetch_composite(self):
582         obj = {}
583         for key, value in self._read_pairs(binary=True):
584             key = key.lower()
585             obj[key] = value
586             if key == 'binary':
587                 break
588         if not obj:
589             # If the song file was recognized, but there is no picture, the
590             # response is successful, but is otherwise empty.
591             return obj
592         amount = int(obj['binary'])
593         try:
594             obj['data'] = self._read_binary(amount)
595         except IOError as err:
596             raise ConnectionError(f'Error reading binary content: {err}') from err
597         data_bytes = len(obj['data'])
598         if data_bytes != amount:  # can we ever get there?
599             raise ConnectionError('Error reading binary content: '
600                     f'Expects {amount}B, got {data_bytes}')
601         # Fetches trailing new line
602         self._read_line(binary=True)
603         # Fetches SUCCESS code
604         self._read_line(binary=True)
605         return obj
606
607     @iterator_wrapper
608     def _fetch_command_list(self):
609         return self._read_command_list()
610
611     def _hello(self):
612         line = self._rfile.readline()
613         if not line.endswith("\n"):
614             raise ConnectionError("Connection lost while reading MPD hello")
615         line = line.rstrip("\n")
616         if not line.startswith(HELLO_PREFIX):
617             raise ProtocolError(f"Got invalid MPD hello: '{line}'")
618         self.mpd_version = line[len(HELLO_PREFIX):].strip()
619
620     def _reset(self):
621         self.mpd_version = ''
622         self._iterating = False
623         self._pending = []
624         self._command_list = None
625         self._sock = None
626         self._rfile = _NotConnected()
627         self._rbfile = _NotConnected()
628         self._wfile = _NotConnected()
629
630     def _connect_unix(self, path):
631         if not hasattr(socket, "AF_UNIX"):
632             raise ConnectionError("Unix domain sockets not supported on this platform")
633         # abstract socket
634         if path.startswith('@'):
635             path = '\0'+path[1:]
636         sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
637         sock.settimeout(self.mpd_timeout)
638         sock.connect(path)
639         sock.settimeout(self.socket_timeout)
640         return sock
641
642     def _connect_tcp(self, host, port):
643         try:
644             flags = socket.AI_ADDRCONFIG
645         except AttributeError:
646             flags = 0
647         err = None
648         for res in socket.getaddrinfo(host, port, socket.AF_UNSPEC,
649                                       socket.SOCK_STREAM, socket.IPPROTO_TCP,
650                                       flags):
651             af, socktype, proto, _, sa = res
652             sock = None
653             try:
654                 sock = socket.socket(af, socktype, proto)
655                 sock.settimeout(self.mpd_timeout)
656                 sock.connect(sa)
657                 sock.settimeout(self.socket_timeout)
658                 return sock
659             except socket.error as socket_err:
660                 err = socket_err
661                 if sock is not None:
662                     sock.close()
663         if err is not None:
664             raise ConnectionError(str(err))
665         raise ConnectionError("getaddrinfo returns an empty list")
666
667     def noidle(self):
668         # noidle's special case
669         if not self._pending or self._pending[0] != 'idle':
670             raise CommandError('cannot send noidle if send_idle was not called')
671         del self._pending[0]
672         self._write_command("noidle")
673         return self._fetch_list()
674
675     def connect(self, host=None, port=None):
676         """Connects the MPD server
677
678         :param str host: hostname, IP or FQDN (defaults to *localhost* or socket)
679         :param port: port number (defaults to *6600*)
680         :type port: str or int
681
682         If host/port are :py:obj:`None` the socket uses :py:attr:`host`/:py:attr:`port`
683         attributes as defaults. Cf. :py:obj:`MPDClient` for the logic behind default host/port.
684
685         The underlying socket also honors :envvar:`MPD_TIMEOUT` environment variable
686         and defaults to :py:obj:`musicpd.CONNECTION_TIMEOUT` (connect command only).
687
688         If you want to have a timeout for each command once you got connected,
689         set its value in :py:obj:`MPDClient.socket_timeout` (in second) or at
690         module level in :py:obj:`musicpd.SOCKET_TIMEOUT`.
691         """
692         if not host:
693             host = self.host
694         else:
695             self.host = host
696         if not port:
697             port = self.port
698         else:
699             self.port = port
700         if self._sock is not None:
701             raise ConnectionError("Already connected")
702         if host[0] in ['/', '@']:
703             self._sock = self._connect_unix(host)
704         else:
705             self._sock = self._connect_tcp(host, port)
706         self._rfile = self._sock.makefile("r", encoding='utf-8', errors='surrogateescape')
707         self._rbfile = self._sock.makefile("rb")
708         self._wfile = self._sock.makefile("w", encoding='utf-8')
709         try:
710             self._hello()
711         except:
712             self.disconnect()
713             raise
714
715     @property
716     def socket_timeout(self):
717         """Socket timeout in second (defaults to :py:obj:`SOCKET_TIMEOUT`).
718         Use None to disable socket timout."""
719         return self._socket_timeout
720
721     @socket_timeout.setter
722     def socket_timeout(self, timeout):
723         self._socket_timeout = timeout
724         if getattr(self._sock, 'settimeout', False):
725             self._sock.settimeout(self._socket_timeout)
726
727     def disconnect(self):
728         """Closes the MPD connection.
729         The client closes the actual socket, it does not use the
730         'close' request from MPD protocol (as suggested in documentation).
731         """
732         if hasattr(self._rfile, 'close'):
733             self._rfile.close()
734         if hasattr(self._rbfile, 'close'):
735             self._rbfile.close()
736         if hasattr(self._wfile, 'close'):
737             self._wfile.close()
738         if hasattr(self._sock, 'close'):
739             self._sock.close()
740         self._reset()
741
742     def __enter__(self):
743         self.connect()
744         return self
745
746     def __exit__(self, exception_type, exception_value, exception_traceback):
747         self.disconnect()
748
749     def fileno(self):
750         """Return the socket’s file descriptor (a small integer).
751         This is useful with :py:obj:`select.select`.
752         """
753         if self._sock is None:
754             raise ConnectionError("Not connected")
755         return self._sock.fileno()
756
757     def command_list_ok_begin(self):
758         if self._command_list is not None:
759             raise CommandListError("Already in command list")
760         if self._iterating:
761             raise IteratingError("Cannot begin command list while iterating")
762         if self._pending:
763             raise PendingCommandError("Cannot begin command list with pending commands")
764         self._write_command("command_list_ok_begin")
765         self._command_list = []
766
767     def command_list_end(self):
768         if self._command_list is None:
769             raise CommandListError("Not in command list")
770         if self._iterating:
771             raise IteratingError("Already iterating over a command list")
772         self._write_command("command_list_end")
773         return self._fetch_command_list()
774
775
776 def escape(text):
777     return text.replace("\\", "\\\\").replace('"', '\\"')
778
779 # vim: set expandtab shiftwidth=4 softtabstop=4 textwidth=79: