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