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