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).