def __init__(self, *args, **kwargs): # pylint: disable=too-many-locals,too-many-arguments super(TVmaze, self).__init__(*args, **kwargs) # List of language from http://thetvmaze.com/api/0629B785CE550C8D/languages.xml # Hard-coded here as it is realtively static, and saves another HTTP request, as # recommended on http://thetvmaze.com/wiki/index.php/API:languages.xml self.config['valid_languages'] = [ 'da', 'fi', 'nl', 'de', 'it', 'es', 'fr', 'pl', 'hu', 'el', 'tr', 'ru', 'he', 'ja', 'pt', 'zh', 'cs', 'sl', 'hr', 'ko', 'en', 'sv', 'no' ] # thetvdb.com should be based around numeric language codes, # but to link to a series like http://thetvdb.com/?tab=series&id=79349&lid=16 # requires the language ID, thus this mapping is required (mainly # for usage in tvdb_ui - internally tvdb_api will use the language abbreviations) self.config['langabbv_to_id'] = {'el': 20, 'en': 7, 'zh': 27, 'it': 15, 'cs': 28, 'es': 16, 'ru': 22, 'nl': 13, 'pt': 26, 'no': 9, 'tr': 21, 'pl': 18, 'fr': 17, 'hr': 31, 'de': 14, 'da': 10, 'fi': 11, 'hu': 19, 'ja': 25, 'he': 24, 'ko': 32, 'sv': 8, 'sl': 30} # Initiate the pytvmaze API self.tvmaze_api = TVMaze(session=self.config['session']) self.config['artwork_prefix'] = '{base_url}{image_size}{file_path}' # An api to indexer series/episode object mapping self.series_map = { 'id': 'id', 'maze_id': 'id', 'name': 'seriesname', 'summary': 'overview', 'premiered': 'firstaired', 'image': 'fanart', 'url': 'show_url', 'genres': 'genre', 'epnum': 'absolute_number', 'title': 'episodename', 'airdate': 'firstaired', 'screencap': 'filename', 'episode_number': 'episodenumber', 'season_number': 'seasonnumber', }
class TVmaze(BaseIndexer): """Create easy-to-use interface to name of season/episode name >>> indexer_api = tvmaze() >>> indexer_api['Scrubs'][1][24]['episodename'] u'My Last Day' """ def __init__(self, *args, **kwargs): # pylint: disable=too-many-locals,too-many-arguments super(TVmaze, self).__init__(*args, **kwargs) # List of language from http://thetvmaze.com/api/0629B785CE550C8D/languages.xml # Hard-coded here as it is realtively static, and saves another HTTP request, as # recommended on http://thetvmaze.com/wiki/index.php/API:languages.xml self.config['valid_languages'] = [ 'da', 'fi', 'nl', 'de', 'it', 'es', 'fr', 'pl', 'hu', 'el', 'tr', 'ru', 'he', 'ja', 'pt', 'zh', 'cs', 'sl', 'hr', 'ko', 'en', 'sv', 'no' ] # thetvdb.com should be based around numeric language codes, # but to link to a series like http://thetvdb.com/?tab=series&id=79349&lid=16 # requires the language ID, thus this mapping is required (mainly # for usage in tvdb_ui - internally tvdb_api will use the language abbreviations) self.config['langabbv_to_id'] = { 'el': 20, 'en': 7, 'zh': 27, 'it': 15, 'cs': 28, 'es': 16, 'ru': 22, 'nl': 13, 'pt': 26, 'no': 9, 'tr': 21, 'pl': 18, 'fr': 17, 'hr': 31, 'de': 14, 'da': 10, 'fi': 11, 'hu': 19, 'ja': 25, 'he': 24, 'ko': 32, 'sv': 8, 'sl': 30 } # Initiate the pytvmaze API self.tvmaze_api = TVMaze(session=self.config['session']) self.config['artwork_prefix'] = '{base_url}{image_size}{file_path}' # An api to indexer series/episode object mapping self.series_map = { 'id': 'id', 'maze_id': 'id', 'name': 'seriesname', 'summary': 'overview', 'premiered': 'firstaired', 'image': 'fanart', 'url': 'show_url', 'genres': 'genre', 'epnum': 'absolute_number', 'title': 'episodename', 'airdate': 'firstaired', 'screencap': 'filename', 'episode_number': 'episodenumber', 'season_number': 'seasonnumber', 'rating': 'contentrating', } def _map_results(self, tvmaze_response, key_mappings=None, list_separator='|'): """ Map results to a a key_mapping dict. :param tvmaze_response: tvmaze response obect, or a list of response objects. :type tvmaze_response: list(object) :param key_mappings: Dict of tvmaze attributes, that are mapped to normalized keys. :type key_mappings: dictionary :param list_separator: A list separator used to transform lists to a character separator string. :type list_separator: string. """ parsed_response = [] if not isinstance(tvmaze_response, list): tvmaze_response = [tvmaze_response] # TVmaze does not number their special episodes. It does map it to a season. And that's something, medusa # Doesn't support. So for now, we increment based on the order, we process the specials. And map it to # season 0. We start with episode 1. index_special_episodes = 1 for item in tvmaze_response: return_dict = {} try: for key, value in item.__dict__.iteritems(): if value is None or value == []: continue # These keys have more complex dictionaries, let's map these manually if key in [ 'schedule', 'network', 'image', 'externals', 'rating' ]: if key == 'schedule': return_dict['airs_time'] = value.get( 'time') or '0:00AM' return_dict['airs_dayofweek'] = value.get( 'days')[0] if value.get('days') else None if key == 'network': return_dict['network'] = value.name return_dict['code'] = value.code return_dict['timezone'] = value.timezone if key == 'image': if value.get('medium'): return_dict['image_medium'] = value.get( 'medium') return_dict['image_original'] = value.get( 'original') return_dict['poster'] = value.get('medium') if key == 'externals': return_dict['tvrage_id'] = value.get('tvrage') return_dict['tvdb_id'] = value.get('thetvdb') return_dict['imdb_id'] = value.get('imdb') if key == 'rating': return_dict['contentrating'] = value.get('average')\ if isinstance(value, dict) else value else: # Do some value sanitizing if isinstance(value, list): if all( isinstance(x, (string_types, integer_types)) for x in value): value = list_separator.join( text_type(v) for v in value) # Try to map the key if key in key_mappings: key = key_mappings[key] # Set value to key return_dict[key] = text_type(value) if isinstance( value, (float, integer_types)) else value # For episodes if hasattr(item, 'season_number') and getattr( item, 'episode_number') is None: return_dict['episodenumber'] = text_type( index_special_episodes) return_dict['seasonnumber'] = 0 index_special_episodes += 1 # If there is a web_channel available, let's use that in stead of the network field. network = getattr(item, 'web_channel', None) if network and getattr(network, 'name', None): return_dict['network'] = network.name except Exception as error: log.warning( 'Exception trying to parse attribute: {0}, with exception: {1!r}', key, error) parsed_response.append(return_dict) return parsed_response if len( parsed_response) != 1 else parsed_response[0] def _show_search(self, show, request_language='en'): """ Uses the TVMaze API to search for a show :param show: The show name that's searched for as a string :param request_language: Language in two letter code. TVMaze fallsback to en itself. :return: A list of Show objects. """ try: results = self.tvmaze_api.get_show_list(show) except ShowNotFound as error: raise IndexerShowNotFound( 'Show search failed in getting a result with reason: {0}'. format(error)) except BaseError as error: raise IndexerException( 'Show search failed in getting a result with error: {0!r}'. format(error)) if results: return results else: return None # Tvdb implementation def search(self, series): """This searches tvmaze.com for the series name :param series: the query for the series name :return: An ordered dict with the show searched for. In the format of OrderedDict{"series": [list of shows]} """ series = series.encode('utf-8') log.debug('Searching for show {0}', series) results = self._show_search(series, request_language=self.config['language']) if not results: return mapped_results = self._map_results(results, self.series_map, '|') return OrderedDict({'series': mapped_results})['series'] def _get_show_by_id(self, tvmaze_id, request_language='en'): # pylint: disable=unused-argument """ Retrieve tvmaze show information by tvmaze id, or if no tvmaze id provided by passed external id. :param tvmaze_id: The shows tvmaze id :return: An ordered dict with the show searched for. """ results = None if tvmaze_id: log.debug('Getting all show data for {0}', tvmaze_id) results = self.tvmaze_api.get_show(maze_id=tvmaze_id) if not results: return mapped_results = self._map_results(results, self.series_map) return OrderedDict({'series': mapped_results}) def _get_episodes(self, tvmaze_id, specials=False, aired_season=None): # pylint: disable=unused-argument """ Get all the episodes for a show by tvmaze id :param tvmaze_id: Series tvmaze id. :return: An ordered dict with the show searched for. In the format of OrderedDict{"episode": [list of episodes]} """ # Parse episode data log.debug('Getting all episodes of {0}', tvmaze_id) try: results = self.tvmaze_api.episode_list(tvmaze_id, specials=specials) except IDNotFound: log.debug('Episode search did not return any results.') return False except BaseError as e: raise IndexerException( 'Show episodes search failed in getting a result with error: {0!r}' .format(e)) episodes = self._map_results(results, self.series_map) if not episodes: return False if not isinstance(episodes, list): episodes = [episodes] for cur_ep in episodes: if self.config['dvdorder']: log.debug('Using DVD ordering.') use_dvd = cur_ep.get('dvd_season') is not None and cur_ep.get( 'dvd_episodenumber') is not None else: use_dvd = False if use_dvd: seasnum, epno = cur_ep.get('dvd_season'), cur_ep.get( 'dvd_episodenumber') else: seasnum, epno = cur_ep.get('seasonnumber'), cur_ep.get( 'episodenumber') if self.config['dvdorder']: log.warning( 'No DVD order for episode (season: {0}, episode: {1}). ' 'Falling back to non-DVD order. ' 'Please consider disabling DVD order for the show with TVmaze ID: {2}', seasnum, epno, tvmaze_id) if seasnum is None or epno is None: log.warning( 'An episode has incomplete season/episode number (season: {0!r}, episode: {1!r})', seasnum, epno) continue # Skip to next episode seas_no = int(seasnum) ep_no = int(epno) for k, v in cur_ep.items(): k = k.lower() if v is not None: if k == 'image_medium': self._set_item(tvmaze_id, seas_no, ep_no, 'filename', v) self._set_item(tvmaze_id, seas_no, ep_no, k, v) def _parse_images(self, tvmaze_id): """Parse Show and Season posters. images are retrieved using t['show name]['_banners'], for example: >>> indexer_api = TVMaze(images = True) >>> indexer_api['scrubs']['_banners'].keys() ['fanart', 'poster', 'series', 'season'] >>> t['scrubs']['_banners']['poster']['680x1000']['35308']['_bannerpath'] u'http://thetvmaze.com/banners/posters/76156-2.jpg' >>> Any key starting with an underscore has been processed (not the raw data from the XML) This interface will be improved in future versions. """ log.debug('Getting show banners for {0}', tvmaze_id) try: image_medium = self.shows[tvmaze_id]['image_medium'] except Exception: log.debug('Could not parse Poster for showid: {0}', tvmaze_id) return False # Set the poster (using the original uploaded poster for now, as the medium formated is 210x195 _images = { u'poster': { u'1014x1500': { u'1': { u'rating': 1, u'language': u'en', u'ratingcount': 1, u'bannerpath': image_medium.split('/')[-1], u'bannertype': u'poster', u'bannertype2': u'210x195', u'_bannerpath': image_medium, u'id': u'1035106' } } } } season_images = self._parse_season_images(tvmaze_id) if season_images: _images.update(season_images) self._save_images(tvmaze_id, _images) self._set_show_data(tvmaze_id, '_banners', _images) def _parse_season_images(self, tvmaze_id): """Parse Show and Season posters.""" seasons = {} if tvmaze_id: log.debug('Getting all show data for {0}', tvmaze_id) try: seasons = self.tvmaze_api.show_seasons(maze_id=tvmaze_id) except BaseError as e: log.warning( 'Getting show seasons for the season images failed. Cause: {0}', e) _images = {'season': {'original': {}}} # Get the season posters for season in seasons.keys(): if not getattr(seasons[season], 'image', None): continue if season not in _images['season']['original']: _images['season']['original'][season] = { seasons[season].id: {} } _images['season']['original'][season][seasons[season].id][ '_bannerpath'] = seasons[season].image['original'] _images['season']['original'][season][ seasons[season].id]['rating'] = 1 return _images def _parse_actors(self, tvmaze_id): """Parsers actors XML, from http://thetvmaze.com/api/[APIKEY]/series/[SERIES ID]/actors.xml Actors are retrieved using t['show name]['_actors'], for example: >>> indexer_api = TVMaze(actors = True) >>> actors = indexer_api['scrubs']['_actors'] >>> type(actors) <class 'tvmaze_api.Actors'> >>> type(actors[0]) <class 'tvmaze_api.Actor'> >>> actors[0] <Actor "Zach Braff"> >>> sorted(actors[0].keys()) ['id', 'image', 'name', 'role', 'sortorder'] >>> actors[0]['name'] u'Zach Braff' >>> actors[0]['image'] u'http://thetvmaze.com/banners/actors/43640.jpg' Any key starting with an underscore has been processed (not the raw data from the indexer) """ log.debug('Getting actors for {0}', tvmaze_id) try: actors = self.tvmaze_api.show_cast(tvmaze_id) except CastNotFound: log.debug('Actors result returned zero') return except BaseError as e: log.warning('Getting actors failed. Cause: {0}', e) return cur_actors = Actors() for order, cur_actor in enumerate(actors.people): save_actor = Actor() save_actor['id'] = cur_actor.id save_actor['image'] = cur_actor.image.get( 'original') if cur_actor.image else '' save_actor['name'] = cur_actor.name save_actor['role'] = cur_actor.character.name save_actor['sortorder'] = order cur_actors.append(save_actor) self._set_show_data(tvmaze_id, '_actors', cur_actors) def _get_show_data(self, tvmaze_id, language='en'): # pylint: disable=too-many-branches,too-many-statements,too-many-locals """Takes a series ID, gets the epInfo URL and parses the tvmaze json response into the shows dict in layout: shows[series_id][season_number][episode_number] """ if self.config['language'] is None: log.debug('Config language is none, using show language') if language is None: raise IndexerError( "config['language'] was None, this should not happen") get_show_in_language = language else: log.debug('Configured language {0} override show language of {1}', self.config['language'], language) get_show_in_language = self.config['language'] # Parse show information log.debug('Getting all series data for {0}', tvmaze_id) # Parse show information series_info = self._get_show_by_id( tvmaze_id, request_language=get_show_in_language) if not series_info: log.debug('Series result returned zero') raise IndexerError('Series result returned zero') # save all retrieved show information to Show object. for k, v in series_info['series'].items(): if v is not None: self._set_show_data(tvmaze_id, k, v) # Get external ids. # As the external id's are not part of the shows default response, we need to make an additional call for it. # Im checking for the external value. to make sure only externals with a value get in. self._set_show_data( tvmaze_id, 'externals', { external_id: text_type( getattr(self.shows[tvmaze_id], external_id, None)) for external_id in ['tvdb_id', 'imdb_id', 'tvrage_id'] if getattr(self.shows[tvmaze_id], external_id, None) }) # get episode data if self.config['episodes_enabled']: self._get_episodes(tvmaze_id, specials=False, aired_season=None) # Parse banners if self.config['banners_enabled']: self._parse_images(tvmaze_id) # Parse actors if self.config['actors_enabled']: self._parse_actors(tvmaze_id) return True def _get_all_updates(self, start_date=None, end_date=None): """Retrieve all updates (show,season,episode) from TVMaze.""" results = [] try: updates = self.tvmaze_api.show_updates() except (ShowIndexError, UpdateNotFound): return results except BaseError as e: log.warning('Getting show updates failed. Cause: {0}', e) return results if getattr(updates, 'updates', None): for show_id, update_ts in updates.updates.items(): if start_date < update_ts.seconds_since_epoch < (end_date or int(time())): results.append(int(show_id)) return results # Public methods, usable separate from the default api's interface api['show_id'] def get_last_updated_series(self, from_time, weeks=1, filter_show_list=None): """Retrieve a list with updated shows :param from_time: epoch timestamp, with the start date/time :param weeks: number of weeks to get updates for. :param filter_show_list: Optional list of show objects, to use for filtering the returned list. """ total_updates = [] updates = self._get_all_updates( from_time, from_time + (weeks * 604800)) # + seconds in a week if updates and filter_show_list: new_list = [] for show in filter_show_list: if show.indexerid in total_updates: new_list.append(show.indexerid) updates = new_list return updates def get_id_by_external(self, **kwargs): """Search tvmaze for a show, using an external id. Accepts as kwargs, so you'l need to add the externals as key/values. :param tvrage: The tvrage id. :param thetvdb: The tvdb id. :param imdb: An imdb id (inc. tt). :returns: A dict with externals, including the tvmaze id. """ mapping = { 'thetvdb': 'tvdb_id', 'tvrage': 'tvrage_id', 'imdb': 'imdb_id' } for external_id in mapping.values(): if kwargs.get(external_id): try: result = self.tvmaze_api.get_show( **{external_id: kwargs.get(external_id)}) if result: externals = { mapping[tvmaze_external_id]: external_value for tvmaze_external_id, external_value in result.externals.items() if external_value and mapping.get(tvmaze_external_id) } externals['tvmaze_id'] = result.maze_id return externals except ShowNotFound: log.debug( 'Could not get tvmaze externals using external key {0} and id {1}', external_id, kwargs.get(external_id)) continue except BaseError as e: log.warning('Could not get tvmaze externals. Cause: {0}', e) continue return {}
def __init__(self, *args, **kwargs): # pylint: disable=too-many-locals,too-many-arguments """Tvmaze api constructor.""" super(TVmaze, self).__init__(*args, **kwargs) # List of language from http://thetvmaze.com/api/0629B785CE550C8D/languages.xml # Hard-coded here as it is realtively static, and saves another HTTP request, as # recommended on http://thetvmaze.com/wiki/index.php/API:languages.xml self.config['valid_languages'] = [ 'da', 'fi', 'nl', 'de', 'it', 'es', 'fr', 'pl', 'hu', 'el', 'tr', 'ru', 'he', 'ja', 'pt', 'zh', 'cs', 'sl', 'hr', 'ko', 'en', 'sv', 'no' ] # for usage in the indexer UI - the api will use the language abbreviations) self.config['langabbv_to_id'] = { 'el': 20, 'en': 7, 'zh': 27, 'it': 15, 'cs': 28, 'es': 16, 'ru': 22, 'nl': 13, 'pt': 26, 'no': 9, 'tr': 21, 'pl': 18, 'fr': 17, 'hr': 31, 'de': 14, 'da': 10, 'fi': 11, 'hu': 19, 'ja': 25, 'he': 24, 'ko': 32, 'sv': 8, 'sl': 30 } # Initiate the pytvmaze API self.tvmaze_api = TVMaze(session=self.config['session']) self.config['artwork_prefix'] = '{base_url}{image_size}{file_path}' # An api to indexer series/episode object mapping self.series_map = [ ('id', 'id'), ('id', 'maze_id'), ('rating', 'rating.average'), ('status', 'status'), ('seriesname', 'name'), ('overview', 'summary'), ('firstaired', 'premiered'), ('poster_thumb', 'image'), ('show_url', 'url'), ('genre', 'genres'), ('absolute_number', 'epnum'), ('episodename', 'title'), ('firstaired', 'airdate'), ('filename', 'screencap'), ('episodenumber', 'episode_number'), ('seasonnumber', 'season_number'), ('airs_time', 'schedule.time'), ('airs_dayofweek', 'schedule.days[0]'), ('network', 'network.name'), ('code', 'network.code'), ('timezone', 'network.timezone'), ('tvrage_id', 'externals.tvrage'), ('tvdb_id', 'externals.thetvdb'), ('imdb_id', 'externals.imdb'), ]