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
)
147 self
._writecommand
(command
, args
)
148 if self
._commandlist
is None:
152 self
._commandlist
.append(retval
)
154 def _writeline(self
, line
):
155 self
._wfile
.write("%s\n" % line
)
158 def _writecommand(self
, command
, args
=[]):
161 parts
.append('"%s"' % escape(str(arg
)))
162 self
._writeline
(" ".join(parts
))
165 line
= self
._rfile
.readline()
166 if not line
.endswith("\n"):
167 raise ConnectionError("Connection lost while reading line")
168 line
= line
.rstrip("\n")
169 if line
.startswith(ERROR_PREFIX
):
170 error
= line
[len(ERROR_PREFIX
):].strip()
171 raise CommandError(error
)
172 if self
._commandlist
is not None:
176 raise ProtocolError("Got unexpected '%s'" % SUCCESS
)
177 elif line
== SUCCESS
:
181 def _readitem(self
, separator
):
182 line
= self
._readline
()
185 item
= line
.split(separator
, 1)
187 raise ProtocolError("Could not parse item: '%s'" % line
)
190 def _readitems(self
, separator
=": "):
191 item
= self
._readitem
(separator
)
194 item
= self
._readitem
(separator
)
199 for key
, value
in self
._readitems
():
202 raise ProtocolError("Expected key '%s', got '%s'" %
208 def _readplaylist(self
):
209 for key
, value
in self
._readitems
(":"):
213 def _readobjects(self
, delimiters
=[]):
215 for key
, value
in self
._readitems
():
218 if key
in delimiters
:
221 elif obj
.has_key(key
):
222 if not isinstance(obj
[key
], list):
223 obj
[key
] = [obj
[key
], value
]
225 obj
[key
].append(value
)
232 def _readcommandlist(self
):
233 for retval
in self
._commandlist
:
235 self
._commandlist
= None
240 line
= self
._readline
()
242 raise ProtocolError("Got unexpected return value: '%s'" % line
)
245 items
= list(self
._readitems
())
251 return self
._readlist
()
253 def _getplaylist(self
):
254 return self
._readplaylist
()
256 def _getobject(self
):
257 objs
= list(self
._readobjects
())
262 def _getobjects(self
, delimiters
):
263 return self
._readobjects
(delimiters
)
266 return self
._getobjects
(["file"])
268 def _getdatabase(self
):
269 return self
._getobjects
(["file", "directory", "playlist"])
271 def _getoutputs(self
):
272 return self
._getobjects
(["outputid"])
274 def _getchanges(self
):
275 return self
._getobjects
(["cpos"])
277 def _getcommandlist(self
):
279 return self
._readcommandlist
()
281 self
._commandlist
= None
285 self
.mpd_version
= None
286 self
._commandlist
= None
288 self
._rfile
= _NotConnected()
289 self
._wfile
= _NotConnected()
291 def connect_mpd(self
, host
, port
):
293 self
.logger
.error('Already connected.')
294 msg
= "getaddrinfo returns an empty list"
296 flags
= socket
.AI_ADDRCONFIG
297 except AttributeError:
299 if port
== None: #assume Unix domain socket
301 self
._sock
= socket
.socket(socket
.AF_UNIX
, socket
.SOCK_STREAM
)
302 self
._sock
.connect(host
)
303 except socket
.error
, e
:
307 self
.logger
.error('Error connecting to MPD: %s.'%e)
309 for res
in socket
.getaddrinfo(host
, port
, socket
.AF_UNSPEC
,
310 socket
.SOCK_STREAM
, socket
.IPPROTO_TCP
,
312 af
, socktype
, proto
, canonname
, sa
= res
314 self
._sock
= socket
.socket(af
, socktype
, proto
)
315 self
._sock
.connect(sa
)
316 except socket
.error
, e
:
320 self
.logger
.error('Error connecting to MPD: %s.'%e)
325 self
._rfile
= self
._sock
.makefile('rb')
326 self
._wfile
= self
._sock
.makefile('wb')
329 line
= self
._rfile
.readline()
330 if not line
.endswith("\n"):
331 self
.logger
.error('Connnection lost while reading MPD hello')
332 self
.disconnect_mpd()
334 line
= line
.rstrip("\n")
335 if not line
.startswith(HELLO_PREFIX
):
336 self
.logger
.error('Got invalid MPD hello: %s' % line
)
337 self
.disconnect_mpd()
339 self
.mpd_version
= line
[len(HELLO_PREFIX
):].strip()
341 self
.connect_changed
.emit(True)
343 def disconnect_mpd(self
):
348 self
.connect_changed
.emit(False)
350 def command_list_ok_begin(self
):
351 if self
._commandlist
is not None:
352 raise CommandListError("Already in command list")
353 self
._writecommand
("command_list_ok_begin")
354 self
._commandlist
= []
356 def command_list_end(self
):
357 if self
._commandlist
is None:
358 raise CommandListError("Not in command list")
359 self
._writecommand
("command_list_end")
360 return self
._getcommandlist
()
364 return text
.replace("\\", "\\\\").replace('"', '\\"')
367 # vim: set expandtab shiftwidth=4 softtabstop=4 textwidth=79: