]> kaliko git repositories - python-musicpdaio.git/blob - musicpdaio.py
Support commands arguments
[python-musicpdaio.git] / musicpdaio.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     # environ['PYTHONASYNCIODEBUG'] = '1'
31     logging.basicConfig(level=logging.DEBUG)
32
33 HELLO_PREFIX = "OK MPD "
34 ERROR_PREFIX = "ACK "
35 SUCCESS = "OK"
36 NEXT = "list_OK"
37 VERSION = '0.0.1b'
38
39
40 class MPDError(Exception):
41     pass
42
43 class ConnectionError(MPDError):
44     pass
45
46 class ProtocolError(MPDError):
47     pass
48
49 class CommandError(MPDError):
50     pass
51
52
53 class Response:
54     def __init__(self):
55         self.version = None
56         self.resp = ''
57
58     def __repr__(self):
59         return '"{0}…" ({1})'.format(
60                 ' '.join(self.resp.split('\n')[:2]),
61                 self.version)
62
63 class MPDProto(asyncio.Protocol):
64     def __init__(self, future, payload):
65         logging.debug('payload: "%s"', payload)
66         self.transport = None
67         self.future = future
68         self.payload = payload
69         self.sess = Response()
70
71     def connection_made(self, transport):
72         self.transport = transport
73         self.transport.write(bytes('{}\n'.format(self.payload), 'utf-8'))
74
75     def eof_received(self):
76         self.transport.close()
77         err = ConnectionError('Connection lost while reading line')
78         self.future.set_exception(err)
79
80     def data_received(self, data):
81         #logging.debug(data.decode('utf-8'))
82         rcv = self._hello(data.decode('utf-8'))
83
84         if rcv.startswith(ERROR_PREFIX):
85             err = rcv[len(ERROR_PREFIX):].strip()
86             self.future.set_exception(CommandError(err))
87
88         self.sess.resp += rcv
89         if rcv.endswith(SUCCESS+'\n'):
90             # Strip final SUCCESS
91             self.sess.resp = self.sess.resp[:len(SUCCESS+'\n')*-1]
92             logging.debug('set future result')
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             return rcv[rcv.find('\n')+1:]
102         return rcv
103
104 class MPDClient:
105     """MPD Client
106     :param string host: Server name or IP, default to 'localhost'
107     :param integer port: Server port, default to 6600
108     :param string passwd: Password, default to ``None``
109
110     """
111
112     def __init__(self, host='localhost', port=6600, passwd=None):
113         self._evloop = asyncio.get_event_loop()
114         self.asio = False
115         self.futures = []
116         self._host = host
117         self._port = port
118         #self._pwd = passwd  # TODO: authentication yet to implement
119         self._commands = {
120                 'currentsong',
121                 'stats',
122                 'playlistinfo',
123                 'next',
124                 'find',
125         }
126
127     def __getattr__(self, attr):
128         #logging.debug(attr)
129         command = attr
130         wrapper = self._command
131         if command not in self._commands:
132             command = command.replace("_", " ")
133             if command not in self._commands:
134                 raise AttributeError("'%s' object has no attribute '%s'" %
135                                      (self.__class__.__name__, attr))
136         return lambda *args: wrapper(command, args)
137
138     @asyncio.coroutine
139     def _connect(self, proto):
140         # coroutine allowing Exception handling
141         # src: http://comments.gmane.org/gmane.comp.python.tulip/1401
142         try:
143             yield from self._evloop.create_connection(lambda: proto,
144                     host=self._host,
145                     port=self._port)
146         except Exception as err:
147             proto.future.set_exception(ConnectionError(err))
148
149     def _command(self, command, args):
150         payload = command
151         for arg in args:
152             payload += ' "{}"'.format(escape(arg))
153         future = asyncio.Future()
154         # kick off a task to create the connection to MPD
155         coro = self._connect(MPDProto(future, payload))
156         asyncio.async(coro)
157         self.futures.append(future)
158         if not self.asio:
159             # return once completed.
160             self._evloop.run_until_complete(future)
161         return future
162         # alternative w/ callback
163         #if not self.asio:
164         #    future.add_done_callback(lambda ftr: MPDClient.loop.stop())
165         #    self._evloop.run_forever()
166         #return future
167
168     def run(self):
169         """Run event loop gathering tasks from self.futures
170         """
171         if self.futures:
172            self._evloop.run_until_complete(asyncio.gather(*self.futures))
173            self.futures = []
174         else:
175             logging.info('No task found in queue, need to set self.asio?')
176
177
178 def escape(text):
179     """Escapting quotes and backslash"""
180     return text.replace('\\', '\\\\').replace('"', '\\"')
181