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._getnone,
56 "currentsong": self._getobject,
57 "idle": self._getlist,
59 "status": self._getobject,
60 "stats": self._getobject,
61 # Playback Option Commands
62 "consume": self._getnone,
63 "crossfade": self._getnone,
64 "random": self._getnone,
65 "repeat": self._getnone,
66 "setvol": self._getnone,
67 "single": self._getnone,
68 "volume": self._getnone,
69 # Playback Control Commands
70 "next": self._getnone,
71 "pause": self._getnone,
72 "play": self._getnone,
73 "playid": self._getnone,
74 "previous": self._getnone,
75 "seek": self._getnone,
76 "seekid": self._getnone,
77 "stop": self._getnone,
80 "addid": self._getitem,
81 "clear": self._getnone,
82 "delete": self._getnone,
83 "deleteid": self._getnone,
84 "move": self._getnone,
85 "moveid": self._getnone,
86 "playlist": self._getplaylist,
87 "playlistfind": self._getsongs,
88 "playlistid": self._getsongs,
89 "playlistinfo": self._getsongs,
90 "playlistsearch": self._getsongs,
91 "plchanges": self._getsongs,
92 "plchangesposid": self._getchanges,
93 "shuffle": self._getnone,
94 "swap": self._getnone,
95 "swapid": self._getnone,
96 # Stored Playlist Commands
97 "listplaylist": self._getlist,
98 "listplaylistinfo": self._getsongs,
99 "listplaylists": self._getplaylists,
100 "load": self._getnone,
101 "playlistadd": self._getnone,
102 "playlistclear": self._getnone,
103 "playlistdelete": self._getnone,
104 "playlistmove": self._getnone,
105 "rename": self._getnone,
107 "save": self._getnone,
109 "count": self._getobject,
110 "find": self._getsongs,
111 "list": self._getlist,
112 "listall": self._getdatabase,
113 "listallinfo": self._getdatabase,
114 "lsinfo": self._getdatabase,
115 "search": self._getsongs,
116 "update": self._getitem,
117 # Connection Commands
120 "password": self._getnone,
121 "ping": self._getnone,
122 # Audio Output Commands
123 "disableoutput": self._getnone,
124 "enableoutput": self._getnone,
125 "outputs": self._getoutputs,
126 # Reflection Commands
127 "commands": self._getlist,
128 "notcommands": self._getlist,
129 "tagtypes": self._getlist,
130 "urlhandlers": self._getlist,
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._docommand(attr, args, retval)
141 def _docommand(self, command, args, retval):
142 if self._commandlist is not None and not callable(retval):
143 raise CommandListError("%s not allowed in command list" % command)
144 self._writecommand(command, args)
145 if self._commandlist is None:
149 self._commandlist.append(retval)
151 def _writeline(self, line):
152 self._wfile.write("%s\n" % line)
155 def _writecommand(self, command, args=[]):
158 parts.append('"%s"' % escape(str(arg)))
159 self._writeline(" ".join(parts))
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._commandlist is not None:
173 raise ProtocolError("Got unexpected '%s'" % SUCCESS)
174 elif line == SUCCESS:
178 def _readitem(self, separator):
179 line = self._readline()
182 item = line.split(separator, 1)
184 raise ProtocolError("Could not parse item: '%s'" % line)
187 def _readitems(self, separator=": "):
188 item = self._readitem(separator)
191 item = self._readitem(separator)
196 for key, value in self._readitems():
199 raise ProtocolError("Expected key '%s', got '%s'" %
205 def _readplaylist(self):
206 for key, value in self._readitems(":"):
210 def _readobjects(self, delimiters=[]):
212 for key, value in self._readitems():
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 _readcommandlist(self):
230 for retval in self._commandlist:
232 self._commandlist = None
236 def _wrapiterator(self, iterator):
238 return list(iterator)
242 line = self._readline()
244 raise ProtocolError("Got unexpected return value: '%s'" % line)
247 items = list(self._readitems())
253 return self._wrapiterator(self._readlist())
255 def _getplaylist(self):
256 return self._wrapiterator(self._readplaylist())
258 def _getobject(self):
259 objs = list(self._readobjects())
264 def _getobjects(self, delimiters):
265 return self._wrapiterator(self._readobjects(delimiters))
268 return self._getobjects(["file"])
270 def _getplaylists(self):
271 return self._getobjects(["playlist"])
273 def _getdatabase(self):
274 return self._getobjects(["file", "directory", "playlist"])
276 def _getoutputs(self):
277 return self._getobjects(["outputid"])
279 def _getchanges(self):
280 return self._getobjects(["cpos"])
282 def _getcommandlist(self):
283 return self._wrapiterator(self._readcommandlist())
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._commandlist = None
298 self._rfile = _NotConnected()
299 self._wfile = _NotConnected()
301 def connect(self, host, port):
303 raise ConnectionError("Already connected")
304 msg = "getaddrinfo returns an empty list"
306 flags = socket.AI_ADDRCONFIG
307 except AttributeError:
309 for res in socket.getaddrinfo(host, port, socket.AF_UNSPEC,
310 socket.SOCK_STREAM, socket.IPPROTO_TCP,
312 af, socktype, proto, canonname, sa = res
314 self._sock = socket.socket(af, socktype, proto)
315 self._sock.connect(sa)
316 except socket.error, msg:
323 raise socket.error(msg)
324 self._rfile = self._sock.makefile("rb")
325 self._wfile = self._sock.makefile("wb")
332 def disconnect(self):
338 def command_list_ok_begin(self):
339 if self._commandlist is not None:
340 raise CommandListError("Already in command list")
341 self._writecommand("command_list_ok_begin")
342 self._commandlist = []
344 def command_list_end(self):
345 if self._commandlist is None:
346 raise CommandListError("Not in command list")
347 self._writecommand("command_list_end")
348 return self._getcommandlist()
352 return text.replace("\\", "\\\\").replace('"', '\\"')
355 # vim: set expandtab shiftwidth=4 softtabstop=4 textwidth=79: