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