winMain: remove useless variable
[nephilim.git] / nephilim / mpdsocket.py
blob005ddd20cb322fb4157663a1b068370046856e12
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/>.
19 import logging
20 from PyQt4 import QtCore, QtNetwork
21 from PyQt4.QtCore import pyqtSignal as Signal
23 class MPDSocket(QtCore.QObject):
24 """
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.
28 """
30 #### PUBLIC ####
31 # signals #
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)
39 # read-only
40 """A tuple of (major, minor, micro)."""
41 version = None
43 #### PRIVATE ####
44 _logger = None
45 _sock = None
46 """A list of callbacks that are waiting for a response from MPD."""
47 _cmd_queue = None
48 _is_idle = False
50 # MPD strings
51 SEPARATOR = ': '
52 HELLO_PREFIX = "OK MPD "
53 ERROR_PREFIX = "ACK "
54 SUCCESS = "OK"
56 #### PUBLIC ####
57 def __init__(self, parent = None):
58 QtCore.QObject.__init__(self, parent)
59 self._logger = logging.getLogger('%smpdsocket'%(unicode(parent) + "." if parent else ""))
60 self._cmd_queue = []
61 self.version = (0, 0, 0)
63 def connect_mpd(self, host, port = None):
64 """
65 Connect to MPD at given host:port. If port is omitted, then connection
66 using Unix domain sockets is used.
67 """
68 self._logger.info('Connecting to MPD.')
69 if self._sock:
70 if self._sock.state() == QtNetwork.QAbstractSocket.ConnectedState:
71 self._logger.warning('Already connected, disconnect first.')
72 else:
73 self._logger.warning('Stale socket found, discarding.')
75 if not port:
76 #assume Unix domain socket
77 self._sock = QtNetwork.QLocalSocket(self)
78 c = lambda host, port: self._sock.connectToServer(host)
79 else:
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)
87 c(host, port)
89 def disconnect_mpd(self):
90 """
91 Disconnect from MPD.
92 """
93 self._logger.info('Disconnecting from MPD.')
94 if self._sock:
95 try:
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']
111 else:
112 self._logger.error('Supplied callback is not callable. Will discard data instead.')
113 callback = self._parse_discard
114 else:
115 callback = self._parse_discard
117 self._cmd_queue.append(callback)
118 if self._is_idle:
119 self._sock.write('noidle\n')
120 self._sock.write(args[0])
121 for arg in args[1:]:
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])
144 for arg in args[1:]:
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():
152 yield line
153 self._idle()
154 self._sock.blockSignals(False)
156 def state(self):
158 Return a socket state as one of the values in QtNetwork.QAbstractSocket.SocketState enum.
160 if self._sock:
161 return self._sock.state()
162 return QtNetwork.QAbstractSocket.UnconnectedState
164 #### PRIVATE ####
165 def _escape(self, text):
167 Escape backslashes and quotes before sending.
169 return text.replace('\\', '\\\\').replace('"', '\\"')
170 def _readline(self):
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)
178 return
179 if line == self.SUCCESS:
180 return
181 return line
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():
197 return
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)
208 self._idle()
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.
216 self._sock = None
217 self.version = None
219 self._logger.debug('Disconnected from MPD.')
220 self.connect_changed.emit(False)
222 def _idle(self):
224 Enter idle mode.
226 self._logger.debug('Entering idle mode.')
227 self._sock.write('idle\n')
228 self._is_idle = True
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():
237 return
239 if self._is_idle:
240 self._logger.debug('Exited idle mode, reading changed subsystems.')
242 self._is_idle = False
243 subsystems = []
244 line = self._readline()
245 while line:
246 parts = line.partition(self.SEPARATOR)
247 if parts[1]:
248 subsystems.append(parts[2])
249 self._logger.debug('Subsystem changed: %s'%parts[2])
250 else:
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()
258 if subsystems:
259 self.subsystems_changed.emit(subsystems)
261 while self._cmd_queue:
262 # wait for more data
263 if not self._sock.canReadLine():
264 return
265 self._cmd_queue[0](self._read_response())
266 del self._cmd_queue[0]
267 self._idle()
269 def _read_response(self):
271 An iterator over all lines in one response.
273 while self._sock.canReadLine():
274 line = self._readline()
275 if not line:
276 raise StopIteration
277 yield line
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()
281 raise StopIteration
283 def _parse_discard(self, data):
285 (Almost) silently discard response data.
287 for line in data:
288 self._logger.debug('Ignoring line: %s.'%line)