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