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