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