]> kaliko git repositories - python-musicpdaio.git/blob - musicpdasio.py
Plain asio API
[python-musicpdaio.git] / musicpdasio.py
1 # -*- coding: utf-8 -*-
2 #
3 # python-musicpd: Python MPD client library
4 # Copyright (C) 2014-2015  Kaliko Jack <kaliko@azylum.org>
5 #
6 # python-musicpdasio is free software: you can redistribute it and/or modify
7 # it under the terms of the GNU Lesser General Public License as published by
8 # the Free Software Foundation, either version 3 of the License, or
9 # (at your option) any later version.
10 #
11 # python-musicpd is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14 # GNU Lesser General Public License for more details.
15 #
16 # You should have received a copy of the GNU Lesser General Public License
17 # along with python-musicpd.  If not, see <http://www.gnu.org/licenses/>.
18
19 try:
20     import asyncio
21 except ImportError:
22     import sys
23     print('Failed to import asyncio, need python >= 3.4')
24     sys.exit(1)
25
26 import logging
27
28 from os import environ
29 if 'DEBUG' in environ or 'PYTHONASYNCIODEBUG' in environ:
30     logging.basicConfig(level=logging.DEBUG)
31
32 HELLO_PREFIX = "OK MPD "
33 ERROR_PREFIX = "ACK "
34 SUCCESS = "OK"
35 NEXT = "list_OK"
36 VERSION = '0.0.1b'
37
38
39 class MPDError(Exception):
40     pass
41
42 class ConnectionError(MPDError):
43     pass
44
45 class ProtocolError(MPDError):
46     pass
47
48 class CommandError(MPDError):
49     pass
50
51 class CommandListError(MPDError):
52     pass
53
54 class PendingCommandError(MPDError):
55     pass
56
57 class IteratingError(MPDError):
58     pass
59
60
61 class Response:
62     def __init__(self):
63         self.version = None
64         self.resp = ''
65
66     def __repr__(self):
67         return '"{0}…" ({1})'.format(
68                 ' '.join(self.resp.split('\n')[:2]),
69                 self.version)
70
71 class MPDProto(asyncio.Protocol):
72     def __init__(self, future, payload, cred):
73         self.transport = None
74         self.future = future
75         self.payload = payload
76         self.sess = Response()
77
78     def connection_made(self, transport):
79         self.transport = transport
80         self.transport.write(bytes('{}\n'.format(self.payload), 'utf-8'))
81
82     def eof_received(self):
83         self.transport.close()
84         err = ConnectionError('Connection lost while reading line')
85         self.future.set_exception(err)
86
87     def data_received(self, data):
88         #logging.debug(data.decode('utf-8'))
89         rcv = self._hello(data.decode('utf-8'))
90
91         if rcv.startswith(ERROR_PREFIX):
92             err = rcv[len(ERROR_PREFIX):].strip()
93             self.future.set_exception(CommandError(err))
94
95         self.sess.resp += rcv
96         if rcv.endswith(SUCCESS+'\n'):
97             logging.debug('set future result')
98             self.transport.close()
99             self.future.set_result(self.sess)
100
101     def _hello(self, rcv):
102         """Consume HELLO_PREFIX"""
103         if rcv.startswith(HELLO_PREFIX):
104             logging.debug('consumed hello prefix')
105             self.sess.version = rcv.split('\n')[0][len(HELLO_PREFIX):]
106             return rcv[rcv.find('\n')+1:]
107         return rcv
108
109 class MPDClient:
110
111     def __init__(self, host='localhost', port=6600, cred=None):
112         self.eloop = asyncio.get_event_loop()
113         self.asio = False
114         self.futures = []
115         self._host = host
116         self._port = port
117         self._cred = cred
118         self._commands = {
119                 'currentsong',
120                 'stats',
121                 'playlistinfo',
122         }
123
124     def __getattr__(self, attr):
125         #logging.debug(attr)
126         command = attr
127         wrapper = self._command
128         if command not in self._commands:
129             command = command.replace("_", " ")
130             if command not in self._commands:
131                 raise AttributeError("'%s' object has no attribute '%s'" %
132                                      (self.__class__.__name__, attr))
133         return lambda *args: wrapper(command, args)
134
135     @asyncio.coroutine
136     def _connect(self, proto):
137         # coroutine allowing Exception handling
138         # src: http://comments.gmane.org/gmane.comp.python.tulip/1401
139         try:
140             yield from self.eloop.create_connection(lambda: proto,
141                     host=self._host,
142                     port=self._port)
143         except Exception as err:
144             proto.future.set_exception(ConnectionError(err))
145
146     def _command(self, command, args):
147         payload = '{} {}'.format(command, ''.join(args))
148         future = asyncio.Future()
149         # kick off a task to create the connection to MPD
150         coro = self._connect(MPDProto(future, payload, self._cred))
151         asyncio.async(coro)
152         self.futures.append(future)
153         if not self.asio:
154             # return once completed.
155             self.eloop.run_until_complete(future)
156         return future
157         # alternative w/ callback
158         #if not self.asio:
159         #    future.add_done_callback(lambda ftr: MPDClient.loop.stop())
160         #    self.eloop.run_forever()
161         #return future
162
163     def run(self):
164         if self.futures:
165            self.eloop.run_until_complete(asyncio.gather(*self.futures))
166            self.futures = []
167         else:
168             logging.info('No task found in queue, need to set self.asio?')