Passed
Push — beta ( 72a57d...7d0ef0 )
by Dean
03:02
created

SyncArtifacts.send_action()   C

Complexity

Conditions 7

Size

Total Lines 31

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 48.2727

Importance

Changes 0
Metric Value
dl 0
loc 31
ccs 1
cts 18
cp 0.0556
rs 5.5
c 0
b 0
f 0
cc 7
crap 48.2727
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 *
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):
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
    def store_show(self, data, action, guid, p_show=None, **kwargs):
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):
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
    def store_movie(self, data, action, guid, p_key=None, p_movie=None, **kwargs):
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
        scrobbled = ActionHistory.has_scrobbled(
294
            self.task.account, p_key,
295
            after=datetime.utcnow() - timedelta(minutes=duplication_period)
296
        )
297
298
        if scrobbled:
299
            log.info(
300
                'Ignoring duplicate history addition, scrobble already performed in the last %d minutes',
301
                duplication_period
302
            )
303
            return True
304
305
        return False
306
307 1
    @classmethod
308
    def _validate_request(cls, guid, p_item):
309
        # Build item identifier
310 1
        if p_item:
311 1
            identifier = '<%r (%r)>' % (p_item.get('title'), p_item.get('year'))
312
        else:
313
            identifier = repr(guid)
314
315
        # Validate parameters
316 1
        if p_item is not None and (not p_item.get('title') or not p_item.get('year')):
317
            log.info('Invalid "title" or "year" attribute on %s', identifier)
318
            return False
319
320 1
        if not guid:
321
            log.warn('Invalid GUID attribute on %s', identifier)
322
            return False
323
324 1
        if not guid or guid.service not in GUID_SERVICES:
325
            log.warn('GUID service %r is not supported on %s', guid.service if guid else None, identifier)
326
            return False
327
328 1
        return True
329
330 1
    @staticmethod
331
    def _set_kwargs(request, kwargs):
332 1
        for key, value in kwargs.items():
333 1
            if type(value) is datetime:
334 1
                try:
335
                    # Convert `datetime` object to string
336 1
                    value = value.strftime('%Y-%m-%dT%H:%M:%S') + '.000-00:00'
337
                except Exception, ex:
338
                    log.warn('Unable to convert %r to string', value)
339
                    return False
340
341 1
            request[key] = value
342
343 1
        return True
344
345
    #
346
    # Flatten
347
    #
348
349 1
    def flatten(self):
350
        for data, actions in self.artifacts.items():
351
            for action, request in actions.items():
352
                if 'shows' in request:
353
                    request['shows'] = list(self.flatten_shows(request['shows']))
354
355
                if 'movies' in request:
356
                    request['movies'] = request['movies'].values()
357
358
                yield data, action, request
359
360 1
    @staticmethod
361
    def flatten_shows(shows):
362
        for show in shows.itervalues():
363
            if 'seasons' not in show:
364
                yield show
365
                continue
366
367
            show['seasons'] = show['seasons'].values()
368
369
            for season in show['seasons']:
370
                if 'episodes' not in season:
371
                    continue
372
373
                season['episodes'] = season['episodes'].values()
374
375
            yield show
376