Test Failed
Push — beta ( c9094c...317f5f )
by Dean
03:05
created

Mapper   F

Complexity

Total Complexity 65

Size/Duplication

Total Lines 334
Duplicated Lines 0 %

Test Coverage

Coverage 0%

Importance

Changes 11
Bugs 0 Features 1
Metric Value
wmc 65
c 11
b 0
f 1
dl 0
loc 334
ccs 0
cts 102
cp 0
rs 3.3333

12 Methods

Rating   Name   Duplication   Size   Complexity  
B map_movie() 0 13 5
A request_movie() 0 17 3
A _map_handler() 0 20 4
D id() 0 32 8
B match() 0 22 5
F _build_request() 0 75 17
A start() 0 11 1
B request_episode() 0 26 4
A __init__() 0 6 1
B map_episode() 0 22 5
B _iter_services() 0 15 5
C map() 0 30 7

How to fix   Complexity   

Complex Class

Complex classes like Mapper 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
from plugin.core.environment import Environment
2
from plugin.core.helpers.variable import try_convert
3
from plugin.modules.core.base import Module
4
from plugin.modules.mapper.handlers.hama import HamaMapper
5
6
from oem import OemClient, AbsoluteNumberRequiredError
7
from oem.media.movie import MovieMatch
8
from oem.media.show import EpisodeIdentifier, EpisodeMatch
9
from oem_client_provider_release import IncrementalReleaseProvider
10
from oem_storage_codernitydb.main import CodernityDbStorage
11
from plex_metadata import Guid
12
import logging
13
import os
14
15
log = logging.getLogger(__name__)
16
17
18
class Mapper(Module):
19
    __key__ = 'mapper'
20
21
    services = {
22
        'anidb': [
23
            # Prefer movies
24
            'imdb', 'tmdb:movie',
25
26
            # Fallback to shows
27
            'tvdb'
28
        ],
29
        'tvdb': [
30
            'anidb'
31
        ]
32
    }
33
34
    def __init__(self):
35
        self._client = None
36
37
        # Construct handlers
38
        self._handlers = {
39
            'hama': HamaMapper(self)
40
        }
41
42
    def start(self):
43
        # Construct oem client
44
        self._client = OemClient(
45
            services=[
46
                'anidb'
47
            ],
48
            provider=IncrementalReleaseProvider(
49
                fmt='minimize+msgpack',
50
                storage=CodernityDbStorage(os.path.join(
51
                    Environment.path.plugin_caches,
52
                    'oem'
53
                ))
54
            )
55
        )
56
57
    #
58
    # Movie
59
    #
60
61
    def map_movie(self, guid, movie, progress=None, part=None, resolve_mappings=True):
62
        # Ensure guid has been parsed
63
        if type(guid) is str:
64
            guid = Guid.parse(guid, strict=True)
65
66
        # Ensure parsed guid is valid
67
        if not guid or not isinstance(guid, Guid) or not guid.valid:
68
            return False, None
69
70
        # Try match movie against database
71
        return self.map(
72
            guid.service, guid.id,
73
            resolve_mappings=resolve_mappings
74
        )
75
76
    def request_movie(self, guid, movie, progress=None, part=None):
77
        # Try match movie against database
78
        supported, match = self.map_movie(
79
            guid, movie,
80
81
            progress=progress,
82
            part=part
83
        )
84
85
        if not match:
86
            return supported, None
87
88
        if supported:
89
            log.debug('[%s/%s] - Mapped to: %r', guid.service, guid.id, match)
90
91
        # Build request for Trakt.tv
92
        return supported, self._build_request(match, movie)
93
94
    #
95
    # Shows
96
    #
97
98
    def map_episode(self, guid, season_num, episode_num, progress=None, part=None, resolve_mappings=True):
99
        # Ensure guid has been parsed
100
        if type(guid) is str:
101
            guid = Guid.parse(guid, strict=True)
102
103
        # Ensure parsed guid is valid
104
        if not guid or not isinstance(guid, Guid) or not guid.valid:
105
            return False, None
106
107
        # Build episode identifier
108
        identifier = EpisodeIdentifier(
109
            season_num=season_num,
110
            episode_num=episode_num,
111
112
            progress=progress,
113
            part=part
114
        )
115
116
        # Try match episode against database
117
        return self.map(
118
            guid.service, guid.id, identifier,
119
            resolve_mappings=resolve_mappings
120
        )
121
122
    def request_episode(self, guid, episode, progress=None, part=None):
123
        season_num = episode.season.index
124
        episode_num = episode.index
125
126
        # Process guid episode identifier overrides
127
        if guid.season is not None:
128
            season_num = guid.season
129
130
        # Try match episode against database
131
        supported, match = self.map_episode(
132
            guid,
133
            season_num,
134
            episode_num,
135
136
            progress=progress,
137
            part=part
138
        )
139
140
        if not match:
141
            return supported, None
142
143
        if supported:
144
            log.debug('[%s/%s] - Mapped to: %r', guid.service, guid.id, match)
145
146
        # Build request for Trakt.tv
147
        return supported, self._build_request(match, episode.show, episode)
148
149
    #
150
    # Helper methods
151
    #
152
153
    def id(self, source, key, identifier=None, resolve_mappings=True):
154
        # Retrieve mapping from database
155
        supported, match = self.map(
156
            source, key,
157
            identifier=identifier,
158
            resolve_mappings=resolve_mappings
159
        )
160
161
        if not supported:
162
            return False, (None, None)
163
164
        if not match or not match.valid:
165
            return True, (None, None)
166
167
        # Find valid identifier
168
        for id_service, id_key in match.identifiers.items():
169
            if id_service == source:
170
                continue
171
172
            # Strip media from identifier key
173
            id_service_parts = id_service.split(':', 1)
174
175
            if len(id_service_parts) == 2:
176
                id_service, _ = tuple(id_service_parts)
177
178
            if id_service in ['tvdb', 'tmdb', 'tvrage']:
179
                id_key = try_convert(id_key, int, id_key)
180
181
            return True, (id_service, id_key)
182
183
        log.info('[%s/%s] - Unable to find valid identifier in %r', source, key, match.identiifers)
184
        return True, (None, None)
185
186
    def map(self, source, key, identifier=None, resolve_mappings=True, use_handlers=True):
187
        if source not in self.services:
188
            if use_handlers:
189
                # Try find handler to map the identifier
190
                return self._map_handler(
191
                    source, key,
192
                    identifier=identifier,
193
                    resolve_mappings=resolve_mappings
194
                )
195
196
            return False, None
197
198
        # Iterate through available services until we find a match
199
        for target, service in self._iter_services(source):
200
            try:
201
                match = service.map(
202
                    key, identifier,
203
                    resolve_mappings=resolve_mappings
204
                )
205
            except AbsoluteNumberRequiredError:
206
                log.info('Unable to retrieve mapping for %r (%s -> %s) - Absolute mappings are not supported yet', key, source, target)
207
                continue
208
            except Exception, ex:
209
                log.warn('Unable to retrieve mapping for %r (%s -> %s) - %s', key, source, target, ex, exc_info=True)
210
                continue
211
212
            if match:
213
                return True, match
214
215
        return True, None
216
217
    def match(self, source, key):
218
        if source not in self.services:
219
            return False, None
220
221
        for target, service in self._iter_services(source):
222
            try:
223
                result = service.get(key)
224
            except Exception, ex:
225
                log.warn('Unable to retrieve item for %r (%s -> %s) - %s', key, source, target, ex, exc_info=True)
226
                continue
227
228
            if result:
229
                return True, result
230
231
        log.warn('Unable to find item for %s: %r' % (source, key), extra={
232
            'event': {
233
                'module': __name__,
234
                'name': 'match.missing_item',
235
                'key': (source, key)
236
            }
237
        })
238
        return True, None
239
240
    def _build_request(self, match, item, episode=None):
241
        if not match:
242
            log.warn('Invalid value provided for "match" parameter')
243
            return None
244
245
        if not item:
246
            log.warn('Invalid value provided for "item" parameter')
247
            return None
248
249
        # Retrieve identifier
250
        id_service = match.identifiers.keys()[0]
251
        id_key = try_convert(match.identifiers[id_service], int, match.identifiers[id_service])
252
253
        if type(id_key) not in [int, str]:
254
            log.info('Unsupported key: %r', id_key)
255
            return None
256
257
        # Determine media type
258
        if isinstance(match, MovieMatch):
259
            media = 'movie'
260
        elif isinstance(match, EpisodeMatch):
261
            media = 'show'
262
        else:
263
            log.warn('Unknown match: %r', match)
264
            return None
265
266
        # Strip media from identifier key
267
        id_service_parts = id_service.split(':', 1)
268
269
        if len(id_service_parts) == 2:
270
            id_service, id_media = tuple(id_service_parts)
271
        else:
272
            id_media = None
273
274
        if id_media and id_media != media:
275
            log.warn('Identifier mismatch, [%s: %r] doesn\'t match %r', id_service, id_key, media)
276
            return None
277
278
        # Build request
279
        request = {
280
            media: {
281
                'title': item.title,
282
283
                'ids': {
284
                    id_service: id_key
285
                }
286
            }
287
        }
288
289
        if item.year:
290
            request[media]['year'] = item.year
291
        elif episode and episode.year:
292
            request[media]['year'] = episode.year
293
        else:
294
            log.warn('Missing "year" parameter on %r', item)
295
296
        # Add episode parameters
297
        if isinstance(match, EpisodeMatch):
298
            if match.absolute_num is not None:
299
                log.info('Absolute mappings are not supported')
300
                return None
301
302
            if match.season_num is None or match.episode_num is None:
303
                log.warn('Missing season or episode number in %r', match)
304
                return None
305
306
            request['episode'] = {
307
                'season': match.season_num,
308
                'number': match.episode_num
309
            }
310
311
            if episode:
312
                request['episode']['title'] = episode.title
313
314
        return request
315
316
    def _iter_services(self, source):
317
        if source not in self.services:
318
            return
319
320
        for target in self.services[source]:
321
            try:
322
                service = self._client[source].to(target)
323
            except KeyError:
324
                log.warn('Unable to find service: %s -> %s', source, target)
325
                continue
326
            except Exception, ex:
327
                log.warn('Unable to retrieve service: %s -> %s - %s', source, target, ex, exc_info=True)
328
                continue
329
330
            yield target, service
331
332
    def _map_handler(self, source, key, identifier=None, resolve_mappings=True):
333
        if not source:
334
            return False, None
335
336
        parts = source.split('/', 1)
337
338
        if len(parts) != 2:
339
            return False, None
340
341
        # Try find a matching handler
342
        handler, source = tuple(parts)
343
344
        if handler not in self._handlers:
345
            return False, None
346
347
        # Map identifier with handler
348
        return self._handlers[handler].map(
349
            source, key,
350
            identifier=identifier,
351
            resolve_mappings=resolve_mappings
352
        )
353