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
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
):
56 connect_changed
= QtCore
.pyqtSignal(bool)
58 QtCore
.QObject
.__init
__(self
)
59 self
.logger
= logging
.getLogger('mpclient.mpdsocket')
63 "disableoutput": self
._getnone
,
64 "enableoutput": self
._getnone
,
66 "update": self
._getitem
,
67 # Informational Commands
68 "status": self
._getobject
,
69 "stats": self
._getobject
,
70 "outputs": self
._getoutputs
,
71 "commands": self
._getlist
,
72 "notcommands": self
._getlist
,
73 "tagtypes": self
._getlist
,
74 "urlhandlers": self
._getlist
,
76 "find": self
._getsongs
,
77 "findadd": self
._getnone
,
78 "list": self
._getlist
,
79 "listall": self
._getdatabase
,
80 "listallinfo": self
._getdatabase
,
81 "lsinfo": self
._getdatabase
,
82 "search": self
._getsongs
,
83 "count": self
._getobject
,
86 "addid": self
._getitem
,
87 "clear": self
._getnone
,
88 "currentsong": self
._getobject
,
89 "delete": self
._getnone
,
90 "deleteid": self
._getnone
,
91 "load": self
._getnone
,
92 "rename": self
._getnone
,
93 "move": self
._getnone
,
94 "moveid": self
._getnone
,
95 "playlist": self
._getplaylist
,
96 "playlistinfo": self
._getsongs
,
97 "playlistid": self
._getsongs
,
98 "plchanges": self
._getsongs
,
99 "plchangesposid": self
._getchanges
,
101 "save": self
._getnone
,
102 "shuffle": self
._getnone
,
103 "swap": self
._getnone
,
104 "swapid": self
._getnone
,
105 "listplaylist": self
._getlist
,
106 "listplaylistinfo": self
._getsongs
,
107 "playlistadd": self
._getnone
,
108 "playlistclear": self
._getnone
,
109 "playlistdelete": self
._getnone
,
110 "playlistmove": self
._getnone
,
111 "playlistfind": self
._getsongs
,
112 "playlistsearch": self
._getsongs
,
114 "consume": self
._getnone
,
115 "crossfade": self
._getnone
,
116 "next": self
._getnone
,
117 "pause": self
._getnone
,
118 "play": self
._getnone
,
119 "playid": self
._getnone
,
120 "previous": self
._getnone
,
121 "random": self
._getnone
,
122 "repeat": self
._getnone
,
123 "seek": self
._getnone
,
124 "seekid": self
._getnone
,
125 "setvol": self
._getnone
,
126 "single": self
._getnone
,
127 "stop": self
._getnone
,
128 "volume": self
._getnone
,
129 # Miscellaneous Commands
130 "clearerror": self
._getnone
,
132 "password": self
._getnone
,
133 "ping": self
._getnone
,
136 def __getattr__(self
, attr
):
138 retval
= self
._commands
[attr
]
140 raise AttributeError("'%s' object has no attribute '%s'" %
141 (self
.__class
__.__name
__, attr
))
142 return lambda *args
: self
._docommand
(attr
, args
, retval
)
144 def _docommand(self
, command
, args
, retval
):
145 if self
._commandlist
is not None and not callable(retval
):
146 raise CommandListError("%s not allowed in command list" % command
)
148 self
._writecommand
(command
, args
)
149 except socket
.error
, e
:
150 self
.logger
.error('Error sending command: %s.'%e)
151 self
.disconnect_mpd()
154 if self
._commandlist
is None:
158 self
._commandlist
.append(retval
)
160 def _writeline(self
, line
):
161 self
._wfile
.write("%s\n" % line
)
164 def _writecommand(self
, command
, args
=[]):
167 parts
.append('"%s"' % escape(str(arg
)))
168 self
._writeline
(" ".join(parts
))
171 line
= self
._rfile
.readline()
172 if not line
.endswith("\n"):
173 raise ConnectionError("Connection lost while reading line")
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
291 self
.mpd_version
= None
292 self
._commandlist
= None
294 self
._rfile
= _NotConnected()
295 self
._wfile
= _NotConnected()
297 def connect_mpd(self
, host
, port
):
299 self
.logger
.error('Already connected.')
300 msg
= "getaddrinfo returns an empty list"
302 flags
= socket
.AI_ADDRCONFIG
303 except AttributeError:
305 if port
== None: #assume Unix domain socket
307 self
._sock
= socket
.socket(socket
.AF_UNIX
, socket
.SOCK_STREAM
)
308 self
._sock
.connect(host
)
309 except socket
.error
, e
:
313 self
.logger
.error('Error connecting to MPD: %s.'%e)
315 for res
in socket
.getaddrinfo(host
, port
, socket
.AF_UNSPEC
,
316 socket
.SOCK_STREAM
, socket
.IPPROTO_TCP
,
318 af
, socktype
, proto
, canonname
, sa
= res
320 self
._sock
= socket
.socket(af
, socktype
, proto
)
321 self
._sock
.connect(sa
)
322 except socket
.error
, e
:
326 self
.logger
.error('Error connecting to MPD: %s.'%e)
331 self
._rfile
= self
._sock
.makefile('rb')
332 self
._wfile
= self
._sock
.makefile('wb')
335 line
= self
._rfile
.readline()
336 if not line
.endswith("\n"):
337 self
.logger
.error('Connnection lost while reading MPD hello')
338 self
.disconnect_mpd()
340 line
= line
.rstrip("\n")
341 if not line
.startswith(HELLO_PREFIX
):
342 self
.logger
.error('Got invalid MPD hello: %s' % line
)
343 self
.disconnect_mpd()
345 self
.mpd_version
= line
[len(HELLO_PREFIX
):].strip()
347 self
.connect_changed
.emit(True)
349 def disconnect_mpd(self
):
354 self
.connect_changed
.emit(False)
356 def command_list_ok_begin(self
):
357 if self
._commandlist
is not None:
358 raise CommandListError("Already in command list")
359 self
._writecommand
("command_list_ok_begin")
360 self
._commandlist
= []
362 def command_list_end(self
):
363 if self
._commandlist
is None:
364 raise CommandListError("Not in command list")
365 self
._writecommand
("command_list_end")
366 return self
._getcommandlist
()
370 return text
.replace("\\", "\\\\").replace('"', '\\"')
373 # vim: set expandtab shiftwidth=4 softtabstop=4 textwidth=79: