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