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