]> kaliko git repositories - python-musicpd.git/blob - mpd.py
rearranging commands to match new protocol documentation
[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             "status":           self._getobject,
58             "stats":            self._getobject,
59             # Playback Option Commands
60             "crossfade":        self._getnone,
61             "random":           self._getnone,
62             "repeat":           self._getnone,
63             "setvol":           self._getnone,
64             "volume":           self._getnone,
65             # Playback Control Commands
66             "next":             self._getnone,
67             "pause":            self._getnone,
68             "play":             self._getnone,
69             "playid":           self._getnone,
70             "previous":         self._getnone,
71             "seek":             self._getnone,
72             "seekid":           self._getnone,
73             "stop":             self._getnone,
74             # Playlist Commands
75             "add":              self._getnone,
76             "addid":            self._getitem,
77             "clear":            self._getnone,
78             "delete":           self._getnone,
79             "deleteid":         self._getnone,
80             "move":             self._getnone,
81             "moveid":           self._getnone,
82             "playlist":         self._getplaylist,
83             "playlistfind":     self._getsongs,
84             "playlistid":       self._getsongs,
85             "playlistinfo":     self._getsongs,
86             "playlistsearch":   self._getsongs,
87             "plchanges":        self._getsongs,
88             "plchangesposid":   self._getchanges,
89             "shuffle":          self._getnone,
90             "swap":             self._getnone,
91             "swapid":           self._getnone,
92             # Stored Playlist Commands
93             "listplaylist":     self._getlist,
94             "listplaylistinfo": self._getsongs,
95             "load":             self._getnone,
96             "playlistadd":      self._getnone,
97             "playlistclear":    self._getnone,
98             "playlistdelete":   self._getnone,
99             "playlistmove":     self._getnone,
100             "rename":           self._getnone,
101             "rm":               self._getnone,
102             "save":             self._getnone,
103             # Database Commands
104             "count":            self._getobject,
105             "find":             self._getsongs,
106             "list":             self._getlist,
107             "listall":          self._getdatabase,
108             "listallinfo":      self._getdatabase,
109             "lsinfo":           self._getdatabase,
110             "search":           self._getsongs,
111             "update":           self._getitem,
112             # Connection Commands
113             "close":            None,
114             "kill":             None,
115             "password":         self._getnone,
116             "ping":             self._getnone,
117             # Audio Output Commands
118             "disableoutput":    self._getnone,
119             "enableoutput":     self._getnone,
120             "outputs":          self._getoutputs,
121             # Reflection Commands
122             "commands":         self._getlist,
123             "notcommands":      self._getlist,
124             "tagtypes":         self._getlist,
125             "urlhandlers":      self._getlist,
126         }
127
128     def __getattr__(self, attr):
129         try:
130             retval = self._commands[attr]
131         except KeyError:
132             raise AttributeError("'%s' object has no attribute '%s'" %
133                                  (self.__class__.__name__, attr))
134         return lambda *args: self._docommand(attr, args, retval)
135
136     def _docommand(self, command, args, retval):
137         if self._commandlist is not None and not callable(retval):
138             raise CommandListError("%s not allowed in command list" % command)
139         self._writecommand(command, args)
140         if self._commandlist is None:
141             if callable(retval):
142                 return retval()
143             return retval
144         self._commandlist.append(retval)
145
146     def _writeline(self, line):
147         self._wfile.write("%s\n" % line)
148         self._wfile.flush()
149
150     def _writecommand(self, command, args=[]):
151         parts = [command]
152         for arg in args:
153             parts.append('"%s"' % escape(str(arg)))
154         self._writeline(" ".join(parts))
155
156     def _readline(self):
157         line = self._rfile.readline()
158         if not line.endswith("\n"):
159             raise ConnectionError("Connection lost while reading line")
160         line = line.rstrip("\n")
161         if line.startswith(ERROR_PREFIX):
162             error = line[len(ERROR_PREFIX):].strip()
163             raise CommandError(error)
164         if self._commandlist is not None:
165             if line == NEXT:
166                 return
167             if line == SUCCESS:
168                 raise ProtocolError("Got unexpected '%s'" % SUCCESS)
169         elif line == SUCCESS:
170             return
171         return line
172
173     def _readitem(self, separator):
174         line = self._readline()
175         if line is None:
176             return
177         item = line.split(separator, 1)
178         if len(item) < 2:
179             raise ProtocolError("Could not parse item: '%s'" % line)
180         return item
181
182     def _readitems(self, separator=": "):
183         item = self._readitem(separator)
184         while item:
185             yield item
186             item = self._readitem(separator)
187         raise StopIteration
188
189     def _readlist(self):
190         seen = None
191         for key, value in self._readitems():
192             if key != seen:
193                 if seen is not None:
194                     raise ProtocolError("Expected key '%s', got '%s'" %
195                                         (seen, key))
196                 seen = key
197             yield value
198         raise StopIteration
199
200     def _readplaylist(self):
201         for key, value in self._readitems(":"):
202             yield value
203         raise StopIteration
204
205     def _readobjects(self, delimiters=[]):
206         obj = {}
207         for key, value in self._readitems():
208             key = key.lower()
209             if obj:
210                 if key in delimiters:
211                     yield obj
212                     obj = {}
213                 elif obj.has_key(key):
214                     if not isinstance(obj[key], list):
215                         obj[key] = [obj[key], value]
216                     else:
217                         obj[key].append(value)
218                     continue
219             obj[key] = value
220         if obj:
221             yield obj
222         raise StopIteration
223
224     def _readcommandlist(self):
225         for retval in self._commandlist:
226             yield retval()
227         self._commandlist = None
228         self._getnone()
229         raise StopIteration
230
231     def _wrapiterator(self, iterator):
232         if not self.iterate:
233             return list(iterator)
234         return iterator
235
236     def _getnone(self):
237         line = self._readline()
238         if line is not None:
239             raise ProtocolError("Got unexpected return value: '%s'" % line)
240
241     def _getitem(self):
242         items = list(self._readitems())
243         if len(items) != 1:
244             return
245         return items[0][1]
246
247     def _getlist(self):
248         return self._wrapiterator(self._readlist())
249
250     def _getplaylist(self):
251         return self._wrapiterator(self._readplaylist())
252
253     def _getobject(self):
254         objs = list(self._readobjects())
255         if not objs:
256             return {}
257         return objs[0]
258
259     def _getobjects(self, delimiters):
260         return self._wrapiterator(self._readobjects(delimiters))
261
262     def _getsongs(self):
263         return self._getobjects(["file"])
264
265     def _getdatabase(self):
266         return self._getobjects(["file", "directory", "playlist"])
267
268     def _getoutputs(self):
269         return self._getobjects(["outputid"])
270
271     def _getchanges(self):
272         return self._getobjects(["cpos"])
273
274     def _getcommandlist(self):
275         return self._wrapiterator(self._readcommandlist())
276
277     def _hello(self):
278         line = self._rfile.readline()
279         if not line.endswith("\n"):
280             raise ConnectionError("Connection lost while reading MPD hello")
281         line = line.rstrip("\n")
282         if not line.startswith(HELLO_PREFIX):
283             raise ProtocolError("Got invalid MPD hello: '%s'" % line)
284         self.mpd_version = line[len(HELLO_PREFIX):].strip()
285
286     def _reset(self):
287         self.mpd_version = None
288         self._commandlist = None
289         self._sock = None
290         self._rfile = _NotConnected()
291         self._wfile = _NotConnected()
292
293     def connect(self, host, port):
294         if self._sock:
295             raise ConnectionError("Already connected")
296         msg = "getaddrinfo returns an empty list"
297         try:
298             flags = socket.AI_ADDRCONFIG
299         except AttributeError:
300             flags = 0
301         for res in socket.getaddrinfo(host, port, socket.AF_UNSPEC,
302                                       socket.SOCK_STREAM, socket.IPPROTO_TCP,
303                                       flags):
304             af, socktype, proto, canonname, sa = res
305             try:
306                 self._sock = socket.socket(af, socktype, proto)
307                 self._sock.connect(sa)
308             except socket.error, msg:
309                 if self._sock:
310                     self._sock.close()
311                 self._sock = None
312                 continue
313             break
314         if not self._sock:
315             raise socket.error(msg)
316         self._rfile = self._sock.makefile("rb")
317         self._wfile = self._sock.makefile("wb")
318         try:
319             self._hello()
320         except:
321             self.disconnect()
322             raise
323
324     def disconnect(self):
325         self._rfile.close()
326         self._wfile.close()
327         self._sock.close()
328         self._reset()
329
330     def command_list_ok_begin(self):
331         if self._commandlist is not None:
332             raise CommandListError("Already in command list")
333         self._writecommand("command_list_ok_begin")
334         self._commandlist = []
335
336     def command_list_end(self):
337         if self._commandlist is None:
338             raise CommandListError("Not in command list")
339         self._writecommand("command_list_end")
340         return self._getcommandlist()
341
342
343 def escape(text):
344     return text.replace("\\", "\\\\").replace('"', '\\"')
345
346
347 # vim: set expandtab shiftwidth=4 softtabstop=4 textwidth=79: