python-mpd

python-mpd Git Source Tree

Root/mpd.py

1# python-mpd: Python MPD client library
2# Copyright (C) 2008-2010 J. Alexander Treuman <jat@spatialrift.net>
3#
4# python-mpd is free software: you can redistribute it and/or modify
5# it under the terms of the GNU Lesser 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# python-mpd 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 Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with python-mpd. If not, see <http://www.gnu.org/licenses/>.
16
17import socket
18
19
20HELLO_PREFIX = "OK MPD "
21ERROR_PREFIX = "ACK "
22SUCCESS = "OK"
23NEXT = "list_OK"
24
25
26class MPDError(Exception):
27    pass
28
29class ConnectionError(MPDError):
30    pass
31
32class ProtocolError(MPDError):
33    pass
34
35class CommandError(MPDError):
36    pass
37
38class CommandListError(MPDError):
39    pass
40
41class PendingCommandError(MPDError):
42    pass
43
44class IteratingError(MPDError):
45    pass
46
47
48class _NotConnected(object):
49    def __getattr__(self, attr):
50        return self._dummy
51
52    def _dummy(*args):
53        raise ConnectionError("Not connected")
54
55class MPDClient(object):
56    def __init__(self):
57        self.iterate = False
58        self._reset()
59        self._commands = {
60            # Status Commands
61            "clearerror": self._fetch_nothing,
62            "currentsong": self._fetch_object,
63            "idle": self._fetch_list,
64            "noidle": None,
65            "status": self._fetch_object,
66            "stats": self._fetch_object,
67            # Playback Option Commands
68            "consume": self._fetch_nothing,
69            "crossfade": self._fetch_nothing,
70            "mixrampdb": self._fetch_nothing,
71            "mixrampdelay": self._fetch_nothing,
72            "random": self._fetch_nothing,
73            "repeat": self._fetch_nothing,
74            "setvol": self._fetch_nothing,
75            "single": self._fetch_nothing,
76            "replay_gain_mode": self._fetch_nothing,
77            "replay_gain_status": self._fetch_item,
78            "volume": self._fetch_nothing,
79            # Playback Control Commands
80            "next": self._fetch_nothing,
81            "pause": self._fetch_nothing,
82            "play": self._fetch_nothing,
83            "playid": self._fetch_nothing,
84            "previous": self._fetch_nothing,
85            "seek": self._fetch_nothing,
86            "seekid": self._fetch_nothing,
87            "stop": self._fetch_nothing,
88            # Playlist Commands
89            "add": self._fetch_nothing,
90            "addid": self._fetch_item,
91            "clear": self._fetch_nothing,
92            "delete": self._fetch_nothing,
93            "deleteid": self._fetch_nothing,
94            "move": self._fetch_nothing,
95            "moveid": self._fetch_nothing,
96            "playlist": self._fetch_playlist,
97            "playlistfind": self._fetch_songs,
98            "playlistid": self._fetch_songs,
99            "playlistinfo": self._fetch_songs,
100            "playlistsearch": self._fetch_songs,
101            "plchanges": self._fetch_songs,
102            "plchangesposid": self._fetch_changes,
103            "shuffle": self._fetch_nothing,
104            "swap": self._fetch_nothing,
105            "swapid": self._fetch_nothing,
106            # Stored Playlist Commands
107            "listplaylist": self._fetch_list,
108            "listplaylistinfo": self._fetch_songs,
109            "listplaylists": self._fetch_playlists,
110            "load": self._fetch_nothing,
111            "playlistadd": self._fetch_nothing,
112            "playlistclear": self._fetch_nothing,
113            "playlistdelete": self._fetch_nothing,
114            "playlistmove": self._fetch_nothing,
115            "rename": self._fetch_nothing,
116            "rm": self._fetch_nothing,
117            "save": self._fetch_nothing,
118            # Database Commands
119            "count": self._fetch_object,
120            "find": self._fetch_songs,
121            "findadd": self._fetch_nothing,
122            "list": self._fetch_list,
123            "listall": self._fetch_database,
124            "listallinfo": self._fetch_database,
125            "lsinfo": self._fetch_database,
126            "search": self._fetch_songs,
127            "update": self._fetch_item,
128            "rescan": self._fetch_item,
129            # Sticker Commands
130            "sticker get": self._fetch_item,
131            "sticker set": self._fetch_nothing,
132            "sticker delete": self._fetch_nothing,
133            "sticker list": self._fetch_list,
134            "sticker find": self._fetch_songs,
135            # Connection Commands
136            "close": None,
137            "kill": None,
138            "password": self._fetch_nothing,
139            "ping": self._fetch_nothing,
140            # Audio Output Commands
141            "disableoutput": self._fetch_nothing,
142            "enableoutput": self._fetch_nothing,
143            "outputs": self._fetch_outputs,
144            # Reflection Commands
145            "commands": self._fetch_list,
146            "notcommands": self._fetch_list,
147            "tagtypes": self._fetch_list,
148            "urlhandlers": self._fetch_list,
149            "decoders": self._fetch_plugins,
150        }
151
152    def __getattr__(self, attr):
153        if attr.startswith("send_"):
154            command = attr.replace("send_", "", 1)
155            wrapper = self._send
156        elif attr.startswith("fetch_"):
157            command = attr.replace("fetch_", "", 1)
158            wrapper = self._fetch
159        else:
160            command = attr
161            wrapper = self._execute
162        if command not in self._commands:
163            command = command.replace("_", " ")
164            if command not in self._commands:
165                raise AttributeError("'%s' object has no attribute '%s'" %
166                                     (self.__class__.__name__, attr))
167        return lambda *args: wrapper(command, args)
168
169    def _send(self, command, args):
170        if self._command_list is not None:
171            raise CommandListError("Cannot use send_%s in a command list" %
172                                   command.replace(" ", "_"))
173        self._write_command(command, args)
174        retval = self._commands[command]
175        if retval is not None:
176            self._pending.append(command)
177
178    def _fetch(self, command, args=None):
179        if self._command_list is not None:
180            raise CommandListError("Cannot use fetch_%s in a command list" %
181                                   command.replace(" ", "_"))
182        if self._iterating:
183            raise IteratingError("Cannot use fetch_%s while iterating" %
184                                 command.replace(" ", "_"))
185        if not self._pending:
186            raise PendingCommandError("No pending commands to fetch")
187        if self._pending[0] != command:
188            raise PendingCommandError("'%s' is not the currently "
189                                      "pending command" % command)
190        del self._pending[0]
191        retval = self._commands[command]
192        if callable(retval):
193            return retval()
194        return retval
195
196    def _execute(self, command, args):
197        if self._iterating:
198            raise IteratingError("Cannot execute '%s' while iterating" %
199                                 command)
200        if self._pending:
201            raise PendingCommandError("Cannot execute '%s' with "
202                                      "pending commands" % command)
203        retval = self._commands[command]
204        if self._command_list is not None:
205            if not callable(retval):
206                raise CommandListError("'%s' not allowed in command list" %
207                                        command)
208            self._write_command(command, args)
209            self._command_list.append(retval)
210        else:
211            self._write_command(command, args)
212            if callable(retval):
213                return retval()
214            return retval
215
216    def _write_line(self, line):
217        self._wfile.write("%s\n" % line)
218        self._wfile.flush()
219
220    def _write_command(self, command, args=[]):
221        parts = [command]
222        for arg in args:
223            parts.append('"%s"' % escape(str(arg)))
224        self._write_line(" ".join(parts))
225
226    def _read_line(self):
227        line = self._rfile.readline()
228        if not line.endswith("\n"):
229            raise ConnectionError("Connection lost while reading line")
230        line = line.rstrip("\n")
231        if line.startswith(ERROR_PREFIX):
232            error = line[len(ERROR_PREFIX):].strip()
233            raise CommandError(error)
234        if self._command_list is not None:
235            if line == NEXT:
236                return
237            if line == SUCCESS:
238                raise ProtocolError("Got unexpected '%s'" % SUCCESS)
239        elif line == SUCCESS:
240            return
241        return line
242
243    def _read_pair(self, separator):
244        line = self._read_line()
245        if line is None:
246            return
247        pair = line.split(separator, 1)
248        if len(pair) < 2:
249            raise ProtocolError("Could not parse pair: '%s'" % line)
250        return pair
251
252    def _read_pairs(self, separator=": "):
253        pair = self._read_pair(separator)
254        while pair:
255            yield pair
256            pair = self._read_pair(separator)
257
258    def _read_list(self):
259        seen = None
260        for key, value in self._read_pairs():
261            if key != seen:
262                if seen is not None:
263                    raise ProtocolError("Expected key '%s', got '%s'" %
264                                        (seen, key))
265                seen = key
266            yield value
267
268    def _read_playlist(self):
269        for key, value in self._read_pairs(":"):
270            yield value
271
272    def _read_objects(self, delimiters=[]):
273        obj = {}
274        for key, value in self._read_pairs():
275            key = key.lower()
276            if obj:
277                if key in delimiters:
278                    yield obj
279                    obj = {}
280                elif key in obj:
281                    if not isinstance(obj[key], list):
282                        obj[key] = [obj[key], value]
283                    else:
284                        obj[key].append(value)
285                    continue
286            obj[key] = value
287        if obj:
288            yield obj
289
290    def _read_command_list(self):
291        try:
292            for retval in self._command_list:
293                yield retval()
294        finally:
295            self._command_list = None
296        self._fetch_nothing()
297
298    def _iterator_wrapper(self, iterator):
299        try:
300            for item in iterator:
301                yield item
302        finally:
303            self._iterating = False
304
305    def _wrap_iterator(self, iterator):
306        if not self.iterate:
307            return list(iterator)
308        self._iterating = True
309        return self._iterator_wrapper(iterator)
310
311    def _fetch_nothing(self):
312        line = self._read_line()
313        if line is not None:
314            raise ProtocolError("Got unexpected return value: '%s'" % line)
315
316    def _fetch_item(self):
317        pairs = list(self._read_pairs())
318        if len(pairs) != 1:
319            return
320        return pairs[0][1]
321
322    def _fetch_list(self):
323        return self._wrap_iterator(self._read_list())
324
325    def _fetch_playlist(self):
326        return self._wrap_iterator(self._read_playlist())
327
328    def _fetch_object(self):
329        objs = list(self._read_objects())
330        if not objs:
331            return {}
332        return objs[0]
333
334    def _fetch_objects(self, delimiters):
335        return self._wrap_iterator(self._read_objects(delimiters))
336
337    def _fetch_changes(self):
338        return self._fetch_objects(["cpos"])
339
340    def _fetch_songs(self):
341        return self._fetch_objects(["file"])
342
343    def _fetch_playlists(self):
344        return self._fetch_objects(["playlist"])
345
346    def _fetch_database(self):
347        return self._fetch_objects(["file", "directory", "playlist"])
348
349    def _fetch_outputs(self):
350        return self._fetch_objects(["outputid"])
351
352    def _fetch_plugins(self):
353        return self._fetch_objects(["plugin"])
354
355    def _fetch_command_list(self):
356        return self._wrap_iterator(self._read_command_list())
357
358    def _hello(self):
359        line = self._rfile.readline()
360        if not line.endswith("\n"):
361            raise ConnectionError("Connection lost while reading MPD hello")
362        line = line.rstrip("\n")
363        if not line.startswith(HELLO_PREFIX):
364            raise ProtocolError("Got invalid MPD hello: '%s'" % line)
365        self.mpd_version = line[len(HELLO_PREFIX):].strip()
366
367    def _reset(self):
368        self.mpd_version = None
369        self._iterating = False
370        self._pending = []
371        self._command_list = None
372        self._sock = None
373        self._rfile = _NotConnected()
374        self._wfile = _NotConnected()
375
376    def _connect_unix(self, path):
377        if not hasattr(socket, "AF_UNIX"):
378            raise ConnectionError("Unix domain sockets not supported "
379                                  "on this platform")
380        sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
381        sock.connect(path)
382        return sock
383
384    def _connect_tcp(self, host, port):
385        try:
386            flags = socket.AI_ADDRCONFIG
387        except AttributeError:
388            flags = 0
389        err = None
390        for res in socket.getaddrinfo(host, port, socket.AF_UNSPEC,
391                                      socket.SOCK_STREAM, socket.IPPROTO_TCP,
392                                      flags):
393            af, socktype, proto, canonname, sa = res
394            sock = None
395            try:
396                sock = socket.socket(af, socktype, proto)
397                sock.connect(sa)
398                return sock
399            except socket.error, err:
400                if sock is not None:
401                    sock.close()
402        if err is not None:
403            raise err
404        else:
405            raise ConnectionError("getaddrinfo returns an empty list")
406
407    def connect(self, host, port):
408        if self._sock is not None:
409            raise ConnectionError("Already connected")
410        if host.startswith("/"):
411            self._sock = self._connect_unix(host)
412        else:
413            self._sock = self._connect_tcp(host, port)
414        self._rfile = self._sock.makefile("rb")
415        self._wfile = self._sock.makefile("wb")
416        try:
417            self._hello()
418        except:
419            self.disconnect()
420            raise
421
422    def disconnect(self):
423        self._rfile.close()
424        self._wfile.close()
425        self._sock.close()
426        self._reset()
427
428    def fileno(self):
429        if self._sock is None:
430            raise ConnectionError("Not connected")
431        return self._sock.fileno()
432
433    def command_list_ok_begin(self):
434        if self._command_list is not None:
435            raise CommandListError("Already in command list")
436        if self._iterating:
437            raise IteratingError("Cannot begin command list while iterating")
438        if self._pending:
439            raise PendingCommandError("Cannot begin command list "
440                                      "with pending commands")
441        self._write_command("command_list_ok_begin")
442        self._command_list = []
443
444    def command_list_end(self):
445        if self._command_list is None:
446            raise CommandListError("Not in command list")
447        if self._iterating:
448            raise IteratingError("Already iterating over a command list")
449        self._write_command("command_list_end")
450        return self._fetch_command_list()
451
452
453def escape(text):
454    return text.replace("\\", "\\\\").replace('"', '\\"')
455
456
457# vim: set expandtab shiftwidth=4 softtabstop=4 textwidth=79:
458

Archive Download this file

Branches

Tags