Test Failed
Push — beta ( 918fe2...7d3e07 )
by Dean
03:03
created

ActionManager.queue()   C

Complexity

Conditions 10

Size

Total Lines 54

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 110

Importance

Changes 0
Metric Value
dl 0
loc 54
ccs 0
cts 29
cp 0
rs 5.1428
c 0
b 0
f 0
cc 10
crap 110

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

Complexity

Complex classes like ActionManager.queue() often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
from plugin.core.helpers.thread import module
2
from plugin.managers.core.base import Manager
3
from plugin.models import ActionHistory, ActionQueue
4
from plugin.preferences import Preferences
5
6
from datetime import datetime, timedelta
7
from threading import Thread
8
from trakt import Trakt
9
import apsw
10
import json
11
import logging
12
import peewee
13
import time
14
15
log = logging.getLogger(__name__)
16
17
18
@module(start=True, blocking=True)
19
class ActionManager(Manager):
20
    _process_enabled = True
21
    _process_thread = None
22
23
    #
24
    # Queue
25
    #
26
27
    @classmethod
28
    def queue(cls, event, request, session=None, account=None):
29
        if event is None:
30
            return None
31
32
        obj = None
33
34
        if request is not None:
35
            request = json.dumps(request)
36
37
        # Retrieve `account_id` for action
38
        account_id = None
39
40
        if session:
41
            try:
42
                account_id = session.account_id
43
            except KeyError:
44
                account_id = None
45
46
        if account_id is None and account:
47
            account_id = account.id
48
49
        if account_id is None:
50
            log.debug('Unable to find valid account for event %r, session %r', event, session)
51
            return None
52
53
        if not Preferences.get('scrobble.enabled', account_id):
54
            log.debug('Scrobbler not enabled for account %r', account_id)
55
            return None
56
57
        # Try queue the event
58
        try:
59
            obj = ActionQueue.create(
60
                account=account_id,
61
                session=session,
62
63
                progress=session.progress,
64
65
                part=session.part,
66
                rating_key=session.rating_key,
67
68
                event=event,
69
                request=request,
70
71
                queued_at=datetime.utcnow()
72
            )
73
            log.debug('Queued %r event for %r', event, session)
74
        except (apsw.ConstraintError, peewee.IntegrityError), ex:
75
            log.warn('Unable to queue event %r for %r: %s', event, session, ex, exc_info=True)
76
77
        # Ensure process thread is started
78
        cls.start()
79
80
        return obj
81
82
    @classmethod
83
    def delete(cls, session_id, event):
84
        ActionQueue.delete().where(
85
            ActionQueue.session == session_id,
86
            ActionQueue.event == event
87
        ).execute()
88
89
    #
90
    # Process
91
    #
92
    @classmethod
93
    def start(cls):
94
        if cls._process_thread is not None:
95
            return
96
97
        cls._process_thread = Thread(target=cls.run)
98
        cls._process_thread.daemon = True
99
100
        cls._process_thread.start()
101
102
    @classmethod
103
    def run(cls):
104
        while cls._process_enabled:
105
            # Retrieve one action from the queue
106
            try:
107
                action = ActionQueue.get()
108
            except ActionQueue.DoesNotExist:
109
                time.sleep(5)
110
                continue
111
            except Exception, ex:
112
                log.warn('Unable to retrieve action from queue - %s', ex, exc_info=True)
113
                time.sleep(5)
114
                continue
115
116
            log.debug('Retrieved %r action from queue', action.event)
117
118
            try:
119
                performed = cls.process(action)
120
121
                cls.resolve(action, performed)
122
123
                log.debug('Action %r sent, moved action to history', action.event)
124
            except Exception, ex:
125
                log.warn('Unable to process action %%r - %s' % ex.message, action.event, exc_info=True, extra={
126
                    'event': {
127
                        'module': __name__,
128
                        'name': 'run.process_exception',
129
                        'key': ex.message
130
                    }
131
                })
132
            finally:
133
                time.sleep(5)
134
135
    @classmethod
136
    def process(cls, action):
137
        if not action.request:
138
            return None
139
140
        if cls.is_duplicate(action):
141
            return None
142
143
        interface, method = action.event.split('/')
144
        request = str(action.request)
145
146
        log.debug('Sending action %r (account: %r, interface: %r, method: %r)', action.event, action.account, interface, method)
147
148
        try:
149
            result = cls.send(action, Trakt[interface][method], request)
150
        except Exception, ex:
151
            log.error('Unable to send action %r: %r', action.event, ex, exc_info=True)
152
            return None
153
154
        if not result:
155
            # Invalid response
156
            return None
157
158
        if interface == 'scrobble':
159
            return result.get('action')
160
161
        log.warn('result: %r', result)
162
        return None
163
164
    @classmethod
165
    def is_duplicate(cls, action):
166
        if action.event != 'scrobble/stop':
167
            return False
168
169
        # Retrieve scrobble duplication period
170
        duplication_period = Preferences.get('scrobble.duplication_period')
171
172
        if duplication_period is None:
173
            return False
174
175
        # Check for duplicate scrobbles in `duplication_period`
176
        scrobbled = ActionHistory.has_scrobbled(
177
            action.account, action.rating_key,
178
            part=action.part,
179
            after=action.queued_at - timedelta(minutes=duplication_period)
180
        )
181
182
        if scrobbled:
183
            log.info(
184
                'Ignoring duplicate %r action, scrobble already performed in the last %d minutes',
185
                action.event, duplication_period
186
            )
187
            return True
188
189
        return False
190
191
    @classmethod
192
    def send(cls, action, func, request):
193
        # Retrieve `Account` for action
194
        account = action.account
195
196
        if not account:
197
            log.info('Missing `account` for action, unable to send')
198
            return None
199
200
        # Retrieve request data
201
        request = json.loads(request)
202
        log.debug('request: %r', request)
203
204
        # Send request with account authorization
205
        trakt_account = account.trakt
206
207
        if trakt_account is None:
208
            log.info('Missing trakt account for %r', account)
209
            return None
210
        
211
        with trakt_account.authorization():
212
            return func(**request)
213
214
    @classmethod
215
    def resolve(cls, action, performed):
216
        # Store action in history
217
        ActionHistory.create(
218
            account=action.account_id,
219
            session=action.session_id,
220
221
            part=action.part,
222
            rating_key=action.rating_key,
223
224
            event=action.event,
225
            performed=performed,
226
227
            queued_at=action.queued_at,
228
            sent_at=datetime.utcnow()
229
        )
230
231
        # Delete queued action
232
        cls.delete(action.session_id, action.event)
233