SyncArtifacts._validate_request()   D
last analyzed

Complexity

Conditions 9

Size

Total Lines 22

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 19.125

Importance

Changes 0
Metric Value
dl 0
loc 22
ccs 7
cts 14
cp 0.5
rs 4.7088
c 0
b 0
f 0
cc 9
crap 19.125
1 1
from plugin.core.constants import GUID_SERVICES
2 1
from plugin.core.helpers.variable import dict_path
3 1
from plugin.models import *
0 ignored issues
show
Bug Best Practice introduced by
This seems to re-define the built-in Exception.

It is generally discouraged to redefine built-ins as this makes code very hard to read.

Loading history...
Coding Style introduced by
The usage of wildcard imports like plugin.models should generally be avoided.
Loading history...
4 1
from plugin.preferences import Preferences
5 1
from plugin.sync.core.enums import SyncActionMode, SyncData
6
7 1
from datetime import datetime, timedelta
8 1
from trakt import Trakt
9 1
from trakt_sync.cache.main import Cache
10 1
import elapsed
11 1
import logging
12
13 1
log = logging.getLogger(__name__)
14
15
16 1
class SyncArtifacts(object):
17 1
    def __init__(self, task):
18 1
        self.task = task
19
20 1
        self.artifacts = {}
21
22
    #
23
    # Log/Send artifacts
24
    #
25
26 1
    def send(self):
27
        action_mode = self.task.configuration['sync.action.mode']
28
29
        if action_mode == SyncActionMode.Update:
30
            self.send_actions()
31
            return True
32
33
        if action_mode == SyncActionMode.Log:
34
            self.log_actions()
35
            return True
36
37
        raise NotImplementedError('Unable to send artifacts to trakt, action mode %r not supported', action_mode)
38
39 1
    @elapsed.clock
40
    def send_actions(self):
41
        changes = False
42
43
        for data, action, request in self.flatten():
44
            changes = True
45
46
            # Send artifact to trakt.tv
47
            self.send_action(data, action, **request)
48
49
            # Invalidate cache to ensure actions aren't resent
50
            for key, value in request.items():
51
                if not value:
52
                    # Empty media request
53
                    continue
54
55
                if key == 'shows':
56
                    media = Cache.Media.Shows
57
                elif key == 'movies':
58
                    media = Cache.Media.Movies
59
                else:
60
                    # Unknown media type
61
                    continue
62
63
                self.task.state.trakt.invalidate(
64
                    Cache.Media.get(media),
65
                    Cache.Data.get(data)
66
                )
67
68
            # Task checkpoint
69
            self.task.checkpoint()
70
71
        if not changes:
72
            log.info('trakt.tv profile is up-to-date')
73
            return
74
75
        log.info('trakt.tv profile has been updated')
76
77 1
    @classmethod
78
    def send_action(cls, data, action, **kwargs):
79
        # Ensure items exist in `kwargs`
80
        if not kwargs:
81
            return False
82
83
        if not kwargs.get('movies') and not kwargs.get('shows'):
84
            return False
85
86
        # Try retrieve interface for `data`
87
        interface = cls._get_interface(data)
88
89
        if interface is None:
90
            log.warn('[%s](%s) Unknown data type', data, action)
91
            return False
92
93
        # Try retrieve method for `action`
94
        func = getattr(Trakt[interface], action, None)
95
96
        if func is None:
97
            log.warn('[%s](%s) Unable find action in interface', data, action)
98
            return False
99
100
        # Send request to trakt.tv
101
        response = func(kwargs)
102
103
        if response is None:
104
            return False
105
106
        log.debug('[%s](%s) Response: %r', data, action, response)
107
        return True
108
109 1
    def log_actions(self):
110
        for data, action, request in self.flatten():
111
            # Try retrieve interface for `data`
112
            interface = self._get_interface(data)
113
114
            if interface is None:
115
                log.warn('[%s](%s) Unknown data type', data, action)
116
                continue
117
118
            # Log request items
119
            for media, items in request.items():
120
                self.log_items(interface, action, media, items)
121
122 1
    def log_items(self, interface, action, media, items):
123
        if not items:
124
            return
125
126
            # Log each item
127
        for item in items:
128
            if not item:
129
                continue
130
131
            log.info('[%s:%s](%s) %r (%r)', interface, action, media, item.get('title'), item.get('year'))
132
133
            if media == 'shows':
134
                # Log each episode
135
                self.log_episodes(item)
136
137 1
    def log_episodes(self, item):
0 ignored issues
show
Coding Style introduced by
This method could be written as a function/class method.

If a method does not access any attributes of the class, it could also be implemented as a function or static method. This can help improve readability. For example

class Foo:
    def some_method(self, x, y):
        return x + y;

could be written as

class Foo:
    @classmethod
    def some_method(cls, x, y):
        return x + y;
Loading history...
138
        for season in item.get('seasons', []):
139
            episodes = season.get('episodes')
140
141
            if episodes is None:
142
                log.info('    S%02d', season.get('number'))
143
                continue
144
145
            for episode in episodes:
146
                log.info('    S%02dE%02d', season.get('number'), episode.get('number'))
147
148 1
    @staticmethod
149
    def _get_interface(data):
150
        # Try retrieve interface for `data`
151
        interface = Cache.Data.get_interface(data)
152
153
        if interface == 'sync/watched':
154
            # Watched add/remove functions are on the "sync/history" interface
155
            return 'sync/history'
156
157
        return interface
158
159
    #
160
    # Artifact storage
161
    #
162
163 1 View Code Duplication
    def store_show(self, data, action, guid, p_show=None, **kwargs):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
164
        key = (guid.service, guid.id)
165
166
        shows = dict_path(self.artifacts, [
167
            data,
168
            action,
169
            'shows'
170
        ])
171
172
        # Build show
173
        if key in shows:
174
            show = shows[key]
175
        else:
176
            show = self._build_request(guid, p_show, **kwargs)
177
178
            if show is None:
179
                return False
180
181
            # Store `show` in artifacts
182
            shows[key] = show
183
184
        # Set `kwargs` on `show`
185
        self._set_kwargs(show, kwargs)
186
        return True
187
188 1
    def store_episode(self, data, action, guid, identifier, p_key=None, p_show=None, p_episode=None, **kwargs):
0 ignored issues
show
Unused Code introduced by
The argument p_episode seems to be unused.
Loading history...
189 1
        key = (guid.service, guid.id)
190 1
        season_num, episode_num = identifier
191
192 1
        shows = dict_path(self.artifacts, [
193
            data,
194
            action,
195
            'shows'
196
        ])
197
198
        # Check for duplicate history addition
199 1
        if self._is_duplicate(data, action, p_key):
200
            return False
201
202
        # Build show
203 1
        if key in shows:
204
            show = shows[key]
205
        else:
206 1
            show = self._build_request(guid, p_show)
207
208 1
            if show is None:
209
                return False
210
211 1
            shows[key] = show
212
213
        # Ensure 'seasons' attribute exists
214 1
        if 'seasons' not in show:
215 1
            show['seasons'] = {}
216
217
        # Build season
218 1
        if season_num in show['seasons']:
219
            season = show['seasons'][season_num]
220
        else:
221 1
            season = show['seasons'][season_num] = {'number': season_num}
222 1
            season['episodes'] = {}
223
224
        # Build episode
225 1
        if episode_num in season['episodes']:
226
            episode = season['episodes'][episode_num]
227
        else:
228 1
            episode = season['episodes'][episode_num] = {'number': episode_num}
229
230
        # Set `kwargs` on `episode`
231 1
        self._set_kwargs(episode, kwargs)
232 1
        return True
233
234 1 View Code Duplication
    def store_movie(self, data, action, guid, p_key=None, p_movie=None, **kwargs):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
235 1
        key = (guid.service, guid.id)
236
237 1
        movies = dict_path(self.artifacts, [
238
            data,
239
            action,
240
            'movies'
241
        ])
242
243
        # Check for duplicate history addition
244 1
        if self._is_duplicate(data, action, p_key):
245
            return False
246
247
        # Build movie
248 1
        if key in movies:
249
            movie = movies[key]
250
        else:
251 1
            movie = self._build_request(guid, p_movie, **kwargs)
252
253 1
            if movie is None:
254
                return False
255
256
            # Store `movie` in artifacts
257 1
            movies[key] = movie
258
259
        # Set `kwargs` on `movie`
260 1
        self._set_kwargs(movie, kwargs)
261 1
        return True
262
263 1
    @classmethod
264
    def _build_request(cls, guid, p_item, **kwargs):
265
        # Validate request
266 1
        if not cls._validate_request(guid, p_item):
267
            return None
268
269
        # Build request
270 1
        request = {
271
            'ids': {}
272
        }
273
274
        # Set identifier
275 1
        request['ids'][guid.service] = guid.id
276
277
        # Set extra attributes
278 1
        cls._set_kwargs(request, kwargs)
279
280 1
        return request
281
282 1
    def _is_duplicate(self, data, action, p_key):
283 1
        if data != SyncData.Watched or action != 'add':
284 1
            return False
285
286
        # Retrieve scrobble duplication period
287
        duplication_period = Preferences.get('scrobble.duplication_period')
288
289
        if duplication_period is None:
290
            return False
291
292
        # Check for duplicate scrobbles in `duplication_period`
293
        # TODO check `part` attribute
0 ignored issues
show
Coding Style introduced by
TODO and FIXME comments should generally be avoided.
Loading history...
294
        scrobbled = ActionHistory.has_scrobbled(
295
            self.task.account, p_key,
296
            after=datetime.utcnow() - timedelta(minutes=duplication_period)
297
        )
298
299
        if scrobbled:
300
            log.info(
301
                'Ignoring duplicate history addition, scrobble already performed in the last %d minutes',
302
                duplication_period
303
            )
304
            return True
305
306
        return False
307
308 1
    @classmethod
309
    def _validate_request(cls, guid, p_item):
310
        # Build item identifier
311 1
        if p_item:
312 1
            identifier = '<%r (%r)>' % (p_item.get('title'), p_item.get('year'))
313
        else:
314
            identifier = repr(guid)
315
316
        # Validate parameters
317 1
        if p_item is not None and (not p_item.get('title') or not p_item.get('year')):
318
            log.info('Invalid "title" or "year" attribute on %s', identifier)
319
            return False
320
321 1
        if not guid or not guid.valid:
322
            log.warn('Invalid GUID attribute on %s (guid: %r)', identifier, guid)
323
            return False
324
325 1
        if guid.service not in GUID_SERVICES:
326
            log.warn('GUID service %r is not supported on %s', guid.service if guid else None, identifier)
327
            return False
328
329 1
        return True
330
331 1
    @staticmethod
332
    def _set_kwargs(request, kwargs):
333 1
        for key, value in kwargs.items():
334 1
            if type(value) is datetime:
335 1
                try:
336
                    # Convert `datetime` object to string
337 1
                    value = value.strftime('%Y-%m-%dT%H:%M:%S') + '.000-00:00'
338
                except Exception:
339
                    log.warn('Unable to convert %r to string', value)
340
                    return False
341
342 1
            request[key] = value
343
344 1
        return True
345
346
    #
347
    # Flatten
348
    #
349
350 1
    def flatten(self):
351
        for data, actions in self.artifacts.items():
352
            for action, request in actions.items():
353
                if 'shows' in request:
354
                    request['shows'] = list(self.flatten_shows(request['shows']))
355
356
                if 'movies' in request:
357
                    request['movies'] = request['movies'].values()
358
359
                yield data, action, request
360
361 1
    @staticmethod
362
    def flatten_shows(shows):
363
        for show in shows.itervalues():
364
            if 'seasons' not in show:
365
                yield show
366
                continue
367
368
            show['seasons'] = show['seasons'].values()
369
370
            for season in show['seasons']:
371
                if 'episodes' not in season:
372
                    continue
373
374
                season['episodes'] = season['episodes'].values()
375
376
            yield show
377