Mapper.map_movie()   B
last analyzed

Complexity

Conditions 5

Size

Total Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 19.4658

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 13
ccs 1
cts 6
cp 0.1666
rs 8.5454
cc 5
crap 19.4658
1 1
from plugin.core.environment import Environment
2 1
from plugin.core.helpers.variable import try_convert
3 1
from plugin.modules.core.base import Module
4 1
from plugin.modules.mapper.handlers.hama import HamaMapper
5
6 1
from oem import OemClient, AbsoluteNumberRequiredError
7 1
from oem.media.movie import MovieMatch
8 1
from oem.media.show import EpisodeIdentifier, EpisodeMatch
9 1
from oem_client_provider_release import IncrementalReleaseProvider
10 1
from oem_storage_codernitydb.main import CodernityDbStorage
11 1
from plex_metadata import Guid
12 1
import logging
13 1
import os
14
15 1
log = logging.getLogger(__name__)
16
17
18 1
class Mapper(Module):
19 1
    __key__ = 'mapper'
20
21 1
    services = {
22
        'anidb': [
23
            # Prefer movies
24
            'tmdb:movie', 'imdb',
25
26
            # Fallback to shows
27
            'tvdb'
28
        ],
29
        'tvdb': [
30
            'anidb'
31
        ]
32
    }
33
34 1
    def __init__(self):
35 1
        self._client = None
36
37
        # Construct handlers
38 1
        self._handlers = {
39
            'hama': HamaMapper(self)
40
        }
41
42 1
    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 1
    def map_movie(self, guid, movie, progress=None, part=None, resolve_mappings=True):
0 ignored issues
show
Unused Code introduced by
The argument progress seems to be unused.
Loading history...
Unused Code introduced by
The argument part seems to be unused.
Loading history...
Unused Code introduced by
The argument movie seems to be unused.
Loading history...
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 1
    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 1
    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 1
    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 1
    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 1
    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)
0 ignored issues
show
Coding Style introduced by
This line is too long as per the coding-style (135/120).

This check looks for lines that are too long. You can specify the maximum line length.

Loading history...
207
                continue
208
            except Exception as ex:
0 ignored issues
show
Best Practice introduced by
Catching very general exceptions such as Exception is usually not recommended.

Generally, you would want to handle very specific errors in the exception handler. This ensure that you do not hide other types of errors which should be fixed.

So, unless you specifically plan to handle any error, consider adding a more specific exception.

Loading history...
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 1
    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 as ex:
0 ignored issues
show
Best Practice introduced by
Catching very general exceptions such as Exception is usually not recommended.

Generally, you would want to handle very specific errors in the exception handler. This ensure that you do not hide other types of errors which should be fixed.

So, unless you specifically plan to handle any error, consider adding a more specific exception.

Loading history...
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={
0 ignored issues
show
Coding Style Best Practice introduced by
Specify string format arguments as logging function parameters
Loading history...
232
            'event': {
233
                'module': __name__,
234
                'name': 'match.missing_item',
235
                'key': (source, key)
236
            }
237
        })
238
        return True, None
239
240 1
    def _build_request(self, match, item, episode=None):
0 ignored issues
show
Coding Style introduced by
This method could be written as a function/class method.

If a method does not access any attributes of the class, it could also be implemented as a function or static method. This can help improve readability. For example

class Foo:
    def some_method(self, x, y):
        return x + y;

could be written as

class Foo:
    @classmethod
    def some_method(cls, x, y):
        return x + y;
Loading history...
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 1
    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 as ex:
0 ignored issues
show
Best Practice introduced by
Catching very general exceptions such as Exception is usually not recommended.

Generally, you would want to handle very specific errors in the exception handler. This ensure that you do not hide other types of errors which should be fixed.

So, unless you specifically plan to handle any error, consider adding a more specific exception.

Loading history...
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 1
    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