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