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