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