|
1 | 1 | # -*- coding: utf-8 -*- |
| 2 | +# GNU Lesser General Public License v2.1 (see COPYING or https://www.gnu.org/licenses/gpl-2.1.txt) |
| 3 | +''' The AddonSignals module provides signal/slot mechanism for inter-addon communication in Kodi ''' |
2 | 4 |
|
| 5 | +from __future__ import absolute_import, division, unicode_literals |
3 | 6 | import sys |
4 | 7 | import base64 |
5 | 8 | import json |
6 | 9 | import xbmc |
7 | 10 | import xbmcaddon |
8 | 11 |
|
9 | | -RECEIVER = None |
10 | 12 |
|
| 13 | +def _addon_id(): |
| 14 | + ''' Return the Kodi add-on id and cache it as a static variable ''' |
| 15 | + if not hasattr(_addon_id, 'cached'): |
| 16 | + _addon_id.cached = xbmcaddon.Addon().getAddonInfo('id') |
| 17 | + return getattr(_addon_id, 'cached') |
11 | 18 |
|
12 | | -def _getReceiver(): |
13 | | - global RECEIVER # pylint: disable=global-statement |
14 | | - if not RECEIVER: |
15 | | - RECEIVER = SignalReceiver() |
16 | | - return RECEIVER |
17 | 19 |
|
18 | | - |
19 | | -def _decodeData(data): |
| 20 | +def _decode_data(data): |
| 21 | + ''' Decode base64-encoded JSON data and return Python data structure ''' |
20 | 22 | encoded_data = json.loads(data) |
21 | | - if encoded_data: |
22 | | - json_data = base64.b64decode(encoded_data[0]) |
23 | | - # NOTE: With Python 3.5 and older json.loads() does not support bytes or bytearray |
24 | | - if isinstance(json_data, bytes): |
25 | | - json_data = json_data.decode('utf-8') |
26 | | - return json.loads(json_data) |
27 | | - |
28 | | - return None |
| 23 | + if not encoded_data: |
| 24 | + return None |
| 25 | + json_data = base64.b64decode(encoded_data[0]) |
| 26 | + # NOTE: With Python 3.5 and older json.loads() does not support bytes or bytearray |
| 27 | + return json.loads(_to_unicode(json_data)) |
29 | 28 |
|
30 | 29 |
|
31 | | -def _encodeData(data): |
| 30 | +def _encode_data(data): |
| 31 | + ''' Encode Python data structure into base64-encoded JSON data ''' |
32 | 32 | json_data = json.dumps(data) |
33 | 33 | if not isinstance(json_data, bytes): |
34 | 34 | json_data = json_data.encode('utf-8') |
35 | 35 | encoded_data = base64.b64encode(json_data) |
36 | | - if sys.version_info[0] > 2: |
37 | | - encoded_data = encoded_data.decode('ascii') |
38 | | - return encoded_data |
| 36 | + return encoded_data.decode('ascii') |
| 37 | + |
| 38 | + |
| 39 | +def _get_receiver(): |
| 40 | + ''' Return a SignalReceiver instance and cache it as a static variable ''' |
| 41 | + if not hasattr(_get_receiver, 'cached'): |
| 42 | + _get_receiver.cached = SignalReceiver() |
| 43 | + return getattr(_get_receiver, 'cached') |
39 | 44 |
|
40 | 45 |
|
41 | 46 | def _jsonrpc(**kwargs): |
42 | 47 | ''' Perform JSONRPC calls ''' |
43 | 48 | if 'id' not in kwargs: |
44 | | - kwargs.update(id=1) |
| 49 | + kwargs.update(id=0) |
45 | 50 | if 'jsonrpc' not in kwargs: |
46 | 51 | kwargs.update(jsonrpc='2.0') |
47 | 52 | return json.loads(xbmc.executeJSONRPC(json.dumps(kwargs))) |
48 | 53 |
|
49 | 54 |
|
| 55 | +def _to_unicode(text, encoding='utf-8', errors='strict'): |
| 56 | + ''' Force text to unicode ''' |
| 57 | + if isinstance(text, bytes): |
| 58 | + return text.decode(encoding, errors=errors) |
| 59 | + return text |
| 60 | + |
| 61 | + |
50 | 62 | class SignalReceiver(xbmc.Monitor): |
| 63 | + ''' The AddonSignals receiver class ''' |
| 64 | + |
51 | 65 | def __init__(self): # pylint: disable=super-init-not-called |
| 66 | + ''' The SignalReceiver constructor ''' |
52 | 67 | self._slots = {} |
53 | 68 |
|
54 | 69 | def registerSlot(self, signaler_id, signal, callback): |
| 70 | + ''' Register a slot in the AddonSignals receiver ''' |
55 | 71 | if signaler_id not in self._slots: |
56 | 72 | self._slots[signaler_id] = {} |
57 | 73 | self._slots[signaler_id][signal] = callback |
58 | 74 |
|
59 | 75 | def unRegisterSlot(self, signaler_id, signal): |
| 76 | + ''' Unregister a slot in the AddonSignals receiver ''' |
60 | 77 | if signaler_id not in self._slots: |
61 | 78 | return |
62 | 79 | if signal not in self._slots[signaler_id]: |
63 | 80 | return |
64 | 81 | del self._slots[signaler_id][signal] |
65 | 82 |
|
66 | 83 | def onNotification(self, sender, method, data): |
67 | | - if not sender[-7:] == '.SIGNAL': |
| 84 | + ''' The Kodi Monitor event handler for notifications ''' |
| 85 | + if not sender.endswith('.SIGNAL'): |
68 | 86 | return |
69 | 87 | sender = sender[:-7] |
70 | 88 | if sender not in self._slots: |
71 | 89 | return |
72 | 90 | signal = method.split('.', 1)[-1] |
73 | 91 | if signal not in self._slots[sender]: |
74 | 92 | return |
75 | | - self._slots[sender][signal](_decodeData(data)) |
| 93 | + self._slots[sender][signal](_decode_data(data)) |
76 | 94 |
|
77 | 95 |
|
78 | 96 | class CallHandler: |
| 97 | + ''' The AddonSignals event handler class ''' |
| 98 | + |
79 | 99 | def __init__(self, signal, data, source_id, timeout=1000): |
| 100 | + ''' The CallHandler constructor ''' |
80 | 101 | self.signal = signal |
81 | | - self.data = data |
82 | 102 | self.timeout = timeout |
83 | | - self.sourceID = source_id |
| 103 | + self.source_id = source_id |
84 | 104 | self._return = None |
85 | | - registerSlot(self.sourceID, '_return.{0}'.format(self.signal), self.callback) |
86 | | - sendSignal(signal, data, self.sourceID) |
| 105 | + registerSlot(self.source_id, '_return.{0}'.format(self.signal), self.callback) |
| 106 | + sendSignal(signal, data, self.source_id) |
87 | 107 |
|
88 | 108 | def callback(self, data): |
| 109 | + ''' Method to register function as callback ''' |
89 | 110 | self._return = data |
90 | 111 |
|
91 | 112 | def waitForReturn(self): |
| 113 | + ''' Wait for callback to trigger ''' |
92 | 114 | waited = 0 |
93 | 115 | while waited < self.timeout: |
94 | 116 | if self._return is not None: |
95 | 117 | break |
96 | 118 | xbmc.sleep(100) |
97 | 119 | waited += 100 |
98 | 120 |
|
99 | | - unRegisterSlot(self.sourceID, self.signal) |
| 121 | + unRegisterSlot(self.source_id, self.signal) |
100 | 122 |
|
101 | 123 | return self._return |
102 | 124 |
|
103 | 125 |
|
104 | 126 | def registerSlot(signaler_id, signal, callback): |
105 | | - receiver = _getReceiver() |
| 127 | + ''' API method to register a slot ''' |
| 128 | + receiver = _get_receiver() |
106 | 129 | receiver.registerSlot(signaler_id, signal, callback) |
107 | 130 |
|
108 | 131 |
|
109 | 132 | def unRegisterSlot(signaler_id, signal): |
110 | | - receiver = _getReceiver() |
| 133 | + ''' API method to unregister a slot ''' |
| 134 | + receiver = _get_receiver() |
111 | 135 | receiver.unRegisterSlot(signaler_id, signal) |
112 | 136 |
|
113 | 137 |
|
114 | 138 | def sendSignal(signal, data=None, source_id=None, sourceID=None): |
| 139 | + ''' API method to send a signal ''' |
115 | 140 | if sourceID: |
116 | 141 | xbmc.log('++++==== script.module.addon.signals: sourceID keyword is DEPRECATED - use source_id ====++++', xbmc.LOGNOTICE) |
117 | | - source_id = source_id or sourceID or xbmcaddon.Addon().getAddonInfo('id') |
118 | | - |
119 | 142 | _jsonrpc(method='JSONRPC.NotifyAll', params=dict( |
120 | | - sender='%s.SIGNAL' % source_id, |
| 143 | + sender='%s.SIGNAL' % source_id or sourceID or _addon_id(), |
121 | 144 | message=signal, |
122 | | - data=[_encodeData(data)], |
| 145 | + data=[_encode_data(data)], |
123 | 146 | )) |
124 | 147 |
|
125 | 148 |
|
126 | 149 | def registerCall(signaler_id, signal, callback): |
| 150 | + ''' API method to register a callback slot ''' |
127 | 151 | registerSlot(signaler_id, signal, callback) |
128 | 152 |
|
129 | 153 |
|
130 | 154 | def returnCall(signal, data=None, source_id=None): |
| 155 | + ''' API method to return a callback signal ''' |
131 | 156 | sendSignal('_return.{0}'.format(signal), data, source_id) |
132 | 157 |
|
133 | 158 |
|
134 | 159 | def makeCall(signal, data=None, source_id=None, timeout_ms=1000): |
| 160 | + ''' API method to perform a callback signal and wait ''' |
135 | 161 | return CallHandler(signal, data, source_id, timeout_ms).waitForReturn() |
0 commit comments