import socket # a line is either: # # key:val pair # OK # ACK class socket_talker(object): def __init__(self, host, port): self.host = host self.port = port self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.sock.settimeout(3) try: self.sock.connect((host, port)) except socket.error, msg: print 'Cannot connect to MPD:', msg self.file = self.sock.makefile("rb+") self.current_line = '' self.ack = '' self.done = True # this SUCKS def get_line(self): if not self.current_line: try: self.current_line = self.file.readline().rstrip("\n") except socket.error, msg: # This can happen if there's a socket timeout. # We'll just pass and hit the EOFError below.. pass if not self.current_line: raise EOFError if self.current_line == "OK" or self.current_line.startswith("ACK"): self.done = True return self.current_line def putline(self, line): self.file.write("%s\n" % line) self.file.flush() self.done = False def get_pair(self): line = self.get_line() self.ack = '' if self.done: if line.startswith("ACK"): self.ack = line.split(None, 1)[1] return () pair = line.split(": ", 1) if len(pair) != 2: raise RuntimeError("bogus response: ``%s''" % line) return pair ZERO = 0 ONE = 1 MANY = 2 plitem_delim = ["file", "directory", "playlist"] litem_delim = ["album", "artist", "title", "track", "name", "genre", "date", "composer", "performer", "comment" ] #For use in list command. commands = { # (name, nargs): (format string, nresults, results_type_name, delimiter_key) # delimiter key is for commands that return multiple results. we use this # string to detect the beginning of a new result object. # if results_type_name is empty, the result object's .type will be set to # the key of the first key/val pair in it; otherwise, it will be set to # results_type_name. ("kill", 0): ('%s', ZERO, '', []), ("outputs", 0): ('%s', MANY, 'outputs', ['outputid']), ("clear", 0): ('%s', ZERO, '', []), ("currentsong", 0): ('%s', ONE, '', []), ("shuffle", 0): ('%s', ZERO, '', []), ("next", 0): ('%s', ZERO, '', []), ("previous", 0): ('%s', ZERO, '', []), ("stop", 0): ('%s', ZERO, '', []), ("clearerror", 0): ('%s', ZERO, '', []), ("close", 0): ('%s', ZERO, '', []), ("commands", 0): ('%s', MANY, 'commands', ['command']), ("notcommands", 0): ('%s', MANY, 'notcommands', ['command']), ("ping", 0): ('%s', ZERO, '', []), ("stats", 0): ('%s', ONE, 'stats', []), ("status", 0): ('%s', ONE, 'status', []), ("play", 0): ('%s', ZERO, '', []), ("playlistinfo", 0): ('%s', MANY, '', plitem_delim), ("playlistid", 0): ('%s', MANY, '', plitem_delim), ("getqueueinfo", 0): ('%s', MANY, '', plitem_delim), ("lsinfo", 0): ('%s', MANY, '', plitem_delim), ("update", 0): ('%s', ZERO, '', []), ("listall", 0): ('%s', MANY, '', plitem_delim), ("listallinfo", 0): ('%s', MANY, '', plitem_delim), ("command_list_begin", 0): ('%s', ZERO, '', []), ("command_list_end", 0): ('%s', ZERO, '', []), ("disableoutput", 1): ("%s %d", ZERO, '', []), ("enableoutput", 1): ("%s %d", ZERO, '', []), ("delete", 1): ('%s %d', ZERO, '', []), # ("deleteid", 1): ('%s %d', ZERO, '', []), # ("playlistinfo", 1): ('%s %d', MANY, '', plitem_delim), # ("playlistid", 1): ('%s %d', MANY, '', plitem_delim), # ("queue", 1): ('%s %d', ZERO, '', []), # ("dequeue", 1): ('%s %d', ZERO, '', []), # ("crossfade", 1): ('%s %d', ZERO, '', []), # ("play", 1): ('%s %d', ZERO, '', []), # ("playid", 1): ('%s %d', ZERO, '', []), # ("random", 1): ('%s %d', ZERO, '', []), # ("repeat", 1): ('%s %d', ZERO, '', []), # ("setvol", 1): ('%s %d', ZERO, '', []), # ("plchanges", 1): ('%s %d', MANY, '', plitem_delim), # ("pause", 1): ('%s %d', ZERO, '', []), # ("update", 1): ('%s "%s"', ONE, 'update', []), # ("listall", 1): ('%s "%s"', MANY, '', plitem_delim), # ("listallinfo", 1): ('%s "%s"', MANY, '', plitem_delim), # ("lsinfo", 1): ('%s "%s"', MANY, '', plitem_delim), # ("add", 1): ('%s "%s"', ZERO, '', []), # ("load", 1): ('%s "%s"', ZERO, '', []), # ("rm", 1): ('%s "%s"', ZERO, '', []), # ("save", 1): ('%s "%s"', ZERO, '', []), # ("password", 1): ('%s "%s"', ZERO, '', []), # ("move", 2): ("%s %d %d", ZERO, '', []), # ("moveid", 2): ("%s %d %d", ZERO, '', []), # ("swap", 2): ("%s %d %d", ZERO, '', []), # ("swapid", 2): ("%s %d %d", ZERO, '', []), # ("seek", 2): ("%s %d %d", ZERO, '', []), # ("seekid", 2): ("%s %d %d", ZERO, '', []), # # ("find", 2): ('%s "%s" "%s"', MANY, '', plitem_delim), ("rename", 2): ('%s "%s" "%s"', ZERO, '', []), # ("search", 2): ('%s "%s" "%s"', MANY, '', plitem_delim), ("search", 4): ('%s "%s" "%s" "%s" "%s"', MANY, '', plitem_delim), # list [ ] # ("list", 1): ('%s "%s"', MANY, '', litem_delim), # ("list", 3): ('%s "%s" "%s" "%s"', MANY, '', litem_delim), } def is_command(cmd): return cmd in [ k[0] for k in commands.keys() ] def escape(text): # join/split is faster than replace text = '\\\\'.join(text.split('\\')) # \ -> \\ text = '\\"'.join(text.split('"')) # " -> \" return text def get_command(cmd, args): try: return commands[(cmd, len(args))] except KeyError: raise RuntimeError("no such command: %s (%d args)" % (cmd, len(args))) def send_command(talker, cmd, args): args = list(args[:]) for i, arg in enumerate(args): if not isinstance(arg, int): args[i] = escape(str(arg)) format = get_command(cmd, args)[0] talker.putline(format % tuple([cmd] + list(args))) class sender_n_fetcher(object): def __init__(self, sender, fetcher): self.sender = sender self.fetcher = fetcher self.iterate = False def __getattr__(self, cmd): return lambda *args: self.send_n_fetch(cmd, args) def send_n_fetch(self, cmd, args): getattr(self.sender, cmd)(*args) junk, howmany, type, keywords = get_command(cmd, args) if howmany == ZERO: self.fetcher.clear() return if howmany == ONE: return self.fetcher.one_object(keywords, type) assert howmany == MANY result = self.fetcher.all_objects(keywords, type) if not self.iterate: result = list(result) self.fetcher.clear() return result # stupid hack because you apparently can't return non-None and yield # within the same function def yield_then_clear(it): for x in it: yield x self.fetcher.clear() return yield_then_clear(result) class command_sender(object): def __init__(self, talker): self.talker = talker def __getattr__(self, cmd): return lambda *args: send_command(self.talker, cmd, args) class response_fetcher(object): def __init__(self, talker): self.talker = talker def clear(self): while not self.talker.done: self.talker.current_line = '' self.talker.get_line() self.talker.current_line = '' def one_object(self, keywords, type): # if type isn't empty, then the object's type is set to it. otherwise # the type is set to the key of the first key/val pair. # keywords lists the keys that indicate a new object -- like for the # 'outputs' command, keywords would be ['outputid']. entity = dictobj() if type: entity['type'] = type # make these functions local getline = self.talker.get_line getpair = self.talker.get_pair done = self.talker.done while not done: #self.talker.get_line() getline() # pair = self.talker.get_pair() pair = getpair() if not pair: self.talker.current_line = '' return entity key, val = pair key = key.lower() if key in keywords and key in entity.keys(): return entity if key in keywords: for item in keywords: if item in entity.keys(): return entity if not type and 'type' not in entity.keys(): entity['type'] = key entity[key] = val self.talker.current_line = '' return entity def all_objects(self, keywords, type): while 1: obj = self.one_object(keywords, type) if not obj: raise StopIteration yield obj if self.talker.done: raise StopIteration class dictobj(dict): def __getattr__(self, attr): try: return self[attr] except KeyError: raise AttributeError def __repr__(self): # return (object.__repr__(self).rstrip('>') + ' ..\n' + ' {\n ' + ',\n '.join([ '%s: %s' % (k, v) for k, v in self.items() ]) + '\n }>') class mpd_connection(object): def __init__(self, host, port, password): self.talker = socket_talker(host, port) self.send = command_sender(self.talker) self.fetch = response_fetcher(self.talker) self.do = sender_n_fetcher(self.send, self.fetch) self.doit = sender_n_fetcher(self.send, self.fetch) self.doit.iterate = True self._hello() def _hello(self): line = self.talker.get_line() if not line.startswith("OK MPD "): raise RuntimeError("this ain't mpd") self.mpd_version = line[len("OK MPD "):].strip() self.talker.current_line = '' # conn.foo() is equivalent to conn.do.foo(), but nicer #def __getattr__(self, attr): # if is_command(attr): # return getattr(self.do, attr) # raise AttributeError(attr) def parse_host(host): if '@' in host: return host.split('@', 1) return '', host def connect(**kw): import os port = int(kw.get('port', os.environ.get('MPD_PORT', 6600))) password, host = parse_host(os.environ.get('MPD_HOST', 'localhost')) host = kw.get('host', host) password = kw.get('password', password) conn = mpd_connection(host, port, password) if conn and password: conn.do.password(password) return conn