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