Passed
Push — develop ( b585db...289973 )
by Dean
03:02
created

LibraryState.on_added()   B

Complexity

Conditions 5

Size

Total Lines 31

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 24.2584

Importance

Changes 1
Bugs 0 Features 1
Metric Value
c 1
b 0
f 1
dl 0
loc 31
ccs 1
cts 12
cp 0.0833
rs 8.0894
cc 5
crap 24.2584
1 1
from plugin.models import Account, SyncResult
2 1
from plugin.preferences import Preferences
3 1
from plugin.sync.core.enums import SyncMode
4 1
from plugin.sync.core.exceptions import QueueError
5 1
from plugin.sync.triggers.core.base import Trigger
6
7 1
from datetime import datetime, timedelta
8 1
from dateutil.tz import tzutc
0 ignored issues
show
Configuration introduced by
The import dateutil.tz could not be resolved.

This can be caused by one of the following:

1. Missing Dependencies

This error could indicate a configuration issue of Pylint. Make sure that your libraries are available by adding the necessary commands.

# .scrutinizer.yml
before_commands:
    - sudo pip install abc # Python2
    - sudo pip3 install abc # Python3
Tip: We are currently not using virtualenv to run pylint, when installing your modules make sure to use the command for the correct version.

2. Missing __init__.py files

This error could also result from missing __init__.py files in your module folders. Make sure that you place one file in each sub-folder.

Loading history...
9 1
from plex_activity import Activity
10 1
from threading import Lock, Thread
11 1
import logging
12 1
import time
13
14
15 1
log = logging.getLogger(__name__)
16
17 1
TRIGGER_DELAY = 60 * 2
18
19
20 1
class LibraryUpdateTrigger(Trigger):
21 1
    def __init__(self, sync):
22 1
        super(LibraryUpdateTrigger, self).__init__(sync)
23
24 1
        self._state = LibraryState()
25 1
        self._trigger_lock = Lock()
26
27 1
        self._activity_at = None
28 1
        self._thread = None
29
30
        # Bind to scanner/timeline events
31 1
        Activity.on('websocket.scanner.finished', self.trigger)
32 1
        Activity.on('websocket.timeline.loading', self.trigger)
33 1
        Activity.on('websocket.timeline.finished', self.trigger)
34
35 1
    def trigger(self, *args, **kwargs):
0 ignored issues
show
Unused Code introduced by
The argument kwargs seems to be unused.
Loading history...
Unused Code introduced by
The argument args seems to be unused.
Loading history...
36
        with self._trigger_lock:
37
            return self._trigger()
38
39 1
    def _trigger(self):
40
        log.debug('Scanner activity, sync will be triggered in %d seconds', TRIGGER_DELAY)
41
42
        # Bump trigger
43
        self._activity_at = time.time()
44
45
        # Ensure thread has started
46
        self._start()
47
48 1
    def _start(self):
49
        if self._thread is not None:
50
            return
51
52
        # Construct thread
53
        self._thread = Thread(target=self._run, name='LibraryUpdateTrigger')
54
        self._thread.daemon = True
55
56
        self._thread.start()
57
58 1
    def _run_wrapper(self):
59
        try:
60
            self._run()
61
        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...
62
            log.error('Exception raised in _run(): %s', ex, exc_info=True)
63
64 1
    def _run(self):
65
        while True:
66
            if self._activity_at is None:
67
                log.debug('Invalid activity timestamp, cancelling sync trigger')
68
                return
69
70
            # Calculate seconds since last activity
71
            since = time.time() - self._activity_at
72
73
            # Check if scanner is complete
74
            if since >= TRIGGER_DELAY:
75
                # Break out of loop to trigger a sync
76
                break
77
78
            # Waiting until metadata has finished downloading
79
            time.sleep(float(TRIGGER_DELAY) / 6)
80
81
        try:
82
            self._queue()
83
        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...
84
            log.error('Unable to queue sync: %s', ex, exc_info=True)
85
86
        # Reset state
87
        self._activity_at = None
88
        self._thread = None
89
90 1
    def _queue(self):
91
        started_at = self._state.started_at
92
93
        if started_at:
94
            log.info('Scanner started at: %r', started_at)
95
96
        # Retrieve accounts
97
        accounts = Account.select(
98
            Account.id
0 ignored issues
show
Bug introduced by
The Class Account does not seem to have a member named id.

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...
99
        ).where(
100
            Account.id > 0
0 ignored issues
show
Bug introduced by
The Class Account does not seem to have a member named id.

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...
101
        )
102
103
        # Trigger sync on enabled accounts
104
        for account in accounts:
105
            if account.deleted:
106
                continue
107
108
            # Ensure account has the library update trigger enabled
109
            enabled = Preferences.get('sync.library_update', account)
110
111
            if not enabled:
112
                continue
113
114
            # Retrieve recently added items
115
            items_added = self._state.get(account.id).pop_added()
116
117
            log.info(
118
                'Detected %d item(s) have been added for account %r',
119
                account.id,
120
                len(items_added)
121
            )
122
123
            # Build pull parameters
124
            pull = {
125
                # Run pull on items we explicitly know have been created
126
                'ids': set(items_added)
127
            }
128
129
            if started_at:
130
                # Run pull on items created since the scanner started
131
                pull['created_since'] = started_at - timedelta(seconds=30)
132
133
            # Queue sync for account
134
            try:
135
                self.sync.queue(
136
                    account=account,
137
                    mode=SyncMode.Full,
138
139
                    priority=100,
140
                    trigger=SyncResult.Trigger.LibraryUpdate,
141
142
                    pull=pull
143
                )
144
            except QueueError as ex:
145
                log.info('Queue error: %s', ex)
146
147
                # Unable to queue sync, add items back to the account library state
148
                self._state.get(account.id).extend_added(items_added)
149
            finally:
150
                # Reset library state
151
                self._state.reset()
152
153
154 1
class LibraryState(object):
155 1
    def __init__(self):
156 1
        self._accounts = {}
157 1
        self._accounts_lock = Lock()
158
159 1
        self._started_at = None
160
161
        # Bind to activity events
162 1
        Activity.on('websocket.scanner.started', self.on_started)
163 1
        Activity.on('websocket.timeline.created', self.on_added)
164
165 1
    @property
166
    def started_at(self):
167
        return self._started_at
168
169 1
    def get(self, account_id):
170
        with self._accounts_lock:
171
            if account_id not in self._accounts:
172
                self._accounts[account_id] = AccountLibraryState(account_id)
173
174
            return self._accounts[account_id]
175
176 1
    def reset(self):
177
        self._started_at = None
178
179 1
    def on_started(self, *args, **kwargs):
0 ignored issues
show
Unused Code introduced by
The argument kwargs seems to be unused.
Loading history...
Unused Code introduced by
The argument args seems to be unused.
Loading history...
180
        log.debug('Scanner started')
181
182
        self._started_at = datetime.utcnow().replace(tzinfo=tzutc())
183
184 1
    def on_added(self, data, *args, **kwargs):
0 ignored issues
show
Unused Code introduced by
The argument kwargs seems to be unused.
Loading history...
Unused Code introduced by
The argument args seems to be unused.
Loading history...
185
        if data.get('type') not in [1, 2, 4]:
186
            return
187
188
        log.debug(
189
            'Item added: %s (id: %r, type: %r)',
190
            data.get('title'),
191
            data.get('itemID'),
192
            data.get('type')
193
        )
194
195
        # Retrieve accounts
196
        accounts = Account.select(
197
            Account.id
0 ignored issues
show
Bug introduced by
The Class Account does not seem to have a member named id.

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...
198
        ).where(
199
            Account.id > 0
0 ignored issues
show
Bug introduced by
The Class Account does not seem to have a member named id.

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...
200
        )
201
202
        # Update library state for accounts
203
        for account in accounts:
204
            if account.deleted:
205
                continue
206
207
            # Ensure account has the library update trigger enabled
208
            enabled = Preferences.get('sync.library_update', account)
209
210
            if not enabled:
211
                continue
212
213
            # Update library state for account
214
            self.get(account.id).on_added(data)
215
216
217 1
class AccountLibraryState(object):
218 1
    def __init__(self, account_id):
219
        self.account_id = account_id
220
221
        self._items_added = []
222
        self._items_added_lock = Lock()
223
224 1
    def extend_added(self, items):
225
        with self._items_added_lock:
226
            self._items_added.extend(items)
227
228 1
    def pop_added(self):
229
        with self._items_added_lock:
230
            # Retrieve current items
231
            items = self._items_added
232
233
            # Reset state
234
            self._items_added = []
235
236
        return items
237
238 1
    def on_added(self, data, *args, **kwargs):
0 ignored issues
show
Unused Code introduced by
The argument args seems to be unused.
Loading history...
Unused Code introduced by
The argument kwargs seems to be unused.
Loading history...
239
        if not data or not data.get('itemID'):
240
            return
241
242
        log.debug(
243
            'Item added for account %r: %s (id: %r, type: %r)',
244
            self.account_id,
245
            data.get('title'),
246
            data.get('itemID'),
247
            data.get('type')
248
        )
249
250
        # Append item to list
251
        with self._items_added_lock:
252
            self._items_added.append(data.get('itemID'))
253