1 # Copyright (C) 2008 J. Alexander Treuman <jat@spatialrift.net>
3 # This program is free software: you can redistribute it and/or modify
4 # it under the terms of the GNU General Public License as published by
5 # the Free Software Foundation, either version 3 of the License, or
6 # (at your option) any later version.
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
13 # You should have received a copy of the GNU General Public License
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
19 HELLO_PREFIX = "OK MPD "
25 class MPDError(Exception):
28 class ProtocolError(MPDError):
31 class CommandError(MPDError):
34 class CommandListError(MPDError):
38 class MPDClient(object):
44 "disableoutput": self._getnone,
45 "enableoutput": self._getnone,
47 "update": self._getitem,
48 # Informational Commands
49 "status": self._getobject,
50 "stats": self._getobject,
51 "outputs": self._getoutputs,
52 "commands": self._getlist,
53 "notcommands": self._getlist,
54 "tagtypes": self._getlist,
55 "urlhandlers": self._getlist,
57 "find": self._getsongs,
58 "list": self._getlist,
59 "listall": self._getdatabase,
60 "listallinfo": self._getdatabase,
61 "lsinfo": self._getdatabase,
62 "search": self._getsongs,
63 "count": self._getobject,
66 "addid": self._getitem,
67 "clear": self._getnone,
68 "currentsong": self._getobject,
69 "delete": self._getnone,
70 "deleteid": self._getnone,
71 "load": self._getnone,
72 "rename": self._getnone,
73 "move": self._getnone,
74 "moveid": self._getnone,
75 "playlist": self._getplaylist,
76 "playlistinfo": self._getsongs,
77 "playlistid": self._getsongs,
78 "plchanges": self._getsongs,
79 "plchangesposid": self._getchanges,
81 "save": self._getnone,
82 "shuffle": self._getnone,
83 "swap": self._getnone,
84 "swapid": self._getnone,
85 "listplaylist": self._getlist,
86 "listplaylistinfo": self._getsongs,
87 "playlistadd": self._getnone,
88 "playlistclear": self._getnone,
89 "playlistdelete": self._getnone,
90 "playlistmove": self._getnone,
91 "playlistfind": self._getsongs,
92 "playlistsearch": self._getsongs,
94 "crossfade": self._getnone,
95 "next": self._getnone,
96 "pause": self._getnone,
97 "play": self._getnone,
98 "playid": self._getnone,
99 "previous": self._getnone,
100 "random": self._getnone,
101 "repeat": self._getnone,
102 "seek": self._getnone,
103 "seekid": self._getnone,
104 "setvol": self._getnone,
105 "stop": self._getnone,
106 "volume": self._getnone,
107 # Miscellaneous Commands
108 "clearerror": self._getnone,
110 "password": self._getnone,
111 "ping": self._getnone,
114 def __getattr__(self, attr):
116 retval = self._commands[attr]
118 raise AttributeError, "'%s' object has no attribute '%s'" % \
119 (self.__class__.__name__, attr)
120 return lambda *args: self._docommand(attr, args, retval)
122 def _docommand(self, command, args, retval):
123 if self._commandlist is not None and not callable(retval):
124 raise CommandListError, "%s not allowed in command list" % command
125 self._writecommand(command, args)
126 if self._commandlist is None:
130 self._commandlist.append(retval)
132 def _writeline(self, line):
133 self._sockfile.write("%s\n" % line)
134 self._sockfile.flush()
136 def _writecommand(self, command, args=[]):
139 parts.append('"%s"' % escape(str(arg)))
140 self._writeline(" ".join(parts))
143 line = self._sockfile.readline().rstrip("\n")
144 if line.startswith(ERROR_PREFIX):
145 error = line[len(ERROR_PREFIX):].strip()
146 raise CommandError, error
147 if self._commandlist is not None:
151 raise ProtocolError, "Got unexpected '%s'" % SUCCESS
152 elif line == SUCCESS:
156 def _readitem(self, separator):
157 line = self._readline()
160 item = line.split(separator, 1)
162 raise ProtocolError, "Could not parse item: '%s'" % line
165 def _readitems(self, separator=": "):
166 item = self._readitem(separator)
169 item = self._readitem(separator)
174 for key, value in self._readitems():
177 raise ProtocolError, "Expected key '%s', got '%s'" % \
183 def _readplaylist(self):
184 for key, value in self._readitems(":"):
188 def _readobjects(self, delimiters=[]):
190 for key, value in self._readitems():
193 if key in delimiters:
196 elif obj.has_key(key):
197 if not isinstance(obj[key], list):
198 obj[key] = [obj[key], value]
200 obj[key].append(value)
207 def _readcommandlist(self):
208 for retval in self._commandlist:
210 self._commandlist = None
214 def _wrapiterator(self, iterator):
216 return list(iterator)
220 line = self._readline()
222 raise ProtocolError, "Got unexpected return value: '%s'" % line
225 items = list(self._readitems())
227 raise ProtocolError, "Expected 1 item, got %i" % len(items)
231 return self._wrapiterator(self._readlist())
233 def _getplaylist(self):
234 return self._wrapiterator(self._readplaylist())
236 def _getobject(self):
237 objs = list(self._readobjects())
242 def _getobjects(self, delimiters):
243 return self._wrapiterator(self._readobjects(delimiters))
246 return self._getobjects(["file"])
248 def _getdatabase(self):
249 return self._getobjects(["file", "directory", "playlist"])
251 def _getoutputs(self):
252 return self._getobjects(["outputid"])
254 def _getchanges(self):
255 return self._getobjects(["cpos"])
257 def _getcommandlist(self):
258 return self._wrapiterator(self._readcommandlist())
261 line = self._sockfile.readline().rstrip("\n")
262 if not line.startswith(HELLO_PREFIX):
263 raise ProtocolError, "Got invalid MPD hello: '%s'" % line
264 self.mpd_version = line[len(HELLO_PREFIX):].strip()
267 self.mpd_version = None
268 self._commandlist = None
269 self._sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
270 self._sockfile = self._sock.makefile("rb+")
272 def connect(self, host, port):
274 self._sock.connect((host, port))
277 def disconnect(self):
278 self._sockfile.close()
282 def command_list_ok_begin(self):
283 if self._commandlist is not None:
284 raise CommandListError, "Already in command list"
285 self._writecommand("command_list_ok_begin")
286 self._commandlist = []
288 def command_list_end(self):
289 if self._commandlist is None:
290 raise CommandListError, "Not in command list"
291 self._writecommand("command_list_end")
292 return self._getcommandlist()
296 return text.replace("\\", "\\\\").replace('"', '\\"')
299 # vim: set expandtab shiftwidth=4 softtabstop=4 textwidth=79: