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):
42 class _NotConnected(object):
43 def __getattr__(self, attr):
47 raise ConnectionError("Not connected")
49 class MPDClient(object):
55 "clearerror": self._fetch_nothing,
56 "currentsong": self._fetch_object,
57 "idle": self._fetch_list,
59 "status": self._fetch_object,
60 "stats": self._fetch_object,
61 # Playback Option Commands
62 "consume": self._fetch_nothing,
63 "crossfade": self._fetch_nothing,
64 "random": self._fetch_nothing,
65 "repeat": self._fetch_nothing,
66 "setvol": self._fetch_nothing,
67 "single": self._fetch_nothing,
68 "volume": self._fetch_nothing,
69 # Playback Control Commands
70 "next": self._fetch_nothing,
71 "pause": self._fetch_nothing,
72 "play": self._fetch_nothing,
73 "playid": self._fetch_nothing,
74 "previous": self._fetch_nothing,
75 "seek": self._fetch_nothing,
76 "seekid": self._fetch_nothing,
77 "stop": self._fetch_nothing,
79 "add": self._fetch_nothing,
80 "addid": self._fetch_item,
81 "clear": self._fetch_nothing,
82 "delete": self._fetch_nothing,
83 "deleteid": self._fetch_nothing,
84 "move": self._fetch_nothing,
85 "moveid": self._fetch_nothing,
86 "playlist": self._fetch_playlist,
87 "playlistfind": self._fetch_songs,
88 "playlistid": self._fetch_songs,
89 "playlistinfo": self._fetch_songs,
90 "playlistsearch": self._fetch_songs,
91 "plchanges": self._fetch_songs,
92 "plchangesposid": self._fetch_changes,
93 "shuffle": self._fetch_nothing,
94 "swap": self._fetch_nothing,
95 "swapid": self._fetch_nothing,
96 # Stored Playlist Commands
97 "listplaylist": self._fetch_list,
98 "listplaylistinfo": self._fetch_songs,
99 "listplaylists": self._fetch_playlists,
100 "load": self._fetch_nothing,
101 "playlistadd": self._fetch_nothing,
102 "playlistclear": self._fetch_nothing,
103 "playlistdelete": self._fetch_nothing,
104 "playlistmove": self._fetch_nothing,
105 "rename": self._fetch_nothing,
106 "rm": self._fetch_nothing,
107 "save": self._fetch_nothing,
109 "count": self._fetch_object,
110 "find": self._fetch_songs,
111 "list": self._fetch_list,
112 "listall": self._fetch_database,
113 "listallinfo": self._fetch_database,
114 "lsinfo": self._fetch_database,
115 "search": self._fetch_songs,
116 "update": self._fetch_item,
117 # Connection Commands
120 "password": self._fetch_nothing,
121 "ping": self._fetch_nothing,
122 # Audio Output Commands
123 "disableoutput": self._fetch_nothing,
124 "enableoutput": self._fetch_nothing,
125 "outputs": self._fetch_outputs,
126 # Reflection Commands
127 "commands": self._fetch_list,
128 "notcommands": self._fetch_list,
129 "tagtypes": self._fetch_list,
130 "urlhandlers": self._fetch_list,
133 def __getattr__(self, attr):
135 retval = self._commands[attr]
137 raise AttributeError("'%s' object has no attribute '%s'" %
138 (self.__class__.__name__, attr))
139 return lambda *args: self._execute(attr, args, retval)
141 def _execute(self, command, args, retval):
142 if self._command_list is not None and not callable(retval):
143 raise CommandListError("%s not allowed in command list" % command)
144 self._write_command(command, args)
145 if self._command_list is None:
149 self._command_list.append(retval)
151 def _write_line(self, line):
152 self._wfile.write("%s\n" % line)
155 def _write_command(self, command, args=[]):
158 parts.append('"%s"' % escape(str(arg)))
159 self._write_line(" ".join(parts))
161 def _read_line(self):
162 line = self._rfile.readline()
163 if not line.endswith("\n"):
164 raise ConnectionError("Connection lost while reading line")
165 line = line.rstrip("\n")
166 if line.startswith(ERROR_PREFIX):
167 error = line[len(ERROR_PREFIX):].strip()
168 raise CommandError(error)
169 if self._command_list is not None:
173 raise ProtocolError("Got unexpected '%s'" % SUCCESS)
174 elif line == SUCCESS:
178 def _read_pair(self, separator):
179 line = self._read_line()
182 pair = line.split(separator, 1)
184 raise ProtocolError("Could not parse pair: '%s'" % line)
187 def _read_pairs(self, separator=": "):
188 pair = self._read_pair(separator)
191 pair = self._read_pair(separator)
194 def _read_list(self):
196 for key, value in self._read_pairs():
199 raise ProtocolError("Expected key '%s', got '%s'" %
205 def _read_playlist(self):
206 for key, value in self._read_pairs(":"):
210 def _read_objects(self, delimiters=[]):
212 for key, value in self._read_pairs():
215 if key in delimiters:
218 elif obj.has_key(key):
219 if not isinstance(obj[key], list):
220 obj[key] = [obj[key], value]
222 obj[key].append(value)
229 def _read_command_list(self):
230 for retval in self._command_list:
232 self._command_list = None
233 self._fetch_nothing()
236 def _wrap_iterator(self, iterator):
238 return list(iterator)
241 def _fetch_nothing(self):
242 line = self._read_line()
244 raise ProtocolError("Got unexpected return value: '%s'" % line)
246 def _fetch_item(self):
247 pairs = list(self._read_pairs())
252 def _fetch_list(self):
253 return self._wrap_iterator(self._read_list())
255 def _fetch_playlist(self):
256 return self._wrap_iterator(self._read_playlist())
258 def _fetch_object(self):
259 objs = list(self._read_objects())
264 def _fetch_objects(self, delimiters):
265 return self._wrap_iterator(self._read_objects(delimiters))
267 def _fetch_songs(self):
268 return self._fetch_objects(["file"])
270 def _fetch_playlists(self):
271 return self._fetch_objects(["playlist"])
273 def _fetch_database(self):
274 return self._fetch_objects(["file", "directory", "playlist"])
276 def _fetch_outputs(self):
277 return self._fetch_objects(["outputid"])
279 def _fetch_changes(self):
280 return self._fetch_objects(["cpos"])
282 def _fetch_command_list(self):
283 return self._wrap_iterator(self._read_command_list())
286 line = self._rfile.readline()
287 if not line.endswith("\n"):
288 raise ConnectionError("Connection lost while reading MPD hello")
289 line = line.rstrip("\n")
290 if not line.startswith(HELLO_PREFIX):
291 raise ProtocolError("Got invalid MPD hello: '%s'" % line)
292 self.mpd_version = line[len(HELLO_PREFIX):].strip()
295 self.mpd_version = None
296 self._command_list = None
298 self._rfile = _NotConnected()
299 self._wfile = _NotConnected()
301 def _connect_unix(self, path):
302 if not hasattr(socket, "AF_UNIX"):
303 raise ConnectionError("Unix domain sockets not supported "
305 sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
309 def _connect_tcp(self, host, port):
311 flags = socket.AI_ADDRCONFIG
312 except AttributeError:
314 msg = "getaddrinfo returns an empty list"
315 for res in socket.getaddrinfo(host, port, socket.AF_UNSPEC,
316 socket.SOCK_STREAM, socket.IPPROTO_TCP,
318 af, socktype, proto, canonname, sa = res
320 sock = socket.socket(af, socktype, proto)
322 except socket.error, msg:
329 raise socket.error(msg)
332 def connect(self, host, port):
334 raise ConnectionError("Already connected")
335 if host.startswith("/"):
336 self._sock = self._connect_unix(host)
338 self._sock = self._connect_tcp(host, port)
339 self._rfile = self._sock.makefile("rb")
340 self._wfile = self._sock.makefile("wb")
347 def disconnect(self):
353 def command_list_ok_begin(self):
354 if self._command_list is not None:
355 raise CommandListError("Already in command list")
356 self._write_command("command_list_ok_begin")
357 self._command_list = []
359 def command_list_end(self):
360 if self._command_list is None:
361 raise CommandListError("Not in command list")
362 self._write_command("command_list_end")
363 return self._fetch_command_list()
367 return text.replace("\\", "\\\\").replace('"', '\\"')
370 # vim: set expandtab shiftwidth=4 softtabstop=4 textwidth=79: