Passed
Push — master ( 9792e6...87dae5 )
by Dean
08:18 queued 04:48
created

ActionManager.run()   C

Complexity

Conditions 7

Size

Total Lines 38

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 56

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 38
ccs 0
cts 24
cp 0
rs 5.5
cc 7
crap 56
1
from plugin.core.helpers.thread import module
2
from plugin.core.message import InterfaceMessages
3
from plugin.managers.core.base import Manager
4
from plugin.models import ActionHistory, ActionQueue
5
from plugin.preferences import Preferences
6
7
from datetime import datetime, timedelta
8
from exception_wrappers.libraries import apsw
9
from exception_wrappers.exceptions import DisabledError
10
from threading import Thread
11
from trakt import Trakt
12
import json
13
import logging
14
import peewee
15
import time
16
17
log = logging.getLogger(__name__)
18
19
20
@module(start=True, blocking=True)
21
class ActionManager(Manager):
22
    _process_enabled = True
23
    _process_thread = None
24
25
    #
26
    # Queue
27
    #
28
29
    @classmethod
30
    def queue(cls, event, request, session=None, account=None):
31
        if event is None:
32
            return None
33
34
        obj = None
35
36
        if request is not None:
37
            request = json.dumps(request)
38
39
        # Retrieve `account_id` for action
40
        account_id = None
41
42
        if session:
43
            try:
44
                account_id = session.account_id
45
            except KeyError:
46
                account_id = None
47
48
        if account_id is None and account:
49
            account_id = account.id
50
51
        if account_id is None:
52
            log.debug('Unable to find valid account for event %r, session %r', event, session)
53
            return None
54
55
        if not Preferences.get('scrobble.enabled', account_id):
56
            log.debug('Scrobbler not enabled for account %r', account_id)
57
            return None
58
59
        # Try queue the event
60
        try:
61
            obj = ActionQueue.create(
62
                account=account_id,
63
                session=session,
64
65
                progress=session.progress,
66
67
                part=session.part,
68
                rating_key=session.rating_key,
69
70
                event=event,
71
                request=request,
72
73
                queued_at=datetime.utcnow()
74
            )
75
            log.debug('Queued %r event for %r', event, session)
76
        except (apsw.ConstraintError, peewee.IntegrityError) as ex:
77
            log.info('Event %r has already been queued for session %r: %s', event, session.session_key, ex, exc_info=True)
0 ignored issues
show
Coding Style introduced by
This line is too long as per the coding-style (122/120).

This check looks for lines that are too long. You can specify the maximum line length.

Loading history...
78
        except Exception as ex:
0 ignored issues
show
Best Practice introduced by
Catching very general exceptions such as Exception is usually not recommended.

Generally, you would want to handle very specific errors in the exception handler. This ensure that you do not hide other types of errors which should be fixed.

So, unless you specifically plan to handle any error, consider adding a more specific exception.

Loading history...
79
            log.warn('Unable to queue event %r for %r: %s', event, session, ex, exc_info=True)
80
81
        # Ensure process thread is started
82
        cls.start()
83
84
        return obj
85
86
    @classmethod
87
    def delete(cls, session_id, event):
88
        ActionQueue.delete().where(
89
            ActionQueue.session == session_id,
90
            ActionQueue.event == event
91
        ).execute()
92
93
    #
94
    # Process
95
    #
96
    @classmethod
97
    def start(cls):
98
        if cls._process_thread is not None:
99
            return
100
101
        cls._process_thread = Thread(target=cls.run)
102
        cls._process_thread.daemon = True
103
104
        cls._process_thread.start()
105
106
    @classmethod
107
    def run(cls):
108
        while cls._process_enabled:
109
            if InterfaceMessages.critical:
0 ignored issues
show
Bug introduced by
The Class InterfaceMessages does not seem to have a member named critical.

This check looks for calls to members that are non-existent. These calls will fail.

The member could have been renamed or removed.

Loading history...
110
                cls._process_enabled = False
111
                return
112
113
            # Retrieve one action from the queue
114
            try:
115
                action = ActionQueue.get()
116
            except ActionQueue.DoesNotExist:
0 ignored issues
show
Bug introduced by
The Class ActionQueue does not seem to have a member named DoesNotExist.

This check looks for calls to members that are non-existent. These calls will fail.

The member could have been renamed or removed.

Loading history...
117
                time.sleep(5)
118
                continue
119
            except DisabledError:
120
                break
121
            except Exception as ex:
0 ignored issues
show
Best Practice introduced by
Catching very general exceptions such as Exception is usually not recommended.

Generally, you would want to handle very specific errors in the exception handler. This ensure that you do not hide other types of errors which should be fixed.

So, unless you specifically plan to handle any error, consider adding a more specific exception.

