1 # Python MPD client library
2 # Copyright (C) 2008 J. Alexander Treuman <jat@spatialrift.net>
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.
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.
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/>.
20 HELLO_PREFIX = "OK MPD "
26 class MPDError(Exception):
29 class ConnectionError(MPDError):
32 class ProtocolError(MPDError):
35 class CommandError(MPDError):
38 class CommandListError(MPDError):
41 class PendingCommandError(MPDError):
45 class _NotConnected(object):
46 def __getattr__(self, attr):
50 raise ConnectionError("Not connected")
52 class MPDClient(object):
58 "clearerror": self._fetch_nothing,
59 "currentsong": self._fetch_object,
60 "idle": self._fetch_list,
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,
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,
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
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,
136 def __getattr__(self, attr):
137 if attr.startswith("send_"):
138 command = attr.replace("send_", "", 1)
140 elif attr.startswith("fetch_"):
141 command = attr.replace("fetch_", "", 1)
142 wrapper = self._fetch
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)
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" %
155 self._write_command(command, args)
156 self._pending.append(command)
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" %
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)
168 retval = self._commands[command]
172 def _execute(self, command, args):
174 raise PendingCommandError("Cannot execute %s with "
175 "pending commands" % command)
176 retval = self._commands[command]
177 if self._command_list is not None and not callable(retval):
178 raise CommandListError("%s not allowed in command list" % command)
179 self._write_command(command, args)
180 if self._command_list is None:
184 self._command_list.append(retval)
186 def _write_line(self, line):
187 self._wfile.write("%s\n" % line)
190 def _write_command(self, command, args=[]):
193 parts.append('"%s"' % escape(str(arg)))
194 self._write_line(" ".join(parts))
196 def _read_line(self):
197 line = self._rfile.readline()
198 if not line.endswith("\n"):
199 raise ConnectionError("Connection lost while reading line")
200 line = line.rstrip("\n")
201 if line.startswith(ERROR_PREFIX):
202 error = line[len(ERROR_PREFIX):].strip()
203 raise CommandError(error)
204 if self._command_list is not None:
208 raise ProtocolError("Got unexpected '%s'" % SUCCESS)
209 elif line == SUCCESS:
213 def _read_pair(self, separator):
214 line = self._read_line()
217 pair = line.split(separator, 1)
219 raise ProtocolError("Could not parse pair: '%s'" % line)
222 def _read_pairs(self, separator=": "):
223 pair = self._read_pair(separator)
226 pair = self._read_pair(separator)
229 def _read_list(self):
231 for key, value in self._read_pairs():
234 raise ProtocolError("Expected key '%s', got '%s'" %
240 def _read_playlist(self):
241 for key, value in self._read_pairs(":"):
245 def _read_objects(self, delimiters=[]):
247 for key, value in self._read_pairs():
250 if key in delimiters:
254 if not isinstance(obj[key], list):
255 obj[key] = [obj[key], value]
257 obj[key].append(value)
264 def _read_command_list(self):
265 for retval in self._command_list:
267 self._command_list = None
268 self._fetch_nothing()
271 def _wrap_iterator(self, iterator):
273 return list(iterator)
276 def _fetch_nothing(self):
277 line = self._read_line()
279 raise ProtocolError("Got unexpected return value: '%s'" % line)
281 def _fetch_item(self):
282 pairs = list(self._read_pairs())
287 def _fetch_list(self):
288 return self._wrap_iterator(self._read_list())
290 def _fetch_playlist(self):
291 return self._wrap_iterator(self._read_playlist())
293 def _fetch_object(self):
294 objs = list(self._read_objects())
299 def _fetch_objects(self, delimiters):
300 return self._wrap_iterator(self._read_objects(delimiters))
302 def _fetch_songs(self):
303 return self._fetch_objects(["file"])
305 def _fetch_playlists(self):
306 return self._fetch_objects(["playlist"])
308 def _fetch_database(self):
309 return self._fetch_objects(["file", "directory", "playlist"])
311 def _fetch_outputs(self):
312 return self._fetch_objects(["outputid"])
314 def _fetch_changes(self):
315 return self._fetch_objects(["cpos"])
317 def _fetch_command_list(self):
318 return self._wrap_iterator(self._read_command_list())
321 line = self._rfile.readline()
322 if not line.endswith("\n"):
323 raise ConnectionError("Connection lost while reading MPD hello")
324 line = line.rstrip("\n")
325 if not line.startswith(HELLO_PREFIX):
326 raise ProtocolError("Got invalid MPD hello: '%s'" % line)
327 self.mpd_version = line[len(HELLO_PREFIX):].strip()
330 self.mpd_version = None
332 self._command_list = None
334 self._rfile = _NotConnected()
335 self._wfile = _NotConnected()
337 def _connect_unix(self, path):
338 if not hasattr(socket, "AF_UNIX"):
339 raise ConnectionError("Unix domain sockets not supported "
341 sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
345 def _connect_tcp(self, host, port):
347 flags = socket.AI_ADDRCONFIG
348 except AttributeError:
350 msg = "getaddrinfo returns an empty list"
351 for res in socket.getaddrinfo(host, port, socket.AF_UNSPEC,
352 socket.SOCK_STREAM, socket.IPPROTO_TCP,
354 af, socktype, proto, canonname, sa = res
356 sock = socket.socket(af, socktype, proto)
358 except socket.error, msg:
365 raise socket.error(msg)
368 def connect(self, host, port):
370 raise ConnectionError("Already connected")
371 if host.startswith("/"):
372 self._sock = self._connect_unix(host)
374 self._sock = self._connect_tcp(host, port)
375 self._rfile = self._sock.makefile("rb")
376 self._wfile = self._sock.makefile("wb")
383 def disconnect(self):
389 def command_list_ok_begin(self):
390 if self._command_list is not None:
391 raise CommandListError("Already in command list")
393 raise PendingCommandError("Cannot begin command list "
394 "with pending commands")
395 self._write_command("command_list_ok_begin")
396 self._command_list = []
398 def command_list_end(self):
399 if self._command_list is None:
400 raise CommandListError("Not in command list")
401 self._write_command("command_list_end")
402 return self._fetch_command_list()
406 return text.replace("\\", "\\\\").replace('"', '\\"')
409 # vim: set expandtab shiftwidth=4 softtabstop=4 textwidth=79: