Dugan Chen's Homepage

Various Things

The MPD Client Library Begins

This is a follow-up from a post made a couple of days ago: I’m starting to write a reactive MPD client library for PyQt. Well, I’ve started. There has been some soul-searching since then about just how far I would like to go with this. Do I want to limited it to idle() calls (which are the only ones that aren’t expected to return data immediately) and then use python-mpd for everything else?

…in a GUI application, blocking sockets should only be used in non-GUI threads… We discourage the use of the blocking functions together with signals.

What made my decision was Nokia’s documentation. It was best, I eventually decided, to go all the way and do it right.

Here’s what I have as of now. Remember, I’ve only worked on it for a few days. My new client supports four commands: status(), update(), idle() and noidle(). As you would expect from an asynchronous data communication library, each of those commands takes a callback to handle any data sent back from the server. There is currently no error handling and no conversion of raw data into more programmer-friendly formats. It also doesn’t actually implement the Reactor pattern (although I would assume that the Qt services that I use do so underneath), and thus isn’t “reactive”. It does support both local (Unix socket) and TCP connections.

This is, enough to prove that the architecture works. The following demonstration program opens two connections. One sends commands at the request of the user (either “fetch status” or “update the server database”. The other listens for notifications that the server database has been updated. Here it is:


#!/usr/bin/env python2.6

from sip import setapi

setapi("QDate", 2)
setapi("QDateTime", 2)
setapi("QTextStream", 2)
setapi("QTime", 2)
setapi("QVariant", 2)
setapi("QString", 2)
setapi("QUrl", 2)

from PyQt4.QtGui import QApplication, QGridLayout, QHBoxLayout, QMainWindow
from PyQt4.QtGui import QPushButton, QTextEdit, QWidget
from sys import argv
from qtmpd import QMPDClient


def main():
    app = QApplication(argv)
    main = MainWindow()
    main.show()
    exit(app.exec_())


class MainWindow(QMainWindow):

    def __init__(self, parent=None):
        super(MainWindow, self).__init__(parent)

        centralWidget = QWidget()
        appLayout = QGridLayout()
        self.__messageText = QTextEdit()
        self.__messageText.setReadOnly(True)
        appLayout.addWidget(self.__messageText, 0, 0)
        controlLayout = QHBoxLayout()
        self.__statusButton = QPushButton('Status')
        self.__statusButton.setEnabled(False)
        self.__statusButton.clicked.connect(self.sendStatus)
        controlLayout.addWidget(self.__statusButton)
        self.__updateButton = QPushButton('Update')
        self.__updateButton.setEnabled(False)
        self.__updateButton.clicked.connect(self.sendUpdate)
        controlLayout.addWidget(self.__updateButton)
        appLayout.addLayout(controlLayout, 1, 0)

        centralWidget.setLayout(appLayout)
        self.setCentralWidget(centralWidget)

        self.__commander = QMPDClient.create('localhost')
        self.__commander.greetingReceived.connect(self.handleConnection)
        self.__commander.connectToMPD('localhost', 6600)

        self.__idler = QMPDClient.create('localhost')
        self.__idler.greetingReceived.connect(self.handleConnection)
        self.__idler.connectToMPD('localhost', 6600)

        self.__connection_count = 0

    def handleConnection(self, text):
        self.appendText(text)
        self.__connection_count += 1
        if self.__connection_count == 2:
            self.__statusButton.setEnabled(True)
            self.__updateButton.setEnabled(True)
            self.__idler.idle(self.handleIdle)

    def appendText(self, text):
        oldText = self.__messageText.toPlainText()
        self.__messageText.setPlainText('\n'.join([oldText, text]))

    def sendStatus(self):
        self.__commander.status(self.appendText)

    def closeEvent(self, event):
        self.__idler.noidle()

    def handleIdle(self, text):
        self.appendText(text)
        self.__idler.idle(self.handleIdle)

    def sendIdle(self):
        self.__idler.idle(self.handleIdle)

    def sendUpdate(self):
        self.__commander.update(self.appendText)


if __name__ == "__main__":
    main()

And here is the library that it demonstrates:

#!/usr/bin/env python2.6

from sip import setapi

setapi("QDate", 2)
setapi("QDateTime", 2)
setapi("QTextStream", 2)
setapi("QTime", 2)
setapi("QVariant", 2)
setapi("QString", 2)
setapi("QUrl", 2)

from PyQt4.QtNetwork import QLocalSocket, QTcpSocket
from PyQt4.QtCore import pyqtSignal, QObject


class QMPDClient(QObject):
    """
    An asynchronous, reactive MPD client for Qt.

    Tested only with PyQt. Not intended for use with
    multiple threads.
    """

    greetingReceived = pyqtSignal(str)

    def __init__(self, socket, parent=None):

        """
        Takes an mpd socket.

        Please do not call this directly. Use the create() factory method
        instead.
        """

        super(QMPDClient, self).__init__(parent)
        self.__socket = socket
        self.__socket.readyRead.connect(self.__read)
        self.__callbacks = []

    @classmethod
    def create(cls, host, parent=None):

        """
        Creates client for the given host.
        """

        socket = MPDLocalSocket() if host.startswith('/') else MPDTcpSocket()
        return QMPDClient(socket, parent)

    def status(self, callback):
        self.call('status\n', callback)

    def update(self, callback):
        self.call('update\n', callback)

    def idle(self, callback):
        self.call('idle\n', callback)

    def noidle(self, callback):
        self.call('noidle\n', callback)

    def connectToMPD(self, host, port=None):
        self.__callbacks.append(self.__handleConnect)
        self.__socket.connectToMPD((host, port))

    def disconnectFromMPD(self, host):
        self.__socket.disconnectFromMPD()

    def call(self, command, callback=None):

        """
        Actually makes the call to mpd. Takes the raw command-string
        to write to the socket, and a callback to handle any input sent
        back.
        """

        if callback:
            self.__callbacks.append(callback)

        self.__socket.write(command)

    def __read(self):

        data = self.__socket.readAll().data()

        callback = self.__callbacks[-1]
        self.__callbacks = self.__callbacks[0:-1]
        callback(data)

    def __handleConnect(self, data):
        self.greetingReceived.emit(data)


class MPDLocalSocket(QLocalSocket):

    def __init__(self, parent=None):
        super(MPDLocalSocket, self).__init__(parent)

    def connectToMPD(self, sockaddr):
        self.connectToServer(sockaddr[0])

    def disconnectFromMPD(self):
        self.disconnectFromServer()


class MPDTcpSocket(QTcpSocket):

    def __init__(self, parent=None):
        super(MPDTcpSocket, self).__init__(parent)

    def connectToMPD(self, sockaddr):
        host, port = sockaddr
        self.connectToHost(host, port)

    def disconnectFromMPD(self):
        self.disconnectFromHost()

Future updates will focus on error-handling (always important) and command-lists (because the drag-and-drop-the-songs-onto-the-playlist feature would otherwise be very hairy to do asynchronously).