summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAnton Khirnov <wyskas@gmail.com>2010-08-09 18:18:01 +0200
committerAnton Khirnov <wyskas@gmail.com>2010-08-12 20:50:11 +0200
commitd1a10219c77e8000e2a82e40376d08225f4e92f4 (patch)
treecf0a9cc2192a6d9cc5d597ad459a334a0dc46ccf
parentfd54e0eca9a8ddcfae00fc50f119de0299a3c420 (diff)
mpdsocket: add a new asynchronous low-level MPD interface
some parts are based on mpd library.
-rw-r--r--nephilim/mpdsocket.py288
1 files changed, 288 insertions, 0 deletions
diff --git a/nephilim/mpdsocket.py b/nephilim/mpdsocket.py
new file mode 100644
index 0000000..72d4ef9
--- /dev/null
+++ b/nephilim/mpdsocket.py
@@ -0,0 +1,288 @@
+#
+# Copyright (C) 2010 Anton Khirnov <wyskas@gmail.com>
+# Copyright (C) 2008 J. Alexander Treuman <jat@spatialrift.net>
+#
+# Nephilim is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Nephilim 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 General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Nephilim. If not, see <http://www.gnu.org/licenses/>.
+#
+
+import logging
+from PyQt4 import QtCore, QtNetwork
+from PyQt4.QtCore import pyqtSignal as Signal
+
+class MPDError(Exception):
+ pass
+
+class ConnectionError(MPDError):
+ pass
+
+class MPDSocket(QtCore.QObject):
+ """
+ A dumb TCP/domain socket wrapper -- has a very basic understanding of MPD protocol.
+ The caller should make sure that all returned iterators are exhausted or
+ the world will explode.
+ """
+
+ #### PUBLIC ####
+ # signals #
+ """True is emitted when the socket succesfully connectes to MPD.
+ False is emitted when it's disconnected."""
+ connect_changed = Signal(bool)
+ """Emitted whenever MPD signals that some of its subsystems have changed.
+ For a list of subsystems see MPD protocol documentation."""
+ subsystems_changed = Signal(list)
+
+ # read-only
+ """A tuple of (major, minor, micro)."""
+ version = None
+
+ #### PRIVATE ####
+ _logger = None
+ _sock = None
+ """A list of callbacks that are waiting for a response from MPD."""
+ _cmd_queue = None
+ _is_idle = False
+
+ # MPD strings
+ SEPARATOR = ': '
+ HELLO_PREFIX = "OK MPD "
+ ERROR_PREFIX = "ACK "
+ SUCCESS = "OK"
+
+ #### PUBLIC ####
+ def __init__(self, parent = None):
+ QtCore.QObject.__init__(self, parent)
+ self._logger = logging.getLogger('%smpdsocket'%(unicode(parent) + "." if parent else ""))
+ self._cmd_queue = []
+ self.version = (0, 0, 0)
+
+ def connect_mpd(self, host, port = None):
+ """
+ Connect to MPD at given host:port. If port is omitted, then connection
+ using Unix domain sockets is used.
+ """
+ self._logger.info('Connecting to MPD.')
+ if self._sock:
+ raise ConnectionError('Stale socket found.')
+
+ if not port:
+ #assume Unix domain socket
+ self._sock = QtNetwork.QLocalSocket(self)
+ c = lambda host, port: self._sock.connectToServer(host)
+ else:
+ self._sock = QtNetwork.QTcpSocket(self)
+ c = self._sock.connectToHost
+
+ self._sock.disconnected.connect(self._finish_disconnect)
+ self._sock.error.connect( self._handle_error)
+ self._sock.readyRead.connect( self._finish_connect)
+
+ c(host, port)
+
+ def disconnect_mpd(self):
+ """
+ Disconnect from MPD.
+ """
+ self._logger.info('Disconnecting from MPD.')
+ if self._sock:
+ try:
+ self._sock.disconnectFromHost()
+ except AttributeError:
+ self._sock.disconnectFromServer()
+
+ def write_command(self, *args, **kwargs):
+ """
+ Send a command contained in args to MPD. If a response from
+ MPD is desired, then kwargs must contain a 'callback' memeber,
+ which shall be called with an iterator over response lines as
+ an argument. Otherwise the response is silently discarded.
+ """
+ self._logger.debug('Executing command:' + ' '.join(map(unicode, args)))
+ if 'callback' in kwargs:
+ if callable(kwargs['callback']):
+ callback = kwargs['callback']
+ else:
+ self._logger.error('Supplied callback is not callable. Will discard data instead.')
+ callback = self._parse_discard
+ else:
+ callback = self._parse_discard
+
+ self._cmd_queue.append(callback)
+ if self._is_idle:
+ self._sock.write('noidle\n')
+ self._sock.write(args[0])
+ for arg in args[1:]:
+ self._sock.write((' "%s" '%self._escape(unicode(arg))).encode('utf-8'))
+ self._sock.write('\n')
+ def write_command_sync(self, *args):
+ """
+ Send a command contained in args to MPD synchronously. An iterator over
+ response lines is returned.
+ """
+ # XXX i don't really like this solution. can't it be done better?
+ self._logger.debug('Synchronously executing command:' + ' '.join(map(unicode, args)))
+ self._sock.blockSignals(True)
+ while not self._is_idle:
+ # finish all outstanding responses
+ if not self._sock.canReadLine():
+ self._sock.waitForReadyRead()
+ self._handle_response()
+
+ self._sock.write('noidle\n')
+ self._sock.waitForBytesWritten()
+ self._sock.waitForReadyRead()
+ self._parse_discard(self._read_response())
+
+ self._sock.write(args[0])
+ for arg in args[1:]:
+ self._sock.write((' "%s" '%self._escape(unicode(arg))).encode('utf-8'))
+ self._sock.write('\n')
+ self._sock.waitForBytesWritten()
+
+ while not self._sock.canReadLine():
+ self._sock.waitForReadyRead()
+ for line in self._read_response():
+ yield line
+ self._idle()
+ self._sock.blockSignals(False)
+
+ def state(self):
+ """
+ Return a socket state as one of the values in QtNetwork.QAbstractSocket.SocketState enum.
+ """
+ if self._sock:
+ return self._sock.state()
+ return QtNetwork.QAbstractSocket.UnconnectedState
+
+ #### PRIVATE ####
+ def _escape(self, text):
+ """
+ Escape backslashes and quotes before sending.
+ """
+ return text.replace('\\', '\\\\').replace('"', '\\"')
+ def _readline(self):
+ """
+ Read from the socket one line and return it, unless it's an
+ error or end of response.
+ """
+ line = str(self._sock.readLine()).decode('utf-8').rstrip('\n')
+ if line.startswith(self.ERROR_PREFIX):
+ self._logger.error('MPD returned error: %s'%line)
+ return
+ if line == self.SUCCESS:
+ return
+ return line
+
+ def _handle_error(self, error):
+ """
+ Called on socket error signal, print it and disconnect.
+ """
+ self._logger.error(self._sock.errorString())
+ self.disconnect_mpd()
+
+ def _finish_connect(self):
+ """
+ Called when a connection is established. Read hello and emit
+ a corresponding signal.
+ """
+ # wait until we can read MPD hello
+ if not self._sock.canReadLine():
+ return
+ line = str(self._sock.readLine())
+ if not line.startswith(self.HELLO_PREFIX):
+ self.logger.error('Got invalid MPD hello: %s' % line)
+ return self.disconnect_mpd()
+ self.version = tuple(map(int, line[len(self.HELLO_PREFIX):].strip().split('.')))
+
+ self._sock.readyRead.disconnect(self._finish_connect)
+ self._logger.debug('Successfully connected to MPD, protocol version %s.'%('.'.join(map(str,self.version))))
+
+ self._sock.readyRead.connect(self._handle_response)
+ self._idle()
+ self.connect_changed.emit(True)
+
+ def _finish_disconnect(self):
+ """
+ Called when a socket has been disconnected. Reset the socket state and
+ emit corresponding signal.
+ """
+ self._sock = None
+ self.version = None
+
+ self._logger.debug('Disconnected from MPD.')
+ self.connect_changed.emit(False)
+
+ def _idle(self):
+ """
+ Enter idle mode.
+ """
+ self._logger.debug('Entering idle mode.')
+ self._sock.write('idle\n')
+ self._is_idle = True
+
+ def _handle_response(self):
+ """
+ Called when some data has arrived on the socket. Parse it according to
+ current state either as change subsystems or a response to a command.
+ Then reenter idle mode.
+ """
+ if not self._sock.canReadLine():
+ return
+
+ if self._is_idle:
+ self._logger.debug('Exited idle mode, reading changed subsystems.')
+
+ self._is_idle = False
+ subsystems = []
+ line = self._readline()
+ while line:
+ parts = line.partition(self.SEPARATOR)
+ if parts[1]:
+ subsystems.append(parts[2])
+ self._logger.debug('Subsystem changed: %s'%parts[2])
+ else:
+ self._logger.error('Malformed line: %s.'%line)
+
+ if not self._sock.canReadLine() and not self._sock.waitForReadyRead():
+ raise MPDError
+ line = self._readline()
+
+ if subsystems:
+ self.subsystems_changed.emit(subsystems)
+
+ while self._cmd_queue:
+ # wait for more data
+ if not self._sock.canReadLine():
+ return
+ self._cmd_queue[0](self._read_response())
+ del self._cmd_queue[0]
+ self._idle()
+
+ def _read_response(self):
+ """
+ An iterator over all lines in one response.
+ """
+ while self._sock.canReadLine():
+ line = self._readline()
+ if not line:
+ raise StopIteration
+ yield line
+ if not self._sock.canReadLine() and not self._sock.waitForReadyRead():
+ raise MPDError
+
+ def _parse_discard(self, data):
+ """
+ (Almost) silently discard response data.
+ """
+ for line in data:
+ self._logger.debug('Ignoring line: %s.'%line)