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