2 # Copyright (C) 2010 Anton Khirnov <wyskas@gmail.com>
3 # Copyright (C) 2008 J. Alexander Treuman <jat@spatialrift.net>
5 # Nephilim is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU General Public License as published by
7 # the Free Software Foundation, either version 3 of the License, or
8 # (at your option) any later version.
10 # Nephilim is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU General Public License for more details.
15 # You should have received a copy of the GNU General Public License
16 # along with Nephilim. If not, see <http://www.gnu.org/licenses/>.
20 from PyQt4
import QtCore
, QtNetwork
21 from PyQt4
.QtCore
import pyqtSignal
as Signal
23 class MPDSocket(QtCore
.QObject
):
25 A dumb TCP/domain socket wrapper -- has a very basic understanding of MPD protocol.
26 The caller should make sure that all returned iterators are exhausted or
27 the world will explode.
32 """True is emitted when the socket succesfully connectes to MPD.
33 False is emitted when it's disconnected."""
34 connect_changed
= Signal(bool)
35 """Emitted whenever MPD signals that some of its subsystems have changed.
36 For a list of subsystems see MPD protocol documentation."""
37 subsystems_changed
= Signal(list)
40 """A tuple of (major, minor, micro)."""
46 """A list of callbacks that are waiting for a response from MPD."""
52 HELLO_PREFIX
= "OK MPD "
57 def __init__(self
, parent
= None):
58 QtCore
.QObject
.__init
__(self
, parent
)
59 self
._logger
= logging
.getLogger('%smpdsocket'%(unicode(parent
) + "." if parent
else ""))
61 self
.version
= (0, 0, 0)
63 def connect_mpd(self
, host
, port
= None):
65 Connect to MPD at given host:port. If port is omitted, then connection
66 using Unix domain sockets is used.
68 self
._logger
.info('Connecting to MPD.')
70 if self
._sock
.state() == QtNetwork
.QAbstractSocket
.ConnectedState
:
71 self
._logger
.warning('Already connected, disconnect first.')
73 self
._logger
.warning('Stale socket found, discarding.')
76 #assume Unix domain socket
77 self
._sock
= QtNetwork
.QLocalSocket(self
)
78 c
= lambda host
, port
: self
._sock
.connectToServer(host
)
80 self
._sock
= QtNetwork
.QTcpSocket(self
)
81 c
= self
._sock
.connectToHost
83 self
._sock
.disconnected
.connect(self
._finish
_disconnect
)
84 self
._sock
.error
.connect( self
._handle
_error
)
85 self
._sock
.readyRead
.connect( self
._finish
_connect
)
89 def disconnect_mpd(self
):
93 self
._logger
.info('Disconnecting from MPD.')
96 self
._sock
.disconnectFromHost()
97 except AttributeError:
98 self
._sock
.disconnectFromServer()
100 def write_command(self
, *args
, **kwargs
):
102 Send a command contained in args to MPD. If a response from
103 MPD is desired, then kwargs must contain a 'callback' memeber,
104 which shall be called with an iterator over response lines as
105 an argument. Otherwise the response is silently discarded.
107 self
._logger
.debug('Executing command:' + ' '.join(map(unicode, args
)))
108 if 'callback' in kwargs
:
109 if callable(kwargs
['callback']):
110 callback
= kwargs
['callback']
112 self
._logger
.error('Supplied callback is not callable. Will discard data instead.')
113 callback
= self
._parse
_discard
115 callback
= self
._parse
_discard
117 self
._cmd
_queue
.append(callback
)
119 self
._sock
.write('noidle\n')
120 self
._sock
.write(args
[0])
122 self
._sock
.write((' "%s" '%self
._escape
(unicode(arg
))).encode('utf-8'))
123 self
._sock
.write('\n')
124 def write_command_sync(self
, *args
):
126 Send a command contained in args to MPD synchronously. An iterator over
127 response lines is returned.
129 # XXX i don't really like this solution. can't it be done better?
130 self
._logger
.debug('Synchronously executing command:' + ' '.join(map(unicode, args
)))
131 self
._sock
.blockSignals(True)
132 while not self
._is
_idle
:
133 # finish all outstanding responses
134 if not self
._sock
.canReadLine():
135 self
._sock
.waitForReadyRead()
136 self
._handle
_response
()
138 self
._sock
.write('noidle\n')
139 self
._sock
.waitForBytesWritten()
140 self
._sock
.waitForReadyRead()
141 self
._parse
_discard
(self
._read
_response
())
143 self
._sock
.write(args
[0])
145 self
._sock
.write((' "%s" '%self
._escape
(unicode(arg
))).encode('utf-8'))
146 self
._sock
.write('\n')
147 self
._sock
.waitForBytesWritten()
149 while not self
._sock
.canReadLine():
150 self
._sock
.waitForReadyRead()
151 for line
in self
._read
_response
():
154 self
._sock
.blockSignals(False)
158 Return a socket state as one of the values in QtNetwork.QAbstractSocket.SocketState enum.
161 return self
._sock
.state()
162 return QtNetwork
.QAbstractSocket
.UnconnectedState
165 def _escape(self
, text
):
167 Escape backslashes and quotes before sending.
169 return text
.replace('\\', '\\\\').replace('"', '\\"')
172 Read from the socket one line and return it, unless it's an
173 error or end of response.
175 line
= str(self
._sock
.readLine()).decode('utf-8').rstrip('\n')
176 if line
.startswith(self
.ERROR_PREFIX
):
177 self
._logger
.error('MPD returned error: %s'%line
)
179 if line
== self
.SUCCESS
:
183 def _handle_error(self
, error
):
185 Called on socket error signal, print it and disconnect.
187 self
._logger
.error(self
._sock
.errorString())
188 self
.disconnect_mpd()
190 def _finish_connect(self
):
192 Called when a connection is established. Read hello and emit
193 a corresponding signal.
195 # wait until we can read MPD hello
196 if not self
._sock
.canReadLine():
198 line
= str(self
._sock
.readLine())
199 if not line
.startswith(self
.HELLO_PREFIX
):
200 self
.logger
.error('Got invalid MPD hello: %s' % line
)
201 return self
.disconnect_mpd()
202 self
.version
= tuple(map(int, line
[len(self
.HELLO_PREFIX
):].strip().split('.')))
204 self
._sock
.readyRead
.disconnect(self
._finish
_connect
)
205 self
._logger
.debug('Successfully connected to MPD, protocol version %s.'%('.'.join(map(str,self
.version
))))
207 self
._sock
.readyRead
.connect(self
._handle
_response
)
209 self
.connect_changed
.emit(True)
211 def _finish_disconnect(self
):
213 Called when a socket has been disconnected. Reset the socket state and
214 emit corresponding signal.
219 self
._logger
.debug('Disconnected from MPD.')
220 self
.connect_changed
.emit(False)
226 self
._logger
.debug('Entering idle mode.')
227 self
._sock
.write('idle\n')
230 def _handle_response(self
):
232 Called when some data has arrived on the socket. Parse it according to
233 current state either as change subsystems or a response to a command.
234 Then reenter idle mode.
236 if not self
._sock
.canReadLine():
240 self
._logger
.debug('Exited idle mode, reading changed subsystems.')
242 self
._is
_idle
= False
244 line
= self
._readline
()
246 parts
= line
.partition(self
.SEPARATOR
)
248 subsystems
.append(parts
[2])
249 self
._logger
.debug('Subsystem changed: %s'%parts
[2])
251 self
._logger
.error('Malformed line: %s.'%line
)
253 if not self
._sock
.canReadLine() and not self
._sock
.waitForReadyRead():
254 self
._logger
.error('Reading server response timed out, disconnecting.')
255 return self
.disconnect_mpd()
256 line
= self
._readline
()
259 self
.subsystems_changed
.emit(subsystems
)
261 while self
._cmd
_queue
:
263 if not self
._sock
.canReadLine():
265 self
._cmd
_queue
[0](self
._read
_response
())
266 del self
._cmd
_queue
[0]
269 def _read_response(self
):
271 An iterator over all lines in one response.
273 while self
._sock
.canReadLine():
274 line
= self
._readline
()
278 if not self
._sock
.canReadLine() and not self
._sock
.waitForReadyRead():
279 self
._logger
.error('Reading server response timed out, disconnecting.')
280 self
.disconnect_mpd()
283 def _parse_discard(self
, data
):
285 (Almost) silently discard response data.
288 self
._logger
.debug('Ignoring line: %s.'%line
)