Total Complexity | 77 |
Total Lines | 361 |
Duplicated Lines | 14.4 % |
Coverage | 34.57% |
Changes | 0 |
Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.
Common duplication problems, and corresponding solutions are:
Complex classes like SyncArtifacts often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
1 | 1 | from plugin.core.constants import GUID_SERVICES |
|
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 | View Code Duplication | 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 | View Code Duplication | 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 | # TODO check `part` attribute |
||
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 |
It is generally discouraged to redefine built-ins as this makes code very hard to read.