Sessions._get_session_account_id()   A
last analyzed

Complexity

Conditions 2

Size

Total Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 4.048

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 6
ccs 1
cts 5
cp 0.2
rs 9.4285
cc 2
crap 4.048
1 1
from plugin.core.helpers.thread import synchronized
2 1
from plugin.modules.core.base import Module
3 1
from plugin.preferences import Preferences
4
5 1
from datetime import datetime, timedelta
6 1
from threading import RLock
7 1
import logging
8
9 1
CLEANUP_INTERVAL = timedelta(minutes=1)
10 1
SESSION_TIMEOUT = timedelta(minutes=2)
11
12 1
log = logging.getLogger(__name__)
13
14
15 1
class Sessions(Module):
16 1
    __key__ = 'sessions'
17
18 1
    def __init__(self):
19 1
        self._lock = RLock()
20
21 1
        self._by_account = {}
22 1
        self._by_session_key = {}
23
24 1
        self._cleaned_at = None
25 1
        self._idle_since = datetime.min
26
27 1
    def is_account_streaming(self, account, rating_key=None):
28
        if type(account) is not int:
29
            account = account.id
30
31
        # Cleanup stale sessions
32
        self.cleanup()
33
34
        # Try find matching `account` and `rating_key`
35
        return self._by_account.get(account, {}).get(rating_key, False)
36
37 1
    def is_idle(self):
38
        # Cleanup stale sessions
39
        self.cleanup()
40
41
        # Check if server has been idle for `sync.idle_delay` seconds
42
        return self._idle_since and datetime.utcnow() - self._idle_since > timedelta(minutes=Preferences.get('sync.idle_delay'))
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...
43
44 1
    def is_streaming(self):
45
        # Cleanup stale sessions
46
        self.cleanup()
47
48
        # Check if there is any active sessions
49
        if self._by_session_key:
50
            return True
51
52
        return False
53
54
    #
55
    # Session methods
56
    #
57
58 1
    @synchronized(lambda self: self._lock)
59 1
    def cleanup(self, force=False):
60
        if not force and self._cleaned_at and datetime.utcnow() - self._cleaned_at < CLEANUP_INTERVAL:
61
            return 0
62
63
        states = self._by_session_key.values()
64
        removed = 0
65
66
        for state in states:
67
            if state.is_stale() and self.delete(state):
68
                removed += 1
69
70
        if removed:
71
            log.info('Removed %d stale session(s)', removed)
72
        else:
73
            log.debug('Removed %d stale session(s)', removed)
74
75
        self._cleaned_at = datetime.utcnow()
76
        return removed
77
78 1
    @synchronized(lambda self: self._lock)
79
    def create(self, session):
80
        if session.state not in ['create', 'start', 'pause']:
81
            return
82
83
        # Retrieve session parameters
84
        account_id = self._get_session_account_id(session)
85
        rating_key = session.rating_key
86
87
        # Build session key
88
        session_key = self._get_session_key(session)
89
90
        if session_key is None or session_key.endswith(':None'):
0 ignored issues
show
Bug introduced by
The Instance of int does not seem to have a member named endswith.

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...
91
            return
92
93
        # Construct session
94
        state = SessionState(session_key, account_id, rating_key)
95
96
        # Ensure account exists in `active`
97
        if account_id not in self._by_account:
98
            self._by_account[account_id] = {}
99
100
        # Store item in `active`
101
        self._by_account[account_id][rating_key] = state
102
103
        # Store session in `active_by_id` map
104
        self._by_session_key[session_key] = state
105
106
        # Clear idle status
107
        self._idle_since = None
108
109 1
    @synchronized(lambda self: self._lock)
110
    def delete(self, state):
111
        # Remove item from `active` dictionary
112
        if state.account_id in self._by_account and state.rating_key in self._by_account[state.account_id]:
113
            try:
114
                del self._by_account[state.account_id][state.rating_key]
115
            except KeyError:
0 ignored issues
show
Unused Code introduced by
This except handler seems to be unused and could be removed.

Except handlers which only contain pass and do not have an else clause can usually simply be removed:

try:
    raises_exception()
except:  # Could be removed
    pass
Loading history...
116
                pass
117
        else:
118
            log.debug('Unable to find item %r for account %r', state.rating_key, state.account_id)
119
120
        # Remove item from `active_by_id` dictionary
121
        if state.session_key in self._by_session_key:
122
            try:
123
                del self._by_session_key[state.session_key]
124
            except KeyError:
0 ignored issues
show
Unused Code introduced by
This except handler seems to be unused and could be removed.

Except handlers which only contain pass and do not have an else clause can usually simply be removed:

try:
    raises_exception()
except:  # Could be removed
    pass
Loading history...
125
                pass
126
        else:
127
            log.debug('Unable to find session %r', state.session_key)
128
129
        # Update idle status
130
        if not self._by_session_key:
131
            self._idle_since = datetime.utcnow()
132
133
        return True
134
135 1
    @synchronized(lambda self: self._lock)
136
    def replace(self, state, session):
137
        # Remove current state
138
        self.delete(state)
139
140
        # Create new session
141
        self.create(session)
142
143 1
    @synchronized(lambda self: self._lock)
144
    def update(self, session):
145
        # Retrieve session parameters
146
        s_account_id = self._get_session_account_id(session)
147
148
        if s_account_id is None:
149
            return
150
151
        # Build session key
152
        session_key = self._get_session_key(session)
153
154
        if session_key is None:
155
            return
156
157
        # Check if session exists
158
        if session_key not in self._by_session_key:
159
            self.create(session)
160
            return
161
162
        # Check if an update is required
163
        state = self._by_session_key[session_key]
164
165
        if state.account_id != s_account_id or state.rating_key != session.rating_key:
166
            # Replace session details
167
            self.replace(state, session)
168
        elif session.state == 'stop':
169
            # Delete current session
170
            self.delete(state)
171
        else:
172
            # Update session
173
            state.update(session)
174
175
    #
176
    # Event handlers
177
    #
178
179 1
    @synchronized(lambda self: self._lock)
180
    def on_created(self, session):
181
        # Store session
182
        self.create(session)
183
184
        # Display active session message
185
        self._log()
186
187 1
    @synchronized(lambda self: self._lock)
188
    def on_updated(self, session):
189
        # Update session
190
        self.update(session)
191
192
        # Display active session message
193
        self._log()
194
195
    #
196
    # Helpers
197
    #
198
199 1
    def _log(self):
200
        if not self._by_session_key:
201
            return
202
203
        log.debug('%d active session(s): %r', len(self._by_session_key), self._by_session_key)
204
205 1
    @classmethod
206
    def _get_session_account_id(cls, session):
207
        try:
208
            return session.account_id
209
        except KeyError:
210
            return None
211
212 1
    @classmethod
213
    def _get_session_key(cls, session):
214
        if session.session_key is None:
215
            return None
216
217
        try:
218
            return int(session.session_key)
219
        except ValueError:
0 ignored issues
show
Unused Code introduced by
This except handler seems to be unused and could be removed.

Except handlers which only contain pass and do not have an else clause can usually simply be removed:

try:
    raises_exception()
except:  # Could be removed
    pass
Loading history...
220
            pass
221
222
        return session.session_key
223
224
225 1
class SessionState(object):
226 1
    def __init__(self, session_key, account_id, rating_key):
227
        self.session_key = session_key
228
229
        self.account_id = account_id
230
        self.rating_key = rating_key
231
232
        self.seen_at = datetime.utcnow()
233
234 1
    def is_stale(self):
235
        return datetime.utcnow() - self.seen_at > SESSION_TIMEOUT
236
237 1
    def update(self, session):
0 ignored issues
show
Unused Code introduced by
The argument session seems to be unused.
Loading history...
238
        self.seen_at = datetime.utcnow()
239
240 1
    def __repr__(self):
241
        return '<Session %s>' % (
242
            ', '.join([
243
                ('%s: %r' % (key, getattr(self, key))) for key in [
244
                    'session_key',
245
246
                    'account_id',
247
                    'rating_key',
248
249
                    'seen_at'
250
                ]
251
            ])
252
        )
253