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