Table.map_items()   F
last analyzed

Complexity

Conditions 20

Size

Total Lines 79

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 394.9805

Importance

Changes 0
Metric Value
dl 0
loc 79
ccs 1
cts 47
cp 0.0213
rs 2.221
c 0
b 0
f 0
cc 20
crap 394.9805

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

Complexity

Complex classes like Table.map_items() 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.helpers.variable import try_convert
2 1
from plugin.core.backup import BackupManager
3 1
from plugin.core.constants import GUID_SERVICES
4 1
from plugin.core.database.manager import DatabaseManager
5 1
from plugin.core.exceptions import AccountAuthenticationError
6
7 1
from plex.objects.library import metadata as plex_objects
8 1
from stash import ApswArchive
9 1
from trakt import objects as trakt_objects
10 1
from trakt_sync.cache.backends import StashBackend
11 1
from trakt_sync.cache.main import Cache
12 1
import elapsed
13 1
import logging
14 1
import os
15
16 1
IGNORED_DATA = [
17
    Cache.Data.get(Cache.Data.Liked),
18
    Cache.Data.get(Cache.Data.Personal)
19
]
20
21 1
log = logging.getLogger(__name__)
22
23
24 1
class SyncStateTrakt(object):
25 1
    def __init__(self, state):
26 1
        self.state = state
27 1
        self.task = state.task
28
29 1
        self.cache = None
30
31 1
        self.changes = None
32 1
        self.table = Table(self.task)
33
34 1
    def load(self):
35
        # Construct cache
36
        self.cache = self._build_cache()
37
38
        # Load table handler
39
        self.table.load()
40
41 1
    def _build_cache(self):
42
        def storage(name):
43
            return StashBackend(
44
                ApswArchive(DatabaseManager.cache('trakt'), name),
45
                'lru:///?capacity=500&compact_threshold=1500',
46
                'pickle:///?protocol=2'
47
            )
48
49
        cache = Cache(self.task.media, self.task.data, storage)
50
51
        # Bind to cache events
52
        cache.events.on([
53
            'refresh.sync.progress',
54
            'refresh.list.progress'
55
        ], self.on_refresh_progress)
56
57
        return cache
58
59 1
    def on_refresh_progress(self, source, current):
0 ignored issues
show
Unused Code introduced by
The argument current seems to be unused.
Loading history...
60
        # Step refresh progress for `source`
61
        self.task.progress.group(SyncStateTrakt, 'refresh:%s' % source).step()
62
63 1
    def __getitem__(self, key):
64
        collection = [
65
            self.task.account.trakt.username,
66
            Cache.Media.get(key[0]),
67
            Cache.Data.get(key[1])
68
        ]
69
70
        if len(key) > 2:
71
            # Include extra parameters (list id)
72
            collection.extend(key[2:])
73
74
        return self.cache[collection]
75
76 1
    def invalidate(self, *key):
77
        """Invalidate collection in trakt cache"""
78
        username = self.task.account.trakt.username
79
80
        # Invalidate collection
81
        self.cache.invalidate([username] + list(key))
82
83
        log.debug('Invalidated trakt cache %r for account: %r', key, username)
84
85 1
    @elapsed.clock
86
    def refresh(self):
87
        account = self.task.account
88
89
        if not account.trakt or not account.trakt.username:
90
            raise AccountAuthenticationError("Trakt account hasn't been authenticated")
91
92
        # Task checkpoint
93
        self.task.checkpoint()
94
95
        # Construct progress groups
96
        def setup_progress_group(source):
97
            # Retrieve steps from cache source
98
            steps = self.cache.source(source).steps()
99
100
            # Setup progress group with total steps
101
            self.task.progress.group(SyncStateTrakt, 'refresh:%s' % source).add(steps)
102
103
        setup_progress_group('list')
104
        setup_progress_group('sync')
105
106
        # Refresh cache for account, store changes
107
        self.changes = self.cache.refresh(account.trakt.username)
108
109
        # Resolve changes
110
        self.changes = list(self.changes)
111
112
        # Reset current table
113
        self.table.reset()
114
115 1
    @elapsed.clock
116
    def build_table(self):
117
        # Build table from cache
118
        self.table.build(self.cache)
119
120 1
    @elapsed.clock
121
    def flush(self):
122
        with elapsed.clock(SyncStateTrakt, 'flush:collections'):
123
            # Flush trakt collections to disk
124
            self.cache.collections.flush()
125
126
        with elapsed.clock(SyncStateTrakt, 'flush:stores'):
127
            # Flush trakt stores to disk
128
            for key, store in self.cache.stores.items():
129
                log.debug('[%-38s] Flushing collection...', '/'.join(key))
130
131
                store.flush()
132
133
        # Store backup of trakt data
134
        group = os.path.join('trakt', str(self.task.account.id))
135
136
        BackupManager.database.backup(group, DatabaseManager.cache('trakt'), self.task.id, {
137
            'account': {
138
                'id': self.task.account.id,
139
                'name': self.task.account.name,
140
141
                'trakt': {
142
                    'username': self.task.account.trakt.username
143
                }
144
            }
145
        })
146
147
148 1
class Table(object):
149 1
    def __init__(self, task):
150 1
        self.task = task
151
152 1
        self.movies = None
153 1
        self.shows = None
154
155 1
        self.movie_keys = None
156 1
        self.show_keys = None
157 1
        self.episode_keys = None
158
159 1
        self._data = None
160 1
        self._media = None
161
162 1
    def load(self):
163
        # Parse data/media enums into lists
164
        self._data = [
165
            Cache.Data.get(d)
166
            for d in Cache.Data.parse(self.task.data)
167
        ]
168
169
        self._media = [
170
            Cache.Media.get(m)
171
            for m in Cache.Media.parse(self.task.media)
172
        ]
173
174 1
    def reset(self):
175
        self.movies = None
176
        self.shows = None
177
178
        self.movie_keys = None
179
        self.show_keys = None
180
        self.episode_keys = None
181
182 1
    def build(self, cache):
183
        # Map item `keys` into tables
184
        self.movies = {}
185
        self.shows = {}
186
187
        self.movie_keys = set()
188
        self.show_keys = set()
189
        self.episode_keys = {}
190
191
        log.debug('Building tables...')
192
193
        log.debug(' - Data: %s', ', '.join([
194
            '/'.join(x) if type(x) is tuple else x
195
            for x in self._data
196
        ]))
197
198
        log.debug(' - Media: %s', ', '.join([
199
            '/'.join(x) if type(x) is tuple else x
200
            for x in self._media
201
        ]))
202
203
        # Construct progress group
204
        self.task.progress.group(Table, 'build').add(len(cache.collections))
205
206
        # Map each item in cache collections
207
        for key in cache.collections:
208
            # Increment one step
209
            self.task.progress.group(Table, 'build').step()
210
211
            # Parse `key`
212
            if len(key) == 3:
213
                # Sync
214
                username, media, data = key
215
            elif len(key) == 4:
216
                # Lists
217
                username, media, data = tuple(key[0:3])
218
            else:
219
                log.warn('Unknown key: %r', key)
220
                continue
221
222
            if username != self.task.account.trakt.username:
223
                # Collection isn't for the current account
224
                continue
225
226
            if media and media not in self._media:
227
                log.debug('[%-38s] Media %r has not been enabled', '/'.join(key), media)
228
                continue
229
230
            if data not in self._data:
231
                log.debug('[%-38s] Data %r has not been enabled', '/'.join(key), data)
232
                continue
233
234
            # Map store items
235
            if data not in IGNORED_DATA:
236
                self.map_items(key, cache[key], media)
237
238
        log.debug(
239
            'Built tables with %d keys (movies: %d, shows: %d, episodes: %d)',
240
            len(self.movies) + len(self.shows),
241
            len(self.movie_keys),
242
            len(self.show_keys),
243
            len(self.episode_keys)
244
        )
245
246 1
    def map_items(self, key, store, media=None):
247
        # Retrieve key map
248
        if media is not None:
249
            keys = self.keys(media)
250
            table = self.table(media)
251
252
            if keys is None or table is None:
253
                log.debug('[%-38s] Collection has been ignored (unknown/unsupported media)', '/'.join(key))
254
                return
255
        else:
256
            keys = None
257
            table = None
258
259
        # Map each item in store
260
        log.debug('[%-38s] Building table from collection...', '/'.join(key))
261
262
        for pk, item in store.iteritems():
263
            # Trim `pk` season/episode values
264
            if len(pk) > 2:
265
                pk = tuple(pk[:2])
266
267
            if pk[0] not in GUID_SERVICES:
268
                log.info('Ignoring item %r with an unknown primary agent: %r', item, pk)
269
                continue
270
271
            # Detect media type from `item`
272
            if media is not None:
273
                i_media = media
274
                i_keys = keys
275
                i_table = table
276
            else:
277
                i_media = self.media(item)
278
                i_keys = self.keys(i_media)
279
                i_table = self.table(i_media)
280
281
            # Store `pk` in `keys
282
            if i_keys is not None:
283
                i_keys.add(pk)
284
285
            # Map `item.keys` -> `pk`
286
            for key in item.keys:
287
                # Expand `key`
288
                if type(key) is not tuple or len(key) != 2:
289
                    continue
290
291
                service, id = key
0 ignored issues
show
Bug Best Practice introduced by
This seems to re-define the built-in id.

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

Loading history...
292
293
                # Check if agent is supported
294
                if service not in GUID_SERVICES:
295
                    continue
296
297
                # Cast service id to integer
298
                if service in ['tvdb', 'tmdb', 'tvrage']:
299
                    id = try_convert(id, int, id)
300
301
                # Store key in table
302
                key = (service, id)
303
304
                if key in i_table:
305
                    continue
306
307
                i_table[key] = pk
308
309
            # Map episodes in show
310
            if i_media == 'episodes':
311
                if type(item) is trakt_objects.Show:
312
                    if pk not in self.episode_keys:
313
                        self.episode_keys[pk] = set()
314
315
                    for identifier, _ in item.episodes():
316
                        self.episode_keys[pk].add(identifier)
317
                elif type(item) is trakt_objects.Episode:
318
                    # TODO
319
                    pass
320
                else:
321
                    log.debug('Unknown episode item: %r', item)
322
323
        # Task checkpoint
324
        self.task.checkpoint()
325
326 1
    def keys(self, media):
327
        if type(media) is not str:
328
            media = self.media(media)
329
330
        if media == 'movies':
331
            return self.movie_keys
332
333
        if media in ['shows', 'seasons', 'episodes']:
334
            return self.show_keys
335
336
        log.warn('Unknown media: %r', media)
337
        return None
338
339 1
    @staticmethod
340
    def media(item):
341 1
        if type(item) is not type:
342 1
            i_type = type(item)
343
        else:
344
            i_type = item
345
346 1
        if issubclass(i_type, (trakt_objects.Movie, plex_objects.Movie)):
347 1
            return 'movies'
348
349 1
        if issubclass(i_type, (trakt_objects.Show, plex_objects.Show)):
350
            return 'shows'
351
352 1
        if issubclass(i_type, (trakt_objects.Season, plex_objects.Season)):
353
            return 'seasons'
354
355 1
        if issubclass(i_type, (trakt_objects.Episode, plex_objects.Episode)):
356 1
            return 'episodes'
357
358
        log.warn('Unknown item type: %r', i_type)
359
        return None
360
361 1
    def table(self, media):
362 1
        if type(media) is not str:
363 1
            media = self.media(media)
364
365 1
        if media == 'movies':
366 1
            return self.movies
367
368 1
        if media in ['shows', 'seasons', 'episodes']:
369 1
            return self.shows
370
371
        log.warn('Unknown media: %r', media)
372
        return None
373
374 1
    def __call__(self, media):
375
        return self.table(media)
376