]> kaliko git repositories - python-musicpdaio.git/blobdiff - musicpdaio.py
Renaming module and coding style changes
[python-musicpdaio.git] / musicpdaio.py
diff --git a/musicpdaio.py b/musicpdaio.py
new file mode 100644 (file)
index 0000000..1663b27
--- /dev/null
@@ -0,0 +1,173 @@
+# -*- coding: utf-8 -*-
+#
+# python-musicpd: Python MPD client library
+# Copyright (C) 2014-2015  Kaliko Jack <kaliko@azylum.org>
+#
+# python-musicpdasio 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.
+#
+# python-musicpd 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 Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with python-musicpd.  If not, see <http://www.gnu.org/licenses/>.
+
+try:
+    import asyncio
+except ImportError:
+    import sys
+    print('Failed to import asyncio, need python >= 3.4')
+    sys.exit(1)
+
+import logging
+
+from os import environ
+if 'DEBUG' in environ or 'PYTHONASYNCIODEBUG' in environ:
+    logging.basicConfig(level=logging.DEBUG)
+
+HELLO_PREFIX = "OK MPD "
+ERROR_PREFIX = "ACK "
+SUCCESS = "OK"
+NEXT = "list_OK"
+VERSION = '0.0.1b'
+
+
+class MPDError(Exception):
+    pass
+
+class ConnectionError(MPDError):
+    pass
+
+class ProtocolError(MPDError):
+    pass
+
+class CommandError(MPDError):
+    pass
+
+class CommandListError(MPDError):
+    pass
+
+class PendingCommandError(MPDError):
+    pass
+
+class IteratingError(MPDError):
+    pass
+
+
+class Response:
+    def __init__(self):
+        self.version = None
+        self.resp = ''
+
+    def __repr__(self):
+        return '"{0}…" ({1})'.format(
+                ' '.join(self.resp.split('\n')[:2]),
+                self.version)
+
+class MPDProto(asyncio.Protocol):
+    def __init__(self, future, payload, pwd):
+        self.pwd = pwd
+        self.transport = None
+        self.future = future
+        self.payload = payload
+        self.sess = Response()
+
+    def connection_made(self, transport):
+        self.transport = transport
+        self.transport.write(bytes('{}\n'.format(self.payload), 'utf-8'))
+
+    def eof_received(self):
+        self.transport.close()
+        err = ConnectionError('Connection lost while reading line')
+        self.future.set_exception(err)
+
+    def data_received(self, data):
+        #logging.debug(data.decode('utf-8'))
+        rcv = self._hello(data.decode('utf-8'))
+
+        if rcv.startswith(ERROR_PREFIX):
+            err = rcv[len(ERROR_PREFIX):].strip()
+            self.future.set_exception(CommandError(err))
+
+        self.sess.resp += rcv
+        if rcv.endswith(SUCCESS+'\n'):
+            # Strip final SUCCESS
+            self.sess.resp = self.sess.resp[:len(SUCCESS+'\n')*-1]
+            logging.debug('set future result')
+            self.transport.close()
+            self.future.set_result(self.sess)
+
+    def _hello(self, rcv):
+        """Consume HELLO_PREFIX"""
+        if rcv.startswith(HELLO_PREFIX):
+            logging.debug('consumed hello prefix')
+            self.sess.version = rcv.split('\n')[0][len(HELLO_PREFIX):]
+            return rcv[rcv.find('\n')+1:]
+        return rcv
+
+class MPDClient:
+
+    def __init__(self, host='localhost', port=6600, passwd=None):
+        self._evloop = asyncio.get_event_loop()
+        self.asio = False
+        self.futures = []
+        self._host = host
+        self._port = port
+        self._pwd = passwd
+        self._commands = {
+                'currentsong',
+                'stats',
+                'playlistinfo',
+        }
+
+    def __getattr__(self, attr):
+        #logging.debug(attr)
+        command = attr
+        wrapper = self._command
+        if command not in self._commands:
+            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)
+
+    @asyncio.coroutine
+    def _connect(self, proto):
+        # coroutine allowing Exception handling
+        # src: http://comments.gmane.org/gmane.comp.python.tulip/1401
+        try:
+            yield from self._evloop.create_connection(lambda: proto,
+                    host=self._host,
+                    port=self._port)
+        except Exception as err:
+            proto.future.set_exception(ConnectionError(err))
+
+    def _command(self, command, args):
+        payload = '{} {}'.format(command, ''.join(args))
+        future = asyncio.Future()
+        # kick off a task to create the connection to MPD
+        coro = self._connect(MPDProto(future, payload, self._pwd))
+        asyncio.async(coro)
+        self.futures.append(future)
+        if not self.asio:
+            # return once completed.
+            self._evloop.run_until_complete(future)
+        return future
+        # alternative w/ callback
+        #if not self.asio:
+        #    future.add_done_callback(lambda ftr: MPDClient.loop.stop())
+        #    self._evloop.run_forever()
+        #return future
+
+    def run(self):
+        """Run event loop gathering tasks from self.futures
+        """
+        if self.futures:
+           self._evloop.run_until_complete(asyncio.gather(*self.futures))
+           self.futures = []
+        else:
+            logging.info('No task found in queue, need to set self.asio?')