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:
178 if not callable(retval):
179 raise CommandListError("%s not allowed in command list" %
181 self._write_command(command, args)
182 self._command_list.append(retval)
184 self._write_command(command, args)
189 def _write_line(self, line):
190 self._wfile.write("%s\n" % line)
193 def _write_command(self, command, args=[]):
196 parts.append('"%s"' % escape(str(arg)))
197 self._write_line(" ".join(parts))
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:
211 raise ProtocolError("Got unexpected '%s'" % SUCCESS)
212 elif line == SUCCESS:
216 def _read_pair(self, separator):
217 line = self._read_line()
220 pair = line.split(separator, 1)
222 raise ProtocolError("Could not parse pair: '%s'" % line)
225 def _read_pairs(self, separator=": "):
226 pair = self._read_pair(separator)
229 pair = self._read_pair(separator)
232 def _read_list(self):
234 for key, value in self._read_pairs():
237 raise ProtocolError("Expected key '%s', got '%s'" %
243 def _read_playlist(self):
244 for key, value in self._read_pairs(":"):
248 def _read_objects(self, delimiters=[]):
250 for key, value in self._read_pairs():
253 if key in delimiters:
257 if not isinstance(obj[key], list):
258 obj[key] = [obj[key], value]
260 obj[key].append(value)
267 def _read_command_list(self):
268 for retval in self._command_list:
270 self._command_list = None
271 self._fetch_nothing()
274 def _wrap_iterator(self, iterator):
276 return list(iterator)
279 def _fetch_nothing(self):
280 line = self._read_line()
282 raise ProtocolError("Got unexpected return value: '%s'" % line)
284 def _fetch_item(self):
285 pairs = list(self._read_pairs())
290 def _fetch_list(self):
291 return self._wrap_iterator(self._read_list())
293 def _fetch_playlist(self):
294 return self._wrap_iterator(self._read_playlist())
296 def _fetch_object(self):
297 objs = list(self._read_objects())
302 def _fetch_objects(self, delimiters):
303 return self._wrap_iterator(self._read_objects(delimiters))
305 def _fetch_songs(self):
306 return self._fetch_objects(["file"])
308 def _fetch_playlists(self):
309 return self._fetch_objects(["playlist"])
311 def _fetch_database(self):
312 return self._fetch_objects(["file", "directory", "playlist"])
314 def _fetch_outputs(self):
315 return self._fetch_objects(["outputid"])
317 def _fetch_changes(self):
318 return self._fetch_objects(["cpos"])
320 def _fetch_command_list(self):
321 return self._wrap_iterator(self._read_command_list())
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()
333 self.mpd_version = None
335 self._command_list = None
337 self._rfile = _NotConnected()
338 self._wfile = _NotConnected()
340 def _connect_unix(self, path):
341 if not hasattr(socket, "AF_UNIX"):
342 raise ConnectionError("Unix domain sockets not supported "
344 sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
348 def _connect_tcp(self, host, port):
350 flags = socket.AI_ADDRCONFIG
351 except AttributeError:
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,
357 af, socktype, proto, canonname, sa = res
359 sock = socket.socket(af, socktype, proto)
361 except socket.error, msg:
368 raise socket.error(msg)
371 def connect(self, host, port):
373 raise ConnectionError("Already connected")
374 if host.startswith("/"):
375 self._sock = self._connect_unix(host)
377 self._sock = self._connect_tcp(host, port)
378 self._rfile = self._sock.makefile("rb")
379 self._wfile = self._sock.makefile("wb")
386 def disconnect(self):
392 def command_list_ok_begin(self):
393 if self._command_list is not None:
394 raise CommandListError("Already in command list")
396 raise PendingCommandError("Cannot begin command list "
397 "with pending commands")
398 self._write_command("command_list_ok_begin")
399 self._command_list = []
401 def command_list_end(self):
402 if self._command_list is None:
403 raise CommandListError("Not in command list")
404 self._write_command("command_list_end")
405 return self._fetch_command_list()
409 return text.replace("\\", "\\\\").replace('"', '\\"')
412 # vim: set expandtab shiftwidth=4 softtabstop=4 textwidth=79: