Passed
Push — develop ( 456c9f...9a88f4 )
by Dean
03:13
created

SyncArtifacts.log_actions()   A

Complexity

Conditions 4

Size

Total Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 14.7187
Metric Value
cc 4
dl 0
loc 12
ccs 1
cts 8
cp 0.125
crap 14.7187
rs 9.2
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.sync.core.enums import SyncActionMode, SyncData
5
6 1
from datetime import datetime, timedelta
7 1
from trakt import Trakt
8 1
from trakt_sync.cache.main import Cache
9 1
import elapsed
10 1
import logging
11
12 1
log = logging.getLogger(__name__)
13
14
15 1
class SyncArtifacts(object):
16 1
    def __init__(self, task):
17 1
        self.task = task
18
19 1
        self.artifacts = {}
20
21
    #
22
    # Log/Send artifacts
23
    #
24
25 1
    def send(self):
26
        action_mode = self.task.configuration['sync.action.mode']
27
28
        if action_mode == SyncActionMode.Update:
29
            self.send_actions()
30
            return True
31
32
        if action_mode == SyncActionMode.Log:
33
            self.log_actions()
34
            return True
35
36
        raise NotImplementedError('Unable to send artifacts to trakt, action mode %r not supported', action_mode)
37
38 1
    @elapsed.clock
39
    def send_actions(self):
40
        changes = False
41
42
        for data, action, request in self.flatten():
43
            changes = True
44
45
            # Send artifact to trakt.tv
46
            self.send_action(data, action, **request)
47
48
            # Invalidate cache to ensure actions aren't resent
49
            for key, value in request.items():
50
                if not value:
51
                    # Empty media request
52
                    continue
53
54
                if key == 'shows':
55
                    media = Cache.Media.Shows
56
                elif key == 'movies':
57
                    media = Cache.Media.Movies
58
                else:
59
                    # Unknown media type
60
                    continue
61
62
                self.task.state.trakt.invalidate(
63
                    Cache.Media.get(media),
64
                    Cache.Data.get(data)
65
                )
66
67
            # Task checkpoint
68
            self.task.checkpoint()
69
70
        if not changes:
71
            log.info('trakt.tv profile is up-to-date')
72
            return
73
74
        log.info('trakt.tv profile has been updated')
75
76 1
    @classmethod
77
    def send_action(cls, data, action, **kwargs):
78
        # Ensure items exist in `kwargs`
79
        if not kwargs:
80
            return False
81
82
        if not kwargs.get('movies') and not kwargs.get('shows'):
83
            return False
84
85
        # Try retrieve interface for `data`
86
        interface = cls._get_interface(data)
87
88
        if interface is None:
89
            log.warn('[%s](%s) Unknown data type', data, action)
90
            return False
91
92
        # Try retrieve method for `action`
93
        func = getattr(Trakt[interface], action, None)
94
95
        if func is None:
96
            log.warn('[%s](%s) Unable find action in interface', data, action)
97
            return False
98
99
        # Send request to trakt.tv
100
        response = func(kwargs)
101
102
        if response is None:
103
            return False
104
105
        log.debug('[%s](%s) Response: %r', data, action, response)
106
        return True
107
108 1
    def log_actions(self):
109
        for data, action, request in self.flatten():
110
            # Try retrieve interface for `data`
111
            interface = self._get_interface(data)
112
113
            if interface is None:
114
                log.warn('[%s](%s) Unknown data type', data, action)
115
                continue
116
117
            # Log request items
118
            for media, items in request.items():
119
                self.log_items(interface, action, media, items)
120
121 1
    def log_items(self, interface, action, media, items):
122
        if not items:
123
            return
124
125
            # Log each item
126
        for item in items:
127
            if not item:
128
                continue
129
130
            log.info('[%s:%s](%s) %r (%r)', interface, action, media, item.get('title'), item.get('year'))
131
132
            if media == 'shows':
133
                # Log each episode
134
                self.log_episodes(item)
135
136 1
    def log_episodes(self, item):
137
        for season in item.get('seasons', []):
138
            episodes = season.get('episodes')
139
140
            if episodes is None:
141
                log.info('    S%02d', season.get('number'))
142
                continue
143
144
            for episode in episodes:
145
                log.info('    S%02dE%02d', season.get('number'), episode.get('number'))
146
147 1
    @staticmethod
148
    def _get_interface(data):
149
        # Try retrieve interface for `data`
150
        interface = Cache.Data.get_interface(data)
151
152
        if interface == 'sync/watched':
153
            # Watched add/remove functions are on the "sync/history" interface
154
            return 'sync/history'
155
156
        return interface
157
158
    #
159
    # Artifact storage
160
    #
161
162 1
    def store_show(self, data, action, guid, p_show=None, **kwargs):
163
        key = (guid.service, guid.id)
164
165
        shows = dict_path(self.artifacts, [
166
            data,
167
            action,
168
            'shows'
169
        ])
170
171
        # Build show
172
        if key in shows:
173
            show = shows[key]
174
        else:
175
            show = self._build_request(guid, p_show, **kwargs)
176
177
            if show is None:
178
                return False
179
180
            # Store `show` in artifacts
181
            shows[key] = show
182
183
        # Set `kwargs` on `show`
184
        self._set_kwargs(show, kwargs)
185
        return True
186
187 1
    def store_episode(self, data, action, guid, identifier, p_key=None, p_show=None, p_episode=None, **kwargs):
188 1
        key = (guid.service, guid.id)
189 1
        season_num, episode_num = identifier
190
191 1
        shows = dict_path(self.artifacts, [
192
            data,
193
            action,
194
            'shows'
195
        ])
196
197
        # Check for duplicate history addition
198 1
        if self._is_duplicate(data, action, p_key):
199
            return False
200
201
        # Build show
202 1
        if key in shows:
203
            show = shows[key]
204
        else:
205 1
            show = self._build_request(guid, p_show)
206
207 1
            if show is None:
208
                return False
209
210 1
            shows[key] = show
211
212
        # Ensure 'seasons' attribute exists
213 1
        if 'seasons' not in show:
214 1
            show['seasons'] = {}
215
216
        # Build season
217 1
        if season_num in show['seasons']:
218
            season = show['seasons'][season_num]
219
        else:
220 1
            season = show['seasons'][season_num] = {'number': season_num}
221 1
            season['episodes'] = {}
222
223
        # Build episode
224 1
        if episode_num in season['episodes']:
225
            episode = season['episodes'][episode_num]
226
        else:
227 1
            episode = season['episodes'][episode_num] = {'number': episode_num}
228
229
        # Set `kwargs` on `episode`
230 1
        self._set_kwargs(episode, kwargs)
231 1
        return True
232
233 1
    def store_movie(self, data, action, guid, p_key=None, p_movie=None, **kwargs):
234 1
        key = (guid.service, guid.id)
235
236 1
        movies = dict_path(self.artifacts, [
237
            data,
238
            action,
239
            'movies'
240
        ])
241
242
        # Check for duplicate history addition
243 1
        if self._is_duplicate(data, action, p_key):
244
            return False
245
246
        # Build movie
247 1
        if key in movies:
248
            movie = movies[key]
249
        else:
250 1
            movie = self._build_request(guid, p_movie, **kwargs)
251
252 1
            if movie is None:
253
                return False
254
255
            # Store `movie` in artifacts
256 1
            movies[key] = movie
257
258
        # Set `kwargs` on `movie`
259 1
        self._set_kwargs(movie, kwargs)
260 1
        return True
261
262 1
    @classmethod
263
    def _build_request(cls, guid, p_item, **kwargs):
264
        # Validate request
265 1
        if not cls._validate_request(guid, p_item):
266
            return None
267
268
        # Build request
269 1
        request = {
270
            'ids': {}
271
        }
272
273
        # Set identifier
274 1
        request['ids'][guid.service] = guid.id
275
276
        # Set extra attributes
277 1
        cls._set_kwargs(request, kwargs)
278
279 1
        return request
280
281 1
    def _is_duplicate(self, data, action, p_key):
282 1
        if data != SyncData.Watched or action != 'add':
283 1
            return False
284
285
        scrobbled = ActionHistory.has_scrobbled(
286
            self.task.account, p_key,
287
            after=datetime.utcnow() - timedelta(hours=1)
288
        )
289
290
        if scrobbled:
291
            log.info('Ignoring duplicate history addition, scrobble already performed in the last hour')
292
            return True
293
294
        return False
295
296 1
    @classmethod
297
    def _validate_request(cls, guid, p_item):
298
        # Build item identifier
299 1
        if p_item:
300 1
            identifier = '<%r (%r)>' % (p_item.get('title'), p_item.get('year'))
301
        else:
302
            identifier = repr(guid)
303
304
        # Validate parameters
305 1
        if p_item is not None and (not p_item.get('title') or not p_item.get('year')):
306
            log.info('Invalid "title" or "year" attribute on %s', identifier)
307
            return False
308
309 1
        if not guid:
310
            log.warn('Invalid GUID attribute on %s', identifier)
311
            return False
312
313 1
        if not guid or guid.service not in GUID_SERVICES:
314
            log.warn('GUID service %r is not supported on %s', guid.service if guid else None, identifier)
315
            return False
316
317 1
        return True
318
319 1
    @staticmethod
320
    def _set_kwargs(request, kwargs):
321 1
        for key, value in kwargs.items():
322 1
            if type(value) is datetime:
323 1
                try:
324
                    # Convert `datetime` object to string
325 1
                    value = value.strftime('%Y-%m-%dT%H:%M:%S') + '.000-00:00'
326
                except Exception, ex:
327
                    log.warn('Unable to convert %r to string', value)
328
                    return False
329
330 1
            request[key] = value
331
332 1
        return True
333
334
    #
335
    # Flatten
336
    #
337
338 1
    def flatten(self):
339
        for data, actions in self.artifacts.items():
340
            for action, request in actions.items():
341
                if 'shows' in request:
342
                    request['shows'] = list(self.flatten_shows(request['shows']))
343
344
                if 'movies' in request:
345
                    request['movies'] = request['movies'].values()
346
347
                yield data, action, request
348
349 1
    @staticmethod
350
    def flatten_shows(shows):
351
        for show in shows.itervalues():
352
            if 'seasons' not in show:
353
                yield show
354
                continue
355
356
            show['seasons'] = show['seasons'].values()
357
358
            for season in show['seasons']:
359
                if 'episodes' not in season:
360
                    continue
361
362
                season['episodes'] = season['episodes'].values()
363
364
            yield show
365