]> kaliko git repositories - python-musicpdaio.git/blob - musicpdasio.py
665674d32df7456a96a867babfab10c776e8f124
[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     print('Failed to import asyncio, need python >= 3.4')
23
24 import logging
25
26 HELLO_PREFIX = "OK MPD "
27 ERROR_PREFIX = "ACK "
28 SUCCESS = "OK"
29 NEXT = "list_OK"
30 VERSION = '0.0.1b'
31
32
33 class MPDError(Exception):
34     pass
35
36 class ConnectionError(MPDError):
37     pass
38
39 class ProtocolError(MPDError):
40     pass
41
42 class CommandError(MPDError):
43     pass
44
45 class CommandListError(MPDError):
46     pass
47
48 class PendingCommandError(MPDError):
49     pass
50
51 class IteratingError(MPDError):
52     pass
53
54
55 class Response:
56     def __init__(self):
57         self.version = None
58         self.resp = ''
59         self.err = None
60
61     def __repr__(self):
62         return 'err:{0}, "{1}…" ({2})'.format(
63                 self.err,
64                 ' '.join(self.resp.split('\n')[:2]),
65                 self.version)
66
67 class MPDProto(asyncio.Protocol):
68     def __init__(self, future, payload, cred):
69         self.transport = None
70         self.future = future
71         self.payload = payload
72         self.sess = Response()
73
74     def connection_made(self, transport):
75         self.transport = transport
76         self.transport.write(bytes('{}\n'.format(self.payload), 'utf-8'))
77
78     def eof_received(self):
79         self.transport.close()
80         err = ConnectionError('Connection lost while reading line')
81         self.future.set_exception(err)
82
83     def data_received(self, data):
84         #logging.debug(data.decode('utf-8'))
85         rcv = self._hello(data.decode('utf-8'))
86
87         if rcv.startswith(ERROR_PREFIX):
88             err = rcv[len(ERROR_PREFIX):].strip()
89             self.future.set_exception(CommandError(err))
90
91         self.sess.resp += rcv
92         if rcv.endswith(SUCCESS+'\n'):
93             self.transport.close()
94             self.future.set_result(self.sess)
95
96     def _hello(self, rcv):
97         """Consume HELLO_PREFIX"""
98         if rcv.startswith(HELLO_PREFIX):
99             logging.debug('consumed hello prefix')
100             self.sess.version = rcv.split('\n')[0][len(HELLO_PREFIX):]
101             #print('consumed hello prefix: %s' % self.sess.version)
102             return rcv[rcv.find('\n')+1:]
103         return rcv
104
105 class MPDClient:
106     loop = asyncio.get_event_loop()
107
108     def __init__(self, host='localhost', port=6600, cred=None):
109         self._host = host
110         self._port = port
111         self._cred = cred
112         self._commands = {
113                 'currentsong',
114                 'stats',
115                 'playlistinfo',
116         }
117
118     def __getattr__(self, attr):
119         command = attr
120         wrapper = self._command
121         if command not in self._commands:
122             command = command.replace("_", " ")
123             if command not in self._commands:
124                 raise AttributeError("'%s' object has no attribute '%s'" %
125                                      (self.__class__.__name__, attr))
126         return lambda *args: wrapper(command, args)
127
128     @asyncio.coroutine
129     def _connect(self, proto):
130         # coroutine allowing Exception handling
131         # src: http://comments.gmane.org/gmane.comp.python.tulip/1401
132         try:
133             yield from MPDClient.loop.create_connection(lambda: proto,
134                     host=self._host,
135                     port=self._port)
136         except Exception as err:
137             proto.future.set_exception(ConnectionError(err))
138
139     def _command(self, command, args):
140         payload = '{} {}'.format(command, ''.join(args))
141         future = asyncio.Future()
142         # kick off a task to create the connection to MPD
143         coro = self._connect(MPDProto(future, payload, self._cred))
144         asyncio.async(coro)
145         MPDClient.loop.run_until_complete(future)
146         # return once completed.
147         return future.result().resp