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

Mapper.id()   D

Complexity

Conditions 8

Size

Total Lines 32

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 72

Importance

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