Library: improved filtering.
[nephilim.git] / nephilim / mpd.py
blob129c82ea25756aeaebe588a7452146a74b0502c7
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/>.
17 import socket
20 HELLO_PREFIX = "OK MPD "
21 ERROR_PREFIX = "ACK "
22 SUCCESS = "OK"
23 NEXT = "list_OK"
26 class MPDError(Exception):
27 pass
29 class ConnectionError(MPDError):
30 pass
32 class ProtocolError(MPDError):
33 pass
35 class CommandError(MPDError):
36 pass
38 class CommandListError(MPDError):
39 pass
42 class _NotConnected(object):
43 def __getattr__(self, attr):
44 return self._dummy
46 def _dummy(*args):
47 raise ConnectionError("Not connected")
49 class MPDClient(object):
50 def __init__(self):
51 self.iterate = False
52 self._reset()
53 self._commands = {
54 # Admin Commands
55 "disableoutput": self._getnone,
56 "enableoutput": self._getnone,
57 "kill": None,
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,
67 # Database Commands
68 "find": self._getsongs,
69 "findadd": self._getnone,
70 "list": self._getlist,
71 "listall": self._getdatabase,
72 "listallinfo": self._getdatabase,
73 "lsinfo": self._getdatabase,
74 "search": self._getsongs,
75 "count": self._getobject,
76 # Playlist Commands
77 "add": self._getnone,
78 "addid": self._getitem,
79 "clear": self._getnone,
80 "currentsong": self._getobject,
81 "delete": self._getnone,
82 "deleteid": self._getnone,
83 "load": self._getnone,
84 "rename": self._getnone,
85 "move": self._getnone,
86 "moveid": self._getnone,
87 "playlist": self._getplaylist,
88 "playlistinfo": self._getsongs,
89 "playlistid": self._getsongs,
90 "plchanges": self._getsongs,
91 "plchangesposid": self._getchanges,
92 "rm": self._getnone,
93 "save": self._getnone,
94 "shuffle": self._getnone,
95 "swap": self._getnone,
96 "swapid": self._getnone,
97 "listplaylist": self._getlist,
98 "listplaylistinfo": self._getsongs,
99 "playlistadd": self._getnone,
100 "playlistclear": self._getnone,
101 "playlistdelete": self._getnone,
102 "playlistmove": self._getnone,
103 "playlistfind": self._getsongs,
104 "playlistsearch": self._getsongs,
105 # Playback Commands
106 "consume": self._getnone,
107 "crossfade": self._getnone,
108 "next": self._getnone,
109 "pause": self._getnone,
110 "play": self._getnone,
111 "playid": self._getnone,
112 "previous": self._getnone,
113 "random": self._getnone,
114 "repeat": self._getnone,
115 "seek": self._getnone,
116 "seekid": self._getnone,
117 "setvol": self._getnone,
118 "single": self._getnone,
119 "stop": self._getnone,
120 "volume": self._getnone,
121 # Miscellaneous Commands
122 "clearerror": self._getnone,
123 "close": None,
124 "password": self._getnone,
125 "ping": self._getnone,
128 def __getattr__(self, attr):
129 try:
130 retval = self._commands[attr]
131 except KeyError:
132 raise AttributeError("'%s' object has no attribute '%s'" %
133 (self.__class__.__name__, attr))
134 return lambda *args: self._docommand(attr, args, retval)
136 def _docommand(self, command, args, retval):
137 if self._commandlist is not None and not callable(retval):
138 raise CommandListError("%s not allowed in command list" % command)
139 self._writecommand(command, args)
140 if self._commandlist is None:
141 if callable(retval):
142 return retval()
143 return retval
144 self._commandlist.append(retval)
146 def _writeline(self, line):
147 self._wfile.write("%s\n" % line)
148 self._wfile.flush()
150 def _writecommand(self, command, args=[]):
151 parts = [command]
152 for arg in args:
153 parts.append('"%s"' % escape(str(arg)))
154 self._writeline(" ".join(parts))
156 def _readline(self):
157 line = self._rfile.readline()
158 if not line.endswith("\n"):
159 raise ConnectionError("Connection lost while reading line")
160 line = line.rstrip("\n")
161 if line.startswith(ERROR_PREFIX):
162 error = line[len(ERROR_PREFIX):].strip()
163 raise CommandError(error)
164 if self._commandlist is not None:
165 if line == NEXT:
166 return
167 if line == SUCCESS:
168 raise ProtocolError("Got unexpected '%s'" % SUCCESS)
169 elif line == SUCCESS:
170 return
171 return line
173 def _readitem(self, separator):
174 line = self._readline()
175 if line is None:
176 return
177 item = line.split(separator, 1)
178 if len(item) < 2:
179 raise ProtocolError("Could not parse item: '%s'" % line)
180 return item
182 def _readitems(self, separator=": "):
183 item = self._readitem(separator)
184 while item:
185 yield item
186 item = self._readitem(separator)
187 raise StopIteration
189 def _readlist(self):
190 seen = None
191 for key, value in self._readitems():
192 if key != seen:
193 if seen is not None:
194 raise ProtocolError("Expected key '%s', got '%s'" %
195 (seen, key))
196 seen = key
197 yield value
198 raise StopIteration
200 def _readplaylist(self):
201 for key, value in self._readitems(":"):
202 yield value
203 raise StopIteration
205 def _readobjects(self, delimiters=[]):
206 obj = {}
207 for key, value in self._readitems():
208 key = key.lower()
209 if obj:
210 if key in delimiters:
211 yield obj
212 obj = {}
213 elif obj.has_key(key):
214 if not isinstance(obj[key], list):
215 obj[key] = [obj[key], value]
216 else:
217 obj[key].append(value)
218 continue
219 obj[key] = value
220 if obj:
221 yield obj
222 raise StopIteration
224 def _readcommandlist(self):
225 for retval in self._commandlist:
226 yield retval()
227 self._commandlist = None
228 self._getnone()
229 raise StopIteration
231 def _wrapiterator(self, iterator):
232 if not self.iterate:
233 return list(iterator)
234 return iterator
236 def _getnone(self):
237 line = self._readline()
238 if line is not None:
239 raise ProtocolError("Got unexpected return value: '%s'" % line)
241 def _getitem(self):
242 items = list(self._readitems())
243 if len(items) != 1:
244 return
245 return items[0][1]
247 def _getlist(self):
248 return self._wrapiterator(self._readlist())
250 def _getplaylist(self):
251 return self._wrapiterator(self._readplaylist())
253 def _getobject(self):
254 objs = list(self._readobjects())
255 if not objs:
256 return {}
257 return objs[0]
259 def _getobjects(self, delimiters):
260 return self._wrapiterator(self._readobjects(delimiters))
262 def _getsongs(self):
263 return self._getobjects(["file"])
265 def _getdatabase(self):
266 return self._getobjects(["file", "directory", "playlist"])
268 def _getoutputs(self):
269 return self._getobjects(["outputid"])
271 def _getchanges(self):
272 return self._getobjects(["cpos"])
274 def _getcommandlist(self):
275 try:
276 return self._wrapiterator(self._readcommandlist())
277 except CommandError:
278 self._commandlist = None
279 raise
281 def _hello(self):
282 line = self._rfile.readline()
283 if not line.endswith("\n"):
284 raise ConnectionError("Connection lost while reading MPD hello")
285 line = line.rstrip("\n")
286 if not line.startswith(HELLO_PREFIX):
287 raise ProtocolError("Got invalid MPD hello: '%s'" % line)
288 self.mpd_version = line[len(HELLO_PREFIX):].strip()
290 def _reset(self):
291 self.mpd_version = None
292 self._commandlist = None
293 self._sock = None
294 self._rfile = _NotConnected()
295 self._wfile = _NotConnected()
297 def connect(self, host, port):
298 if self._sock:
299 raise ConnectionError("Already connected")
300 msg = "getaddrinfo returns an empty list"
301 try:
302 flags = socket.AI_ADDRCONFIG
303 except AttributeError:
304 flags = 0
305 if port == None: #assume Unix domain socket
306 try:
307 self._sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
308 self._sock.connect(host)
309 except socket.error, msg:
310 if self._sock:
311 self._sock.close()
312 self._sock = None
313 else:
314 for res in socket.getaddrinfo(host, port, socket.AF_UNSPEC,
315 socket.SOCK_STREAM, socket.IPPROTO_TCP,
316 flags):
317 af, socktype, proto, canonname, sa = res
318 try:
319 self._sock = socket.socket(af, socktype, proto)
320 self._sock.connect(sa)
321 except socket.error, msg:
322 if self._sock:
323 self._sock.close()
324 self._sock = None
325 continue
326 break
327 if not self._sock:
328 raise socket.error(msg)
329 self._rfile = self._sock.makefile("rb")
330 self._wfile = self._sock.makefile("wb")
331 try:
332 self._hello()
333 except:
334 self.disconnect()
335 raise
337 def disconnect(self):
338 self._rfile.close()
339 self._wfile.close()
340 self._sock.close()
341 self._reset()
343 def command_list_ok_begin(self):
344 if self._commandlist is not None:
345 raise CommandListError("Already in command list")
346 self._writecommand("command_list_ok_begin")
347 self._commandlist = []
349 def command_list_end(self):
350 if self._commandlist is None:
351 raise CommandListError("Not in command list")
352 self._writecommand("command_list_end")
353 return self._getcommandlist()
356 def escape(text):
357 return text.replace("\\", "\\\\").replace('"', '\\"')
360 # vim: set expandtab shiftwidth=4 softtabstop=4 textwidth=79: