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/>.
20 HELLO_PREFIX = "OK MPD "
26 class MPDError(Exception):
29 class ConnectionError(MPDError):
32 class ProtocolError(MPDError):
35 class CommandError(MPDError):
38 class CommandListError(MPDError):
42 class MPDClient(object):
48 "disableoutput": self._getnone,
49 "enableoutput": self._getnone,
51 "update": self._getitem,
52 # Informational Commands
53 "status": self._getobject,
54 "stats": self._getobject,
55 "outputs": self._getoutputs,
56 "commands": self._getlist,
57 "notcommands": self._getlist,
58 "tagtypes": self._getlist,
59 "urlhandlers": self._getlist,
61 "find": self._getsongs,
62 "list": self._getlist,
63 "listall": self._getdatabase,
64 "listallinfo": self._getdatabase,
65 "lsinfo": self._getdatabase,
66 "search": self._getsongs,
67 "count": self._getobject,
70 "addid": self._getitem,
71 "clear": self._getnone,
72 "currentsong": self._getobject,
73 "delete": self._getnone,
74 "deleteid": self._getnone,
75 "load": self._getnone,
76 "rename": self._getnone,
77 "move": self._getnone,
78 "moveid": self._getnone,
79 "playlist": self._getplaylist,
80 "playlistinfo": self._getsongs,
81 "playlistid": self._getsongs,
82 "plchanges": self._getsongs,
83 "plchangesposid": self._getchanges,
85 "save": self._getnone,
86 "shuffle": self._getnone,
87 "swap": self._getnone,
88 "swapid": self._getnone,
89 "listplaylist": self._getlist,
90 "listplaylistinfo": self._getsongs,
91 "playlistadd": self._getnone,
92 "playlistclear": self._getnone,
93 "playlistdelete": self._getnone,
94 "playlistmove": self._getnone,
95 "playlistfind": self._getsongs,
96 "playlistsearch": self._getsongs,
98 "crossfade": self._getnone,
99 "next": self._getnone,
100 "pause": self._getnone,
101 "play": self._getnone,
102 "playid": self._getnone,
103 "previous": self._getnone,
104 "random": self._getnone,
105 "repeat": self._getnone,
106 "seek": self._getnone,
107 "seekid": self._getnone,
108 "setvol": self._getnone,
109 "stop": self._getnone,
110 "volume": self._getnone,
111 # Miscellaneous Commands
112 "clearerror": self._getnone,
114 "password": self._getnone,
115 "ping": self._getnone,
118 def __getattr__(self, attr):
120 retval = self._commands[attr]
122 raise AttributeError, "'%s' object has no attribute '%s'" % \
123 (self.__class__.__name__, attr)
124 return lambda *args: self._docommand(attr, args, retval)
126 def _docommand(self, command, args, retval):
127 if self._commandlist is not None and not callable(retval):
128 raise CommandListError, "%s not allowed in command list" % command
129 self._writecommand(command, args)
130 if self._commandlist is None:
134 self._commandlist.append(retval)
136 def _writeline(self, line):
137 self._sockfile.write("%s\n" % line)
138 self._sockfile.flush()
140 def _writecommand(self, command, args=[]):
143 parts.append('"%s"' % escape(str(arg)))
144 self._writeline(" ".join(parts))
147 line = self._sockfile.readline()
148 if not line.endswith("\n"):
149 raise ConnectionError, "Connection lost while reading line"
150 line = line.rstrip("\n")
151 if line.startswith(ERROR_PREFIX):
152 error = line[len(ERROR_PREFIX):].strip()
153 raise CommandError, error
154 if self._commandlist is not None:
158 raise ProtocolError, "Got unexpected '%s'" % SUCCESS
159 elif line == SUCCESS:
163 def _readitem(self, separator):
164 line = self._readline()
167 item = line.split(separator, 1)
169 raise ProtocolError, "Could not parse item: '%s'" % line
172 def _readitems(self, separator=": "):
173 item = self._readitem(separator)
176 item = self._readitem(separator)
181 for key, value in self._readitems():
184 raise ProtocolError, "Expected key '%s', got '%s'" % \
190 def _readplaylist(self):
191 for key, value in self._readitems(":"):
195 def _readobjects(self, delimiters=[]):
197 for key, value in self._readitems():
200 if key in delimiters:
203 elif obj.has_key(key):
204 if not isinstance(obj[key], list):
205 obj[key] = [obj[key], value]
207 obj[key].append(value)
214 def _readcommandlist(self):
215 for retval in self._commandlist:
217 self._commandlist = None
221 def _wrapiterator(self, iterator):
223 return list(iterator)
227 line = self._readline()
229 raise ProtocolError, "Got unexpected return value: '%s'" % line
232 items = list(self._readitems())
238 return self._wrapiterator(self._readlist())
240 def _getplaylist(self):
241 return self._wrapiterator(self._readplaylist())
243 def _getobject(self):
244 objs = list(self._readobjects())
249 def _getobjects(self, delimiters):
250 return self._wrapiterator(self._readobjects(delimiters))
253 return self._getobjects(["file"])
255 def _getdatabase(self):
256 return self._getobjects(["file", "directory", "playlist"])
258 def _getoutputs(self):
259 return self._getobjects(["outputid"])
261 def _getchanges(self):
262 return self._getobjects(["cpos"])
264 def _getcommandlist(self):
265 return self._wrapiterator(self._readcommandlist())
268 line = self._sockfile.readline()
269 if not line.endswith("\n"):
270 raise ConnectionError, "Connection lost while reading MPD hello"
271 if not line.startswith(HELLO_PREFIX):
272 raise ProtocolError, "Got invalid MPD hello: '%s'" % line
273 self.mpd_version = line[len(HELLO_PREFIX):].strip()
276 self.mpd_version = None
277 self._commandlist = None
278 self._sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
279 self._sockfile = self._sock.makefile("rb+")
281 def connect(self, host, port):
283 self._sock.connect((host, port))
286 def disconnect(self):
287 self._sockfile.close()
291 def command_list_ok_begin(self):
292 if self._commandlist is not None:
293 raise CommandListError, "Already in command list"
294 self._writecommand("command_list_ok_begin")
295 self._commandlist = []
297 def command_list_end(self):
298 if self._commandlist is None:
299 raise CommandListError, "Not in command list"
300 self._writecommand("command_list_end")
301 return self._getcommandlist()
305 return text.replace("\\", "\\\\").replace('"', '\\"')
308 # vim: set expandtab shiftwidth=4 softtabstop=4 textwidth=79: