]> kaliko git repositories - python-musicpd.git/blob - mpd.py
adding idle and noidle commands
[python-musicpd.git] / mpd.py
1 # Python MPD client library
2 # Copyright (C) 2008  J. Alexander Treuman <jat@spatialrift.net>
3 #
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.
8 #
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.
13 #
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/>.
16
17 import socket
18
19
20 HELLO_PREFIX = "OK MPD "
21 ERROR_PREFIX = "ACK "
22 SUCCESS = "OK"
23 NEXT = "list_OK"
24
25
26 class MPDError(Exception):
27     pass
28
29 class ConnectionError(MPDError):
30     pass
31
32 class ProtocolError(MPDError):
33     pass
34
35 class CommandError(MPDError):
36     pass
37
38 class CommandListError(MPDError):
39     pass
40
41
42 class _NotConnected(object):
43     def __getattr__(self, attr):
44         return self._dummy
45
46     def _dummy(*args):
47         raise ConnectionError("Not connected")
48
49 class MPDClient(object):
50     def __init__(self):
51         self.iterate = False
52         self._reset()
53         self._commands = {
54             # Status Commands
55             "clearerror":       self._getnone,
56             "currentsong":      self._getobject,
57             "idle":             self._getlist,
58             "noidle":           None,
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,
76             # Playlist Commands
77             "add":              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,
104             "rm":               self._getnone,
105             "save":             self._getnone,
106             # Database Commands
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
116             "close":            None,
117             "kill":             None,
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,
129         }
130
131     def __getattr__(self, attr):
132         try:
133             retval = self._commands[attr]
134         except KeyError:
135             raise AttributeError("'%s' object has no attribute '%s'" %
136                                  (self.__class__.__name__, attr))
137         return lambda *args: self._docommand(attr, args, retval)
138
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:
144             if callable(retval):
145                 return retval()
146             return retval
147         self._commandlist.append(retval)
148
149     def _writeline(self, line):
150         self._wfile.write("%s\n" % line)
151         self._wfile.flush()
152
153     def _writecommand(self, command, args=[]):
154         parts = [command]
155         for arg in args:
156             parts.append('"%s"' % escape(str(arg)))
157         self._writeline(" ".join(parts))
158
159     def _readline(self):
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:
168             if line == NEXT:
169                 return
170             if line == SUCCESS:
171                 raise ProtocolError("Got unexpected '%s'" % SUCCESS)
172         elif line == SUCCESS:
173             return
174         return line
175
176     def _readitem(self, separator):
177         line = self._readline()
178         if line is None:
179             return
180         item = line.split(separator, 1)
181         if len(item) < 2:
182             raise ProtocolError("Could not parse item: '%s'" % line)
183         return item
184
185     def _readitems(self, separator=": "):
186         item = self._readitem(separator)
187         while item:
188             yield item
189             item = self._readitem(separator)
190         raise StopIteration
191
192     def _readlist(self):
193         seen = None
194         for key, value in self._readitems():
195             if key != seen:
196                 if seen is not None:
197                     raise ProtocolError("Expected key '%s', got '%s'" %
198                                         (seen, key))
199                 seen = key
200             yield value
201         raise StopIteration
202
203     def _readplaylist(self):
204         for key, value in self._readitems(":"):
205             yield value
206         raise StopIteration
207
208     def _readobjects(self, delimiters=[]):
209         obj = {}
210         for key, value in self._readitems():
211             key = key.lower()
212             if obj:
213                 if key in delimiters:
214                     yield obj
215                     obj = {}
216                 elif obj.has_key(key):
217                     if not isinstance(obj[key], list):
218                         obj[key] = [obj[key], value]
219                     else:
220                         obj[key].append(value)
221                     continue
222             obj[key] = value
223         if obj:
224             yield obj
225         raise StopIteration
226
227     def _readcommandlist(self):
228         for retval in self._commandlist:
229             yield retval()
230         self._commandlist = None
231         self._getnone()
232         raise StopIteration
233
234     def _wrapiterator(self, iterator):
235         if not self.iterate:
236             return list(iterator)
237         return iterator
238
239     def _getnone(self):
240         line = self._readline()
241         if line is not None:
242             raise ProtocolError("Got unexpected return value: '%s'" % line)
243
244     def _getitem(self):
245         items = list(self._readitems())
246         if len(items) != 1:
247             return
248         return items[0][1]
249
250     def _getlist(self):
251         return self._wrapiterator(self._readlist())
252
253     def _getplaylist(self):
254         return self._wrapiterator(self._readplaylist())
255
256     def _getobject(self):
257         objs = list(self._readobjects())
258         if not objs:
259             return {}
260         return objs[0]
261
262     def _getobjects(self, delimiters):
263         return self._wrapiterator(self._readobjects(delimiters))
264
265     def _getsongs(self):
266         return self._getobjects(["file"])
267
268     def _getplaylists(self):
269         return self._getobjects(["playlist"])
270
271     def _getdatabase(self):
272         return self._getobjects(["file", "directory", "playlist"])
273
274     def _getoutputs(self):
275         return self._getobjects(["outputid"])
276
277     def _getchanges(self):
278         return self._getobjects(["cpos"])
279
280     def _getcommandlist(self):
281         return self._wrapiterator(self._readcommandlist())
282
283     def _hello(self):
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()
291
292     def _reset(self):
293         self.mpd_version = None
294         self._commandlist = None
295         self._sock = None
296         self._rfile = _NotConnected()
297         self._wfile = _NotConnected()
298
299     def connect(self, host, port):
300         if self._sock:
301             raise ConnectionError("Already connected")
302         msg = "getaddrinfo returns an empty list"
303         try:
304             flags = socket.AI_ADDRCONFIG
305         except AttributeError:
306             flags = 0
307         for res in socket.getaddrinfo(host, port, socket.AF_UNSPEC,
308                                       socket.SOCK_STREAM, socket.IPPROTO_TCP,
309                                       flags):
310             af, socktype, proto, canonname, sa = res
311             try:
312                 self._sock = socket.socket(af, socktype, proto)
313                 self._sock.connect(sa)
314             except socket.error, msg:
315                 if self._sock:
316                     self._sock.close()
317                 self._sock = None
318                 continue
319             break
320         if not self._sock:
321             raise socket.error(msg)
322         self._rfile = self._sock.makefile("rb")
323         self._wfile = self._sock.makefile("wb")
324         try:
325             self._hello()
326         except:
327             self.disconnect()
328             raise
329
330     def disconnect(self):
331         self._rfile.close()
332         self._wfile.close()
333         self._sock.close()
334         self._reset()
335
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 = []
341
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()
347
348
349 def escape(text):
350     return text.replace("\\", "\\\\").replace('"', '\\"')
351
352
353 # vim: set expandtab shiftwidth=4 softtabstop=4 textwidth=79: