1from datetime import datetime, timedelta
2from . import tmdbapi
3
4
5class TMDBMovieScraper(object):
6    def __init__(self, url_settings, language, certification_country):
7        self.url_settings = url_settings
8        self.language = language
9        self.certification_country = certification_country
10        self._urls = None
11
12    @property
13    def urls(self):
14        if not self._urls:
15            self._urls = _load_base_urls(self.url_settings)
16        return self._urls
17
18    def search(self, title, year=None):
19        search_media_id = _parse_media_id(title)
20        if search_media_id:
21            if search_media_id['type'] == 'tmdb':
22                result = _get_movie(search_media_id['id'], self.language, True)
23                result = [result]
24            else:
25                response = tmdbapi.find_movie_by_external_id(search_media_id['id'], language=self.language)
26                theerror = response.get('error')
27                if theerror:
28                    return 'error: {}'.format(theerror)
29                result = response.get('movie_results')
30            if 'error' in result:
31                return result
32        else:
33            response = tmdbapi.search_movie(query=title, year=year, language=self.language)
34            theerror = response.get('error')
35            if theerror:
36                return 'error: {}'.format(theerror)
37            result = response['results']
38        urls = self.urls
39
40        def is_best(item):
41            return item['title'].lower() == title and (
42                not year or item.get('release_date', '').startswith(year))
43        if result and not is_best(result[0]):
44            best_first = next((item for item in result if is_best(item)), None)
45            if best_first:
46                result = [best_first] + [item for item in result if item is not best_first]
47
48        for item in result:
49            if item.get('poster_path'):
50                item['poster_path'] = urls['preview'] + item['poster_path']
51            if item.get('backdrop_path'):
52                item['backdrop_path'] = urls['preview'] + item['backdrop_path']
53        return result
54
55    def get_details(self, uniqueids):
56        media_id = uniqueids.get('tmdb') or uniqueids.get('imdb')
57        details = self._gather_details(media_id)
58        if not details:
59            return None
60        if details.get('error'):
61            return details
62        return self._assemble_details(**details)
63
64    def _gather_details(self, media_id):
65        movie = _get_movie(media_id, self.language)
66        if not movie or movie.get('error'):
67            return movie
68
69        # don't specify language to get English text for fallback
70        movie_fallback = _get_movie(media_id)
71
72        collection = _get_moviecollection(movie['belongs_to_collection'].get('id'), self.language) if \
73            movie['belongs_to_collection'] else None
74        collection_fallback = _get_moviecollection(movie['belongs_to_collection'].get('id')) if \
75            movie['belongs_to_collection'] else None
76
77        return {'movie': movie, 'movie_fallback': movie_fallback, 'collection': collection,
78            'collection_fallback': collection_fallback}
79
80    def _assemble_details(self, movie, movie_fallback, collection, collection_fallback):
81        info = {
82            'title': movie['title'],
83            'originaltitle': movie['original_title'],
84            'plot': movie.get('overview') or movie_fallback.get('overview'),
85            'tagline': movie.get('tagline') or movie_fallback.get('tagline'),
86            'studio': _get_names(movie['production_companies']),
87            'genre': _get_names(movie['genres']),
88            'country': _get_names(movie['production_countries']),
89            'credits': _get_cast_members(movie['casts'], 'crew', 'Writing', ['Screenplay', 'Writer', 'Author']),
90            'director': _get_cast_members(movie['casts'], 'crew', 'Directing', ['Director']),
91            'premiered': movie['release_date'],
92            'tag': _get_names(movie['keywords']['keywords'])
93        }
94
95        if 'countries' in movie['releases']:
96            certcountry = self.certification_country.upper()
97            for country in movie['releases']['countries']:
98                if country['iso_3166_1'] == certcountry and country['certification']:
99                    info['mpaa'] = country['certification']
100                    break
101
102        trailer = _parse_trailer(movie.get('trailers', {}), movie_fallback.get('trailers', {}))
103        if trailer:
104            info['trailer'] = trailer
105        if collection:
106            info['set'] = collection.get('name') or collection_fallback.get('name')
107            info['setoverview'] = collection.get('overview') or collection_fallback.get('overview')
108        if movie.get('runtime'):
109            info['duration'] = movie['runtime'] * 60
110
111        ratings = {'themoviedb': {'rating': float(movie['vote_average']), 'votes': int(movie['vote_count'])}}
112        uniqueids = {'tmdb': movie['id'], 'imdb': movie['imdb_id']}
113        cast = [{
114                'name': actor['name'],
115                'role': actor['character'],
116                'thumbnail': self.urls['original'] + actor['profile_path']
117                    if actor['profile_path'] else "",
118                'order': actor['order']
119            }
120            for actor in movie['casts'].get('cast', [])
121        ]
122        available_art = _parse_artwork(movie, collection, self.urls, self.language)
123
124        _info = {'set_tmdbid': movie['belongs_to_collection'].get('id')
125            if movie['belongs_to_collection'] else None}
126
127        return {'info': info, 'ratings': ratings, 'uniqueids': uniqueids, 'cast': cast,
128            'available_art': available_art, '_info': _info}
129
130def _parse_media_id(title):
131    if title.startswith('tt') and title[2:].isdigit():
132        return {'type': 'imdb', 'id':title} # IMDB ID works alone because it is clear
133    title = title.lower()
134    if title.startswith('tmdb/') and title[5:].isdigit(): # TMDB ID
135        return {'type': 'tmdb', 'id':title[5:]}
136    elif title.startswith('imdb/tt') and title[7:].isdigit(): # IMDB ID with prefix to match
137        return {'type': 'imdb', 'id':title[5:]}
138    return None
139
140def _get_movie(mid, language=None, search=False):
141    details = None if search else \
142        'trailers,images,releases,casts,keywords' if language is not None else \
143        'trailers'
144    response = tmdbapi.get_movie(mid, language=language, append_to_response=details)
145    theerror = response.get('error')
146    if theerror:
147        return 'error: {}'.format(theerror)
148    else:
149        return response
150
151def _get_moviecollection(collection_id, language=None):
152    if not collection_id:
153        return None
154    details = 'images'
155    response = tmdbapi.get_collection(collection_id, language=language, append_to_response=details)
156    theerror = response.get('error')
157    if theerror:
158        return 'error: {}'.format(theerror)
159    else:
160        return response
161
162def _parse_artwork(movie, collection, urlbases, language):
163    if language:
164        # Image languages don't have regional variants
165        language = language.split('-')[0]
166    posters = []
167    landscape = []
168    fanart = []
169    if 'images' in movie:
170        posters = _get_images_with_fallback(movie['images']['posters'], urlbases, language)
171        landscape = _get_images(movie['images']['backdrops'], urlbases, language)
172        fanart = _get_images(movie['images']['backdrops'], urlbases, None)
173
174    setposters = []
175    setlandscape = []
176    setfanart = []
177    if collection and 'images' in collection:
178        setposters = _get_images_with_fallback(collection['images']['posters'], urlbases, language)
179        setlandscape = _get_images(collection['images']['backdrops'], urlbases, language)
180        setfanart = _get_images(collection['images']['backdrops'], urlbases, None)
181
182    return {'poster': posters, 'landscape': landscape, 'fanart': fanart,
183        'set.poster': setposters, 'set.landscape': setlandscape, 'set.fanart': setfanart}
184
185def _get_images_with_fallback(imagelist, urlbases, language, language_fallback='en'):
186    images = _get_images(imagelist, urlbases, language)
187
188    # Add backup images
189    if language != language_fallback:
190        images.extend(_get_images(imagelist, urlbases, language_fallback))
191
192    # Add any images if nothing set so far
193    if not images:
194        images = _get_images(imagelist, urlbases)
195
196    return images
197
198def _get_images(imagelist, urlbases, language='_any'):
199    result = []
200    for img in imagelist:
201        if language != '_any' and img['iso_639_1'] != language:
202            continue
203        result.append({
204            'url': urlbases['original'] + img['file_path'],
205            'preview': urlbases['preview'] + img['file_path'],
206        })
207    return result
208
209def _get_date_numeric(datetime_):
210    return (datetime_ - datetime(1970, 1, 1)).total_seconds()
211
212def _load_base_urls(url_settings):
213    urls = {}
214    urls['original'] = url_settings.getSettingString('originalUrl')
215    urls['preview'] = url_settings.getSettingString('previewUrl')
216    last_updated = url_settings.getSettingString('lastUpdated')
217    if not urls['original'] or not urls['preview'] or not last_updated or \
218            float(last_updated) < _get_date_numeric(datetime.now() - timedelta(days=30)):
219        conf = tmdbapi.get_configuration()
220        if conf:
221            urls['original'] = conf['images']['secure_base_url'] + 'original'
222            urls['preview'] = conf['images']['secure_base_url'] + 'w780'
223            url_settings.setSetting('originalUrl', urls['original'])
224            url_settings.setSetting('previewUrl', urls['preview'])
225            url_settings.setSetting('lastUpdated', str(_get_date_numeric(datetime.now())))
226    return urls
227
228def _parse_trailer(trailers, fallback):
229    if trailers.get('youtube'):
230        return 'plugin://plugin.video.youtube/?action=play_video&videoid='+trailers['youtube'][0]['source']
231    if fallback.get('youtube'):
232        return 'plugin://plugin.video.youtube/?action=play_video&videoid='+fallback['youtube'][0]['source']
233    return None
234
235def _get_names(items):
236    return [item['name'] for item in items] if items else []
237
238def _get_cast_members(casts, casttype, department, jobs):
239    result = []
240    if casttype in casts:
241        for cast in casts[casttype]:
242            if cast['department'] == department and cast['job'] in jobs and cast['name'] not in result:
243                result.append(cast['name'])
244    return result
245