]> kaliko git repositories - python-musicpd.git/blob - mpd.py
adding mixrampdb and mixrampdelay commands
[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         command = command.replace("_", " ")
161         if command not in self._commands:
162             raise AttributeError("'%s' object has no attribute '%s'" %
163                                  (self.__class__.__name__, attr))
164         return lambda *args: wrapper(command, args)
165
166     def _send(self, command, args):
167         if self._command_list is not None:
168             raise CommandListError("Cannot use send_%s in a command list" %
169                                    command.replace(" ", "_"))
170         self._write_command(command, args)
171         self._pending.append(command)
172
173     def _fetch(self, command, args=None):
174         if self._command_list is not None:
175             raise CommandListError("Cannot use fetch_%s in a command list" %
176                                    command.replace(" ", "_"))
177         if self._iterating:
178             raise IteratingError("Cannot use fetch_%s while iterating" %
179                                  command.replace(" ", "_"))
180         if not self._pending:
181             raise PendingCommandError("No pending commands to fetch")
182         if self._pending[0] != command:
183             raise PendingCommandError("'%s' is not the currently "
184                                       "pending command" % command)
185         del self._pending[0]
186         retval = self._commands[command]
187         if callable(retval):
188             return retval()
189
190     def _execute(self, command, args):
191         if self._iterating:
192             raise IteratingError("Cannot execute '%s' while iterating" %
193                                  command)
194         if self._pending:
195             raise PendingCommandError("Cannot execute '%s' with "
196                                       "pending commands" % command)
197         retval = self._commands[command]
198         if self._command_list is not None:
199             if not callable(retval):
200                 raise CommandListError("'%s' not allowed in command list" %
201                                         command)
202             self._write_command(command, args)
203             self._command_list.append(retval)
204         else:
205             self._write_command(command, args)
206             if callable(retval):
207                 return retval()
208             return retval
209
210     def _write_line(self, line):
211         self._wfile.write("%s\n" % line)
212         self._wfile.flush()
213
214     def _write_command(self, command, args=[]):
215         parts = [command]
216         for arg in args:
217             parts.append('"%s"' % escape(str(arg)))
218         self._write_line(" ".join(parts))
219
220     def _read_line(self):
221         line = self._rfile.readline()
222         if not line.endswith("\n"):
223             raise ConnectionError("Connection lost while reading line")
224         line = line.rstrip("\n")
225         if line.startswith(ERROR_PREFIX):
226             error = line[len(ERROR_PREFIX):].strip()
227             raise CommandError(error)
228         if self._command_list is not None:
229             if line == NEXT:
230                 return
231             if line == SUCCESS:
232                 raise ProtocolError("Got unexpected '%s'" % SUCCESS)
233         elif line == SUCCESS:
234             return
235         return line
236
237     def _read_pair(self, separator):
238         line = self._read_line()
239         if line is None:
240             return
241         pair = line.split(separator, 1)
242         if len(pair) < 2:
243             raise ProtocolError("Could not parse pair: '%s'" % line)
244         return pair
245
246     def _read_pairs(self, separator=": "):
247         pair = self._read_pair(separator)
248         while pair:
249             yield pair
250             pair = self._read_pair(separator)
251
252     def _read_list(self):
253         seen = None
254         for key, value in self._read_pairs():
255             if key != seen:
256                 if seen is not None:
257                     raise ProtocolError("Expected key '%s', got '%s'" %
258                                         (seen, key))
259                 seen = key
260             yield value
261
262     def _read_playlist(self):
263         for key, value in self._read_pairs(":"):
264             yield value
265
266     def _read_objects(self, delimiters=[]):
267         obj = {}
268         for key, value in self._read_pairs():
269             key = key.lower()
270             if obj:
271                 if key in delimiters:
272                     yield obj
273                     obj = {}
274                 elif key in obj:
275                     if not isinstance(obj[key], list):
276                         obj[key] = [obj[key], value]
277                     else:
278                         obj[key].append(value)
279                     continue
280             obj[key] = value
281         if obj:
282             yield obj
283
284     def _read_command_list(self):
285         try:
286             for retval in self._command_list:
287                 yield retval()
288         finally:
289             self._command_list = None
290         self._fetch_nothing()
291
292     def _iterator_wrapper(self, iterator):
293         try:
294             for item in iterator:
295                 yield item
296         finally:
297             self._iterating = False
298
299     def _wrap_iterator(self, iterator):
300         if not self.iterate:
301             return list(iterator)
302         self._iterating = True
303         return self._iterator_wrapper(iterator)
304
305     def _fetch_nothing(self):
306         line = self._read_line()
307         if line is not None:
308             raise ProtocolError("Got unexpected return value: '%s'" % line)
309
310     def _fetch_item(self):
311         pairs = list(self._read_pairs())
312         if len(pairs) != 1:
313             return
314         return pairs[0][1]
315
316     def _fetch_list(self):
317         return self._wrap_iterator(self._read_list())
318
319     def _fetch_playlist(self):
320         return self._wrap_iterator(self._read_playlist())
321
322     def _fetch_object(self):
323         objs = list(self._read_objects())
324         if not objs:
325             return {}
326         return objs[0]
327
328     def _fetch_objects(self, delimiters):
329         return self._wrap_iterator(self._read_objects(delimiters))
330
331     def _fetch_changes(self):
332         return self._fetch_objects(["cpos"])
333
334     def _fetch_songs(self):
335         return self._fetch_objects(["file"])
336
337     def _fetch_playlists(self):
338         return self._fetch_objects(["playlist"])
339
340     def _fetch_database(self):
341         return self._fetch_objects(["file", "directory", "playlist"])
342
343     def _fetch_outputs(self):
344         return self._fetch_objects(["outputid"])
345
346     def _fetch_plugins(self):
347         return self._fetch_objects(["plugin"])
348
349     def _fetch_command_list(self):
350         return self._wrap_iterator(self._read_command_list())
351
352     def _hello(self):
353         line = self._rfile.readline()
354         if not line.endswith("\n"):
355             raise ConnectionError("Connection lost while reading MPD hello")
356         line = line.rstrip("\n")
357         if not line.startswith(HELLO_PREFIX):
358             raise ProtocolError("Got invalid MPD hello: '%s'" % line)
359         self.mpd_version = line[len(HELLO_PREFIX):].strip()
360
361     def _reset(self):
362         self.mpd_version = None
363         self._iterating = False
364         self._pending = []
365         self._command_list = None
366         self._sock = None
367         self._rfile = _NotConnected()
368         self._wfile = _NotConnected()
369
370     def _connect_unix(self, path):
371         if not hasattr(socket, "AF_UNIX"):
372             raise ConnectionError("Unix domain sockets not supported "
373                                   "on this platform")
374         sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
375         sock.connect(path)
376         return sock
377
378     def _connect_tcp(self, host, port):
379         try:
380             flags = socket.AI_ADDRCONFIG
381         except AttributeError:
382             flags = 0
383         err = None
384         for res in socket.getaddrinfo(host, port, socket.AF_UNSPEC,
385                                       socket.SOCK_STREAM, socket.IPPROTO_TCP,
386                                       flags):
387             af, socktype, proto, canonname, sa = res
388             sock = None
389             try:
390                 sock = socket.socket(af, socktype, proto)
391                 sock.connect(sa)
392                 return sock
393             except socket.error, err:
394                 if sock is not None:
395                     sock.close()
396         if err is not None:
397             raise err
398         else:
399             raise ConnectionError("getaddrinfo returns an empty list")
400
401     def connect(self, host, port):
402         if self._sock is not None:
403             raise ConnectionError("Already connected")
404         if host.startswith("/"):
405             self._sock = self._connect_unix(host)
406         else:
407             self._sock = self._connect_tcp(host, port)
408         self._rfile = self._sock.makefile("rb")
409         self._wfile = self._sock.makefile("wb")
410         try:
411             self._hello()
412         except:
413             self.disconnect()
414             raise
415
416     def disconnect(self):
417         self._rfile.close()
418         self._wfile.close()
419         self._sock.close()
420         self._reset()
421
422     def fileno(self):
423         if self._sock is None:
424             raise ConnectionError("Not connected")
425         return self._sock.fileno()
426
427     def command_list_ok_begin(self):
428         if self._command_list is not None:
429             raise CommandListError("Already in command list")
430         if self._iterating:
431             raise IteratingError("Cannot begin command list while iterating")
432         if self._pending:
433             raise PendingCommandError("Cannot begin command list "
434                                       "with pending commands")
435         self._write_command("command_list_ok_begin")
436         self._command_list = []
437
438     def command_list_end(self):
439         if self._command_list is None:
440             raise CommandListError("Not in command list")
441         if self._iterating:
442             raise IteratingError("Already iterating over a command list")
443         self._write_command("command_list_end")
444         return self._fetch_command_list()
445
446
447 def escape(text):
448     return text.replace("\\", "\\\\").replace('"', '\\"')
449
450
451 # vim: set expandtab shiftwidth=4 softtabstop=4 textwidth=79: