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/>.
20 HELLO_PREFIX
= "OK MPD "
26 class MPDError(Exception):
29 class ConnectionError(MPDError
):
32 class ProtocolError(MPDError
):
35 class CommandError(MPDError
):
38 class CommandListError(MPDError
):
42 class _NotConnected(object):
43 def __getattr__(self
, attr
):
47 raise ConnectionError("Not connected")
49 class MPDClient(object):
55 "disableoutput": self
._getnone
,
56 "enableoutput": self
._getnone
,
58 "update": self
._getitem
,
59 # Informational Commands
60 "status": self
._getobject
,
61 "stats": self
._getobject
,
62 "outputs": self
._getoutputs
,
63 "commands": self
._getlist
,
64 "notcommands": self
._getlist
,
65 "tagtypes": self
._getlist
,
66 "urlhandlers": self
._getlist
,
68 "find": self
._getsongs
,
69 "list": self
._getlist
,
70 "listall": self
._getdatabase
,
71 "listallinfo": self
._getdatabase
,
72 "lsinfo": self
._getdatabase
,
73 "search": self
._getsongs
,
74 "count": self
._getobject
,
77 "addid": self
._getitem
,
78 "clear": self
._getnone
,
79 "currentsong": self
._getobject
,
80 "delete": self
._getnone
,
81 "deleteid": self
._getnone
,
82 "load": self
._getnone
,
83 "rename": self
._getnone
,
84 "move": self
._getnone
,
85 "moveid": self
._getnone
,
86 "playlist": self
._getplaylist
,
87 "playlistinfo": self
._getsongs
,
88 "playlistid": self
._getsongs
,
89 "plchanges": self
._getsongs
,
90 "plchangesposid": self
._getchanges
,
92 "save": self
._getnone
,
93 "shuffle": self
._getnone
,
94 "swap": self
._getnone
,
95 "swapid": self
._getnone
,
96 "listplaylist": self
._getlist
,
97 "listplaylistinfo": self
._getsongs
,
98 "playlistadd": self
._getnone
,
99 "playlistclear": self
._getnone
,
100 "playlistdelete": self
._getnone
,
101 "playlistmove": self
._getnone
,
102 "playlistfind": self
._getsongs
,
103 "playlistsearch": self
._getsongs
,
105 "consume": self
._getnone
,
106 "crossfade": self
._getnone
,
107 "next": self
._getnone
,
108 "pause": self
._getnone
,
109 "play": self
._getnone
,
110 "playid": self
._getnone
,
111 "previous": self
._getnone
,
112 "random": self
._getnone
,
113 "repeat": self
._getnone
,
114 "seek": self
._getnone
,
115 "seekid": self
._getnone
,
116 "setvol": self
._getnone
,
117 "single": self
._getnone
,
118 "stop": self
._getnone
,
119 "volume": self
._getnone
,
120 # Miscellaneous Commands
121 "clearerror": self
._getnone
,
123 "password": self
._getnone
,
124 "ping": self
._getnone
,
127 def __getattr__(self
, attr
):
129 retval
= self
._commands
[attr
]
131 raise AttributeError("'%s' object has no attribute '%s'" %
132 (self
.__class
__.__name
__, attr
))
133 return lambda *args
: self
._docommand
(attr
, args
, retval
)
135 def _docommand(self
, command
, args
, retval
):
136 if self
._commandlist
is not None and not callable(retval
):
137 raise CommandListError("%s not allowed in command list" % command
)
138 self
._writecommand
(command
, args
)
139 if self
._commandlist
is None:
143 self
._commandlist
.append(retval
)
145 def _writeline(self
, line
):
146 self
._wfile
.write("%s\n" % line
)
149 def _writecommand(self
, command
, args
=[]):
152 parts
.append('"%s"' % escape(str(arg
)))
153 self
._writeline
(" ".join(parts
))
156 line
= self
._rfile
.readline()
157 if not line
.endswith("\n"):
158 raise ConnectionError("Connection lost while reading line")
159 line
= line
.rstrip("\n")
160 if line
.startswith(ERROR_PREFIX
):
161 error
= line
[len(ERROR_PREFIX
):].strip()
162 raise CommandError(error
)
163 if self
._commandlist
is not None:
167 raise ProtocolError("Got unexpected '%s'" % SUCCESS
)
168 elif line
== SUCCESS
:
172 def _readitem(self
, separator
):
173 line
= self
._readline
()
176 item
= line
.split(separator
, 1)
178 raise ProtocolError("Could not parse item: '%s'" % line
)
181 def _readitems(self
, separator
=": "):
182 item
= self
._readitem
(separator
)
185 item
= self
._readitem
(separator
)
190 for key
, value
in self
._readitems
():
193 raise ProtocolError("Expected key '%s', got '%s'" %
199 def _readplaylist(self
):
200 for key
, value
in self
._readitems
(":"):
204 def _readobjects(self
, delimiters
=[]):
206 for key
, value
in self
._readitems
():
209 if key
in delimiters
:
212 elif obj
.has_key(key
):
213 if not isinstance(obj
[key
], list):
214 obj
[key
] = [obj
[key
], value
]
216 obj
[key
].append(value
)
223 def _readcommandlist(self
):
224 for retval
in self
._commandlist
:
226 self
._commandlist
= None
230 def _wrapiterator(self
, iterator
):
232 return list(iterator
)
236 line
= self
._readline
()
238 raise ProtocolError("Got unexpected return value: '%s'" % line
)
241 items
= list(self
._readitems
())
247 return self
._wrapiterator
(self
._readlist
())
249 def _getplaylist(self
):
250 return self
._wrapiterator
(self
._readplaylist
())
252 def _getobject(self
):
253 objs
= list(self
._readobjects
())
258 def _getobjects(self
, delimiters
):
259 return self
._wrapiterator
(self
._readobjects
(delimiters
))
262 return self
._getobjects
(["file"])
264 def _getdatabase(self
):
265 return self
._getobjects
(["file", "directory", "playlist"])
267 def _getoutputs(self
):
268 return self
._getobjects
(["outputid"])
270 def _getchanges(self
):
271 return self
._getobjects
(["cpos"])
273 def _getcommandlist(self
):
275 return self
._wrapiterator
(self
._readcommandlist
())
277 self
._commandlist
= None
281 line
= self
._rfile
.readline()
282 if not line
.endswith("\n"):
283 raise ConnectionError("Connection lost while reading MPD hello")
284 line
= line
.rstrip("\n")
285 if not line
.startswith(HELLO_PREFIX
):
286 raise ProtocolError("Got invalid MPD hello: '%s'" % line
)
287 self
.mpd_version
= line
[len(HELLO_PREFIX
):].strip()
290 self
.mpd_version
= None
291 self
._commandlist
= None
293 self
._rfile
= _NotConnected()
294 self
._wfile
= _NotConnected()
296 def connect(self
, host
, port
):
298 raise ConnectionError("Already connected")
299 msg
= "getaddrinfo returns an empty list"
301 flags
= socket
.AI_ADDRCONFIG
302 except AttributeError:
304 if port
== None: #assume Unix domain socket
306 self
._sock
= socket
.socket(socket
.AF_UNIX
, socket
.SOCK_STREAM
)
307 self
._sock
.connect(host
)
308 except socket
.error
, msg
:
313 for res
in socket
.getaddrinfo(host
, port
, socket
.AF_UNSPEC
,
314 socket
.SOCK_STREAM
, socket
.IPPROTO_TCP
,
316 af
, socktype
, proto
, canonname
, sa
= res
318 self
._sock
= socket
.socket(af
, socktype
, proto
)
319 self
._sock
.connect(sa
)
320 except socket
.error
, msg
:
327 raise socket
.error(msg
)
328 self
._rfile
= self
._sock
.makefile("rb")
329 self
._wfile
= self
._sock
.makefile("wb")
336 def disconnect(self
):
342 def command_list_ok_begin(self
):
343 if self
._commandlist
is not None:
344 raise CommandListError("Already in command list")
345 self
._writecommand
("command_list_ok_begin")
346 self
._commandlist
= []
348 def command_list_end(self
):
349 if self
._commandlist
is None:
350 raise CommandListError("Not in command list")
351 self
._writecommand
("command_list_end")
352 return self
._getcommandlist
()
356 return text
.replace("\\", "\\\\").replace('"', '\\"')
359 # vim: set expandtab shiftwidth=4 softtabstop=4 textwidth=79: