]> kaliko git repositories - python-musicpd.git/blob - mpd.py
1704b7b7fc1e4b6b820f70fad09a343bb34ece30
[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._fetch_nothing,
56             "currentsong":      self._fetch_object,
57             "idle":             self._fetch_list,
58             "noidle":           None,
59             "status":           self._fetch_object,
60             "stats":            self._fetch_object,
61             # Playback Option Commands
62             "consume":          self._fetch_nothing,
63             "crossfade":        self._fetch_nothing,
64             "random":           self._fetch_nothing,
65             "repeat":           self._fetch_nothing,
66             "setvol":           self._fetch_nothing,
67             "single":           self._fetch_nothing,
68             "volume":           self._fetch_nothing,
69             # Playback Control Commands
70             "next":             self._fetch_nothing,
71             "pause":            self._fetch_nothing,
72             "play":             self._fetch_nothing,
73             "playid":           self._fetch_nothing,
74             "previous":         self._fetch_nothing,
75             "seek":             self._fetch_nothing,
76             "seekid":           self._fetch_nothing,
77             "stop":             self._fetch_nothing,
78             # Playlist Commands
79             "add":              self._fetch_nothing,
80             "addid":            self._fetch_item,
81             "clear":            self._fetch_nothing,
82             "delete":           self._fetch_nothing,
83             "deleteid":         self._fetch_nothing,
84             "move":             self._fetch_nothing,
85             "moveid":           self._fetch_nothing,
86             "playlist":         self._fetch_playlist,
87             "playlistfind":     self._fetch_songs,
88             "playlistid":       self._fetch_songs,
89             "playlistinfo":     self._fetch_songs,
90             "playlistsearch":   self._fetch_songs,
91             "plchanges":        self._fetch_songs,
92             "plchangesposid":   self._fetch_changes,
93             "shuffle":          self._fetch_nothing,
94             "swap":             self._fetch_nothing,
95             "swapid":           self._fetch_nothing,
96             # Stored Playlist Commands
97             "listplaylist":     self._fetch_list,
98             "listplaylistinfo": self._fetch_songs,
99             "listplaylists":    self._fetch_playlists,
100             "load":             self._fetch_nothing,
101             "playlistadd":      self._fetch_nothing,
102             "playlistclear":    self._fetch_nothing,
103             "playlistdelete":   self._fetch_nothing,
104             "playlistmove":     self._fetch_nothing,
105             "rename":           self._fetch_nothing,
106             "rm":               self._fetch_nothing,
107             "save":             self._fetch_nothing,
108             # Database Commands
109             "count":            self._fetch_object,
110             "find":             self._fetch_songs,
111             "list":             self._fetch_list,
112             "listall":          self._fetch_database,
113             "listallinfo":      self._fetch_database,
114             "lsinfo":           self._fetch_database,
115             "search":           self._fetch_songs,
116             "update":           self._fetch_item,
117             # Connection Commands
118             "close":            None,
119             "kill":             None,
120             "password":         self._fetch_nothing,
121             "ping":             self._fetch_nothing,
122             # Audio Output Commands
123             "disableoutput":    self._fetch_nothing,
124             "enableoutput":     self._fetch_nothing,
125             "outputs":          self._fetch_outputs,
126             # Reflection Commands
127             "commands":         self._fetch_list,
128             "notcommands":      self._fetch_list,
129             "tagtypes":         self._fetch_list,
130             "urlhandlers":      self._fetch_list,
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._execute(attr, args, retval)
140
141     def _execute(self, command, args, retval):
142         if self._command_list is not None and not callable(retval):
143             raise CommandListError("%s not allowed in command list" % command)
144         self._write_command(command, args)
145         if self._command_list is None:
146             if callable(retval):
147                 return retval()
148             return retval
149         self._command_list.append(retval)
150
151     def _write_line(self, line):
152         self._wfile.write("%s\n" % line)
153         self._wfile.flush()
154
155     def _write_command(self, command, args=[]):
156         parts = [command]
157         for arg in args:
158             parts.append('"%s"' % escape(str(arg)))
159         self._write_line(" ".join(parts))
160
161     def _read_line(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._command_list 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 _read_pair(self, separator):
179         line = self._read_line()
180         if line is None:
181             return
182         pair = line.split(separator, 1)
183         if len(pair) < 2:
184             raise ProtocolError("Could not parse pair: '%s'" % line)
185         return pair
186
187     def _read_pairs(self, separator=": "):
188         pair = self._read_pair(separator)
189         while pair:
190             yield pair
191             pair = self._read_pair(separator)
192         raise StopIteration
193
194     def _read_list(self):
195         seen = None
196         for key, value in self._read_pairs():
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 _read_playlist(self):
206         for key, value in self._read_pairs(":"):
207             yield value
208         raise StopIteration
209
210     def _read_objects(self, delimiters=[]):
211         obj = {}
212         for key, value in self._read_pairs():
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 _read_command_list(self):
230         for retval in self._command_list:
231             yield retval()
232         self._command_list = None
233         self._fetch_nothing()
234         raise StopIteration
235
236     def _wrap_iterator(self, iterator):
237         if not self.iterate:
238             return list(iterator)
239         return iterator
240
241     def _fetch_nothing(self):
242         line = self._read_line()
243         if line is not None:
244             raise ProtocolError("Got unexpected return value: '%s'" % line)
245
246     def _fetch_item(self):
247         pairs = list(self._read_pairs())
248         if len(pairs) != 1:
249             return
250         return pairs[0][1]
251
252     def _fetch_list(self):
253         return self._wrap_iterator(self._read_list())
254
255     def _fetch_playlist(self):
256         return self._wrap_iterator(self._read_playlist())
257
258     def _fetch_object(self):
259         objs = list(self._read_objects())
260         if not objs:
261             return {}
262         return objs[0]
263
264     def _fetch_objects(self, delimiters):
265         return self._wrap_iterator(self._read_objects(delimiters))
266
267     def _fetch_songs(self):
268         return self._fetch_objects(["file"])
269
270     def _fetch_playlists(self):
271         return self._fetch_objects(["playlist"])
272
273     def _fetch_database(self):
274         return self._fetch_objects(["file", "directory", "playlist"])
275
276     def _fetch_outputs(self):
277         return self._fetch_objects(["outputid"])
278
279     def _fetch_changes(self):
280         return self._fetch_objects(["cpos"])
281
282     def _fetch_command_list(self):
283         return self._wrap_iterator(self._read_command_list())
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._command_list = None
297         self._sock = None
298         self._rfile = _NotConnected()
299         self._wfile = _NotConnected()
300
301     def _connect_unix(self, path):
302         if not hasattr(socket, "AF_UNIX"):
303             raise ConnectionError("Unix domain sockets not supported "
304                                   "on this platform")
305         sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
306         sock.connect(path)
307         return sock
308
309     def _connect_tcp(self, host, port):
310         try:
311             flags = socket.AI_ADDRCONFIG
312         except AttributeError:
313             flags = 0
314         msg = "getaddrinfo returns an empty list"
315         for res in socket.getaddrinfo(host, port, socket.AF_UNSPEC,
316                                       socket.SOCK_STREAM, socket.IPPROTO_TCP,
317                                       flags):
318             af, socktype, proto, canonname, sa = res
319             try:
320                 sock = socket.socket(af, socktype, proto)
321                 sock.connect(sa)
322             except socket.error, msg:
323                 if sock:
324                     sock.close()
325                 sock = None
326                 continue
327             break
328         if not sock:
329             raise socket.error(msg)
330         return sock
331
332     def connect(self, host, port):
333         if self._sock:
334             raise ConnectionError("Already connected")
335         if host.startswith("/"):
336             self._sock = self._connect_unix(host)
337         else:
338             self._sock = self._connect_tcp(host, port)
339         self._rfile = self._sock.makefile("rb")
340         self._wfile = self._sock.makefile("wb")
341         try:
342             self._hello()
343         except:
344             self.disconnect()
345             raise
346
347     def disconnect(self):
348         self._rfile.close()
349         self._wfile.close()
350         self._sock.close()
351         self._reset()
352
353     def command_list_ok_begin(self):
354         if self._command_list is not None:
355             raise CommandListError("Already in command list")
356         self._write_command("command_list_ok_begin")
357         self._command_list = []
358
359     def command_list_end(self):
360         if self._command_list is None:
361             raise CommandListError("Not in command list")
362         self._write_command("command_list_end")
363         return self._fetch_command_list()
364
365
366 def escape(text):
367     return text.replace("\\", "\\\\").replace('"', '\\"')
368
369
370 # vim: set expandtab shiftwidth=4 softtabstop=4 textwidth=79: