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