3 # TODO: check for EOF when reading and benchmark it
4 # TODO: converter support
5 # TODO: global for parsing MPD_HOST/MPD_PORT
6 # TODO: global for parsing MPD error messages
7 # TODO: IPv6 support (AF_INET6)
12 HELLO_PREFIX = "OK MPD "
18 class MPDError(Exception):
21 class ProtocolError(MPDError):
24 class CommandError(MPDError):
27 class CommandListError(MPDError):
31 class MPDClient(object):
37 "disableoutput": self._getnone,
38 "enableoutput": self._getnone,
40 "update": self._getitem,
41 # Informational Commands
42 "status": self._getobject,
43 "stats": self._getobject,
44 "outputs": self._getoutputs,
45 "commands": self._getlist,
46 "notcommands": self._getlist,
47 "tagtypes": self._getlist,
48 "urlhandlers": self._getlist,
50 "find": self._getsongs,
51 "list": self._getlist,
52 "listall": self._getdatabase,
53 "listallinfo": self._getdatabase,
54 "lsinfo": self._getdatabase,
55 "search": self._getsongs,
56 "count": self._getobject,
59 "addid": self._getitem,
60 "clear": self._getnone,
61 "currentsong": self._getobject,
62 "delete": self._getnone,
63 "deleteid": self._getnone,
64 "load": self._getnone,
65 "rename": self._getnone,
66 "move": self._getnone,
67 "moveid": self._getnone,
68 "playlist": self._getplaylist,
69 "playlistinfo": self._getsongs,
70 "playlistid": self._getsongs,
71 "plchanges": self._getsongs,
72 "plchangesposid": self._getchanges,
74 "save": self._getnone,
75 "shuffle": self._getnone,
76 "swap": self._getnone,
77 "swapid": self._getnone,
78 "listplaylist": self._getlist,
79 "listplaylistinfo": self._getsongs,
80 "playlistadd": self._getnone,
81 "playlistclear": self._getnone,
82 "playlistdelete": self._getnone,
83 "playlistmove": self._getnone,
84 "playlistfind": self._getsongs,
85 "playlistsearch": self._getsongs,
87 "crossfade": self._getnone,
88 "next": self._getnone,
89 "pause": self._getnone,
90 "play": self._getnone,
91 "playid": self._getnone,
92 "previous": self._getnone,
93 "random": self._getnone,
94 "repeat": self._getnone,
95 "seek": self._getnone,
96 "seekid": self._getnone,
97 "setvol": self._getnone,
98 "stop": self._getnone,
99 "volume": self._getnone,
100 # Miscellaneous Commands
101 "clearerror": self._getnone,
103 "password": self._getnone,
104 "ping": self._getnone,
107 def __getattr__(self, attr):
109 retval = self._commands[attr]
111 raise AttributeError, "'%s' object has no attribute '%s'" % \
112 (self.__class__.__name__, attr)
113 return lambda *args: self._docommand(attr, args, retval)
115 def _docommand(self, command, args, retval):
116 if self._commandlist is not None and not callable(retval):
117 raise CommandListError, "%s not allowed in command list" % command
118 self._writecommand(command, args)
119 if self._commandlist is None:
123 self._commandlist.append(retval)
125 def _writeline(self, line):
126 self._sockfile.write("%s\n" % line)
127 self._sockfile.flush()
129 def _writecommand(self, command, args=[]):
132 parts.append('"%s"' % escape(str(arg)))
133 self._writeline(" ".join(parts))
136 line = self._sockfile.readline().rstrip("\n")
137 if line.startswith(ERROR_PREFIX):
138 error = line[len(ERROR_PREFIX):].strip()
139 raise CommandError, error
140 if self._commandlist is not None:
144 raise ProtocolError, "Got unexpected '%s'" % SUCCESS
145 elif line == SUCCESS:
149 def _readitem(self, separator):
150 line = self._readline()
153 item = line.split(separator, 1)
155 raise ProtocolError, "Could not parse item: '%s'" % line
158 def _readitems(self, separator=": "):
159 item = self._readitem(separator)
162 item = self._readitem(separator)
167 for key, value in self._readitems():
170 raise ProtocolError, "Expected key '%s', got '%s'" % \
176 def _readplaylist(self):
177 for key, value in self._readitems(":"):
181 def _readobjects(self, delimiters=[]):
183 for key, value in self._readitems():
186 if key in delimiters:
189 elif obj.has_key(key):
190 if not isinstance(obj[key], list):
191 obj[key] = [obj[key], value]
193 obj[key].append(value)
200 def _readcommandlist(self):
201 for retval in self._commandlist:
203 self._commandlist = None
207 def _wrapiterator(self, iterator):
209 return list(iterator)
213 line = self._readline()
215 raise ProtocolError, "Got unexpected return value: '%s'" % line
218 items = list(self._readitems())
220 raise ProtocolError, "Expected 1 item, got %i" % len(items)
224 return self._wrapiterator(self._readlist())
226 def _getplaylist(self):
227 return self._wrapiterator(self._readplaylist())
229 def _getobject(self):
230 objs = list(self._readobjects())
235 def _getobjects(self, delimiters):
236 return self._wrapiterator(self._readobjects(delimiters))
239 return self._getobjects(["file"])
241 def _getdatabase(self):
242 return self._getobjects(["file", "directory", "playlist"])
244 def _getoutputs(self):
245 return self._getobjects(["outputid"])
247 def _getchanges(self):
248 return self._getobjects(["cpos"])
250 def _getcommandlist(self):
251 return self._wrapiterator(self._readcommandlist())
254 line = self._sockfile.readline().rstrip("\n")
255 if not line.startswith(HELLO_PREFIX):
256 raise ProtocolError, "Got invalid MPD hello: '%s'" % line
257 self.mpd_version = line[len(HELLO_PREFIX):].strip()
260 self.mpd_version = None
261 self._commandlist = None
262 self._sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
263 self._sockfile = self._sock.makefile("rb+")
265 def connect(self, host, port):
267 self._sock.connect((host, port))
270 def disconnect(self):
271 self._sockfile.close()
275 def command_list_ok_begin(self):
276 if self._commandlist is not None:
277 raise CommandListError, "Already in command list"
278 self._writecommand("command_list_ok_begin")
279 self._commandlist = []
281 def command_list_end(self):
282 if self._commandlist is None:
283 raise CommandListError, "Not in command list"
284 self._writecommand("command_list_end")
285 return self._getcommandlist()
289 return text.replace("\\", "\\\\").replace('"', '\\"')
292 # vim: set expandtab shiftwidth=4 softtabstop=4 textwidth=79: