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 "findadd": self
._getnone
,
70 "list": self
._getlist
,
71 "listall": self
._getdatabase
,
72 "listallinfo": self
._getdatabase
,
73 "lsinfo": self
._getdatabase
,
74 "search": self
._getsongs
,
75 "count": self
._getobject
,
78 "addid": self
._getitem
,
79 "clear": self
._getnone
,
80 "currentsong": self
._getobject
,
81 "delete": self
._getnone
,
82 "deleteid": self
._getnone
,
83 "load": self
._getnone
,
84 "rename": self
._getnone
,
85 "move": self
._getnone
,
86 "moveid": self
._getnone
,
87 "playlist": self
._getplaylist
,
88 "playlistinfo": self
._getsongs
,
89 "playlistid": self
._getsongs
,
90 "plchanges": self
._getsongs
,
91 "plchangesposid": self
._getchanges
,
93 "save": self
._getnone
,
94 "shuffle": self
._getnone
,
95 "swap": self
._getnone
,
96 "swapid": self
._getnone
,
97 "listplaylist": self
._getlist
,
98 "listplaylistinfo": self
._getsongs
,
99 "playlistadd": self
._getnone
,
100 "playlistclear": self
._getnone
,
101 "playlistdelete": self
._getnone
,
102 "playlistmove": self
._getnone
,
103 "playlistfind": self
._getsongs
,
104 "playlistsearch": self
._getsongs
,
106 "consume": self
._getnone
,
107 "crossfade": self
._getnone
,
108 "next": self
._getnone
,
109 "pause": self
._getnone
,
110 "play": self
._getnone
,
111 "playid": self
._getnone
,
112 "previous": self
._getnone
,
113 "random": self
._getnone
,
114 "repeat": self
._getnone
,
115 "seek": self
._getnone
,
116 "seekid": self
._getnone
,
117 "setvol": self
._getnone
,
118 "single": self
._getnone
,
119 "stop": self
._getnone
,
120 "volume": self
._getnone
,
121 # Miscellaneous Commands
122 "clearerror": self
._getnone
,
124 "password": self
._getnone
,
125 "ping": self
._getnone
,
128 def __getattr__(self
, attr
):
130 retval
= self
._commands
[attr
]
132 raise AttributeError("'%s' object has no attribute '%s'" %
133 (self
.__class
__.__name
__, attr
))
134 return lambda *args
: self
._docommand
(attr
, args
, retval
)
136 def _docommand(self
, command
, args
, retval
):
137 if self
._commandlist
is not None and not callable(retval
):
138 raise CommandListError("%s not allowed in command list" % command
)
139 self
._writecommand
(command
, args
)
140 if self
._commandlist
is None:
144 self
._commandlist
.append(retval
)
146 def _writeline(self
, line
):
147 self
._wfile
.write("%s\n" % line
)
150 def _writecommand(self
, command
, args
=[]):
153 parts
.append('"%s"' % escape(str(arg
)))
154 self
._writeline
(" ".join(parts
))
157 line
= self
._rfile
.readline()
158 if not line
.endswith("\n"):
159 raise ConnectionError("Connection lost while reading line")
160 line
= line
.rstrip("\n")
161 if line
.startswith(ERROR_PREFIX
):
162 error
= line
[len(ERROR_PREFIX
):].strip()
163 raise CommandError(error
)
164 if self
._commandlist
is not None:
168 raise ProtocolError("Got unexpected '%s'" % SUCCESS
)
169 elif line
== SUCCESS
:
173 def _readitem(self
, separator
):
174 line
= self
._readline
()
177 item
= line
.split(separator
, 1)
179 raise ProtocolError("Could not parse item: '%s'" % line
)
182 def _readitems(self
, separator
=": "):
183 item
= self
._readitem
(separator
)
186 item
= self
._readitem
(separator
)
191 for key
, value
in self
._readitems
():
194 raise ProtocolError("Expected key '%s', got '%s'" %
200 def _readplaylist(self
):
201 for key
, value
in self
._readitems
(":"):
205 def _readobjects(self
, delimiters
=[]):
207 for key
, value
in self
._readitems
():
210 if key
in delimiters
:
213 elif obj
.has_key(key
):
214 if not isinstance(obj
[key
], list):
215 obj
[key
] = [obj
[key
], value
]
217 obj
[key
].append(value
)
224 def _readcommandlist(self
):
225 for retval
in self
._commandlist
:
227 self
._commandlist
= None
231 def _wrapiterator(self
, iterator
):
233 return list(iterator
)
237 line
= self
._readline
()
239 raise ProtocolError("Got unexpected return value: '%s'" % line
)
242 items
= list(self
._readitems
())
248 return self
._wrapiterator
(self
._readlist
())
250 def _getplaylist(self
):
251 return self
._wrapiterator
(self
._readplaylist
())
253 def _getobject(self
):
254 objs
= list(self
._readobjects
())
259 def _getobjects(self
, delimiters
):
260 return self
._wrapiterator
(self
._readobjects
(delimiters
))
263 return self
._getobjects
(["file"])
265 def _getdatabase(self
):
266 return self
._getobjects
(["file", "directory", "playlist"])
268 def _getoutputs(self
):
269 return self
._getobjects
(["outputid"])
271 def _getchanges(self
):
272 return self
._getobjects
(["cpos"])
274 def _getcommandlist(self
):
276 return self
._wrapiterator
(self
._readcommandlist
())
278 self
._commandlist
= None
282 line
= self
._rfile
.readline()
283 if not line
.endswith("\n"):
284 raise ConnectionError("Connection lost while reading MPD hello")
285 line
= line
.rstrip("\n")
286 if not line
.startswith(HELLO_PREFIX
):
287 raise ProtocolError("Got invalid MPD hello: '%s'" % line
)
288 self
.mpd_version
= line
[len(HELLO_PREFIX
):].strip()
291 self
.mpd_version
= None
292 self
._commandlist
= None
294 self
._rfile
= _NotConnected()
295 self
._wfile
= _NotConnected()
297 def connect(self
, host
, port
):
299 raise ConnectionError("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
, msg
:
314 for res
in socket
.getaddrinfo(host
, port
, socket
.AF_UNSPEC
,
315 socket
.SOCK_STREAM
, socket
.IPPROTO_TCP
,
317 af
, socktype
, proto
, canonname
, sa
= res
319 self
._sock
= socket
.socket(af
, socktype
, proto
)
320 self
._sock
.connect(sa
)
321 except socket
.error
, msg
:
328 raise socket
.error(msg
)
329 self
._rfile
= self
._sock
.makefile("rb")
330 self
._wfile
= self
._sock
.makefile("wb")
337 def disconnect(self
):
343 def command_list_ok_begin(self
):
344 if self
._commandlist
is not None:
345 raise CommandListError("Already in command list")
346 self
._writecommand
("command_list_ok_begin")
347 self
._commandlist
= []
349 def command_list_end(self
):
350 if self
._commandlist
is None:
351 raise CommandListError("Not in command list")
352 self
._writecommand
("command_list_end")
353 return self
._getcommandlist
()
357 return text
.replace("\\", "\\\\").replace('"', '\\"')
360 # vim: set expandtab shiftwidth=4 softtabstop=4 textwidth=79: