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 _NotConnected(object):
43 def __getattr__(self, attr):
47 raise ConnectionError("Not connected")
49 class MPDClient(object):
55 "clearerror": self._getnone,
56 "currentsong": self._getobject,
57 "idle": self._getlist,
59 "status": self._getobject,
60 "stats": self._getobject,
61 # Playback Option Commands
62 "crossfade": self._getnone,
63 "random": self._getnone,
64 "repeat": self._getnone,
65 "setvol": self._getnone,
66 "volume": self._getnone,
67 # Playback Control Commands
68 "next": self._getnone,
69 "pause": self._getnone,
70 "play": self._getnone,
71 "playid": self._getnone,
72 "previous": self._getnone,
73 "seek": self._getnone,
74 "seekid": self._getnone,
75 "stop": self._getnone,
78 "addid": self._getitem,
79 "clear": self._getnone,
80 "delete": self._getnone,
81 "deleteid": self._getnone,
82 "move": self._getnone,
83 "moveid": self._getnone,
84 "playlist": self._getplaylist,
85 "playlistfind": self._getsongs,
86 "playlistid": self._getsongs,
87 "playlistinfo": self._getsongs,
88 "playlistsearch": self._getsongs,
89 "plchanges": self._getsongs,
90 "plchangesposid": self._getchanges,
91 "shuffle": self._getnone,
92 "swap": self._getnone,
93 "swapid": self._getnone,
94 # Stored Playlist Commands
95 "listplaylist": self._getlist,
96 "listplaylistinfo": self._getsongs,
97 "listplaylists": self._getplaylists,
98 "load": self._getnone,
99 "playlistadd": self._getnone,
100 "playlistclear": self._getnone,
101 "playlistdelete": self._getnone,
102 "playlistmove": self._getnone,
103 "rename": self._getnone,
105 "save": self._getnone,
107 "count": self._getobject,
108 "find": self._getsongs,
109 "list": self._getlist,
110 "listall": self._getdatabase,
111 "listallinfo": self._getdatabase,
112 "lsinfo": self._getdatabase,
113 "search": self._getsongs,
114 "update": self._getitem,
115 # Connection Commands
118 "password": self._getnone,
119 "ping": self._getnone,
120 # Audio Output Commands
121 "disableoutput": self._getnone,
122 "enableoutput": self._getnone,
123 "outputs": self._getoutputs,
124 # Reflection Commands
125 "commands": self._getlist,
126 "notcommands": self._getlist,
127 "tagtypes": self._getlist,
128 "urlhandlers": self._getlist,
131 def __getattr__(self, attr):
133 retval = self._commands[attr]
135 raise AttributeError("'%s' object has no attribute '%s'" %
136 (self.__class__.__name__, attr))
137 return lambda *args: self._docommand(attr, args, retval)
139 def _docommand(self, command, args, retval):
140 if self._commandlist is not None and not callable(retval):
141 raise CommandListError("%s not allowed in command list" % command)
142 self._writecommand(command, args)
143 if self._commandlist is None:
147 self._commandlist.append(retval)
149 def _writeline(self, line):
150 self._wfile.write("%s\n" % line)
153 def _writecommand(self, command, args=[]):
156 parts.append('"%s"' % escape(str(arg)))
157 self._writeline(" ".join(parts))
160 line = self._rfile.readline()
161 if not line.endswith("\n"):
162 raise ConnectionError("Connection lost while reading line")
163 line = line.rstrip("\n")
164 if line.startswith(ERROR_PREFIX):
165 error = line[len(ERROR_PREFIX):].strip()
166 raise CommandError(error)
167 if self._commandlist is not None:
171 raise ProtocolError("Got unexpected '%s'" % SUCCESS)
172 elif line == SUCCESS:
176 def _readitem(self, separator):
177 line = self._readline()
180 item = line.split(separator, 1)
182 raise ProtocolError("Could not parse item: '%s'" % line)
185 def _readitems(self, separator=": "):
186 item = self._readitem(separator)
189 item = self._readitem(separator)
194 for key, value in self._readitems():
197 raise ProtocolError("Expected key '%s', got '%s'" %
203 def _readplaylist(self):
204 for key, value in self._readitems(":"):
208 def _readobjects(self, delimiters=[]):
210 for key, value in self._readitems():
213 if key in delimiters:
216 elif obj.has_key(key):
217 if not isinstance(obj[key], list):
218 obj[key] = [obj[key], value]
220 obj[key].append(value)
227 def _readcommandlist(self):
228 for retval in self._commandlist:
230 self._commandlist = None
234 def _wrapiterator(self, iterator):
236 return list(iterator)
240 line = self._readline()
242 raise ProtocolError("Got unexpected return value: '%s'" % line)
245 items = list(self._readitems())
251 return self._wrapiterator(self._readlist())
253 def _getplaylist(self):
254 return self._wrapiterator(self._readplaylist())
256 def _getobject(self):
257 objs = list(self._readobjects())
262 def _getobjects(self, delimiters):
263 return self._wrapiterator(self._readobjects(delimiters))
266 return self._getobjects(["file"])
268 def _getplaylists(self):
269 return self._getobjects(["playlist"])
271 def _getdatabase(self):
272 return self._getobjects(["file", "directory", "playlist"])
274 def _getoutputs(self):
275 return self._getobjects(["outputid"])
277 def _getchanges(self):
278 return self._getobjects(["cpos"])
280 def _getcommandlist(self):
281 return self._wrapiterator(self._readcommandlist())
284 line = self._rfile.readline()
285 if not line.endswith("\n"):
286 raise ConnectionError("Connection lost while reading MPD hello")
287 line = line.rstrip("\n")
288 if not line.startswith(HELLO_PREFIX):
289 raise ProtocolError("Got invalid MPD hello: '%s'" % line)
290 self.mpd_version = line[len(HELLO_PREFIX):].strip()
293 self.mpd_version = None
294 self._commandlist = None
296 self._rfile = _NotConnected()
297 self._wfile = _NotConnected()
299 def connect(self, host, port):
301 raise ConnectionError("Already connected")
302 msg = "getaddrinfo returns an empty list"
304 flags = socket.AI_ADDRCONFIG
305 except AttributeError:
307 for res in socket.getaddrinfo(host, port, socket.AF_UNSPEC,
308 socket.SOCK_STREAM, socket.IPPROTO_TCP,
310 af, socktype, proto, canonname, sa = res
312 self._sock = socket.socket(af, socktype, proto)
313 self._sock.connect(sa)
314 except socket.error, msg:
321 raise socket.error(msg)
322 self._rfile = self._sock.makefile("rb")
323 self._wfile = self._sock.makefile("wb")
330 def disconnect(self):
336 def command_list_ok_begin(self):
337 if self._commandlist is not None:
338 raise CommandListError("Already in command list")
339 self._writecommand("command_list_ok_begin")
340 self._commandlist = []
342 def command_list_end(self):
343 if self._commandlist is None:
344 raise CommandListError("Not in command list")
345 self._writecommand("command_list_end")
346 return self._getcommandlist()
350 return text.replace("\\", "\\\\").replace('"', '\\"')
353 # vim: set expandtab shiftwidth=4 softtabstop=4 textwidth=79: