]> kaliko git repositories - python-musicpd.git/blob - mpd.py
TODO.txt: changing header for consistency
[python-musicpd.git] / mpd.py
1 # Python MPD client library
2 # Copyright (C) 2008  J. Alexander Treuman <jat@spatialrift.net>
3 #
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.
8 #
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.
13 #
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/>.
16
17 import socket
18
19
20 HELLO_PREFIX = "OK MPD "
21 ERROR_PREFIX = "ACK "
22 SUCCESS = "OK"
23 NEXT = "list_OK"
24
25
26 class MPDError(Exception):
27     pass
28
29 class ConnectionError(MPDError):
30     pass
31
32 class ProtocolError(MPDError):
33     pass
34
35 class CommandError(MPDError):
36     pass
37
38 class CommandListError(MPDError):
39     pass
40
41
42 class MPDClient(object):
43     def __init__(self):
44         self.iterate = False
45         self._reset()
46         self._commands = {
47             # Admin Commands
48             "disableoutput":    self._getnone,
49             "enableoutput":     self._getnone,
50             "kill":             None,
51             "update":           self._getitem,
52             # Informational Commands
53             "status":           self._getobject,
54             "stats":            self._getobject,
55             "outputs":          self._getoutputs,
56             "commands":         self._getlist,
57             "notcommands":      self._getlist,
58             "tagtypes":         self._getlist,
59             "urlhandlers":      self._getlist,
60             # Database Commands
61             "find":             self._getsongs,
62             "list":             self._getlist,
63             "listall":          self._getdatabase,
64             "listallinfo":      self._getdatabase,
65             "lsinfo":           self._getdatabase,
66             "search":           self._getsongs,
67             "count":            self._getobject,
68             # Playlist Commands
69             "add":              self._getnone,
70             "addid":            self._getitem,
71             "clear":            self._getnone,
72             "currentsong":      self._getobject,
73             "delete":           self._getnone,
74             "deleteid":         self._getnone,
75             "load":             self._getnone,
76             "rename":           self._getnone,
77             "move":             self._getnone,
78             "moveid":           self._getnone,
79             "playlist":         self._getplaylist,
80             "playlistinfo":     self._getsongs,
81             "playlistid":       self._getsongs,
82             "plchanges":        self._getsongs,
83             "plchangesposid":   self._getchanges,
84             "rm":               self._getnone,
85             "save":             self._getnone,
86             "shuffle":          self._getnone,
87             "swap":             self._getnone,
88             "swapid":           self._getnone,
89             "listplaylist":     self._getlist,
90             "listplaylistinfo": self._getsongs,
91             "playlistadd":      self._getnone,
92             "playlistclear":    self._getnone,
93             "playlistdelete":   self._getnone,
94             "playlistmove":     self._getnone,
95             "playlistfind":     self._getsongs,
96             "playlistsearch":   self._getsongs,
97             # Playback Commands
98             "crossfade":        self._getnone,
99             "next":             self._getnone,
100             "pause":            self._getnone,
101             "play":             self._getnone,
102             "playid":           self._getnone,
103             "previous":         self._getnone,
104             "random":           self._getnone,
105             "repeat":           self._getnone,
106             "seek":             self._getnone,
107             "seekid":           self._getnone,
108             "setvol":           self._getnone,
109             "stop":             self._getnone,
110             "volume":           self._getnone,
111             # Miscellaneous Commands
112             "clearerror":       self._getnone,
113             "close":            None,
114             "password":         self._getnone,
115             "ping":             self._getnone,
116         }
117
118     def __getattr__(self, attr):
119         try:
120             retval = self._commands[attr]
121         except KeyError:
122             raise AttributeError, "'%s' object has no attribute '%s'" % \
123                                   (self.__class__.__name__, attr)
124         return lambda *args: self._docommand(attr, args, retval)
125
126     def _docommand(self, command, args, retval):
127         if self._commandlist is not None and not callable(retval):
128             raise CommandListError, "%s not allowed in command list" % command
129         self._writecommand(command, args)
130         if self._commandlist is None:
131             if callable(retval):
132                 return retval()
133             return retval
134         self._commandlist.append(retval)
135
136     def _writeline(self, line):
137         self._sockfile.write("%s\n" % line)
138         self._sockfile.flush()
139
140     def _writecommand(self, command, args=[]):
141         parts = [command]
142         for arg in args:
143             parts.append('"%s"' % escape(str(arg)))
144         self._writeline(" ".join(parts))
145
146     def _readline(self):
147         line = self._sockfile.readline()
148         if not line.endswith("\n"):
149             raise ConnectionError, "Connection lost while reading line"
150         line = line.rstrip("\n")
151         if line.startswith(ERROR_PREFIX):
152             error = line[len(ERROR_PREFIX):].strip()
153             raise CommandError, error
154         if self._commandlist is not None:
155             if line == NEXT:
156                 return
157             if line == SUCCESS:
158                 raise ProtocolError, "Got unexpected '%s'" % SUCCESS
159         elif line == SUCCESS:
160             return
161         return line
162
163     def _readitem(self, separator):
164         line = self._readline()
165         if line is None:
166             return
167         item = line.split(separator, 1)
168         if len(item) < 2:
169             raise ProtocolError, "Could not parse item: '%s'" % line
170         return item
171
172     def _readitems(self, separator=": "):
173         item = self._readitem(separator)
174         while item:
175             yield item
176             item = self._readitem(separator)
177         raise StopIteration
178
179     def _readlist(self):
180         seen = None
181         for key, value in self._readitems():
182             if key != seen:
183                 if seen is not None:
184                     raise ProtocolError, "Expected key '%s', got '%s'" % \
185                                          (seen, key)
186                 seen = key
187             yield value
188         raise StopIteration
189
190     def _readplaylist(self):
191         for key, value in self._readitems(":"):
192             yield value
193         raise StopIteration
194
195     def _readobjects(self, delimiters=[]):
196         obj = {}
197         for key, value in self._readitems():
198             key = key.lower()
199             if obj:
200                 if key in delimiters:
201                     yield obj
202                     obj = {}
203                 elif obj.has_key(key):
204                     if not isinstance(obj[key], list):
205                         obj[key] = [obj[key], value]
206                     else:
207                         obj[key].append(value)
208                     continue
209             obj[key] = value
210         if obj:
211             yield obj
212         raise StopIteration
213
214     def _readcommandlist(self):
215         for retval in self._commandlist:
216             yield retval()
217         self._commandlist = None
218         self._getnone()
219         raise StopIteration
220
221     def _wrapiterator(self, iterator):
222         if not self.iterate:
223             return list(iterator)
224         return iterator
225
226     def _getnone(self):
227         line = self._readline()
228         if line is not None:
229             raise ProtocolError, "Got unexpected return value: '%s'" % line
230
231     def _getitem(self):
232         items = list(self._readitems())
233         if len(items) != 1:
234             return
235         return items[0][1]
236
237     def _getlist(self):
238         return self._wrapiterator(self._readlist())
239
240     def _getplaylist(self):
241         return self._wrapiterator(self._readplaylist())
242
243     def _getobject(self):
244         objs = list(self._readobjects())
245         if not objs:
246             return {}
247         return objs[0]
248
249     def _getobjects(self, delimiters):
250         return self._wrapiterator(self._readobjects(delimiters))
251
252     def _getsongs(self):
253         return self._getobjects(["file"])
254
255     def _getdatabase(self):
256         return self._getobjects(["file", "directory", "playlist"])
257
258     def _getoutputs(self):
259         return self._getobjects(["outputid"])
260
261     def _getchanges(self):
262         return self._getobjects(["cpos"])
263
264     def _getcommandlist(self):
265         return self._wrapiterator(self._readcommandlist())
266
267     def _hello(self):
268         line = self._sockfile.readline()
269         if not line.endswith("\n"):
270             raise ConnectionError, "Connection lost while reading MPD hello"
271         if not line.startswith(HELLO_PREFIX):
272             raise ProtocolError, "Got invalid MPD hello: '%s'" % line
273         self.mpd_version = line[len(HELLO_PREFIX):].strip()
274
275     def _reset(self):
276         self.mpd_version = None
277         self._commandlist = None
278         self._sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
279         self._sockfile = self._sock.makefile("rb+")
280
281     def connect(self, host, port):
282         self.disconnect()
283         self._sock.connect((host, port))
284         self._hello()
285
286     def disconnect(self):
287         self._sockfile.close()
288         self._sock.close()
289         self._reset()
290
291     def command_list_ok_begin(self):
292         if self._commandlist is not None:
293             raise CommandListError, "Already in command list"
294         self._writecommand("command_list_ok_begin")
295         self._commandlist = []
296
297     def command_list_end(self):
298         if self._commandlist is None:
299             raise CommandListError, "Not in command list"
300         self._writecommand("command_list_end")
301         return self._getcommandlist()
302
303
304 def escape(text):
305     return text.replace("\\", "\\\\").replace('"', '\\"')
306
307
308 # vim: set expandtab shiftwidth=4 softtabstop=4 textwidth=79: