]> kaliko git repositories - python-musicpd.git/blob - mpd.py
mpd.py: updating _connect_tcp() with new socket code
[python-musicpd.git] / mpd.py
1 # Python MPD client library
2 # Copyright (C) 2008-2010  J. Alexander Treuman <jat@spatialrift.net>
3 #
4 # This program is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation, either version 3 of the License, or
7 # (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License
15 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
16
17 import socket
18
19
20 HELLO_PREFIX = "OK MPD "
21 ERROR_PREFIX = "ACK "
22 SUCCESS = "OK"
23 NEXT = "list_OK"
24
25
26 class MPDError(Exception):
27     pass
28
29 class ConnectionError(MPDError):
30     pass
31
32 class ProtocolError(MPDError):
33     pass
34
35 class CommandError(MPDError):
36     pass
37
38 class CommandListError(MPDError):
39     pass
40
41 class PendingCommandError(MPDError):
42     pass
43
44 class IteratingError(MPDError):
45     pass
46
47
48 class _NotConnected(object):
49     def __getattr__(self, attr):
50         return self._dummy
51
52     def _dummy(*args):
53         raise ConnectionError("Not connected")
54
55 class MPDClient(object):
56     def __init__(self):
57         self.iterate = False
58         self._reset()
59         self._commands = {
60             # Status Commands
61             "clearerror":       self._fetch_nothing,
62             "currentsong":      self._fetch_object,
63             "idle":             self._fetch_list,
64             "noidle":           None,
65             "status":           self._fetch_object,
66             "stats":            self._fetch_object,
67             # Playback Option Commands
68             "consume":          self._fetch_nothing,
69             "crossfade":        self._fetch_nothing,
70             "random":           self._fetch_nothing,
71             "repeat":           self._fetch_nothing,
72             "setvol":           self._fetch_nothing,
73             "single":           self._fetch_nothing,
74             "volume":           self._fetch_nothing,
75             # Playback Control Commands
76             "next":             self._fetch_nothing,
77             "pause":            self._fetch_nothing,
78             "play":             self._fetch_nothing,
79             "playid":           self._fetch_nothing,
80             "previous":         self._fetch_nothing,
81             "seek":             self._fetch_nothing,
82             "seekid":           self._fetch_nothing,
83             "stop":             self._fetch_nothing,
84             # Playlist Commands
85             "add":              self._fetch_nothing,
86             "addid":            self._fetch_item,
87             "clear":            self._fetch_nothing,
88             "delete":           self._fetch_nothing,
89             "deleteid":         self._fetch_nothing,
90             "move":             self._fetch_nothing,
91             "moveid":           self._fetch_nothing,
92             "playlist":         self._fetch_playlist,
93             "playlistfind":     self._fetch_songs,
94             "playlistid":       self._fetch_songs,
95             "playlistinfo":     self._fetch_songs,
96             "playlistsearch":   self._fetch_songs,
97             "plchanges":        self._fetch_songs,
98             "plchangesposid":   self._fetch_changes,
99             "shuffle":          self._fetch_nothing,
100             "swap":             self._fetch_nothing,
101             "swapid":           self._fetch_nothing,
102             # Stored Playlist Commands
103             "listplaylist":     self._fetch_list,
104             "listplaylistinfo": self._fetch_songs,
105             "listplaylists":    self._fetch_playlists,
106             "load":             self._fetch_nothing,
107             "playlistadd":      self._fetch_nothing,
108             "playlistclear":    self._fetch_nothing,
109             "playlistdelete":   self._fetch_nothing,
110             "playlistmove":     self._fetch_nothing,
111             "rename":           self._fetch_nothing,
112             "rm":               self._fetch_nothing,
113             "save":             self._fetch_nothing,
114             # Database Commands
115             "count":            self._fetch_object,
116             "find":             self._fetch_songs,
117             "list":             self._fetch_list,
118             "listall":          self._fetch_database,
119             "listallinfo":      self._fetch_database,
120             "lsinfo":           self._fetch_database,
121             "search":           self._fetch_songs,
122             "update":           self._fetch_item,
123             # Connection Commands
124             "close":            None,
125             "kill":             None,
126             "password":         self._fetch_nothing,
127             "ping":             self._fetch_nothing,
128             # Audio Output Commands
129             "disableoutput":    self._fetch_nothing,
130             "enableoutput":     self._fetch_nothing,
131             "outputs":          self._fetch_outputs,
132             # Reflection Commands
133             "commands":         self._fetch_list,
134             "notcommands":      self._fetch_list,
135             "tagtypes":         self._fetch_list,
136             "urlhandlers":      self._fetch_list,
137         }
138
139     def __getattr__(self, attr):
140         if attr.startswith("send_"):
141             command = attr.replace("send_", "", 1)
142             wrapper = self._send
143         elif attr.startswith("fetch_"):
144             command = attr.replace("fetch_", "", 1)
145             wrapper = self._fetch
146         else:
147             command = attr
148             wrapper = self._execute
149         if command not in self._commands:
150             raise AttributeError("'%s' object has no attribute '%s'" %
151                                  (self.__class__.__name__, attr))
152         return lambda *args: wrapper(command, args)
153
154     def _send(self, command, args):
155         if self._command_list is not None:
156             raise CommandListError("Cannot use send_%s in a command list" %
157                                    command)
158         self._write_command(command, args)
159         self._pending.append(command)
160
161     def _fetch(self, command, args=None):
162         if self._command_list is not None:
163             raise CommandListError("Cannot use fetch_%s in a command list" %
164                                    command)
165         if self._iterating:
166             raise IteratingError("Cannot use fetch_%s while iterating" %
167                                  command)
168         if not self._pending:
169             raise PendingCommandError("No pending commands to fetch")
170         if self._pending[0] != command:
171             raise PendingCommandError("%s is not the currently "
172                                       "pending command" % command)
173         del self._pending[0]
174         retval = self._commands[command]
175         if callable(retval):
176             return retval()
177
178     def _execute(self, command, args):
179         if self._iterating:
180             raise IteratingError("Cannot execute %s while iterating" % command)
181         if self._pending:
182             raise PendingCommandError("Cannot execute %s with "
183                                       "pending commands" % command)
184         retval = self._commands[command]
185         if self._command_list is not None:
186             if not callable(retval):
187                 raise CommandListError("%s not allowed in command list" %
188                                         command)
189             self._write_command(command, args)
190             self._command_list.append(retval)
191         else:
192             self._write_command(command, args)
193             if callable(retval):
194                 return retval()
195             return retval
196
197     def _write_line(self, line):
198         self._wfile.write("%s\n" % line)
199         self._wfile.flush()
200
201     def _write_command(self, command, args=[]):
202         parts = [command]
203         for arg in args:
204             parts.append('"%s"' % escape(str(arg)))
205         self._write_line(" ".join(parts))
206
207     def _read_line(self):
208         line = self._rfile.readline()
209         if not line.endswith("\n"):
210             raise ConnectionError("Connection lost while reading line")
211         line = line.rstrip("\n")
212         if line.startswith(ERROR_PREFIX):
213             error = line[len(ERROR_PREFIX):].strip()
214             raise CommandError(error)
215         if self._command_list is not None:
216             if line == NEXT:
217                 return
218             if line == SUCCESS:
219                 raise ProtocolError("Got unexpected '%s'" % SUCCESS)
220         elif line == SUCCESS:
221             return
222         return line
223
224     def _read_pair(self, separator):
225         line = self._read_line()
226         if line is None:
227             return
228         pair = line.split(separator, 1)
229         if len(pair) < 2:
230             raise ProtocolError("Could not parse pair: '%s'" % line)
231         return pair
232
233     def _read_pairs(self, separator=": "):
234         pair = self._read_pair(separator)
235         while pair:
236             yield pair
237             pair = self._read_pair(separator)
238
239     def _read_list(self):
240         seen = None
241         for key, value in self._read_pairs():
242             if key != seen:
243                 if seen is not None:
244                     raise ProtocolError("Expected key '%s', got '%s'" %
245                                         (seen, key))
246                 seen = key
247             yield value
248
249     def _read_playlist(self):
250         for key, value in self._read_pairs(":"):
251             yield value
252
253     def _read_objects(self, delimiters=[]):
254         obj = {}
255         for key, value in self._read_pairs():
256             key = key.lower()
257             if obj:
258                 if key in delimiters:
259                     yield obj
260                     obj = {}
261                 elif key in obj:
262                     if not isinstance(obj[key], list):
263                         obj[key] = [obj[key], value]
264                     else:
265                         obj[key].append(value)
266                     continue
267             obj[key] = value
268         if obj:
269             yield obj
270
271     def _read_command_list(self):
272         try:
273             for retval in self._command_list:
274                 yield retval()
275         finally:
276             self._command_list = None
277         self._fetch_nothing()
278
279     def _iterator_wrapper(self, iterator):
280         try:
281             for item in iterator:
282                 yield item
283         finally:
284             self._iterating = False
285
286     def _wrap_iterator(self, iterator):
287         if not self.iterate:
288             return list(iterator)
289         self._iterating = True
290         return self._iterator_wrapper(iterator)
291
292     def _fetch_nothing(self):
293         line = self._read_line()
294         if line is not None:
295             raise ProtocolError("Got unexpected return value: '%s'" % line)
296
297     def _fetch_item(self):
298         pairs = list(self._read_pairs())
299         if len(pairs) != 1:
300             return
301         return pairs[0][1]
302
303     def _fetch_list(self):
304         return self._wrap_iterator(self._read_list())
305
306     def _fetch_playlist(self):
307         return self._wrap_iterator(self._read_playlist())
308
309     def _fetch_object(self):
310         objs = list(self._read_objects())
311         if not objs:
312             return {}
313         return objs[0]
314
315     def _fetch_objects(self, delimiters):
316         return self._wrap_iterator(self._read_objects(delimiters))
317
318     def _fetch_songs(self):
319         return self._fetch_objects(["file"])
320
321     def _fetch_playlists(self):
322         return self._fetch_objects(["playlist"])
323
324     def _fetch_database(self):
325         return self._fetch_objects(["file", "directory", "playlist"])
326
327     def _fetch_outputs(self):
328         return self._fetch_objects(["outputid"])
329
330     def _fetch_changes(self):
331         return self._fetch_objects(["cpos"])
332
333     def _fetch_command_list(self):
334         return self._wrap_iterator(self._read_command_list())
335
336     def _hello(self):
337         line = self._rfile.readline()
338         if not line.endswith("\n"):
339             raise ConnectionError("Connection lost while reading MPD hello")
340         line = line.rstrip("\n")
341         if not line.startswith(HELLO_PREFIX):
342             raise ProtocolError("Got invalid MPD hello: '%s'" % line)
343         self.mpd_version = line[len(HELLO_PREFIX):].strip()
344
345     def _reset(self):
346         self.mpd_version = None
347         self._iterating = False
348         self._pending = []
349         self._command_list = None
350         self._sock = None
351         self._rfile = _NotConnected()
352         self._wfile = _NotConnected()
353
354     def _connect_unix(self, path):
355         if not hasattr(socket, "AF_UNIX"):
356             raise ConnectionError("Unix domain sockets not supported "
357                                   "on this platform")
358         sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
359         sock.connect(path)
360         return sock
361
362     def _connect_tcp(self, host, port):
363         try:
364             flags = socket.AI_ADDRCONFIG
365         except AttributeError:
366             flags = 0
367         err = None
368         for res in socket.getaddrinfo(host, port, socket.AF_UNSPEC,
369                                       socket.SOCK_STREAM, socket.IPPROTO_TCP,
370                                       flags):
371             af, socktype, proto, canonname, sa = res
372             sock = None
373             try:
374                 sock = socket.socket(af, socktype, proto)
375                 sock.connect(sa)
376                 return sock
377             except socket.error, err:
378                 if sock is not None:
379                     sock.close()
380         if err is not None:
381             raise err
382         else:
383             raise ConnectionError("getaddrinfo returns an empty list")
384
385     def connect(self, host, port):
386         if self._sock:
387             raise ConnectionError("Already connected")
388         if host.startswith("/"):
389             self._sock = self._connect_unix(host)
390         else:
391             self._sock = self._connect_tcp(host, port)
392         self._rfile = self._sock.makefile("rb")
393         self._wfile = self._sock.makefile("wb")
394         try:
395             self._hello()
396         except:
397             self.disconnect()
398             raise
399
400     def disconnect(self):
401         self._rfile.close()
402         self._wfile.close()
403         self._sock.close()
404         self._reset()
405
406     def fileno(self):
407         if not self._sock:
408             raise ConnectionError("Not connected")
409         return self._sock.fileno()
410
411     def command_list_ok_begin(self):
412         if self._command_list is not None:
413             raise CommandListError("Already in command list")
414         if self._iterating:
415             raise IteratingError("Cannot begin command list while iterating")
416         if self._pending:
417             raise PendingCommandError("Cannot begin command list "
418                                       "with pending commands")
419         self._write_command("command_list_ok_begin")
420         self._command_list = []
421
422     def command_list_end(self):
423         if self._command_list is None:
424             raise CommandListError("Not in command list")
425         if self._iterating:
426             raise IteratingError("Already iterating over a command list")
427         self._write_command("command_list_end")
428         return self._fetch_command_list()
429
430
431 def escape(text):
432     return text.replace("\\", "\\\\").replace('"', '\\"')
433
434
435 # vim: set expandtab shiftwidth=4 softtabstop=4 textwidth=79: