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