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