1 # Python MPD client library
2 # Copyright (C) 2008 J. Alexander Treuman <jat@spatialrift.net>
4 # This program is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation, either version 3 of the License, or
7 # (at your option) any later version.
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
14 # You should have received a copy of the GNU General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
19 from PyQt4
import QtCore
, QtNetwork
22 HELLO_PREFIX
= "OK MPD "
28 class MPDError(Exception):
31 class ConnectionError(MPDError
):
34 class ProtocolError(MPDError
):
37 class CommandError(MPDError
):
40 class CommandListError(MPDError
):
44 class _NotConnected(object):
45 def __getattr__(self
, attr
):
49 raise ConnectionError("Not connected")
51 class MPDClient(QtCore
.QObject
):
61 connect_changed
= QtCore
.pyqtSignal(bool)
63 QtCore
.QObject
.__init
__(self
)
64 self
.logger
= logging
.getLogger('mpclient.mpdsocket')
67 "disableoutput": self
._getnone
,
68 "enableoutput": self
._getnone
,
70 "update": self
._getitem
,
71 # Informational Commands
72 "status": self
._getobject
,
73 "stats": self
._getobject
,
74 "outputs": self
._getoutputs
,
75 "commands": self
._getlist
,
76 "notcommands": self
._getlist
,
77 "tagtypes": self
._getlist
,
78 "urlhandlers": self
._getlist
,
80 "find": self
._getsongs
,
81 "findadd": self
._getnone
,
82 "list": self
._getlist
,
83 "listall": self
._getdatabase
,
84 "listallinfo": self
._getdatabase
,
85 "lsinfo": self
._getdatabase
,
86 "search": self
._getsongs
,
87 "count": self
._getobject
,
90 "addid": self
._getitem
,
91 "clear": self
._getnone
,
92 "currentsong": self
._getobject
,
93 "delete": self
._getnone
,
94 "deleteid": self
._getnone
,
95 "load": self
._getnone
,
96 "rename": self
._getnone
,
97 "move": self
._getnone
,
98 "moveid": self
._getnone
,
99 "playlist": self
._getplaylist
,
100 "playlistinfo": self
._getsongs
,
101 "playlistid": self
._getsongs
,
102 "plchanges": self
._getsongs
,
103 "plchangesposid": self
._getchanges
,
105 "save": self
._getnone
,
106 "shuffle": self
._getnone
,
107 "swap": self
._getnone
,
108 "swapid": self
._getnone
,
109 "listplaylist": self
._getlist
,
110 "listplaylistinfo": self
._getsongs
,
111 "playlistadd": self
._getnone
,
112 "playlistclear": self
._getnone
,
113 "playlistdelete": self
._getnone
,
114 "playlistmove": self
._getnone
,
115 "playlistfind": self
._getsongs
,
116 "playlistsearch": self
._getsongs
,
118 "consume": self
._getnone
,
119 "crossfade": self
._getnone
,
120 "next": self
._getnone
,
121 "pause": self
._getnone
,
122 "play": self
._getnone
,
123 "playid": self
._getnone
,
124 "previous": self
._getnone
,
125 "random": self
._getnone
,
126 "repeat": self
._getnone
,
127 "seek": self
._getnone
,
128 "seekid": self
._getnone
,
129 "setvol": self
._getnone
,
130 "single": self
._getnone
,
131 "stop": self
._getnone
,
132 "volume": self
._getnone
,
133 # Miscellaneous Commands
134 "clearerror": self
._getnone
,
136 "password": self
._getnone
,
137 "ping": self
._getnone
,
140 def __getattr__(self
, attr
):
142 retval
= self
._commands
[attr
]
144 raise AttributeError("'%s' object has no attribute '%s'" %
145 (self
.__class
__.__name
__, attr
))
146 return lambda *args
: self
._docommand
(attr
, args
, retval
)
148 def _docommand(self
, command
, args
, retval
):
150 self
.logger
.error('Cannot send command: not connected.')
152 if self
._commandlist
is not None and not callable(retval
):
153 raise CommandListError("%s not allowed in command list" % command
)
155 self
._writecommand
(command
, args
)
157 if self
._commandlist
is None:
161 self
._commandlist
.append(retval
)
163 def _writecommand(self
, command
, args
=[]):
166 parts
.append('"%s"' % escape(unicode(arg
)))
167 self
.__sock
.write(' '.join(parts
).encode('utf-8') + '\n')
168 self
.__sock
.waitForBytesWritten()
171 while not self
.__sock
.canReadLine():
172 self
.__sock
.waitForReadyRead()
173 line
= str(self
.__sock
.readLine()).decode('utf-8')
174 line
= line
.rstrip("\n")
175 if line
.startswith(ERROR_PREFIX
):
176 error
= line
[len(ERROR_PREFIX
):].strip()
177 raise CommandError(error
)
178 if self
._commandlist
is not None:
182 raise ProtocolError("Got unexpected '%s'" % SUCCESS
)
183 elif line
== SUCCESS
:
187 def _readitem(self
, separator
):
188 line
= self
._readline
()
191 item
= line
.split(separator
, 1)
193 raise ProtocolError("Could not parse item: '%s'" % line
)
196 def _readitems(self
, separator
=": "):
197 item
= self
._readitem
(separator
)
200 item
= self
._readitem
(separator
)
205 for key
, value
in self
._readitems
():
208 raise ProtocolError("Expected key '%s', got '%s'" %
214 def _readplaylist(self
):
215 for key
, value
in self
._readitems
(":"):
219 def _readobjects(self
, delimiters
=[]):
221 for key
, value
in self
._readitems
():
224 if key
in delimiters
:
227 elif obj
.has_key(key
):
228 if not isinstance(obj
[key
], list):
229 obj
[key
] = [obj
[key
], value
]
231 obj
[key
].append(value
)
238 def _readcommandlist(self
):
239 for retval
in self
._commandlist
:
241 self
._commandlist
= None
246 line
= self
._readline
()
248 raise ProtocolError("Got unexpected return value: '%s'" % line
)
251 items
= list(self
._readitems
())
257 return self
._readlist
()
259 def _getplaylist(self
):
260 return self
._readplaylist
()
262 def _getobject(self
):
263 objs
= list(self
._readobjects
())
268 def _getobjects(self
, delimiters
):
269 return self
._readobjects
(delimiters
)
272 return self
._getobjects
(["file"])
274 def _getdatabase(self
):
275 return self
._getobjects
(["file", "directory", "playlist"])
277 def _getoutputs(self
):
278 return self
._getobjects
(["outputid"])
280 def _getchanges(self
):
281 return self
._getobjects
(["cpos"])
283 def _getcommandlist(self
):
285 return self
._readcommandlist
()
287 self
._commandlist
= None
290 def __handle_error(self
, error
):
291 self
.logger
.error(self
.__sock
.errorString())
292 self
.disconnect_mpd()
294 def connect_mpd(self
, host
, port
):
296 return self
.logger
.error('Already connected.')
299 #assume Unix domain socket
300 self
.__sock
= QtNetwork
.QLocalSocket(self
)
301 self
.__sock
.connectToServer(host
)
303 self
.__sock
= QtNetwork
.QTcpSocket(self
)
304 self
.__sock
.connectToHost(host
, port
)
305 if not self
.__sock
.waitForConnected():
306 self
.logger
.error('Error connecting to MPD: %s.'%self
.__sock
.errorString())
310 self
.__sock
.error
.connect(self
.__handle
_error
)
312 while not self
.__sock
.canReadLine():
313 self
.__sock
.waitForReadyRead()
314 line
= str(self
.__sock
.readLine())
315 if not line
.startswith(HELLO_PREFIX
):
316 self
.logger
.error('Got invalid MPD hello: %s' % line
)
317 self
.disconnect_mpd()
319 self
.mpd_version
= line
[len(HELLO_PREFIX
):].strip()
321 self
.connect_changed
.emit(True)
323 def disconnect_mpd(self
):
326 self
.__sock
.disconnectFromHost()
327 except AttributeError:
328 self
.__sock
.disconnectFromServer()
330 if self
.__sock
.state() != QtNetwork
.QAbstractSocket
.UnconnectedState\
331 and not self
.__sock
.waitForDisconnected(5000):
336 self
.mpd_version
= None
337 self
._commandlist
= None
338 self
.connect_changed
.emit(False)
340 def command_list_ok_begin(self
):
341 if self
._commandlist
is not None:
342 raise CommandListError("Already in command list")
343 self
._writecommand
("command_list_ok_begin")
344 self
._commandlist
= []
346 def command_list_end(self
):
347 if self
._commandlist
is None:
348 raise CommandListError("Not in command list")
349 self
._writecommand
("command_list_end")
350 return self
._getcommandlist
()
354 return text
.replace("\\", "\\\\").replace('"', '\\"')
357 # vim: set expandtab shiftwidth=4 softtabstop=4 textwidth=79: