3 # TODO: implement argument checking/parsing (?)
4 # TODO: check for EOF when reading and benchmark it
5 # TODO: converter support
6 # TODO: global for parsing MPD_HOST/MPD_PORT
7 # TODO: global for parsing MPD error messages
8 # TODO: IPv6 support (AF_INET6)
13 HELLO_PREFIX = "OK MPD "
19 class MPDError(Exception):
22 class ProtocolError(MPDError):
25 class CommandError(MPDError):
28 class CommandListError(MPDError):
32 class MPDClient(object):
38 "disableoutput": self._getnone,
39 "enableoutput": self._getnone,
41 "update": self._getitem,
42 # Informational Commands
43 "status": self._getobject,
44 "stats": self._getobject,
45 "outputs": self._getoutputs,
46 "commands": self._getlist,
47 "notcommands": self._getlist,
48 "tagtypes": self._getlist,
49 "urlhandlers": self._getlist,
51 "find": self._getsongs,
52 "list": self._getlist,
53 "listall": self._getdatabase,
54 "listallinfo": self._getdatabase,
55 "lsinfo": self._getdatabase,
56 "search": self._getsongs,
57 "count": self._getobject,
60 "addid": self._getitem,
61 "clear": self._getnone,
62 "currentsong": self._getobject,
63 "delete": self._getnone,
64 "deleteid": self._getnone,
65 "load": self._getnone,
66 "rename": self._getnone,
67 "move": self._getnone,
68 "moveid": self._getnone,
69 "playlist": self._getplaylist,
70 "playlistinfo": self._getsongs,
71 "playlistid": self._getsongs,
72 "plchanges": self._getsongs,
73 "plchangesposid": self._getchanges,
75 "save": self._getnone,
76 "shuffle": self._getnone,
77 "swap": self._getnone,
78 "swapid": self._getnone,
79 "listplaylist": self._getlist,
80 "listplaylistinfo": self._getsongs,
81 "playlistadd": self._getnone,
82 "playlistclear": self._getnone,
83 "playlistdelete": self._getnone,
84 "playlistmove": self._getnone,
85 "playlistfind": self._getsongs,
86 "playlistsearch": self._getsongs,
88 "crossfade": self._getnone,
89 "next": self._getnone,
90 "pause": self._getnone,
91 "play": self._getnone,
92 "playid": self._getnone,
93 "previous": self._getnone,
94 "random": self._getnone,
95 "repeat": self._getnone,
96 "seek": self._getnone,
97 "seekid": self._getnone,
98 "setvol": self._getnone,
99 "stop": self._getnone,
100 "volume": self._getnone,
101 # Miscellaneous Commands
102 "clearerror": self._getnone,
104 "password": self._getnone,
105 "ping": self._getnone,
108 def __getattr__(self, attr):
110 retval = self._commands[attr]
112 raise AttributeError, "'%s' object has no attribute '%s'" % \
113 (self.__class__.__name__, attr)
114 return lambda *args: self._docommand(attr, args, retval)
116 def _docommand(self, command, args, retval):
117 if self._commandlist is not None and not callable(retval):
118 raise CommandListError, "%s not allowed in command list" % command
119 self._writecommand(command, args)
120 if self._commandlist is None:
124 self._commandlist.append(retval)
126 def _writeline(self, line):
127 self._sockfile.write("%s\n" % line)
128 self._sockfile.flush()
130 def _writecommand(self, command, args=[]):
133 parts.append('"%s"' % escape(str(arg)))
134 self._writeline(" ".join(parts))
137 line = self._sockfile.readline().rstrip("\n")
138 if line.startswith(ERROR_PREFIX):
139 error = line[len(ERROR_PREFIX):].strip()
140 raise CommandError, error
141 if self._commandlist is not None:
145 raise ProtocolError, "Got unexpected '%s'" % SUCCESS
146 elif line == SUCCESS:
150 def _readitem(self, separator):
151 line = self._readline()
154 item = line.split(separator, 1)
156 raise ProtocolError, "Could not parse item: '%s'" % line
159 def _readitems(self, separator=": "):
160 item = self._readitem(separator)
163 item = self._readitem(separator)
168 for key, value in self._readitems():
171 raise ProtocolError, "Expected key '%s', got '%s'" % \
177 def _readplaylist(self):
178 for key, value in self._readitems(":"):
182 def _readobjects(self, delimiters=[]):
184 for key, value in self._readitems():
187 if key in delimiters:
190 elif obj.has_key(key):
191 if not isinstance(obj[key], list):
192 obj[key] = [obj[key], value]
194 obj[key].append(value)
201 def _readcommandlist(self):
202 for retval in self._commandlist:
204 self._commandlist = None
208 def _wrapiterator(self, iterator):
210 return list(iterator)
214 line = self._readline()
216 raise ProtocolError, "Got unexpected return value: '%s'" % line
219 items = list(self._readitems())
221 raise ProtocolError, "Expected 1 item, got %i" % len(items)
225 return self._wrapiterator(self._readlist())
227 def _getplaylist(self):
228 return self._wrapiterator(self._readplaylist())
230 def _getobject(self):
231 objs = list(self._readobjects())
236 def _getobjects(self, delimiters):
237 return self._wrapiterator(self._readobjects(delimiters))
240 return self._getobjects(["file"])
242 def _getdatabase(self):
243 return self._getobjects(["file", "directory", "playlist"])
245 def _getoutputs(self):
246 return self._getobjects(["outputid"])
248 def _getchanges(self):
249 return self._getobjects(["cpos"])
251 def _getcommandlist(self):
252 return self._wrapiterator(self._readcommandlist())
255 line = self._sockfile.readline().rstrip("\n")
256 if not line.startswith(HELLO_PREFIX):
257 raise ProtocolError, "Got invalid MPD hello: '%s'" % line
258 self.mpd_version = line[len(HELLO_PREFIX):].strip()
261 self.mpd_version = None
262 self._commandlist = None
263 self._sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
264 self._sockfile = self._sock.makefile("rb+")
266 def connect(self, host, port):
268 self._sock.connect((host, port))
271 def disconnect(self):
272 self._sockfile.close()
276 def command_list_ok_begin(self):
277 if self._commandlist is not None:
278 raise CommandListError, "Already in command list"
279 self._writecommand("command_list_ok_begin")
280 self._commandlist = []
282 def command_list_end(self):
283 if self._commandlist is None:
284 raise CommandListError, "Not in command list"
285 self._writecommand("command_list_end")
286 return self._getcommandlist()
290 return text.replace("\\", "\\\\").replace('"', '\\"')
293 # vim: set expandtab shiftwidth=4 softtabstop=4 textwidth=79: