X-Git-Url: http://git.kaliko.me/?p=python-musicpd.git;a=blobdiff_plain;f=mpd.py;h=e4279398293c24a82f02de672545a07a28452187;hp=be8b390482e2a18639f94298e025e2282f5400a8;hb=0dfcd8a5c40c7bbed2e7e59a60bcbe046ee2b7af;hpb=94fd67b7f97142bda274187e463320834a58cc15 diff --git a/mpd.py b/mpd.py index be8b390..e427939 100644 --- a/mpd.py +++ b/mpd.py @@ -1,18 +1,18 @@ -# Python MPD client library -# Copyright (C) 2008 J. Alexander Treuman +# python-mpd: Python MPD client library +# Copyright (C) 2008-2010 J. Alexander Treuman # -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by +# python-mpd is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# This program is distributed in the hope that it will be useful, +# python-mpd is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. +# GNU Lesser General Public License for more details. # -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . +# You should have received a copy of the GNU Lesser General Public License +# along with python-mpd. If not, see . import socket @@ -26,6 +26,9 @@ NEXT = "list_OK" class MPDError(Exception): pass +class ConnectionError(MPDError): + pass + class ProtocolError(MPDError): pass @@ -35,166 +38,235 @@ class CommandError(MPDError): class CommandListError(MPDError): pass +class PendingCommandError(MPDError): + pass + +class IteratingError(MPDError): + pass + + +class _NotConnected(object): + def __getattr__(self, attr): + return self._dummy + + def _dummy(*args): + raise ConnectionError("Not connected") class MPDClient(object): def __init__(self): self.iterate = False self._reset() self._commands = { - # Admin Commands - "disableoutput": self._getnone, - "enableoutput": self._getnone, - "kill": None, - "update": self._getitem, - # Informational Commands - "status": self._getobject, - "stats": self._getobject, - "outputs": self._getoutputs, - "commands": self._getlist, - "notcommands": self._getlist, - "tagtypes": self._getlist, - "urlhandlers": self._getlist, - # Database Commands - "find": self._getsongs, - "list": self._getlist, - "listall": self._getdatabase, - "listallinfo": self._getdatabase, - "lsinfo": self._getdatabase, - "search": self._getsongs, - "count": self._getobject, + # Status Commands + "clearerror": self._fetch_nothing, + "currentsong": self._fetch_object, + "idle": self._fetch_list, + "noidle": None, + "status": self._fetch_object, + "stats": self._fetch_object, + # Playback Option Commands + "consume": self._fetch_nothing, + "crossfade": self._fetch_nothing, + "random": self._fetch_nothing, + "repeat": self._fetch_nothing, + "setvol": self._fetch_nothing, + "single": self._fetch_nothing, + "volume": self._fetch_nothing, + # Playback Control Commands + "next": self._fetch_nothing, + "pause": self._fetch_nothing, + "play": self._fetch_nothing, + "playid": self._fetch_nothing, + "previous": self._fetch_nothing, + "seek": self._fetch_nothing, + "seekid": self._fetch_nothing, + "stop": self._fetch_nothing, # Playlist Commands - "add": self._getnone, - "addid": self._getitem, - "clear": self._getnone, - "currentsong": self._getobject, - "delete": self._getnone, - "deleteid": self._getnone, - "load": self._getnone, - "rename": self._getnone, - "move": self._getnone, - "moveid": self._getnone, - "playlist": self._getplaylist, - "playlistinfo": self._getsongs, - "playlistid": self._getsongs, - "plchanges": self._getsongs, - "plchangesposid": self._getchanges, - "rm": self._getnone, - "save": self._getnone, - "shuffle": self._getnone, - "swap": self._getnone, - "swapid": self._getnone, - "listplaylist": self._getlist, - "listplaylistinfo": self._getsongs, - "playlistadd": self._getnone, - "playlistclear": self._getnone, - "playlistdelete": self._getnone, - "playlistmove": self._getnone, - "playlistfind": self._getsongs, - "playlistsearch": self._getsongs, - # Playback Commands - "crossfade": self._getnone, - "next": self._getnone, - "pause": self._getnone, - "play": self._getnone, - "playid": self._getnone, - "previous": self._getnone, - "random": self._getnone, - "repeat": self._getnone, - "seek": self._getnone, - "seekid": self._getnone, - "setvol": self._getnone, - "stop": self._getnone, - "volume": self._getnone, - # Miscellaneous Commands - "clearerror": self._getnone, + "add": self._fetch_nothing, + "addid": self._fetch_item, + "clear": self._fetch_nothing, + "delete": self._fetch_nothing, + "deleteid": self._fetch_nothing, + "move": self._fetch_nothing, + "moveid": self._fetch_nothing, + "playlist": self._fetch_playlist, + "playlistfind": self._fetch_songs, + "playlistid": self._fetch_songs, + "playlistinfo": self._fetch_songs, + "playlistsearch": self._fetch_songs, + "plchanges": self._fetch_songs, + "plchangesposid": self._fetch_changes, + "shuffle": self._fetch_nothing, + "swap": self._fetch_nothing, + "swapid": self._fetch_nothing, + # Stored Playlist Commands + "listplaylist": self._fetch_list, + "listplaylistinfo": self._fetch_songs, + "listplaylists": self._fetch_playlists, + "load": self._fetch_nothing, + "playlistadd": self._fetch_nothing, + "playlistclear": self._fetch_nothing, + "playlistdelete": self._fetch_nothing, + "playlistmove": self._fetch_nothing, + "rename": self._fetch_nothing, + "rm": self._fetch_nothing, + "save": self._fetch_nothing, + # Database Commands + "count": self._fetch_object, + "find": self._fetch_songs, + "list": self._fetch_list, + "listall": self._fetch_database, + "listallinfo": self._fetch_database, + "lsinfo": self._fetch_database, + "search": self._fetch_songs, + "update": self._fetch_item, + # Sticker Commands + "sticker get": self._fetch_item, + "sticker set": self._fetch_nothing, + "sticker delete": self._fetch_nothing, + "sticker list": self._fetch_list, + "sticker find": self._fetch_songs, + # Connection Commands "close": None, - "password": self._getnone, - "ping": self._getnone, + "kill": None, + "password": self._fetch_nothing, + "ping": self._fetch_nothing, + # Audio Output Commands + "disableoutput": self._fetch_nothing, + "enableoutput": self._fetch_nothing, + "outputs": self._fetch_outputs, + # Reflection Commands + "commands": self._fetch_list, + "notcommands": self._fetch_list, + "tagtypes": self._fetch_list, + "urlhandlers": self._fetch_list, } def __getattr__(self, attr): - try: - retval = self._commands[attr] - except KeyError: - raise AttributeError, "'%s' object has no attribute '%s'" % \ - (self.__class__.__name__, attr) - return lambda *args: self._docommand(attr, args, retval) - - def _docommand(self, command, args, retval): - if self._commandlist is not None and not callable(retval): - raise CommandListError, "%s not allowed in command list" % command - self._writecommand(command, args) - if self._commandlist is None: + if attr.startswith("send_"): + command = attr.replace("send_", "", 1) + wrapper = self._send + elif attr.startswith("fetch_"): + command = attr.replace("fetch_", "", 1) + wrapper = self._fetch + else: + command = attr + wrapper = self._execute + command = command.replace("_", " ") + if command not in self._commands: + raise AttributeError("'%s' object has no attribute '%s'" % + (self.__class__.__name__, attr)) + return lambda *args: wrapper(command, args) + + def _send(self, command, args): + if self._command_list is not None: + raise CommandListError("Cannot use send_%s in a command list" % + command.replace(" ", "_")) + self._write_command(command, args) + self._pending.append(command) + + def _fetch(self, command, args=None): + if self._command_list is not None: + raise CommandListError("Cannot use fetch_%s in a command list" % + command.replace(" ", "_")) + if self._iterating: + raise IteratingError("Cannot use fetch_%s while iterating" % + command.replace(" ", "_")) + if not self._pending: + raise PendingCommandError("No pending commands to fetch") + if self._pending[0] != command: + raise PendingCommandError("'%s' is not the currently " + "pending command" % command) + del self._pending[0] + retval = self._commands[command] + if callable(retval): + return retval() + + def _execute(self, command, args): + if self._iterating: + raise IteratingError("Cannot execute '%s' while iterating" % + command) + if self._pending: + raise PendingCommandError("Cannot execute '%s' with " + "pending commands" % command) + retval = self._commands[command] + if self._command_list is not None: + if not callable(retval): + raise CommandListError("'%s' not allowed in command list" % + command) + self._write_command(command, args) + self._command_list.append(retval) + else: + self._write_command(command, args) if callable(retval): return retval() return retval - self._commandlist.append(retval) - def _writeline(self, line): - self._sockfile.write("%s\n" % line) - self._sockfile.flush() + def _write_line(self, line): + self._wfile.write("%s\n" % line) + self._wfile.flush() - def _writecommand(self, command, args=[]): + def _write_command(self, command, args=[]): parts = [command] for arg in args: parts.append('"%s"' % escape(str(arg))) - self._writeline(" ".join(parts)) + self._write_line(" ".join(parts)) - def _readline(self): - line = self._sockfile.readline().rstrip("\n") + def _read_line(self): + line = self._rfile.readline() + if not line.endswith("\n"): + raise ConnectionError("Connection lost while reading line") + line = line.rstrip("\n") if line.startswith(ERROR_PREFIX): error = line[len(ERROR_PREFIX):].strip() - raise CommandError, error - if self._commandlist is not None: + raise CommandError(error) + if self._command_list is not None: if line == NEXT: return if line == SUCCESS: - raise ProtocolError, "Got unexpected '%s'" % SUCCESS + raise ProtocolError("Got unexpected '%s'" % SUCCESS) elif line == SUCCESS: return return line - def _readitem(self, separator): - line = self._readline() + def _read_pair(self, separator): + line = self._read_line() if line is None: return - item = line.split(separator, 1) - if len(item) < 2: - raise ProtocolError, "Could not parse item: '%s'" % line - return item - - def _readitems(self, separator=": "): - item = self._readitem(separator) - while item: - yield item - item = self._readitem(separator) - raise StopIteration - - def _readlist(self): + pair = line.split(separator, 1) + if len(pair) < 2: + raise ProtocolError("Could not parse pair: '%s'" % line) + return pair + + def _read_pairs(self, separator=": "): + pair = self._read_pair(separator) + while pair: + yield pair + pair = self._read_pair(separator) + + def _read_list(self): seen = None - for key, value in self._readitems(): + for key, value in self._read_pairs(): if key != seen: if seen is not None: - raise ProtocolError, "Expected key '%s', got '%s'" % \ - (seen, key) + raise ProtocolError("Expected key '%s', got '%s'" % + (seen, key)) seen = key yield value - raise StopIteration - def _readplaylist(self): - for key, value in self._readitems(":"): + def _read_playlist(self): + for key, value in self._read_pairs(":"): yield value - raise StopIteration - def _readobjects(self, delimiters=[]): + def _read_objects(self, delimiters=[]): obj = {} - for key, value in self._readitems(): + for key, value in self._read_pairs(): key = key.lower() if obj: if key in delimiters: yield obj obj = {} - elif obj.has_key(key): + elif key in obj: if not isinstance(obj[key], list): obj[key] = [obj[key], value] else: @@ -203,94 +275,165 @@ class MPDClient(object): obj[key] = value if obj: yield obj - raise StopIteration - def _readcommandlist(self): - for retval in self._commandlist: - yield retval() - self._commandlist = None - self._getnone() - raise StopIteration + def _read_command_list(self): + try: + for retval in self._command_list: + yield retval() + finally: + self._command_list = None + self._fetch_nothing() + + def _iterator_wrapper(self, iterator): + try: + for item in iterator: + yield item + finally: + self._iterating = False - def _wrapiterator(self, iterator): + def _wrap_iterator(self, iterator): if not self.iterate: return list(iterator) - return iterator + self._iterating = True + return self._iterator_wrapper(iterator) - def _getnone(self): - line = self._readline() + def _fetch_nothing(self): + line = self._read_line() if line is not None: - raise ProtocolError, "Got unexpected return value: '%s'" % line + raise ProtocolError("Got unexpected return value: '%s'" % line) - def _getitem(self): - items = list(self._readitems()) - if len(items) != 1: + def _fetch_item(self): + pairs = list(self._read_pairs()) + if len(pairs) != 1: return - return items[0][1] + return pairs[0][1] - def _getlist(self): - return self._wrapiterator(self._readlist()) + def _fetch_list(self): + return self._wrap_iterator(self._read_list()) - def _getplaylist(self): - return self._wrapiterator(self._readplaylist()) + def _fetch_playlist(self): + return self._wrap_iterator(self._read_playlist()) - def _getobject(self): - objs = list(self._readobjects()) + def _fetch_object(self): + objs = list(self._read_objects()) if not objs: return {} return objs[0] - def _getobjects(self, delimiters): - return self._wrapiterator(self._readobjects(delimiters)) + def _fetch_objects(self, delimiters): + return self._wrap_iterator(self._read_objects(delimiters)) + + def _fetch_songs(self): + return self._fetch_objects(["file"]) - def _getsongs(self): - return self._getobjects(["file"]) + def _fetch_playlists(self): + return self._fetch_objects(["playlist"]) - def _getdatabase(self): - return self._getobjects(["file", "directory", "playlist"]) + def _fetch_database(self): + return self._fetch_objects(["file", "directory", "playlist"]) - def _getoutputs(self): - return self._getobjects(["outputid"]) + def _fetch_outputs(self): + return self._fetch_objects(["outputid"]) - def _getchanges(self): - return self._getobjects(["cpos"]) + def _fetch_changes(self): + return self._fetch_objects(["cpos"]) - def _getcommandlist(self): - return self._wrapiterator(self._readcommandlist()) + def _fetch_command_list(self): + return self._wrap_iterator(self._read_command_list()) def _hello(self): - line = self._sockfile.readline().rstrip("\n") + line = self._rfile.readline() + if not line.endswith("\n"): + raise ConnectionError("Connection lost while reading MPD hello") + line = line.rstrip("\n") if not line.startswith(HELLO_PREFIX): - raise ProtocolError, "Got invalid MPD hello: '%s'" % line + raise ProtocolError("Got invalid MPD hello: '%s'" % line) self.mpd_version = line[len(HELLO_PREFIX):].strip() def _reset(self): self.mpd_version = None - self._commandlist = None - self._sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - self._sockfile = self._sock.makefile("rb+") + self._iterating = False + self._pending = [] + self._command_list = None + self._sock = None + self._rfile = _NotConnected() + self._wfile = _NotConnected() + + def _connect_unix(self, path): + if not hasattr(socket, "AF_UNIX"): + raise ConnectionError("Unix domain sockets not supported " + "on this platform") + sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + sock.connect(path) + return sock + + def _connect_tcp(self, host, port): + try: + flags = socket.AI_ADDRCONFIG + except AttributeError: + flags = 0 + err = None + for res in socket.getaddrinfo(host, port, socket.AF_UNSPEC, + socket.SOCK_STREAM, socket.IPPROTO_TCP, + flags): + af, socktype, proto, canonname, sa = res + sock = None + try: + sock = socket.socket(af, socktype, proto) + sock.connect(sa) + return sock + except socket.error, err: + if sock is not None: + sock.close() + if err is not None: + raise err + else: + raise ConnectionError("getaddrinfo returns an empty list") def connect(self, host, port): - self.disconnect() - self._sock.connect((host, port)) - self._hello() + if self._sock is not None: + raise ConnectionError("Already connected") + if host.startswith("/"): + self._sock = self._connect_unix(host) + else: + self._sock = self._connect_tcp(host, port) + self._rfile = self._sock.makefile("rb") + self._wfile = self._sock.makefile("wb") + try: + self._hello() + except: + self.disconnect() + raise def disconnect(self): - self._sockfile.close() + self._rfile.close() + self._wfile.close() self._sock.close() self._reset() + def fileno(self): + if self._sock is None: + raise ConnectionError("Not connected") + return self._sock.fileno() + def command_list_ok_begin(self): - if self._commandlist is not None: - raise CommandListError, "Already in command list" - self._writecommand("command_list_ok_begin") - self._commandlist = [] + if self._command_list is not None: + raise CommandListError("Already in command list") + if self._iterating: + raise IteratingError("Cannot begin command list while iterating") + if self._pending: + raise PendingCommandError("Cannot begin command list " + "with pending commands") + self._write_command("command_list_ok_begin") + self._command_list = [] def command_list_end(self): - if self._commandlist is None: - raise CommandListError, "Not in command list" - self._writecommand("command_list_end") - return self._getcommandlist() + if self._command_list is None: + raise CommandListError("Not in command list") + if self._iterating: + raise IteratingError("Already iterating over a command list") + self._write_command("command_list_end") + return self._fetch_command_list() def escape(text):