Lyrics: rewrite the system of fetchers to be more flexible.
[nephilim.git] / nephilim / mpd.py
blob8aa6c0fd63512d24d8cadd421818d6cc618d1af7
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 "list": self._getlist,
70 "listall": self._getdatabase,
71 "listallinfo": self._getdatabase,
72 "lsinfo": self._getdatabase,
73 "search": self._getsongs,
74 "count": self._getobject,
75 # Playlist Commands
76 "add": self._getnone,
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,
91 "rm": self._getnone,
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,
104 # Playback Commands
105 "consume": self._getnone,
106 "crossfade": self._getnone,
107 "next": self._getnone,
108 "pause": self._getnone,
109 "play": self._getnone,
110 "playid": self._getnone,
111 "previous": self._getnone,
112 "random": self._getnone,
113 "repeat": self._getnone,
114 "seek": self._getnone,
115 "seekid": self._getnone,
116 "setvol": self._getnone,
117 "single": self._getnone,
118 "stop": self._getnone,
119 "volume": self._getnone,
120 # Miscellaneous Commands
121 "clearerror": self._getnone,
122 "close": None,
123 "password": self._getnone,
124 "ping": self._getnone,
127 def __getattr__(self, attr):
128 try:
129 retval = self._commands[attr]
130 except KeyError:
131 raise AttributeError("'%s' object has no attribute '%s'" %
132 (self.__class__.__name__, attr))
133 return lambda *args: self._docommand(attr, args, retval)
135 def _docommand(self, command, args, retval):
136 if self._commandlist is not None and not callable(retval):
137 raise CommandListError("%s not allowed in command list" % command)
138 self._writecommand(command, args)
139 if self._commandlist is None:
140 if callable(retval):
141 return retval()
142 return retval
143 self._commandlist.append(retval)
145 def _writeline(self, line):
146 self._wfile.write("%s\n" % line)
147 self._wfile.flush()
149 def _writecommand(self, command, args=[]):
150 parts = [command]
151 for arg in args:
152 parts.append('"%s"' % escape(str(arg)))
153 self._writeline(" ".join(parts))
155 def _readline(self):
156 line = self._rfile.readline()
157 if not line.endswith("\n"):
158 raise ConnectionError("Connection lost while reading line")
159 line = line.rstrip("\n")
160 if line.startswith(ERROR_PREFIX):
161 error = line[len(ERROR_PREFIX):].strip()
162 raise CommandError(error)
163 if self._commandlist is not None:
164 if line == NEXT:
165 return
166 if line == SUCCESS:
167 raise ProtocolError("Got unexpected '%s'" % SUCCESS)
168 elif line == SUCCESS:
169 return
170 return line
172 def _readitem(self, separator):
173 line = self._readline()
174 if line is None:
175 return
176 item = line.split(separator, 1)
177 if len(item) < 2:
178 raise ProtocolError("Could not parse item: '%s'" % line)
179 return item
181 def _readitems(self, separator=": "):
182 item = self._readitem(separator)
183 while item:
184 yield item
185 item = self._readitem(separator)
186 raise StopIteration
188 def _readlist(self):
189 seen = None
190 for key, value in self._readitems():
191 if key != seen:
192 if seen is not None:
193 raise ProtocolError("Expected key '%s', got '%s'" %
194 (seen, key))
195 seen = key
196 yield value
197 raise StopIteration
199 def _readplaylist(self):
200 for key, value in self._readitems(":"):
201 yield value
202 raise StopIteration
204 def _readobjects(self, delimiters=[]):
205 obj = {}
206 for key, value in self._readitems():
207 key = key.lower()
208 if obj:
209 if key in delimiters:
210 yield obj
211 obj = {}
212 elif obj.has_key(key):
213 if not isinstance(obj[key], list):
214 obj[key] = [obj[key], value]
215 else:
216 obj[key].append(value)
217 continue
218 obj[key] = value
219 if obj:
220 yield obj
221 raise StopIteration
223 def _readcommandlist(self):
224 for retval in self._commandlist:
225 yield retval()
226 self._commandlist = None
227 self._getnone()
228 raise StopIteration
230 def _wrapiterator(self, iterator):
231 if not self.iterate:
232 return list(iterator)
233 return iterator
235 def _getnone(self):
236 line = self._readline()
237 if line is not None:
238 raise ProtocolError("Got unexpected return value: '%s'" % line)
240 def _getitem(self):
241 items = list(self._readitems())
242 if len(items) != 1:
243 return
244 return items[0][1]
246 def _getlist(self):
247 return self._wrapiterator(self._readlist())
249 def _getplaylist(self):
250 return self._wrapiterator(self._readplaylist())
252 def _getobject(self):
253 objs = list(self._readobjects())
254 if not objs:
255 return {}
256 return objs[0]
258 def _getobjects(self, delimiters):
259 return self._wrapiterator(self._readobjects(delimiters))
261 def _getsongs(self):
262 return self._getobjects(["file"])
264 def _getdatabase(self):
265 return self._getobjects(["file", "directory", "playlist"])
267 def _getoutputs(self):
268 return self._getobjects(["outputid"])
270 def _getchanges(self):
271 return self._getobjects(["cpos"])
273 def _getcommandlist(self):
274 try:
275 return self._wrapiterator(self._readcommandlist())
276 except CommandError:
277 self._commandlist = None
278 raise
280 def _hello(self):
281 line = self._rfile.readline()
282 if not line.endswith("\n"):
283 raise ConnectionError("Connection lost while reading MPD hello")
284 line = line.rstrip("\n")
285 if not line.startswith(HELLO_PREFIX):
286 raise ProtocolError("Got invalid MPD hello: '%s'" % line)
287 self.mpd_version = line[len(HELLO_PREFIX):].strip()
289 def _reset(self):
290 self.mpd_version = None
291 self._commandlist = None
292 self._sock = None
293 self._rfile = _NotConnected()
294 self._wfile = _NotConnected()
296 def connect(self, host, port):
297 if self._sock:
298 raise ConnectionError("Already connected")
299 msg = "getaddrinfo returns an empty list"
300 try:
301 flags = socket.AI_ADDRCONFIG
302 except AttributeError:
303 flags = 0
304 if port == None: #assume Unix domain socket
305 try:
306 self._sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
307 self._sock.connect(host)
308 except socket.error, msg:
309 if self._sock:
310 self._sock.close()
311 self._sock = None
312 else:
313 for res in socket.getaddrinfo(host, port, socket.AF_UNSPEC,
314 socket.SOCK_STREAM, socket.IPPROTO_TCP,
315 flags):
316 af, socktype, proto, canonname, sa = res
317 try:
318 self._sock = socket.socket(af, socktype, proto)
319 self._sock.connect(sa)
320 except socket.error, msg:
321 if self._sock:
322 self._sock.close()
323 self._sock = None
324 continue
325 break
326 if not self._sock:
327 raise socket.error(msg)
328 self._rfile = self._sock.makefile("rb")
329 self._wfile = self._sock.makefile("wb")
330 try:
331 self._hello()
332 except:
333 self.disconnect()
334 raise
336 def disconnect(self):
337 self._rfile.close()
338 self._wfile.close()
339 self._sock.close()
340 self._reset()
342 def command_list_ok_begin(self):
343 if self._commandlist is not None:
344 raise CommandListError("Already in command list")
345 self._writecommand("command_list_ok_begin")
346 self._commandlist = []
348 def command_list_end(self):
349 if self._commandlist is None:
350 raise CommandListError("Not in command list")
351 self._writecommand("command_list_end")
352 return self._getcommandlist()
355 def escape(text):
356 return text.replace("\\", "\\\\").replace('"', '\\"')
359 # vim: set expandtab shiftwidth=4 softtabstop=4 textwidth=79: