+++ /dev/null
-# python-mpd: Python MPD client library
-# Copyright (C) 2008-2010 J. Alexander Treuman <jat@spatialrift.net>
-# Copyright (C) 2012 Kaliko Jack <kaliko@azylum.org>
-#
-# python-mpd is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Lesser General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# python-mpd is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU Lesser General Public License for more details.
-#
-# You should have received a copy of the GNU Lesser General Public License
-# along with python-mpd. If not, see <http://www.gnu.org/licenses/>.
-
-import socket
-
-
-HELLO_PREFIX = "OK MPD "
-ERROR_PREFIX = "ACK "
-SUCCESS = "OK"
-NEXT = "list_OK"
-
-
-class MPDError(Exception):
- pass
-
-class ConnectionError(MPDError):
- pass
-
-class ProtocolError(MPDError):
- pass
-
-class CommandError(MPDError):
- pass
-
-class CommandListError(MPDError):
- pass
-
-class PendingCommandError(MPDError):
- pass
-
-class IteratingError(MPDError):
- pass
-
-
-class _NotConnected(object):
- def __getattr__(self, attr):
- return self._dummy
-
- def _dummy(*args):
- raise ConnectionError("Not connected")
-
-class MPDClient(object):
- def __init__(self):
- self.iterate = False
- self._reset()
- self._commands = {
- # Status Commands
- "clearerror": self._fetch_nothing,
- "currentsong": self._fetch_object,
- "idle": self._fetch_list,
- "noidle": None,
- "status": self._fetch_object,
- "stats": self._fetch_object,
- # Playback Option Commands
- "consume": self._fetch_nothing,
- "crossfade": self._fetch_nothing,
- "mixrampdb": self._fetch_nothing,
- "mixrampdelay": self._fetch_nothing,
- "random": self._fetch_nothing,
- "repeat": self._fetch_nothing,
- "setvol": self._fetch_nothing,
- "single": self._fetch_nothing,
- "replay_gain_mode": self._fetch_nothing,
- "replay_gain_status": self._fetch_item,
- "volume": self._fetch_nothing,
- # Playback Control Commands
- "next": self._fetch_nothing,
- "pause": self._fetch_nothing,
- "play": self._fetch_nothing,
- "playid": self._fetch_nothing,
- "previous": self._fetch_nothing,
- "seek": self._fetch_nothing,
- "seekid": self._fetch_nothing,
- "seekcur": self._fetch_nothing,
- "stop": self._fetch_nothing,
- # Playlist Commands
- "add": self._fetch_nothing,
- "addid": self._fetch_item,
- "clear": self._fetch_nothing,
- "delete": self._fetch_nothing,
- "deleteid": self._fetch_nothing,
- "move": self._fetch_nothing,
- "moveid": self._fetch_nothing,
- "playlist": self._fetch_playlist,
- "playlistfind": self._fetch_songs,
- "playlistid": self._fetch_songs,
- "playlistinfo": self._fetch_songs,
- "playlistsearch": self._fetch_songs,
- "plchanges": self._fetch_songs,
- "plchangesposid": self._fetch_changes,
- "shuffle": self._fetch_nothing,
- "swap": self._fetch_nothing,
- "swapid": self._fetch_nothing,
- # Stored Playlist Commands
- "listplaylist": self._fetch_list,
- "listplaylistinfo": self._fetch_songs,
- "listplaylists": self._fetch_playlists,
- "load": self._fetch_nothing,
- "playlistadd": self._fetch_nothing,
- "playlistclear": self._fetch_nothing,
- "playlistdelete": self._fetch_nothing,
- "playlistmove": self._fetch_nothing,
- "rename": self._fetch_nothing,
- "rm": self._fetch_nothing,
- "save": self._fetch_nothing,
- # Database Commands
- "count": self._fetch_object,
- "find": self._fetch_songs,
- "findadd": self._fetch_nothing,
- "list": self._fetch_list,
- "listall": self._fetch_database,
- "listallinfo": self._fetch_database,
- "lsinfo": self._fetch_database,
- "search": self._fetch_songs,
- "searchadd": self._fetch_nothing,
- "searchaddpl": self._fetch_nothing,
- "update": self._fetch_item,
- "rescan": self._fetch_item,
- # Sticker Commands
- "sticker get": self._fetch_item,
- "sticker set": self._fetch_nothing,
- "sticker delete": self._fetch_nothing,
- "sticker list": self._fetch_list,
- "sticker find": self._fetch_songs,
- # Connection Commands
- "close": None,
- "kill": None,
- "password": self._fetch_nothing,
- "ping": self._fetch_nothing,
- # Audio Output Commands
- "disableoutput": self._fetch_nothing,
- "enableoutput": self._fetch_nothing,
- "outputs": self._fetch_outputs,
- # Reflection Commands
- "commands": self._fetch_list,
- "notcommands": self._fetch_list,
- "tagtypes": self._fetch_list,
- "urlhandlers": self._fetch_list,
- "decoders": self._fetch_plugins,
- # Client to Client
- "subscribe": self._fetch_nothing,
- "unsubscribe": self._fetch_nothing,
- "channels": self._fetch_list,
- "readmessages": self._fetch_messages,
- "sendmessage": self._fetch_nothing,
- }
-
- def __getattr__(self, attr):
- if attr.startswith("send_"):
- command = attr.replace("send_", "", 1)
- wrapper = self._send
- elif attr.startswith("fetch_"):
- command = attr.replace("fetch_", "", 1)
- wrapper = self._fetch
- else:
- command = attr
- wrapper = self._execute
- if command not in self._commands:
- command = command.replace("_", " ")
- if command not in self._commands:
- raise AttributeError("'%s' object has no attribute '%s'" %
- (self.__class__.__name__, attr))
- return lambda *args: wrapper(command, args)
-
- def _send(self, command, args):
- if self._command_list is not None:
- raise CommandListError("Cannot use send_%s in a command list" %
- command.replace(" ", "_"))
- self._write_command(command, args)
- retval = self._commands[command]
- if retval is not None:
- self._pending.append(command)
-
- def _fetch(self, command, args=None):
- if self._command_list is not None:
- raise CommandListError("Cannot use fetch_%s in a command list" %
- command.replace(" ", "_"))
- if self._iterating:
- raise IteratingError("Cannot use fetch_%s while iterating" %
- command.replace(" ", "_"))
- if not self._pending:
- raise PendingCommandError("No pending commands to fetch")
- if self._pending[0] != command:
- raise PendingCommandError("'%s' is not the currently "
- "pending command" % command)
- del self._pending[0]
- retval = self._commands[command]
- if callable(retval):
- return retval()
- return retval
-
- def _execute(self, command, args):
- if self._iterating:
- raise IteratingError("Cannot execute '%s' while iterating" %
- command)
- if self._pending:
- raise PendingCommandError("Cannot execute '%s' with "
- "pending commands" % command)
- retval = self._commands[command]
- if self._command_list is not None:
- if not callable(retval):
- raise CommandListError("'%s' not allowed in command list" %
- command)
- self._write_command(command, args)
- self._command_list.append(retval)
- else:
- self._write_command(command, args)
- if callable(retval):
- return retval()
- return retval
-
- def _write_line(self, line):
- self._wfile.write("%s\n" % line)
- self._wfile.flush()
-
- def _write_command(self, command, args=[]):
- parts = [command]
- for arg in args:
- parts.append('"%s"' % escape(str(arg)))
- self._write_line(" ".join(parts))
-
- def _read_line(self):
- line = self._rfile.readline()
- if not line.endswith("\n"):
- self.disconnect()
- raise ConnectionError("Connection lost while reading line")
- line = line.rstrip("\n")
- if line.startswith(ERROR_PREFIX):
- error = line[len(ERROR_PREFIX):].strip()
- raise CommandError(error)
- if self._command_list is not None:
- if line == NEXT:
- return
- if line == SUCCESS:
- raise ProtocolError("Got unexpected '%s'" % SUCCESS)
- elif line == SUCCESS:
- return
- return line
-
- def _read_pair(self, separator):
- line = self._read_line()
- if line is None:
- return
- pair = line.split(separator, 1)
- if len(pair) < 2:
- raise ProtocolError("Could not parse pair: '%s'" % line)
- return pair
-
- def _read_pairs(self, separator=": "):
- pair = self._read_pair(separator)
- while pair:
- yield pair
- pair = self._read_pair(separator)
-
- def _read_list(self):
- seen = None
- for key, value in self._read_pairs():
- if key != seen:
- if seen is not None:
- raise ProtocolError("Expected key '%s', got '%s'" %
- (seen, key))
- seen = key
- yield value
-
- def _read_playlist(self):
- for key, value in self._read_pairs(":"):
- yield value
-
- def _read_objects(self, delimiters=[]):
- obj = {}
- for key, value in self._read_pairs():
- key = key.lower()
- if obj:
- if key in delimiters:
- yield obj
- obj = {}
- elif key in obj:
- if not isinstance(obj[key], list):
- obj[key] = [obj[key], value]
- else:
- obj[key].append(value)
- continue
- obj[key] = value
- if obj:
- yield obj
-
- def _read_command_list(self):
- try:
- for retval in self._command_list:
- yield retval()
- finally:
- self._command_list = None
- self._fetch_nothing()
-
- def _iterator_wrapper(self, iterator):
- try:
- for item in iterator:
- yield item
- finally:
- self._iterating = False
-
- def _wrap_iterator(self, iterator):
- if not self.iterate:
- return list(iterator)
- self._iterating = True
- return self._iterator_wrapper(iterator)
-
- def _fetch_nothing(self):
- line = self._read_line()
- if line is not None:
- raise ProtocolError("Got unexpected return value: '%s'" % line)
-
- def _fetch_item(self):
- pairs = list(self._read_pairs())
- if len(pairs) != 1:
- return
- return pairs[0][1]
-
- def _fetch_list(self):
- return self._wrap_iterator(self._read_list())
-
- def _fetch_playlist(self):
- return self._wrap_iterator(self._read_playlist())
-
- def _fetch_object(self):
- objs = list(self._read_objects())
- if not objs:
- return {}
- return objs[0]
-
- def _fetch_objects(self, delimiters):
- return self._wrap_iterator(self._read_objects(delimiters))
-
- def _fetch_changes(self):
- return self._fetch_objects(["cpos"])
-
- def _fetch_songs(self):
- return self._fetch_objects(["file"])
-
- def _fetch_playlists(self):
- return self._fetch_objects(["playlist"])
-
- def _fetch_database(self):
- return self._fetch_objects(["file", "directory", "playlist"])
-
- def _fetch_outputs(self):
- return self._fetch_objects(["outputid"])
-
- def _fetch_plugins(self):
- return self._fetch_objects(["plugin"])
-
- def _fetch_messages(self):
- return self._fetch_objects(["channel"])
-
- def _fetch_command_list(self):
- return self._wrap_iterator(self._read_command_list())
-
- def _hello(self):
- line = self._rfile.readline()
- if not line.endswith("\n"):
- raise ConnectionError("Connection lost while reading MPD hello")
- line = line.rstrip("\n")
- if not line.startswith(HELLO_PREFIX):
- raise ProtocolError("Got invalid MPD hello: '%s'" % line)
- self.mpd_version = line[len(HELLO_PREFIX):].strip()
-
- def _reset(self):
- self.mpd_version = None
- self._iterating = False
- self._pending = []
- self._command_list = None
- self._sock = None
- self._rfile = _NotConnected()
- self._wfile = _NotConnected()
-
- def _connect_unix(self, path):
- if not hasattr(socket, "AF_UNIX"):
- raise ConnectionError("Unix domain sockets not supported "
- "on this platform")
- sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
- sock.connect(path)
- return sock
-
- def _connect_tcp(self, host, port):
- try:
- flags = socket.AI_ADDRCONFIG
- except AttributeError:
- flags = 0
- err = None
- for res in socket.getaddrinfo(host, port, socket.AF_UNSPEC,
- socket.SOCK_STREAM, socket.IPPROTO_TCP,
- flags):
- af, socktype, proto, canonname, sa = res
- sock = None
- try:
- sock = socket.socket(af, socktype, proto)
- sock.connect(sa)
- return sock
- except socket.error as socket_err:
- err = socket_err
- if sock is not None:
- sock.close()
- if err is not None:
- raise ConnectionError(str(err))
- else:
- raise ConnectionError("getaddrinfo returns an empty list")
-
- def connect(self, host, port):
- if self._sock is not None:
- raise ConnectionError("Already connected")
- if host.startswith("/"):
- self._sock = self._connect_unix(host)
- else:
- self._sock = self._connect_tcp(host, port)
- self._rfile = self._sock.makefile("r", encoding='utf-8')
- self._wfile = self._sock.makefile("w", encoding='utf-8')
- try:
- self._hello()
- except:
- self.disconnect()
- raise
-
- def disconnect(self):
- if hasattr(self._rfile, 'close'):
- self._rfile.close()
- if hasattr(self._wfile, 'close'):
- self._wfile.close()
- if isinstance(self._sock, socket.socket):
- self._sock.close()
- self._reset()
-
- def fileno(self):
- if self._sock is None:
- raise ConnectionError("Not connected")
- return self._sock.fileno()
-
- def command_list_ok_begin(self):
- if self._command_list is not None:
- raise CommandListError("Already in command list")
- if self._iterating:
- raise IteratingError("Cannot begin command list while iterating")
- if self._pending:
- raise PendingCommandError("Cannot begin command list "
- "with pending commands")
- self._write_command("command_list_ok_begin")
- self._command_list = []
-
- def command_list_end(self):
- if self._command_list is None:
- raise CommandListError("Not in command list")
- if self._iterating:
- raise IteratingError("Already iterating over a command list")
- self._write_command("command_list_end")
- return self._fetch_command_list()
-
-
-def escape(text):
- return text.replace("\\", "\\\\").replace('"', '\\"')
-
-
-# vim: set expandtab shiftwidth=4 softtabstop=4 textwidth=79: