3 # TODO: return {} if no object read (?)
4 # TODO: implement argument checking/parsing (?)
5 # TODO: check for EOF when reading and benchmark it
6 # TODO: converter support
7 # TODO: global for parsing MPD_HOST/MPD_PORT
8 # TODO: global for parsing MPD error messages
9 # TODO: IPv6 support (AF_INET6)
14 HELLO_PREFIX = "OK MPD "
20 class MPDError(Exception):
23 class ProtocolError(MPDError):
26 class CommandError(MPDError):
29 class CommandListError(MPDError):
33 class MPDClient(object):
39 "disableoutput": self._getnone,
40 "enableoutput": self._getnone,
42 "update": self._getitem,
43 # Informational Commands
44 "status": self._getobject,
45 "stats": self._getobject,
46 "outputs": self._getoutputs,
47 "commands": self._getlist,
48 "notcommands": self._getlist,
49 "tagtypes": self._getlist,
50 "urlhandlers": self._getlist,
52 "find": self._getsongs,
53 "list": self._getlist,
54 "listall": self._getdatabase,
55 "listallinfo": self._getdatabase,
56 "lsinfo": self._getdatabase,
57 "search": self._getsongs,
58 "count": self._getobject,
61 "addid": self._getitem,
62 "clear": self._getnone,
63 "currentsong": self._getobject,
64 "delete": self._getnone,
65 "deleteid": self._getnone,
66 "load": self._getnone,
67 "rename": self._getnone,
68 "move": self._getnone,
69 "moveid": self._getnone,
70 "playlist": self._getplaylist,
71 "playlistinfo": self._getsongs,
72 "playlistid": self._getsongs,
73 "plchanges": self._getsongs,
74 "plchangesposid": self._getchanges,
76 "save": self._getnone,
77 "shuffle": self._getnone,
78 "swap": self._getnone,
79 "swapid": self._getnone,
80 "listplaylist": self._getlist,
81 "listplaylistinfo": self._getsongs,
82 "playlistadd": self._getnone,
83 "playlistclear": self._getnone,
84 "playlistdelete": self._getnone,
85 "playlistmove": self._getnone,
86 "playlistfind": self._getsongs,
87 "playlistsearch": self._getsongs,
89 "crossfade": self._getnone,
90 "next": self._getnone,
91 "pause": self._getnone,
92 "play": self._getnone,
93 "playid": self._getnone,
94 "previous": self._getnone,
95 "random": self._getnone,
96 "repeat": self._getnone,
97 "seek": self._getnone,
98 "seekid": self._getnone,
99 "setvol": self._getnone,
100 "stop": self._getnone,
101 "volume": self._getnone,
102 # Miscellaneous Commands
103 "clearerror": self._getnone,
105 "password": self._getnone,
106 "ping": self._getnone,
109 def __getattr__(self, attr):
111 retval = self._commands[attr]
113 raise AttributeError, "'%s' object has no attribute '%s'" % \
114 (self.__class__.__name__, attr)
115 return lambda *args: self._docommand(attr, args, retval)
117 def _docommand(self, command, args, retval):
118 if self._commandlist is not None and not callable(retval):
119 raise CommandListError, "%s not allowed in command list" % command
120 self._writecommand(command, args)
121 if self._commandlist is None:
125 self._commandlist.append(retval)
127 def _writeline(self, line):
128 self._sockfile.write("%s\n" % line)
129 self._sockfile.flush()
131 def _writecommand(self, command, args=[]):
134 parts.append('"%s"' % escape(str(arg)))
135 self._writeline(" ".join(parts))
138 line = self._sockfile.readline().rstrip("\n")
139 if line.startswith(ERROR_PREFIX):
140 error = line[len(ERROR_PREFIX):].strip()
141 raise CommandError, error
142 if self._commandlist is not None:
146 raise ProtocolError, "Got unexpected '%s'" % SUCCESS
147 elif line == SUCCESS:
151 def _readitem(self, separator):
152 line = self._readline()
155 item = line.split(separator, 1)
157 raise ProtocolError, "Could not parse item: '%s'" % line
160 def _readitems(self, separator=": "):
161 item = self._readitem(separator)
164 item = self._readitem(separator)
169 for key, value in self._readitems():
172 raise ProtocolError, "Expected key '%s', got '%s'" % \
178 def _readplaylist(self):
179 for key, value in self._readitems(":"):
183 def _readobjects(self, delimiters=[]):
185 for key, value in self._readitems():
188 if key in delimiters:
191 elif obj.has_key(key):
192 if not isinstance(obj[key], list):
193 obj[key] = [obj[key], value]
195 obj[key].append(value)
202 def _readcommandlist(self):
203 for retval in self._commandlist:
205 self._commandlist = None
209 def _wrapiterator(self, iterator):
211 return list(iterator)
215 line = self._readline()
217 raise ProtocolError, "Got unexpected return value: '%s'" % line
220 items = list(self._readitems())
222 raise ProtocolError, "Expected 1 item, got %i" % len(items)
226 return self._wrapiterator(self._readlist())
228 def _getplaylist(self):
229 return self._wrapiterator(self._readplaylist())
231 def _getobject(self):
232 objs = list(self._readobjects())
237 def _getobjects(self, delimiters):
238 return self._wrapiterator(self._readobjects(delimiters))
241 return self._getobjects(["file"])
243 def _getdatabase(self):
244 return self._getobjects(["file", "directory", "playlist"])
246 def _getoutputs(self):
247 return self._getobjects(["outputid"])
249 def _getchanges(self):
250 return self._getobjects(["cpos"])
252 def _getcommandlist(self):
253 return self._wrapiterator(self._readcommandlist())
256 line = self._sockfile.readline().rstrip("\n")
257 if not line.startswith(HELLO_PREFIX):
258 raise ProtocolError, "Got invalid MPD hello: '%s'" % line
259 self.mpd_version = line[len(HELLO_PREFIX):].strip()
262 self.mpd_version = None
263 self._commandlist = None
264 self._sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
265 self._sockfile = self._sock.makefile("rb+")
267 def connect(self, host, port):
269 self._sock.connect((host, port))
272 def disconnect(self):
273 self._sockfile.close()
277 def command_list_ok_begin(self):
278 if self._commandlist is not None:
279 raise CommandListError, "Already in command list"
280 self._writecommand("command_list_ok_begin")
281 self._commandlist = []
283 def command_list_end(self):
284 if self._commandlist is None:
285 raise CommandListError, "Not in command list"
286 self._writecommand("command_list_end")
287 return self._getcommandlist()
291 return text.replace("\\", "\\\\").replace('"', '\\"')
294 # vim: set expandtab shiftwidth=4 softtabstop=4 textwidth=79: