Test Failed
Push — beta ( 7d3e07...a06fd2 )
by Dean
03:10
created

Mode   F

Complexity

Total Complexity 74

Size/Duplication

Total Lines 304
Duplicated Lines 0 %

Test Coverage

Coverage 15.29%

Importance

Changes 2
Bugs 0 Features 0
Metric Value
wmc 74
c 2
b 0
f 0
dl 0
loc 304
ccs 24
cts 157
cp 0.1529
rs 2.3809

20 Methods

Rating   Name   Duplication   Size   Complexity  
A current() 0 3 1
A checkpoint() 0 5 2
A modes() 0 3 1
A stop() 0 2 1
A __init__() 0 13 3
A construct() 0 2 1
A start() 0 2 1
F get_enabled_data() 0 56 14
B execute_handlers() 0 17 6
A configuration() 0 3 1
A handlers() 0 3 1
A trakt() 0 6 3
A run() 0 2 1
A is_data_enabled() 0 3 1
A get_data() 0 6 3
B execute_children() 0 18 5
A plex() 0 6 3
B sections() 0 27 6
F process_guid_episode() 0 68 12
D process_guid() 0 33 8

How to fix   Complexity   

Complex Class

Complex classes like Mode 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
2 1
from plugin.core.filters import Filters
3 1
from plugin.core.helpers.variable import try_convert
4 1
from plugin.modules.core.manager import ModuleManager
5 1
from plugin.sync import SyncMedia, SyncData, SyncMode
6
7 1
from oem.media.show import EpisodeMatch
8 1
from plex import Plex
9 1
from plex_metadata import Guid
10 1
import elapsed
11 1
import itertools
12 1
import logging
13
14 1
log = logging.getLogger(__name__)
15
16 1
TRAKT_DATA_MAP = {
17
    SyncMedia.Movies: [
18
        SyncData.Collection,
19
        SyncData.Playback,
20
        SyncData.Ratings,
21
        SyncData.Watched,
22
        # SyncData.Watchlist
23
    ],
24
    SyncMedia.Shows: [
25
        SyncData.Ratings
26
    ],
27
    SyncMedia.Seasons: [
28
        SyncData.Ratings
29
    ],
30
    SyncMedia.Episodes: [
31
        SyncData.Collection,
32
        SyncData.Playback,
33
        SyncData.Ratings,
34
        SyncData.Watched,
35
        # SyncData.Watchlist
36
    ]
37
}
38
39 1
DATA_PREFERENCE_MAP = {
40
    SyncData.Collection:    'sync.collection.mode',
41
    SyncData.Playback:      'sync.playback.mode',
42
    SyncData.Ratings:       'sync.ratings.mode',
43
    SyncData.Watched:       'sync.watched.mode',
44
45
    # Lists
46
    SyncData.Liked:         'sync.lists.liked.mode',
47
    SyncData.Personal:      'sync.lists.personal.mode',
48
    SyncData.Watchlist:     'sync.lists.watchlist.mode',
49
}
50
51
52 1
class Mode(object):
53 1
    data = None
54 1
    mode = None
55
56 1
    children = []
57
58 1
    def __init__(self, task):
59
        self.__task = task
60
61
        self.children = [c(task) for c in self.children]
62
63
        # Retrieve enabled data
64
        self.enabled_data = self.get_enabled_data()
65
66
        # Determine if mode should be enabled
67
        self.enabled = len(self.enabled_data) > 0
68
69
        if not self.enabled:
70
            log.debug('Mode %r disabled on: %r', self.mode, self)
71
72 1
    @property
73
    def current(self):
74
        return self.__task
75
76 1
    @property
77
    def configuration(self):
78
        return self.__task.configuration
79
80 1
    @property
81
    def handlers(self):
82
        return self.__task.handlers
83
84 1
    @property
85
    def modes(self):
86
        return self.__task.modes
87
88 1
    @property
89
    def plex(self):
90
        if not self.current or not self.current.state:
91
            return None
92
93
        return self.current.state.plex
94
95 1
    @property
96
    def trakt(self):
97
        if not self.current or not self.current.state:
98
            return None
99
100
        return self.current.state.trakt
101
102 1
    def construct(self):
103
        pass
104
105 1
    def start(self):
106
        pass
107
108 1
    def run(self):
109
        raise NotImplementedError
110
111 1
    def stop(self):
112
        pass
113
114 1
    def checkpoint(self):
115
        if self.current is None:
116
            return
117
118
        self.current.checkpoint()
119
120 1
    def execute_children(self, name, force=None):
121
        # Run method on children
122
        for c in self.children:
123
            if not force and not c.enabled:
124
                log.debug('Ignoring %s() call on child: %r', name, c)
125
                continue
126
127
            # Find method `name` in child
128
            log.info('Executing %s() on child: %r', name, c)
129
130
            func = getattr(c, name, None)
131
132
            if not func:
133
                log.warn('Unknown method: %r', name)
134
                continue
135
136
            # Run method on child
137
            func()
138
139 1
    @elapsed.clock
140
    def execute_handlers(self, media, data, *args, **kwargs):
141
        if type(media) is not list:
142
            media = [media]
143
144
        if type(data) is not list:
145
            data = [data]
146
147
        for m, d in itertools.product(media, data):
148
            if d not in self.handlers:
149
                log.debug('Unable to find handler for data: %r', d)
150
                continue
151
152
            try:
153
                self.handlers[d].run(m, self.mode, *args, **kwargs)
154
            except Exception, ex:
155
                log.warn('Exception raised in handlers[%r].run(%r, ...): %s', d, m, ex, exc_info=True)
156
157 1
    def get_enabled_data(self):
158
        config = self.configuration
159
160
        # Determine accepted modes
161
        modes = [SyncMode.Full]
162
163
        if self.mode == SyncMode.Full:
164
            modes.extend([
165
                SyncMode.FastPull,
166
                SyncMode.Pull,
167
                SyncMode.Push
168
            ])
169
        elif self.mode == SyncMode.FastPull:
170
            modes.extend([
171
                self.mode,
172
                SyncMode.Pull
173
            ])
174
        else:
175
            modes.append(self.mode)
176
177
        # Retrieve enabled data
178
        result = []
179
180
        if config['sync.watched.mode'] in modes:
181
            result.append(SyncData.Watched)
182
183
        if config['sync.ratings.mode'] in modes:
184
            result.append(SyncData.Ratings)
185
186
        if config['sync.playback.mode'] in modes:
187
            result.append(SyncData.Playback)
188
189
        if config['sync.collection.mode'] in modes:
190
            result.append(SyncData.Collection)
191
192
        # Lists
193
        if config['sync.lists.watchlist.mode'] in modes:
194
            result.append(SyncData.Watchlist)
195
196
        if config['sync.lists.liked.mode'] in modes:
197
            result.append(SyncData.Liked)
198
199
        if config['sync.lists.personal.mode'] in modes:
200
            result.append(SyncData.Personal)
201
202
        # Filter `result` to data provided by this mode
203
        if self.data is None:
204
            log.warn('No "data" property defined on %r', self)
205
            return result
206
207
        if self.data == SyncData.All:
208
            return result
209
210
        return [
211
            data for data in result
212
            if data in self.data
213
        ]
214
215 1
    def get_data(self, media):
216
        for data in TRAKT_DATA_MAP[media]:
217
            if not self.is_data_enabled(data):
218
                continue
219
220
            yield data
221
222 1
    @elapsed.clock
223
    def is_data_enabled(self, data):
224
        return data in self.enabled_data
225
226 1
    @elapsed.clock
227
    def process_guid(self, guid):
228
        if not guid:
229
            return False, False, guid
230
231
        if guid.service in GUID_SERVICES:
232
            return True, True, guid
233
234
        # Try map show to a supported service (via OEM)
235
        supported, (id_service, id_key) = ModuleManager['mapper'].id(
236
            guid.service, guid.id,
237
            resolve_mappings=False
238
        )
239
240
        if not supported:
241
            return False, False, guid
242
243
        if not id_service or not id_key:
244
            return True, False, guid
245
246
        # Validate identifier
247
        if type(id_key) is list:
248
            log.info('[%s/%s] - List keys are not supported', guid.service, guid.id)
249
            return True, False, guid
250
251
        if type(id_key) not in [int, str]:
252
            log.info('[%s/%s] - Unsupported key: %r', guid.service, guid.id, id_key)
253
            return True, False, guid
254
255
        log.debug('[%s/%s] - Mapped to: %s/%s', guid.service, guid.id, id_service, id_key)
256
257 1
        # Return mapped guid
258
        return True, True, Guid.construct(id_service, id_key)
259
260
    @elapsed.clock
261
    def process_guid_episode(self, guid, season_num, episode_num):
262
        episodes = [(season_num, episode_num)]
263
264
        if not guid:
265
            return False, False, guid, episodes
266
267
        if guid.service in GUID_SERVICES:
268
            return True, True, guid, episodes
269
270
        # Try map episode to a supported service (via OEM)
271
        supported, match = ModuleManager['mapper'].map_episode(
272
            guid, season_num, episode_num,
273
            resolve_mappings=False
274
        )
275
276
        if not supported:
277
            return False, False, guid, episodes
278
279
        if match and match.identifiers:
280
            if not isinstance(match, EpisodeMatch):
281
                log.info('[%s/%s] - Episode -> Movie mappings are not supported', guid.service, guid.id)
282
                return True, False, guid, episodes
283
284
            if match.absolute_num is not None:
285
                log.info('[%s/%s] - Episode mappings with absolute numbers are not supported yet', guid.service, guid.id)
286
                return True, False, guid, episodes
287
288
            # Retrieve mapped show identifier
289
            id_service = match.identifiers.keys()[0]
290
            id_key = match.identifiers[id_service]
291
292
            # Parse list keys (episode -> movie mappings)
293
            if type(id_key) is list:
294
                log.info('[%s/%s] - List keys are not supported', guid.service, guid.id)
295
                return True, False, guid, episodes
296 1
297
                # if episode_num - 1 >= len(id_key):
298
                #     log.info('[%s/%s] - Episode %r was not found in the key %r', guid.service, guid.id, episode_num, id_key)
299
                #     return True, False, guid, episodes
300
301
                # id_key = id_key[episode_num - 1]
302
303
            # Cast `id_key` numbers to integers
304
            id_key = try_convert(id_key, int, id_key)
305
306
            if type(id_key) not in [int, str]:
307
                log.info('[%s/%s] - Unsupported key: %r', guid.service, guid.id, id_key)
308
                return True, False, guid, episodes
309
310
            # Update `episodes` list
311
            if match.mappings:
312
                # Use match mappings
313
                episodes = []
314
315
                for mapping in match.mappings:
316
                    log.debug('[%s/%s] (S%02dE%02d) - Mapped to: %r', guid.service, guid.id, season_num, episode_num, mapping)
317
                    episodes.append((mapping.season, mapping.number))
318
            else:
319
                # Use match identifier
320
                log.debug('[%s/%s] (S%02dE%02d) - Mapped to: %r', guid.service, guid.id, season_num, episode_num, match)
321
                episodes = [(match.season_num, match.episode_num)]
322
323
            # Return mapped episode result
324
            return True, True, Guid.construct(id_service, id_key), episodes
325
326
        log.debug('Unable to find mapping for %r S%02dE%02d', guid, season_num, episode_num)
327
        return True, False, guid, episodes
328
329
    def sections(self, section_type=None):
330
        # Retrieve "section" for current task
331
        section_key = self.current.kwargs.get('section', None)
332
333
        # Fetch sections from server
334
        p_sections = Plex['library'].sections()
335
336
        if p_sections is None:
337
            return None
338
339
        # Filter sections, map to dictionary
340
        result = {}
341
342
        for section in p_sections.filter(section_type, section_key):
343
            # Apply section name filter
344
            if not Filters.is_valid_section_name(section.title):
345
                continue
346
347
            try:
348
                key = int(section.key)
349
            except Exception, ex:
350
                log.warn('Unable to cast section key %r to integer: %s', section.key, ex, exc_info=True)
351
                continue
352
353
            result[key] = section.uuid
354
355
        return [(key, ) for key in result.keys()], result
356