Loading history...
122
                log.warn('Unable to retrieve action from queue - %s', ex, exc_info=True)
123
                time.sleep(5)
124
                continue
125
126
            log.debug('Retrieved %r action from queue', action.event)
127
128
            try:
129
                performed = cls.process(action)
130
131
                cls.resolve(action, performed)
132
133
                log.debug('Action %r sent, moved action to history', action.event)
134
            except Exception as ex:
0 ignored issues
show
Best Practice introduced by
Catching very general exceptions such as Exception is usually not recommended.

Generally, you would want to handle very specific errors in the exception handler. This ensure that you do not hide other types of errors which should be fixed.

So, unless you specifically plan to handle any error, consider adding a more specific exception.

Loading history...
135
                log.warn('Unable to process action %%r - %s' % ex.message, action.event, exc_info=True, extra={
0 ignored issues
show
Coding Style Best Practice introduced by
Specify string format arguments as logging function parameters
Loading history...
Bug introduced by
The Instance of Exception does not seem to have a member named message.

This check looks for calls to members that are non-existent. These calls will fail.

The member could have been renamed or removed.

Loading history...
136
                    'event': {
137
                        'module': __name__,
138
                        'name': 'run.process_exception',
139
                        'key': ex.message
0 ignored issues
show
Bug introduced by
The Instance of Exception does not seem to have a member named message.

This check looks for calls to members that are non-existent. These calls will fail.

The member could have been renamed or removed.

Loading history...
140
                    }
141
                })
142
            finally:
143
                time.sleep(5)
144
145
    @classmethod
146
    def process(cls, action):
147
        if not action.request:
148
            return None
149
150
        if cls.is_duplicate(action):
151
            return None
152
153
        interface, method = action.event.split('/')
154
        request = str(action.request)
155
156
        log.debug('Sending action %r (account: %r, interface: %r, method: %r)', action.event, action.account, interface, method)
0 ignored issues
show
Coding Style introduced by
This line is too long as per the coding-style (128/120).

This check looks for lines that are too long. You can specify the maximum line length.

Loading history...
157
158
        try:
159
            result = cls.send(action, Trakt[interface][method], request)
160
        except Exception as ex:
0 ignored issues
show
Best Practice introduced by
Catching very general exceptions such as Exception is usually not recommended.

Generally, you would want to handle very specific errors in the exception handler. This ensure that you do not hide other types of errors which should be fixed.

So, unless you specifically plan to handle any error, consider adding a more specific exception.

Loading history...
161
            log.error('Unable to send action %r: %r', action.event, ex, exc_info=True)
162
            return None
163
164
        if not result:
165
            # Invalid response
166
            return None
167
168
        if interface == 'scrobble':
169
            return result.get('action')
170
171
        log.warn('result: %r', result)
172
        return None
173
174
    @classmethod
175
    def is_duplicate(cls, action):
176
        if action.event != 'scrobble/stop':
177
            return False
178
179
        # Retrieve scrobble duplication period
180
        duplication_period = Preferences.get('scrobble.duplication_period')
181
182
        if duplication_period is None:
183
            return False
184
185
        # Check for duplicate scrobbles in `duplication_period`
186
        scrobbled = ActionHistory.has_scrobbled(
187
            action.account, action.rating_key,
188
            part=action.part,
189
            after=action.queued_at - timedelta(minutes=duplication_period)
190
        )
191
192
        if scrobbled:
193
            log.info(
194
                'Ignoring duplicate %r action, scrobble already performed in the last %d minutes',
195
                action.event, duplication_period
196
            )
197
            return True
198
199
        return False
200
201
    @classmethod
202
    def send(cls, action, func, request):
203
        # Retrieve `Account` for action
204
        account = action.account
205
206
        if not account:
207
            log.info('Missing `account` for action, unable to send')
208
            return None
209
210
        # Retrieve request data
211
        request = json.loads(request)
212
        log.debug('request: %r', request)
213
214
        # Send request with account authorization
215
        trakt_account = account.trakt
216
217
        if trakt_account is None:
218
            log.info('Missing trakt account for %r', account)
219
            return None
220
        
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
221
        with trakt_account.authorization():
222
            return func(**request)
223
224
    @classmethod
225
    def resolve(cls, action, performed):
226
        # Store action in history
227
        ActionHistory.create(
228
            account=action.account_id,
229
            session=action.session_id,
230
231
            part=action.part,
232
            rating_key=action.rating_key,
233
234
            event=action.event,
235
            performed=performed,
236
237
            queued_at=action.queued_at,
238
            sent_at=datetime.utcnow()
239
        )
240
241
        # Delete queued action
242
        cls.delete(action.session_id, action.event)
243