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 "disableoutput": self._getnone,
56 "enableoutput": self._getnone,
58 "update": self._getitem,
59 # Informational Commands
60 "status": self._getobject,
61 "stats": self._getobject,
62 "outputs": self._getoutputs,
63 "commands": self._getlist,
64 "notcommands": self._getlist,
65 "tagtypes": self._getlist,
66 "urlhandlers": self._getlist,
68 "find": self._getsongs,
69 "list": self._getlist,
70 "listall": self._getdatabase,
71 "listallinfo": self._getdatabase,
72 "lsinfo": self._getdatabase,
73 "search": self._getsongs,
74 "count": self._getobject,
77 "addid": self._getitem,
78 "clear": self._getnone,
79 "currentsong": self._getobject,
80 "delete": self._getnone,
81 "deleteid": self._getnone,
82 "load": self._getnone,
83 "rename": self._getnone,
84 "move": self._getnone,
85 "moveid": self._getnone,
86 "playlist": self._getplaylist,
87 "playlistinfo": self._getsongs,
88 "playlistid": self._getsongs,
89 "plchanges": self._getsongs,
90 "plchangesposid": self._getchanges,
92 "save": self._getnone,
93 "shuffle": self._getnone,
94 "swap": self._getnone,
95 "swapid": self._getnone,
96 "listplaylist": self._getlist,
97 "listplaylistinfo": self._getsongs,
98 "playlistadd": self._getnone,
99 "playlistclear": self._getnone,
100 "playlistdelete": self._getnone,
101 "playlistmove": self._getnone,
102 "playlistfind": self._getsongs,
103 "playlistsearch": self._getsongs,
105 "crossfade": self._getnone,
106 "next": self._getnone,
107 "pause": self._getnone,
108 "play": self._getnone,
109 "playid": self._getnone,
110 "previous": self._getnone,
111 "random": self._getnone,
112 "repeat": self._getnone,
113 "seek": self._getnone,
114 "seekid": self._getnone,
115 "setvol": self._getnone,
116 "stop": self._getnone,
117 "volume": self._getnone,
118 # Miscellaneous Commands
119 "clearerror": self._getnone,
121 "password": self._getnone,
122 "ping": self._getnone,
125 def __getattr__(self, attr):
127 retval = self._commands[attr]
129 raise AttributeError, "'%s' object has no attribute '%s'" % \
130 (self.__class__.__name__, attr)
131 return lambda *args: self._docommand(attr, args, retval)
133 def _docommand(self, command, args, retval):
134 if self._commandlist is not None and not callable(retval):
135 raise CommandListError, "%s not allowed in command list" % command
136 self._writecommand(command, args)
137 if self._commandlist is None:
141 self._commandlist.append(retval)
143 def _writeline(self, line):
144 self._wfile.write("%s\n" % line)
147 def _writecommand(self, command, args=[]):
150 parts.append('"%s"' % escape(str(arg)))
151 self._writeline(" ".join(parts))
154 line = self._rfile.readline()
155 if not line.endswith("\n"):
156 raise ConnectionError, "Connection lost while reading line"
157 line = line.rstrip("\n")
158 if line.startswith(ERROR_PREFIX):
159 error = line[len(ERROR_PREFIX):].strip()
160 raise CommandError, error
161 if self._commandlist is not None:
165 raise ProtocolError, "Got unexpected '%s'" % SUCCESS
166 elif line == SUCCESS:
170 def _readitem(self, separator):
171 line = self._readline()
174 item = line.split(separator, 1)
176 raise ProtocolError, "Could not parse item: '%s'" % line
179 def _readitems(self, separator=": "):
180 item = self._readitem(separator)
183 item = self._readitem(separator)
188 for key, value in self._readitems():
191 raise ProtocolError, "Expected key '%s', got '%s'" % \
197 def _readplaylist(self):
198 for key, value in self._readitems(":"):
202 def _readobjects(self, delimiters=[]):
204 for key, value in self._readitems():
207 if key in delimiters:
210 elif obj.has_key(key):
211 if not isinstance(obj[key], list):
212 obj[key] = [obj[key], value]
214 obj[key].append(value)
221 def _readcommandlist(self):
222 for retval in self._commandlist:
224 self._commandlist = None
228 def _wrapiterator(self, iterator):
230 return list(iterator)
234 line = self._readline()
236 raise ProtocolError, "Got unexpected return value: '%s'" % line
239 items = list(self._readitems())
245 return self._wrapiterator(self._readlist())
247 def _getplaylist(self):
248 return self._wrapiterator(self._readplaylist())
250 def _getobject(self):
251 objs = list(self._readobjects())
256 def _getobjects(self, delimiters):
257 return self._wrapiterator(self._readobjects(delimiters))
260 return self._getobjects(["file"])
262 def _getdatabase(self):
263 return self._getobjects(["file", "directory", "playlist"])
265 def _getoutputs(self):
266 return self._getobjects(["outputid"])
268 def _getchanges(self):
269 return self._getobjects(["cpos"])
271 def _getcommandlist(self):
272 return self._wrapiterator(self._readcommandlist())
275 line = self._rfile.readline()
276 if not line.endswith("\n"):
277 raise ConnectionError, "Connection lost while reading MPD hello"
278 if not line.startswith(HELLO_PREFIX):
279 raise ProtocolError, "Got invalid MPD hello: '%s'" % line
280 self.mpd_version = line[len(HELLO_PREFIX):].strip()
283 self.mpd_version = None
284 self._commandlist = None
286 self._rfile = _NotConnected()
287 self._wfile = _NotConnected()
289 def connect(self, host, port):
291 raise ConnectionError, "Already connected"
292 msg = "getaddrinfo returns an empty list"
293 for res in socket.getaddrinfo(host, port, socket.AF_UNSPEC,
294 socket.SOCK_STREAM, socket.IPPROTO_TCP,
295 socket.AI_ADDRCONFIG):
296 af, socktype, proto, canonname, sa = res
298 self._sock = socket.socket(af, socktype, proto)
299 self._sock.connect(sa)
300 except socket.error, msg:
307 raise socket.error, msg
308 self._rfile = self._sock.makefile("rb")
309 self._wfile = self._sock.makefile("wb")
312 except (socket.error, MPDError):
316 def disconnect(self):
322 def command_list_ok_begin(self):
323 if self._commandlist is not None:
324 raise CommandListError, "Already in command list"
325 self._writecommand("command_list_ok_begin")
326 self._commandlist = []
328 def command_list_end(self):
329 if self._commandlist is None:
330 raise CommandListError, "Not in command list"
331 self._writecommand("command_list_end")
332 return self._getcommandlist()
336 return text.replace("\\", "\\\\").replace('"', '\\"')
339 # vim: set expandtab shiftwidth=4 softtabstop=4 textwidth=79: