1
0
mirror of https://github.com/Adam-Ant/home-assistant synced 2024-11-03 11:16:22 +00:00
home-assistant/ext_scripts/mpd.py
2016-11-30 14:50:36 +00:00

456 lines
16 KiB
Python

# python-mpd: Python MPD client library
# Copyright (C) 2008-2010 J. Alexander Treuman <jat@spatialrift.net>
#
# 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,
"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,
"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,
}
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"):
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_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, err:
if sock is not None:
sock.close()
if err is not None:
raise 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("rb")
self._wfile = self._sock.makefile("wb")
try:
self._hello()
except:
self.disconnect()
raise
def disconnect(self):
self._rfile.close()
self._wfile.close()
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('"', '\\"')