]> kaliko git repositories - python-musicpd.git/blob - mpd.py
README.txt: give an example that actually works
[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
45 class _NotConnected(object):
46     def __getattr__(self, attr):
47         return self._dummy
48
49     def _dummy(*args):
50         raise ConnectionError("Not connected")
51
52 class MPDClient(object):
53     def __init__(self):
54         self.iterate = False
55         self._reset()
56         self._commands = {
57             # Status Commands
58             "clearerror":       self._fetch_nothing,
59             "currentsong":      self._fetch_object,
60             "idle":             self._fetch_list,
61             "noidle":           None,
62             "status":           self._fetch_object,
63             "stats":            self._fetch_object,
64             # Playback Option Commands
65             "consume":          self._fetch_nothing,
66             "crossfade":        self._fetch_nothing,
67             "random":           self._fetch_nothing,
68             "repeat":           self._fetch_nothing,
69             "setvol":           self._fetch_nothing,
70             "single":           self._fetch_nothing,
71             "volume":           self._fetch_nothing,
72             # Playback Control Commands
73             "next":             self._fetch_nothing,
74             "pause":            self._fetch_nothing,
75             "play":             self._fetch_nothing,
76             "playid":           self._fetch_nothing,
77             "previous":         self._fetch_nothing,
78             "seek":             self._fetch_nothing,
79             "seekid":           self._fetch_nothing,
80             "stop":             self._fetch_nothing,
81             # Playlist Commands
82             "add":              self._fetch_nothing,
83             "addid":            self._fetch_item,
84             "clear":            self._fetch_nothing,
85             "delete":           self._fetch_nothing,
86             "deleteid":         self._fetch_nothing,
87             "move":             self._fetch_nothing,
88             "moveid":           self._fetch_nothing,
89             "playlist":         self._fetch_playlist,
90             "playlistfind":     self._fetch_songs,
91             "playlistid":       self._fetch_songs,
92             "playlistinfo":     self._fetch_songs,
93             "playlistsearch":   self._fetch_songs,
94             "plchanges":        self._fetch_songs,
95             "plchangesposid":   self._fetch_changes,
96             "shuffle":          self._fetch_nothing,
97             "swap":             self._fetch_nothing,
98             "swapid":           self._fetch_nothing,
99             # Stored Playlist Commands
100             "listplaylist":     self._fetch_list,
101             "listplaylistinfo": self._fetch_songs,
102             "listplaylists":    self._fetch_playlists,
103             "load":             self._fetch_nothing,
104             "playlistadd":      self._fetch_nothing,
105             "playlistclear":    self._fetch_nothing,
106             "playlistdelete":   self._fetch_nothing,
107             "playlistmove":     self._fetch_nothing,
108             "rename":           self._fetch_nothing,
109             "rm":               self._fetch_nothing,
110             "save":             self._fetch_nothing,
111             # Database Commands
112             "count":            self._fetch_object,
113             "find":             self._fetch_songs,
114             "list":             self._fetch_list,
115             "listall":          self._fetch_database,
116             "listallinfo":      self._fetch_database,
117             "lsinfo":           self._fetch_database,
118             "search":           self._fetch_songs,
119             "update":           self._fetch_item,
120             # Connection Commands
121             "close":            None,
122             "kill":             None,
123             "password":         self._fetch_nothing,
124             "ping":             self._fetch_nothing,
125             # Audio Output Commands
126             "disableoutput":    self._fetch_nothing,
127             "enableoutput":     self._fetch_nothing,
128             "outputs":          self._fetch_outputs,
129             # Reflection Commands
130             "commands":         self._fetch_list,
131             "notcommands":      self._fetch_list,
132             "tagtypes":         self._fetch_list,
133             "urlhandlers":      self._fetch_list,
134         }
135
136     def __getattr__(self, attr):
137         if attr.startswith("send_"):
138             command = attr.replace("send_", "", 1)
139             wrapper = self._send
140         elif attr.startswith("fetch_"):
141             command = attr.replace("fetch_", "", 1)
142             wrapper = self._fetch
143         else:
144             command = attr
145             wrapper = self._execute
146         if command not in self._commands:
147             raise AttributeError("'%s' object has no attribute '%s'" %
148                                  (self.__class__.__name__, attr))
149         return lambda *args: wrapper(command, args)
150
151     def _send(self, command, args):
152         if self._command_list is not None:
153             raise CommandListError("Cannot use send_%s in a command list" %
154                                    command)
155         self._write_command(command, args)
156         self._pending.append(command)
157
158     def _fetch(self, command, args=None):
159         if self._command_list is not None:
160             raise CommandListError("Cannot use fetch_%s in a command list" %
161                                    command)
162         if not self._pending:
163             raise PendingCommandError("No pending commands to fetch")
164         if self._pending[0] != command:
165             raise PendingCommandError("%s is not the currently "
166                                       "pending command" % command)
167         del self._pending[0]
168         retval = self._commands[command]
169         if callable(retval):
170             return retval()
171
172     def _execute(self, command, args):
173         if self._pending:
174             raise PendingCommandError("Cannot execute %s with "
175                                       "pending commands" % command)
176         retval = self._commands[command]
177         if self._command_list is not None:
178             if not callable(retval):
179                 raise CommandListError("%s not allowed in command list" %
180                                         command)
181             self._write_command(command, args)
182             self._command_list.append(retval)
183         else:
184             self._write_command(command, args)
185             if callable(retval):
186                 return retval()
187             return retval
188
189     def _write_line(self, line):
190         self._wfile.write("%s\n" % line)
191         self._wfile.flush()
192
193     def _write_command(self, command, args=[]):
194         parts = [command]
195         for arg in args:
196             parts.append('"%s"' % escape(str(arg)))
197         self._write_line(" ".join(parts))
198
199     def _read_line(self):
200         line = self._rfile.readline()
201         if not line.endswith("\n"):
202             raise ConnectionError("Connection lost while reading line")
203         line = line.rstrip("\n")
204         if line.startswith(ERROR_PREFIX):
205             error = line[len(ERROR_PREFIX):].strip()
206             raise CommandError(error)
207         if self._command_list is not None:
208             if line == NEXT:
209                 return
210             if line == SUCCESS:
211                 raise ProtocolError("Got unexpected '%s'" % SUCCESS)
212         elif line == SUCCESS:
213             return
214         return line
215
216     def _read_pair(self, separator):
217         line = self._read_line()
218         if line is None:
219             return
220         pair = line.split(separator, 1)
221         if len(pair) < 2:
222             raise ProtocolError("Could not parse pair: '%s'" % line)
223         return pair
224
225     def _read_pairs(self, separator=": "):
226         pair = self._read_pair(separator)
227         while pair:
228             yield pair
229             pair = self._read_pair(separator)
230         raise StopIteration
231
232     def _read_list(self):
233         seen = None
234         for key, value in self._read_pairs():
235             if key != seen:
236                 if seen is not None:
237                     raise ProtocolError("Expected key '%s', got '%s'" %
238                                         (seen, key))
239                 seen = key
240             yield value
241         raise StopIteration
242
243     def _read_playlist(self):
244         for key, value in self._read_pairs(":"):
245             yield value
246         raise StopIteration
247
248     def _read_objects(self, delimiters=[]):
249         obj = {}
250         for key, value in self._read_pairs():
251             key = key.lower()
252             if obj:
253                 if key in delimiters:
254                     yield obj
255                     obj = {}
256                 elif key in obj:
257                     if not isinstance(obj[key], list):
258                         obj[key] = [obj[key], value]
259                     else:
260                         obj[key].append(value)
261                     continue
262             obj[key] = value
263         if obj:
264             yield obj
265         raise StopIteration
266
267     def _read_command_list(self):
268         for retval in self._command_list:
269             yield retval()
270         self._command_list = None
271         self._fetch_nothing()
272         raise StopIteration
273
274     def _wrap_iterator(self, iterator):
275         if not self.iterate:
276             return list(iterator)
277         return iterator
278
279     def _fetch_nothing(self):
280         line = self._read_line()
281         if line is not None:
282             raise ProtocolError("Got unexpected return value: '%s'" % line)
283
284     def _fetch_item(self):
285         pairs = list(self._read_pairs())
286         if len(pairs) != 1:
287             return
288         return pairs[0][1]
289
290     def _fetch_list(self):
291         return self._wrap_iterator(self._read_list())
292
293     def _fetch_playlist(self):
294         return self._wrap_iterator(self._read_playlist())
295
296     def _fetch_object(self):
297         objs = list(self._read_objects())
298         if not objs:
299             return {}
300         return objs[0]
301
302     def _fetch_objects(self, delimiters):
303         return self._wrap_iterator(self._read_objects(delimiters))
304
305     def _fetch_songs(self):
306         return self._fetch_objects(["file"])
307
308     def _fetch_playlists(self):
309         return self._fetch_objects(["playlist"])
310
311     def _fetch_database(self):
312         return self._fetch_objects(["file", "directory", "playlist"])
313
314     def _fetch_outputs(self):
315         return self._fetch_objects(["outputid"])
316
317     def _fetch_changes(self):
318         return self._fetch_objects(["cpos"])
319
320     def _fetch_command_list(self):
321         return self._wrap_iterator(self._read_command_list())
322
323     def _hello(self):
324         line = self._rfile.readline()
325         if not line.endswith("\n"):
326             raise ConnectionError("Connection lost while reading MPD hello")
327         line = line.rstrip("\n")
328         if not line.startswith(HELLO_PREFIX):
329             raise ProtocolError("Got invalid MPD hello: '%s'" % line)
330         self.mpd_version = line[len(HELLO_PREFIX):].strip()
331
332     def _reset(self):
333         self.mpd_version = None
334         self._pending = []
335         self._command_list = None
336         self._sock = None
337         self._rfile = _NotConnected()
338         self._wfile = _NotConnected()
339
340     def _connect_unix(self, path):
341         if not hasattr(socket, "AF_UNIX"):
342             raise ConnectionError("Unix domain sockets not supported "
343                                   "on this platform")
344         sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
345         sock.connect(path)
346         return sock
347
348     def _connect_tcp(self, host, port):
349         try:
350             flags = socket.AI_ADDRCONFIG
351         except AttributeError:
352             flags = 0
353         msg = "getaddrinfo returns an empty list"
354         for res in socket.getaddrinfo(host, port, socket.AF_UNSPEC,
355                                       socket.SOCK_STREAM, socket.IPPROTO_TCP,
356                                       flags):
357             af, socktype, proto, canonname, sa = res
358             try:
359                 sock = socket.socket(af, socktype, proto)
360                 sock.connect(sa)
361             except socket.error, msg:
362                 if sock:
363                     sock.close()
364                 sock = None
365                 continue
366             break
367         if not sock:
368             raise socket.error(msg)
369         return sock
370
371     def connect(self, host, port):
372         if self._sock:
373             raise ConnectionError("Already connected")
374         if host.startswith("/"):
375             self._sock = self._connect_unix(host)
376         else:
377             self._sock = self._connect_tcp(host, port)
378         self._rfile = self._sock.makefile("rb")
379         self._wfile = self._sock.makefile("wb")
380         try:
381             self._hello()
382         except:
383             self.disconnect()
384             raise
385
386     def disconnect(self):
387         self._rfile.close()
388         self._wfile.close()
389         self._sock.close()
390         self._reset()
391
392     def fileno(self):
393         if not self._sock:
394             raise ConnectionError("Not connected")
395         return self._sock.fileno()
396
397     def command_list_ok_begin(self):
398         if self._command_list is not None:
399             raise CommandListError("Already in command list")
400         if self._pending:
401             raise PendingCommandError("Cannot begin command list "
402                                       "with pending commands")
403         self._write_command("command_list_ok_begin")
404         self._command_list = []
405
406     def command_list_end(self):
407         if self._command_list is None:
408             raise CommandListError("Not in command list")
409         self._write_command("command_list_end")
410         return self._fetch_command_list()
411
412
413 def escape(text):
414     return text.replace("\\", "\\\\").replace('"', '\\"')
415
416
417 # vim: set expandtab shiftwidth=4 softtabstop=4 textwidth=79: