]> kaliko git repositories - python-musicpd.git/blob - mpd.py
adding command_list support
[python-musicpd.git] / mpd.py
1 #! /usr/bin/env python
2
3 # TODO: return {} if no object read (?)
4 # TODO: implement argument checking/parsing (?)
5 # TODO: check for EOF when reading and benchmark it
6 # TODO: converter support
7 # TODO: global for parsing MPD_HOST/MPD_PORT
8 # TODO: global for parsing MPD error messages
9 # TODO: IPv6 support (AF_INET6)
10
11 import socket
12
13
14 HELLO_PREFIX = "OK MPD "
15 ERROR_PREFIX = "ACK "
16 SUCCESS = "OK"
17 NEXT = "list_OK"
18
19
20 class MPDError(Exception):
21     pass
22
23 class ProtocolError(MPDError):
24     pass
25
26 class CommandError(MPDError):
27     pass
28
29 class CommandListError(MPDError):
30     pass
31
32
33 class MPDClient(object):
34     def __init__(self):
35         self.iterate = False
36         self._reset()
37         self._commands = {
38             # Admin Commands
39             "disableoutput":    self._getnone,
40             "enableoutput":     self._getnone,
41             "kill":             None,
42             "update":           self._getitem,
43             # Informational Commands
44             "status":           self._getobject,
45             "stats":            self._getobject,
46             "outputs":          self._getoutputs,
47             "commands":         self._getlist,
48             "notcommands":      self._getlist,
49             "tagtypes":         self._getlist,
50             "urlhandlers":      self._getlist,
51             # Database Commands
52             "find":             self._getsongs,
53             "list":             self._getlist,
54             "listall":          self._getdatabase,
55             "listallinfo":      self._getdatabase,
56             "lsinfo":           self._getdatabase,
57             "search":           self._getsongs,
58             "count":            self._getobject,
59             # Playlist Commands
60             "add":              self._getnone,
61             "addid":            self._getitem,
62             "clear":            self._getnone,
63             "currentsong":      self._getobject,
64             "delete":           self._getnone,
65             "deleteid":         self._getnone,
66             "load":             self._getnone,
67             "rename":           self._getnone,
68             "move":             self._getnone,
69             "moveid":           self._getnone,
70             "playlist":         self._getplaylist,
71             "playlistinfo":     self._getsongs,
72             "playlistid":       self._getsongs,
73             "plchanges":        self._getsongs,
74             "plchangesposid":   self._getchanges,
75             "rm":               self._getnone,
76             "save":             self._getnone,
77             "shuffle":          self._getnone,
78             "swap":             self._getnone,
79             "swapid":           self._getnone,
80             "listplaylist":     self._getlist,
81             "listplaylistinfo": self._getsongs,
82             "playlistadd":      self._getnone,
83             "playlistclear":    self._getnone,
84             "playlistdelete":   self._getnone,
85             "playlistmove":     self._getnone,
86             "playlistfind":     self._getsongs,
87             "playlistsearch":   self._getsongs,
88             # Playback Commands
89             "crossfade":        self._getnone,
90             "next":             self._getnone,
91             "pause":            self._getnone,
92             "play":             self._getnone,
93             "playid":           self._getnone,
94             "previous":         self._getnone,
95             "random":           self._getnone,
96             "repeat":           self._getnone,
97             "seek":             self._getnone,
98             "seekid":           self._getnone,
99             "setvol":           self._getnone,
100             "stop":             self._getnone,
101             "volume":           self._getnone,
102             # Miscellaneous Commands
103             "clearerror":       self._getnone,
104             "close":            None,
105             "password":         self._getnone,
106             "ping":             self._getnone,
107         }
108
109     def __getattr__(self, attr):
110         try:
111             retval = self._commands[attr]
112         except KeyError:
113             raise AttributeError, "'%s' object has no attribute '%s'" % \
114                                   (self.__class__.__name__, attr)
115         return lambda *args: self._docommand(attr, args, retval)
116
117     def _docommand(self, command, args, retval):
118         if self._commandlist is not None and not callable(retval):
119             raise CommandListError, "%s not allowed in command list" % command
120         self._writecommand(command, args)
121         if self._commandlist is None:
122             if callable(retval):
123                 return retval()
124             return retval
125         self._commandlist.append(retval)
126
127     def _writeline(self, line):
128         self._sockfile.write("%s\n" % line)
129         self._sockfile.flush()
130
131     def _writecommand(self, command, args=[]):
132         parts = [command]
133         for arg in args:
134             parts.append('"%s"' % escape(str(arg)))
135         self._writeline(" ".join(parts))
136
137     def _readline(self):
138         line = self._sockfile.readline().rstrip("\n")
139         if line.startswith(ERROR_PREFIX):
140             error = line[len(ERROR_PREFIX):].strip()
141             raise CommandError, error
142         if self._commandlist is not None:
143             if line == NEXT:
144                 return
145             if line == SUCCESS:
146                 raise ProtocolError, "Got unexpected '%s'" % SUCCESS
147         elif line == SUCCESS:
148             return
149         return line
150
151     def _readitem(self, separator):
152         line = self._readline()
153         if line is None:
154             return
155         item = line.split(separator, 1)
156         if len(item) < 2:
157             raise ProtocolError, "Could not parse item: '%s'" % line
158         return item
159
160     def _readitems(self, separator=": "):
161         item = self._readitem(separator)
162         while item:
163             yield item
164             item = self._readitem(separator)
165         raise StopIteration
166
167     def _readlist(self):
168         seen = None
169         for key, value in self._readitems():
170             if key != seen:
171                 if seen is not None:
172                     raise ProtocolError, "Expected key '%s', got '%s'" % \
173                                          (seen, key)
174                 seen = key
175             yield value
176         raise StopIteration
177
178     def _readplaylist(self):
179         for key, value in self._readitems(":"):
180             yield value
181         raise StopIteration
182
183     def _readobjects(self, delimiters=[]):
184         obj = {}
185         for key, value in self._readitems():
186             key = key.lower()
187             if obj:
188                 if key in delimiters:
189                     yield obj
190                     obj = {}
191                 elif obj.has_key(key):
192                     if not isinstance(obj[key], list):
193                         obj[key] = [obj[key], value]
194                     else:
195                         obj[key].append(value)
196                     continue
197             obj[key] = value
198         if obj:
199             yield obj
200         raise StopIteration
201
202     def _readcommandlist(self):
203         for retval in self._commandlist:
204             yield retval()
205         self._commandlist = None
206         self._getnone()
207         raise StopIteration
208
209     def _wrapiterator(self, iterator):
210         if not self.iterate:
211             return list(iterator)
212         return iterator
213
214     def _getnone(self):
215         line = self._readline()
216         if line is not None:
217             raise ProtocolError, "Got unexpected return value: '%s'" % line
218
219     def _getitem(self):
220         items = list(self._readitems())
221         if len(items) != 1:
222             raise ProtocolError, "Expected 1 item, got %i" % len(items)
223         return items[0][1]
224
225     def _getlist(self):
226         return self._wrapiterator(self._readlist())
227
228     def _getplaylist(self):
229         return self._wrapiterator(self._readplaylist())
230
231     def _getobject(self):
232         objs = list(self._readobjects())
233         if not objs:
234             return
235         return objs[0]
236
237     def _getobjects(self, delimiters):
238         return self._wrapiterator(self._readobjects(delimiters))
239
240     def _getsongs(self):
241         return self._getobjects(["file"])
242
243     def _getdatabase(self):
244         return self._getobjects(["file", "directory", "playlist"])
245
246     def _getoutputs(self):
247         return self._getobjects(["outputid"])
248
249     def _getchanges(self):
250         return self._getobjects(["cpos"])
251
252     def _getcommandlist(self):
253         return self._wrapiterator(self._readcommandlist())
254
255     def _hello(self):
256         line = self._sockfile.readline().rstrip("\n")
257         if not line.startswith(HELLO_PREFIX):
258             raise ProtocolError, "Got invalid MPD hello: '%s'" % line
259         self.mpd_version = line[len(HELLO_PREFIX):].strip()
260
261     def _reset(self):
262         self.mpd_version = None
263         self._commandlist = None
264         self._sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
265         self._sockfile = self._sock.makefile("rb+")
266
267     def connect(self, host, port):
268         self.disconnect()
269         self._sock.connect((host, port))
270         self._hello()
271
272     def disconnect(self):
273         self._sockfile.close()
274         self._sock.close()
275         self._reset()
276
277     def command_list_ok_begin(self):
278         if self._commandlist is not None:
279             raise CommandListError, "Already in command list"
280         self._writecommand("command_list_ok_begin")
281         self._commandlist = []
282
283     def command_list_end(self):
284         if self._commandlist is None:
285             raise CommandListError, "Not in command list"
286         self._writecommand("command_list_end")
287         return self._getcommandlist()
288
289
290 def escape(text):
291     return text.replace("\\", "\\\\").replace('"', '\\"')
292
293
294 # vim: set expandtab shiftwidth=4 softtabstop=4 textwidth=79: