def __init__(self, api_key, **kwargs): self.config = dict() #cache old searches to avoid hitting the server self.search_buffer = dict() #Store the path to where we are self.path = os.path.abspath(os.path.dirname(__file__)) if 'force_lang' in kwargs: logger.warning( "'force_lang' keyword argument is deprecated as of version 0.4" ) #extract all argument and store for later use self.config['api_key'] = api_key self.config['cache_dir'] = kwargs.get( "cache_dir", os.path.join(tempfile.gettempdir(), name)) self.config['actors'] = kwargs.get('actors', False) self.config['banners'] = kwargs.get('banners', False) self.config['ignore_case'] = kwargs.get('ignore_case', False) #Create the loader object to use self.loader = Loader(self.config['cache_dir']) #Create the list of available mirrors tree = generate_tree( self.loader.load(__mirrors__.format(**self.config))) self.mirrors = MirrorList(tree)
class TestLoader(basetest.pytvdbapiTest): """tests the loader class. At the moment, this also co-tests the httplib2 and could fail if the network connection is not working or the remote server is down. This is certainly not an ideal situation and I have to research how to change it so that httplib2 could load from disk, or if the tests have to start a local server to use to perform the tests. """ def setUp(self): super(TestLoader, self).setUp() self.tmp = tempfile.mkdtemp() self.loader = Loader(self.tmp) self.context = {"api_key": "B43FF87DE395DF56"} def tearDown(self): super(TestLoader, self).tearDown() shutil.rmtree(self.tmp) def test_load(self): """The Loader should successfully load the provided url""" mirror_file = resource_filename(__name__, 'data/mirrors.xml') data = utils.file_loader(mirror_file) url = ("http://www.thetvdb.com/api/%(api_key)s/mirrors.xml" % self.context) result = self.loader.load(url) #Fix any new line issues to assure it does not affect the test data = data.replace('\r\n', '\n') result = result.replace('\r\n', '\n') self.assertEqual(data, result) def test_failed_conection(self): """Loader should raise ConnectionError if it is not able to connect to the provided url """ self.assertRaises(error.ConnectionError, self.loader.load, "http://laba.laba") def test_no_cache(self): """It should be possible to disable the use of cache""" url = ("http://www.thetvdb.com/api/%(api_key)s/mirrors.xml" % self.context) self.loader.load(url, cache=False)
def __init__(self, api_key, **kwargs): self.config = dict() #cache old searches to avoid hitting the server self.search_buffer = dict() #Store the path to where we are self.path = os.path.abspath(os.path.dirname(__file__)) if 'force_lang' in kwargs: logger.warning("'force_lang' keyword argument is deprecated as of version 0.4") #extract all argument and store for later use self.config['api_key'] = api_key self.config['cache_dir'] = kwargs.get("cache_dir", os.path.join(tempfile.gettempdir(), name)) self.config['actors'] = kwargs.get('actors', False) self.config['banners'] = kwargs.get('banners', False) self.config['ignore_case'] = kwargs.get('ignore_case', False) #Create the loader object to use self.loader = Loader(self.config['cache_dir']) #Create the list of available mirrors tree = generate_tree(self.loader.load(__mirrors__.format(**self.config))) self.mirrors = MirrorList(tree)
def __init__(self, api_key, **kwargs): self.config = dict() #cache old searches to avoid hitting the server self.search_buffer = dict() #Store the path to where we are self.path = os.path.abspath(os.path.dirname(__file__)) #extract all argument and store for later use self.config['force_lang'] = kwargs.get("force_lang", False) self.config['api_key'] = api_key self.config['cache_dir'] = kwargs.get("cache_dir", os.path.join(tempfile.gettempdir(), name)) self.config['actors'] = kwargs.get('actors', False) self.config['banners'] = kwargs.get('banners', False) self.config['ignore_case'] = kwargs.get('ignore_case', False) #Create the loader object to use self.loader = Loader(self.config['cache_dir']) language_file = resource_filename(__name__, 'data/languages.xml') #If requested, update the local language file from the server if self.config['force_lang']: logger.debug("updating Language file from server") with open(language_file, "wt", encoding='utf-8') as languages: language_data = self.loader.load(__languages__.format(**self.config)) languages.write(language_data) #Setup the list of supported languages self.languages = LanguageList(generate_tree(language_file)) #Create the list of available mirrors tree = generate_tree(self.loader.load(__mirrors__.format(**self.config))) self.mirrors = MirrorList(tree)
class TestLoader(basetest.pytvdbapiTest): """tests the loader class. At the moment, this also co-tests the httplib2 and could fail if the network connection is not working or the remote server is down. This is certainly not an ideal situation and I have to research how to change it so that httplib2 could load from disk, or if the tests have to start a local server to use to perform the tests. """ def setUp(self): super(TestLoader, self).setUp() self.tmp = tempfile.mkdtemp() self.loader = Loader(self.tmp) self.context = {"api_key": "B43FF87DE395DF56"} def tearDown(self): super(TestLoader, self).tearDown() shutil.rmtree(self.tmp) def test_load(self): """The Loader should successfully load the provided url""" url = ("http://www.thetvdb.com/api/%(api_key)s/mirrors.xml" % self.context) # test that we can load without exceptions self.loader.load(url) def test_failed_connection(self): """Loader should raise ConnectionError if it is not able to connect to the provided url """ self.assertRaises(error.ConnectionError, self.loader.load, "http://laba.laba") def test_no_cache(self): """It should be possible to disable the use of cache""" url = ("http://www.thetvdb.com/api/%(api_key)s/mirrors.xml" % self.context) self.loader.load(url, cache=False)
def __init__(self, api_key, **kwargs): self.config = dict() # cache old searches to avoid hitting the server self.search_buffer = dict() # extract all argument and store for later use self.config['api_key'] = api_key self.config['cache_dir'] = kwargs.get( "cache_dir", make_unicode(os.path.join(tempfile.gettempdir(), __NAME__))) self.config['actors'] = kwargs.get('actors', False) self.config['banners'] = kwargs.get('banners', False) self.config['ignore_case'] = kwargs.get('ignore_case', False) # Create the loader object to use self.loader = Loader(self.config['cache_dir'], timeout=kwargs.get('timeout', None)) # Create the list of available mirrors tree = generate_tree(self.loader.load(mirrors.format(**self.config))) self.mirrors = MirrorList(tree)
def __init__(self, api_key, **kwargs): self.config = dict() # cache old searches to avoid hitting the server self.search_buffer = dict() # extract all argument and store for later use self.config['api_key'] = api_key self.config['cache_dir'] = kwargs.get("cache_dir", make_unicode(os.path.join(tempfile.gettempdir(), __NAME__))) self.config['actors'] = kwargs.get('actors', False) self.config['banners'] = kwargs.get('banners', False) self.config['ignore_case'] = kwargs.get('ignore_case', False) # Create the loader object to use self.loader = Loader(self.config['cache_dir'], timeout=kwargs.get('timeout', None)) # Create the list of available mirrors tree = generate_tree(self.loader.load(mirrors.format(**self.config))) self.mirrors = MirrorList(tree)
class TVDB(object): """ :param api_key: The API key to use to communicate with the server :param kwargs: This is the main entry point for the API. The functionality of the API is controlled by configuring the keyword arguments. The supported keyword arguments are: * **cache_dir** (default=/<system tmp dir>/pytvdbapi/). Specifies the directory to use for caching the server requests. .. versionadded:: 0.3 * **actors** (default=False) The extended actor information is stored in a separate XML file and would require an additional request to the server to obtain. To limit the resource usage, the actor information will only be loaded when explicitly requested. .. note:: The :class:`Show()` object always contain a list of actor names. * **banners** (default=False) The extended banner information is stored in a separate XML file and would require an additional request to the server to obtain. To limit the resource usage, the banner information will only be loaded when explicitly requested. .. versionadded:: 0.4 * **ignore_case** (default=False) If set to True, all attributes on the :class:`Show` and :class:`Episode` instances will be accessible in a case insensitive manner. If set to False, the default, all attributes will be case sensitive and retain the same casing as provided by `thetvdb.com <http://thetvdb.com>`_. * **timeout** (default=None) When set to a number, will cause the http request to timeout after that number of seconds. """ @unicode_arguments def __init__(self, api_key, **kwargs): self.config = dict() # cache old searches to avoid hitting the server self.search_buffer = dict() # extract all argument and store for later use self.config['api_key'] = api_key self.config['cache_dir'] = kwargs.get("cache_dir", make_unicode(os.path.join(tempfile.gettempdir(), __NAME__))) self.config['actors'] = kwargs.get('actors', False) self.config['banners'] = kwargs.get('banners', False) self.config['ignore_case'] = kwargs.get('ignore_case', False) # Create the loader object to use self.loader = Loader(self.config['cache_dir'], timeout=kwargs.get('timeout', None)) # Create the list of available mirrors tree = generate_tree(self.loader.load(mirrors.format(**self.config))) self.mirrors = MirrorList(tree) @unicode_arguments def search(self, show, language, cache=True): """ :param show: The show name to search for :param language: The language abbreviation to search for. E.g. "en" :param cache: If False, the local cache will not be used and the resources will be reloaded from server. :return: A :class:`Search()` instance :raise: :exc:`pytvdbapi.error.TVDBValueError` Searches the server for a show with the provided show name in the provided language. The language should be one of the supported language abbreviations or it could be set to *all* to search all languages. It will raise :class:`pytvdbapi.error.TVDBValueError` if an invalid language is provided. Searches are always cached within a session to make subsequent searches with the same parameters fast. If *cache* is set to True searches will also be cached across sessions, this is recommended to increase speed and to reduce the workload of the servers. Example:: >>> from pytvdbapi import api >>> db = api.TVDB("B43FF87DE395DF56") >>> result = db.search("House", "en") >>> print(result[0]) <Show - House> >>> for show in result: ... print(show) # doctest: +ELLIPSIS <Show - House> ... """ logger.debug(u"Searching for {0} using language {1}".format(show, language)) if language != u'all' and language not in __LANGUAGES__: raise error.TVDBValueError(u"{0} is not a valid language".format(language)) if (show, language) not in self.search_buffer or not cache: context = {'series': quote(make_bytes(show)), "language": language} data = generate_tree(self.loader.load(search.format(**context), cache)) shows = [Show(d, self, language, self.config) for d in parse_xml(data, "Series")] self.search_buffer[(show, language)] = shows return Search(self.search_buffer[(show, language)], show, language) @unicode_arguments def get_series(self, series_id, language, id_type='tvdb', cache=True): """ .. versionadded:: 0.4 .. versionchanged:: 0.5 Added *id_type* parameter :param series_id: The Show Id to fetch :param language: The language abbreviation to search for. E.g. "en" :param id_type: Information about what kind of id is provided. Should be one of *('tvdb', 'imdb', 'zap2it')* :param cache: If False, the local cache will not be used and the resources will be reloaded from server. :return: A :class:`Show()` instance :raise: :exc:`pytvdbapi.error.TVDBValueError`, :exc:`pytvdbapi.error.TVDBIdError` Provided a valid Show ID, the data for the show is fetched and a corresponding :class:`Show()` object is returned. Example:: >>> from pytvdbapi import api >>> db = api.TVDB("B43FF87DE395DF56") >>> show = db.get_series( 79349, "en" ) # Load Dexter >>> print(show.SeriesName) Dexter """ if id_type not in ('tvdb', 'imdb', 'zap2it'): raise error.TVDBValueError("Invalid id type") elif language != 'all' and language not in __LANGUAGES__: raise error.TVDBValueError(u"{0} is not a valid language".format(language)) # Map id type to url template __url__ = {'tvdb': series, 'imdb': imdbid, 'zap2it': zap2itid} try: series_id = text_type(series_id) except ValueError: raise error.TVDBValueError( "Invalid id type, expected {0} or {1}, got {2}".format(text_type, int_types, type(series_id))) if id_type == 'imdb': series_id = series_id[2:] if series_id.startswith('tt') else series_id elif id_type == 'zap2it': series_id = series_id if series_id.startswith('EP') else u'EP' + series_id.rjust(8, '0') logger.debug( u"Getting series with id {0}({2}) with language {1}".format(series_id, language, id_type)) context = {'seriesid': series_id, "language": language, 'mirror': self.mirrors.get_mirror(TypeMask.XML).url, 'api_key': self.config['api_key'], 'imdbid': series_id, 'zap2itid': series_id} url = __url__[id_type].format(**context) logger.debug(u'Getting series from {0}'.format(url)) try: data = self.loader.load(url, cache) except error.TVDBNotFoundError: raise error.TVDBIdError(u"Series id {0} not found".format(series_id)) data = generate_tree(data) series_data = parse_xml(data, "Series") if len(series) == 0: raise error.BadData("Bad data received") else: return Show(series_data[0], self, language, self.config, data) @unicode_arguments @deprecate_episode_id def get_episode(self, language, method="id", cache=True, **kwargs): """ .. versionadded:: 0.4 .. versionchanged:: 0.5 Added the possibility to get an episode using *default*, *dvd*, and *absolute* sort order :param episode_id: *Deprecated in 0.5* Use the *episodeid* keyword argument with the *id* method instead :param language: The language abbreviation to search for. E.g. "en" :param cache: If False, the local cache will not be used and the resources will be reloaded from server. :param method: (default=id) Specify what method should be used to get the episode. Depending on what method is specified, different parameters must be passed as keyword arguments. Should be one of (id, default, dvd, absolute). :param kwargs: *episodeid*, *seriesid*, *seasonnumber*, *episodenumber* and *absolutenumber*. See the examples for information on how to use them. :return: An :class:`Episode()` instance :raise: :exc:`pytvdbapi.error.TVDBValueError`, :exc:`pytvdbapi.error.BadData` Retrieves a single episode. Depending on what method is specified different criteria can be used to retrieve the episode. Examples: Load an episode using the episode id >>> from pytvdbapi import api >>> db = api.TVDB("B43FF87DE395DF56") >>> ep = db.get_episode("en", episodeid=308834) # id is the default method >>> print(ep.EpisodeName) Crocodile Load an episode using dvd and default sort order >>> ep = db.get_episode("en", "dvd", seasonnumber=2, episodenumber=5, seriesid=79349) >>> print(ep.EpisodeName) The Dark Defender >>> ep = db.get_episode("en", "default", seasonnumber=2, episodenumber=6, seriesid=79349) >>> print(ep.EpisodeName) Dex, Lies, and Videotape Load an episode using the absolute number >>> ep = db.get_episode("en", "absolute", absolutenumber=19, seriesid=79349) >>> print(ep.EpisodeName) That Night, A Forest Grew Under some circumstances the backend server fails to return a proper **404** file not found error response when the requested episode can not be found, but instead returns a valid HTML file with the content *404 file not found*. For this reason it is required to check for both :exc:`pytvdbapi.error.TVDBValueError` and :exc:`pytvdbapi.error.BadData` to detect an issue downloading the episode. >>> from pytvdbapi.error import BadData >>> from pytvdbapi.error import TVDBNotFoundError >>> try: ... ep = db.get_episode("en", episodeid=308834) ... except TVDBNotFoundError: ... # this is the standard 404 error code returned from the server ... pass ... except BadData: ... # This is when the server returns a 200 code but with a HTML page saying 404 Nothing found ... pass .. Note:: When the :class:`Episode()` is loaded using :func:`get_episode()` the *season* attribute used to link the episode with a season will be None. """ methods = {"default": default_order, "dvd": dvd_order, "absolute": absolute_order, "id": episode} if language != 'all' and language not in __LANGUAGES__: raise error.TVDBValueError(u"{0} is not a valid language".format(language)) context = {"language": language, 'mirror': self.mirrors.get_mirror(TypeMask.XML).url, 'api_key': self.config['api_key']} kwargs.update(context) try: url = methods[method] except KeyError: raise error.TVDBValueError(u"{0} is not a valid get method".format(method)) try: url = url.format(**kwargs) except KeyError: raise error.TVDBValueError("Missing arguments for method {0}".format(method)) logger.debug(u'Getting episode from {0}'.format(url)) data = self.loader.load(url, cache) data = generate_tree(data) episodes = parse_xml(data, "Episode") if len(episodes) == 0: raise error.BadData("Bad data received") else: return Episode(episodes[0], None, self.config) @unicode_arguments def get_episode_by_air_date(self, series_id, language, air_date, cache=True): """ .. versionadded:: 0.5 :param series_id: The TVDB series id of the episode :param language: The language to search for. Should be a two letter abbreviation e.g. *en*. :param air_date: The air date to search for. Should be of type :class:`datetime.date` :type air_date: datetime.date :param cache: If False, the local cache will not be used and the resources will be reloaded from server. :return: If found, an :class:`Episode` instance :raise: :exc:`pytvdbapi.error.TVDBValueError` .. Note:: When the :class:`Episode()` is loaded using :func:`get_episode_by_air_date` the *season* attribute used to link the episode with a season will be None. """ if type(air_date) not in (datetime.date,): raise error.TVDBValueError("air_date should be of type datetime.date") elif language != 'all' and language not in __LANGUAGES__: raise error.TVDBValueError(u"{0} is not a valid language".format(language)) context = {'seriesid': series_id, 'airdate': air_date, "language": language, 'mirror': self.mirrors.get_mirror(TypeMask.XML).url, 'api_key': self.config['api_key']} url = airdate.format(**context) logger.debug(u'Getting episode from {0}'.format(url)) data = self.loader.load(url, cache) data = generate_tree(data) # The xml has an "Error" element in it if no episode was found if has_element(data, 'Error'): raise error.TVDBNotFoundError(u"".format()) episodes = parse_xml(data, "Episode") if len(episodes) == 0: raise error.BadData("Bad data received") else: return Episode(episodes[0], None, self.config)
class TVDB(object): """ :param api_key: The API key to use to communicate with the server :param kwargs: This is the main entry point for the API. The functionality of the API is controlled by configuring the keyword arguments. The supported keyword arguments are: * *force_lang* (default=False). Deprecated in version 0.4. Using it will have no affect but will issue a warning in the log file. * *cache_dir* (default=/<system tmp dir>/pytvdbapi/). Specifies the directory to use for caching the server requests. .. versionadded:: 0.3 * *actors* (default=False) The extended actor information is stored in a separate XML file and would require an additional request to the server to obtain. To limit the resource usage, the actor information will only be loaded when explicitly requested. .. note:: The :class:`Show()` object always contain a list of actor names. * *banners* (default=False) The extended banner information is stored in a separate XML file and would require an additional request to the server to obtain. To limit the resource usage, the banner information will only be loaded when explicitly requested. .. versionadded:: 0.4 * *ignore_case* (default=False) If set to True, all attributes on the :class:`Show` and :class:`Episode` instances will be accessible in a case insensitive manner. If set to False, the default, all attributes will be case sensitive and retain the same casing as provided by `thetvdb.com <http://thetvdb.com>`_. """ def __init__(self, api_key, **kwargs): self.config = dict() #cache old searches to avoid hitting the server self.search_buffer = dict() #Store the path to where we are self.path = os.path.abspath(os.path.dirname(__file__)) if 'force_lang' in kwargs: logger.warning("'force_lang' keyword argument is deprecated as of version 0.4") #extract all argument and store for later use self.config['api_key'] = api_key self.config['cache_dir'] = kwargs.get("cache_dir", os.path.join(tempfile.gettempdir(), name)) self.config['actors'] = kwargs.get('actors', False) self.config['banners'] = kwargs.get('banners', False) self.config['ignore_case'] = kwargs.get('ignore_case', False) #Create the loader object to use self.loader = Loader(self.config['cache_dir']) #Create the list of available mirrors tree = generate_tree(self.loader.load(__mirrors__.format(**self.config))) self.mirrors = MirrorList(tree) def search(self, show, language, cache=True): """ :param show: The show name to search for :param language: The language abbreviation to search for. E.g. "en" :param cache: If False, the local cache will not be used and the resources will be reloaded from server. :return: A :class:`Search()` instance :raise: :class:`pytvdbapi.error.TVDBValueError` Searches the server for a show with the provided show name in the provided language. The language should be one of the supported language abbreviations or it could be set to *all* to search all languages. It will raise :class:`pytvdbapi.error.TVDBValueError` if an invalid language is provided. Searches are always cached within a session to make subsequent searches with the same parameters really cheap and fast. If *cache* is set to True searches will also be cached across sessions, this is recommended to increase speed and to reduce the workload of the servers. Example:: >>> from pytvdbapi import api >>> db = api.TVDB("B43FF87DE395DF56") >>> search = db.search("Dexter", "en") >>> for s in search: ... print(s) ... <Show - Dexter> """ logger.debug("Searching for {0} using language {1}".format(show, language)) if language != 'all' and language not in __LANGUAGES__: raise error.TVDBValueError("{0} is not a valid language".format(language)) if (show, language) not in self.search_buffer or not cache: if sys.version_info < (3, 0): show = str(show.encode('utf-8')) context = {'series': quote(show), "language": language} data = generate_tree(self.loader.load(__search__.format(**context), cache)) shows = [Show(d, self, language, self.config) for d in parse_xml(data, "Series")] self.search_buffer[(show, language)] = shows return Search(self.search_buffer[(show, language)], show, language) def get(self, series_id, language, cache=True): """ .. versionadded:: 0.3 .. deprecated:: 0.4 Use :func:`get_series` instead. :param series_id: The Show Id to fetch :param language: The language abbreviation to search for. E.g. "en" :param cache: If False, the local cache will not be used and the resources will be reloaded from server. :return: A :class:`Show()` instance :raise: :class:`pytvdbapi.error.TVDBValueError`, :class:`pytvdbapi.error.TVDBIdError` """ logger.warning("Using deprecated function 'get'. Use 'get_series' instead") return self.get_series(series_id, language, cache) def get_series(self, series_id, language, cache=True): """ .. versionadded:: 0.4 :param series_id: The Show Id to fetch :param language: The language abbreviation to search for. E.g. "en" :param cache: If False, the local cache will not be used and the resources will be reloaded from server. :return: A :class:`Show()` instance :raise: :class:`pytvdbapi.error.TVDBValueError`, :class:`pytvdbapi.error.TVDBIdError` Provided a valid Show ID, the data for the show is fetched and a corresponding :class:`Show()` object is returned. Example:: >>> from pytvdbapi import api >>> db = api.TVDB("B43FF87DE395DF56") >>> show = db.get( 79349, "en" ) >>> show.id 79349 >>> show.SeriesName 'Dexter' """ logger.debug("Getting series with id {0} with language {1}".format(series_id, language)) if language != 'all' and language not in __LANGUAGES__: raise error.TVDBValueError("{0} is not a valid language".format(language)) context = {'seriesid': series_id, "language": language, 'mirror': self.mirrors.get_mirror(TypeMask.XML).url, 'api_key': self.config['api_key']} url = __series__.format(**context) try: data = self.loader.load(url, cache) except error.TVDBNotFoundError: raise error.TVDBIdError("Series id {0} not found".format(series_id)) except error.ConnectionError as _error: logger.debug("Unable to connect to URL: {0}. {1}".format(url, _error)) raise if data.strip(): data = generate_tree(data) else: raise error.TVDBIdError("No Show with id {0} found".format(series_id)) series = parse_xml(data, "Series") assert len(series) <= 1, "Should not find more than one series" if len(series) >= 1: return Show(series[0], self, language, self.config) else: raise error.TVDBIdError("No Show with id {0} found".format(series_id)) def get_episode(self, episode_id, language, cache=True): """ .. versionadded:: 0.4 :param episode_id: The Episode Id to fetch :param language: The language abbreviation to search for. E.g. "en" :param cache: If False, the local cache will not be used and the resources will be reloaded from server. :return: An :class:`Episode()` instance :raise: :class:`pytvdbapi.error.TVDBIdError` if no episode is found with the given Id Given a valid episode Id the corresponding episode data is fetched and the :class:`Episode()` instance is returned. .. Note:: When the :class:`Episode()` is loaded using :func:`get_episode()` the episode attribute will be None. Example:: >>> from pytvdbapi import api >>> db = api.TVDB("B43FF87DE395DF56") >>> episode = db.get_episode(308834, "en") >>> episode.id 308834 >>> episode.EpisodeName 'Crocodile' """ logger.debug("Getting episode with id {0} with language {1}".format(episode_id, language)) if language != 'all' and language not in __LANGUAGES__: raise error.TVDBValueError("{0} is not a valid language".format(language)) context = {'episodeid': episode_id, "language": language, 'mirror': self.mirrors.get_mirror(TypeMask.XML).url, 'api_key': self.config['api_key']} url = __episode__.format(**context) try: data = self.loader.load(url, cache) except error.TVDBNotFoundError: raise error.TVDBIdError("No Episode with id {0} found".format(episode_id)) except error.ConnectionError as _error: logger.debug("Unable to connect to URL: {0}. {1}".format(url, _error)) raise if data.strip(): data = generate_tree(data) else: raise error.TVDBIdError("No Episode with id {0} found".format(episode_id)) episodes = parse_xml(data, "Episode") assert len(episodes) <= 1, "Should not find more than one episodes" if len(episodes) >= 1: return Episode(episodes[0], None, self.config) else: raise error.TVDBIdError("No Episode with id {0} found".format(episode_id))
def setUp(self): super(TestLoader, self).setUp() self.tmp = tempfile.mkdtemp() self.loader = Loader(self.tmp) self.context = {"api_key": "B43FF87DE395DF56"}
class TVDB(object): """ :param api_key: The API key to use to communicate with the server :param kwargs: This is the main entry point for the API. The functionality of the API is controlled by configuring the keyword arguments. The supported keyword arguments are: * *force_lang* (default=False). Deprecated in version 0.4. Using it will have no affect but will issue a warning in the log file. * *cache_dir* (default=/<system tmp dir>/pytvdbapi/). Specifies the directory to use for caching the server requests. .. versionadded:: 0.3 * *actors* (default=False) The extended actor information is stored in a separate XML file and would require an additional request to the server to obtain. To limit the resource usage, the actor information will only be loaded when explicitly requested. .. note:: The :class:`Show()` object always contain a list of actor names. * *banners* (default=False) The extended banner information is stored in a separate XML file and would require an additional request to the server to obtain. To limit the resource usage, the banner information will only be loaded when explicitly requested. .. versionadded:: 0.4 * *ignore_case* (default=False) If set to True, all attributes on the :class:`Show` and :class:`Episode` instances will be accessible in a case insensitive manner. If set to False, the default, all attributes will be case sensitive and retain the same casing as provided by `thetvdb.com <http://thetvdb.com>`_. """ def __init__(self, api_key, **kwargs): self.config = dict() #cache old searches to avoid hitting the server self.search_buffer = dict() #Store the path to where we are self.path = os.path.abspath(os.path.dirname(__file__)) if 'force_lang' in kwargs: logger.warning( "'force_lang' keyword argument is deprecated as of version 0.4" ) #extract all argument and store for later use self.config['api_key'] = api_key self.config['cache_dir'] = kwargs.get( "cache_dir", os.path.join(tempfile.gettempdir(), name)) self.config['actors'] = kwargs.get('actors', False) self.config['banners'] = kwargs.get('banners', False) self.config['ignore_case'] = kwargs.get('ignore_case', False) #Create the loader object to use self.loader = Loader(self.config['cache_dir']) #Create the list of available mirrors tree = generate_tree( self.loader.load(__mirrors__.format(**self.config))) self.mirrors = MirrorList(tree) def search(self, show, language, cache=True): """ :param show: The show name to search for :param language: The language abbreviation to search for. E.g. "en" :param cache: If False, the local cache will not be used and the resources will be reloaded from server. :return: A :class:`Search()` instance :raise: :class:`pytvdbapi.error.TVDBValueError` Searches the server for a show with the provided show name in the provided language. The language should be one of the supported language abbreviations or it could be set to *all* to search all languages. It will raise :class:`pytvdbapi.error.TVDBValueError` if an invalid language is provided. Searches are always cached within a session to make subsequent searches with the same parameters really cheap and fast. If *cache* is set to True searches will also be cached across sessions, this is recommended to increase speed and to reduce the workload of the servers. Example:: >>> from pytvdbapi import api >>> db = api.TVDB("B43FF87DE395DF56") >>> search = db.search("Dexter", "en") >>> for s in search: ... print(s) ... <Show - Dexter> """ logger.debug("Searching for {0} using language {1}".format( show, language)) if language != 'all' and language not in __LANGUAGES__: raise error.TVDBValueError( "{0} is not a valid language".format(language)) if (show, language) not in self.search_buffer or not cache: if sys.version_info < (3, 0): show = str(show.encode('utf-8')) context = {'series': quote(show), "language": language} data = generate_tree( self.loader.load(__search__.format(**context), cache)) shows = [ Show(d, self, language, self.config) for d in parse_xml(data, "Series") ] self.search_buffer[(show, language)] = shows return Search(self.search_buffer[(show, language)], show, language) def get(self, series_id, language, cache=True): """ .. versionadded:: 0.3 .. deprecated:: 0.4 Use :func:`get_series` instead. :param series_id: The Show Id to fetch :param language: The language abbreviation to search for. E.g. "en" :param cache: If False, the local cache will not be used and the resources will be reloaded from server. :return: A :class:`Show()` instance :raise: :class:`pytvdbapi.error.TVDBValueError`, :class:`pytvdbapi.error.TVDBIdError` """ logger.warning( "Using deprecated function 'get'. Use 'get_series' instead") return self.get_series(series_id, language, cache) def get_series(self, series_id, language, cache=True): """ .. versionadded:: 0.4 :param series_id: The Show Id to fetch :param language: The language abbreviation to search for. E.g. "en" :param cache: If False, the local cache will not be used and the resources will be reloaded from server. :return: A :class:`Show()` instance :raise: :class:`pytvdbapi.error.TVDBValueError`, :class:`pytvdbapi.error.TVDBIdError` Provided a valid Show ID, the data for the show is fetched and a corresponding :class:`Show()` object is returned. Example:: >>> from pytvdbapi import api >>> db = api.TVDB("B43FF87DE395DF56") >>> show = db.get( 79349, "en" ) >>> show.id 79349 >>> show.SeriesName 'Dexter' """ logger.debug("Getting series with id {0} with language {1}".format( series_id, language)) if language != 'all' and language not in __LANGUAGES__: raise error.TVDBValueError( "{0} is not a valid language".format(language)) context = { 'seriesid': series_id, "language": language, 'mirror': self.mirrors.get_mirror(TypeMask.XML).url, 'api_key': self.config['api_key'] } url = __series__.format(**context) try: data = self.loader.load(url, cache) except error.TVDBNotFoundError: raise error.TVDBIdError( "Series id {0} not found".format(series_id)) except error.ConnectionError as _error: logger.debug("Unable to connect to URL: {0}. {1}".format( url, _error)) raise if data.strip(): data = generate_tree(data) else: raise error.TVDBIdError( "No Show with id {0} found".format(series_id)) series = parse_xml(data, "Series") assert len(series) <= 1, "Should not find more than one series" if len(series) >= 1: return Show(series[0], self, language, self.config) else: raise error.TVDBIdError( "No Show with id {0} found".format(series_id)) def get_episode(self, episode_id, language, cache=True): """ .. versionadded:: 0.4 :param episode_id: The Episode Id to fetch :param language: The language abbreviation to search for. E.g. "en" :param cache: If False, the local cache will not be used and the resources will be reloaded from server. :return: An :class:`Episode()` instance :raise: :class:`pytvdbapi.error.TVDBIdError` if no episode is found with the given Id Given a valid episode Id the corresponding episode data is fetched and the :class:`Episode()` instance is returned. .. Note:: When the :class:`Episode()` is loaded using :func:`get_episode()` the episode attribute will be None. Example:: >>> from pytvdbapi import api >>> db = api.TVDB("B43FF87DE395DF56") >>> episode = db.get_episode(308834, "en") >>> episode.id 308834 >>> episode.EpisodeName 'Crocodile' """ logger.debug("Getting episode with id {0} with language {1}".format( episode_id, language)) if language != 'all' and language not in __LANGUAGES__: raise error.TVDBValueError( "{0} is not a valid language".format(language)) context = { 'episodeid': episode_id, "language": language, 'mirror': self.mirrors.get_mirror(TypeMask.XML).url, 'api_key': self.config['api_key'] } url = __episode__.format(**context) try: data = self.loader.load(url, cache) except error.TVDBNotFoundError: raise error.TVDBIdError( "No Episode with id {0} found".format(episode_id)) except error.ConnectionError as _error: logger.debug("Unable to connect to URL: {0}. {1}".format( url, _error)) raise if data.strip(): data = generate_tree(data) else: raise error.TVDBIdError( "No Episode with id {0} found".format(episode_id)) episodes = parse_xml(data, "Episode") assert len(episodes) <= 1, "Should not find more than one episodes" if len(episodes) >= 1: return Episode(episodes[0], None, self.config) else: raise error.TVDBIdError( "No Episode with id {0} found".format(episode_id))
class TVDB(object): """ :param api_key: The API key to use to communicate with the server :param kwargs: This is the main entry point for the API. The functionality of the API is controlled by configuring the keyword arguments. The supported keyword arguments are: * **cache_dir** (default=/<system tmp dir>/pytvdbapi/). Specifies the directory to use for caching the server requests. .. versionadded:: 0.3 * **actors** (default=False) The extended actor information is stored in a separate XML file and would require an additional request to the server to obtain. To limit the resource usage, the actor information will only be loaded when explicitly requested. .. note:: The :class:`Show()` object always contain a list of actor names. * **banners** (default=False) The extended banner information is stored in a separate XML file and would require an additional request to the server to obtain. To limit the resource usage, the banner information will only be loaded when explicitly requested. .. versionadded:: 0.4 * **ignore_case** (default=False) If set to True, all attributes on the :class:`Show` and :class:`Episode` instances will be accessible in a case insensitive manner. If set to False, the default, all attributes will be case sensitive and retain the same casing as provided by `thetvdb.com <http://thetvdb.com>`_. * **timeout** (default=None) When set to a number, will cause the http request to timeout after that number of seconds. """ @unicode_arguments def __init__(self, api_key, **kwargs): self.config = dict() # cache old searches to avoid hitting the server self.search_buffer = dict() # extract all argument and store for later use self.config['api_key'] = api_key self.config['cache_dir'] = kwargs.get( "cache_dir", make_unicode(os.path.join(tempfile.gettempdir(), __NAME__))) self.config['actors'] = kwargs.get('actors', False) self.config['banners'] = kwargs.get('banners', False) self.config['ignore_case'] = kwargs.get('ignore_case', False) # Create the loader object to use self.loader = Loader(self.config['cache_dir'], timeout=kwargs.get('timeout', None)) # Create the list of available mirrors tree = generate_tree(self.loader.load(mirrors.format(**self.config))) self.mirrors = MirrorList(tree) @unicode_arguments def search(self, show, language, cache=True): """ :param show: The show name to search for :param language: The language abbreviation to search for. E.g. "en" :param cache: If False, the local cache will not be used and the resources will be reloaded from server. :return: A :class:`Search()` instance :raise: :exc:`pytvdbapi.error.TVDBValueError` Searches the server for a show with the provided show name in the provided language. The language should be one of the supported language abbreviations or it could be set to *all* to search all languages. It will raise :class:`pytvdbapi.error.TVDBValueError` if an invalid language is provided. Searches are always cached within a session to make subsequent searches with the same parameters fast. If *cache* is set to True searches will also be cached across sessions, this is recommended to increase speed and to reduce the workload of the servers. Example:: >>> from pytvdbapi import api >>> db = api.TVDB("B43FF87DE395DF56") >>> result = db.search("House", "en") >>> print(result[0]) <Show - House> >>> for show in result: ... print(show) # doctest: +ELLIPSIS <Show - House> ... """ logger.debug(u"Searching for {0} using language {1}".format( show, language)) if language != u'all' and language not in __LANGUAGES__: raise error.TVDBValueError( u"{0} is not a valid language".format(language)) if (show, language) not in self.search_buffer or not cache: context = {'series': quote(make_bytes(show)), "language": language} data = generate_tree( self.loader.load(search.format(**context), cache)) shows = [ Show(d, self, language, self.config) for d in parse_xml(data, "Series") ] self.search_buffer[(show, language)] = shows return Search(self.search_buffer[(show, language)], show, language) @unicode_arguments def get_series(self, series_id, language, id_type='tvdb', cache=True): """ .. versionadded:: 0.4 .. versionchanged:: 0.5 Added *id_type* parameter :param series_id: The Show Id to fetch :param language: The language abbreviation to search for. E.g. "en" :param id_type: Information about what kind of id is provided. Should be one of *('tvdb', 'imdb', 'zap2it')* :param cache: If False, the local cache will not be used and the resources will be reloaded from server. :return: A :class:`Show()` instance :raise: :exc:`pytvdbapi.error.TVDBValueError`, :exc:`pytvdbapi.error.TVDBIdError` Provided a valid Show ID, the data for the show is fetched and a corresponding :class:`Show()` object is returned. Example:: >>> from pytvdbapi import api >>> db = api.TVDB("B43FF87DE395DF56") >>> show = db.get_series( 79349, "en" ) # Load Dexter >>> print(show.SeriesName) Dexter """ if id_type not in ('tvdb', 'imdb', 'zap2it'): raise error.TVDBValueError("Invalid id type") elif language != 'all' and language not in __LANGUAGES__: raise error.TVDBValueError( u"{0} is not a valid language".format(language)) # Map id type to url template __url__ = {'tvdb': series, 'imdb': imdbid, 'zap2it': zap2itid} try: series_id = text_type(series_id) except ValueError: raise error.TVDBValueError( "Invalid id type, expected {0} or {1}, got {2}".format( text_type, int_types, type(series_id))) if id_type == 'imdb': series_id = series_id[2:] if series_id.startswith( 'tt') else series_id elif id_type == 'zap2it': series_id = series_id if series_id.startswith( 'EP') else u'EP' + series_id.rjust(8, '0') logger.debug( u"Getting series with id {0}({2}) with language {1}".format( series_id, language, id_type)) context = { 'seriesid': series_id, "language": language, 'mirror': self.mirrors.get_mirror(TypeMask.XML).url, 'api_key': self.config['api_key'], 'imdbid': series_id, 'zap2itid': series_id } url = __url__[id_type].format(**context) logger.debug(u'Getting series from {0}'.format(url)) try: data = self.loader.load(url, cache) except error.TVDBNotFoundError: raise error.TVDBIdError( u"Series id {0} not found".format(series_id)) data = generate_tree(data) series_data = parse_xml(data, "Series") if len(series) == 0: raise error.BadData("Bad data received") else: return Show(series_data[0], self, language, self.config, data) @unicode_arguments @deprecate_episode_id def get_episode(self, language, method="id", cache=True, **kwargs): """ .. versionadded:: 0.4 .. versionchanged:: 0.5 Added the possibility to get an episode using *default*, *dvd*, and *absolute* sort order :param episode_id: *Deprecated in 0.5* Use the *episodeid* keyword argument with the *id* method instead :param language: The language abbreviation to search for. E.g. "en" :param cache: If False, the local cache will not be used and the resources will be reloaded from server. :param method: (default=id) Specify what method should be used to get the episode. Depending on what method is specified, different parameters must be passed as keyword arguments. Should be one of (id, default, dvd, absolute). :param kwargs: *episodeid*, *seriesid*, *seasonnumber*, *episodenumber* and *absolutenumber*. See the examples for information on how to use them. :return: An :class:`Episode()` instance :raise: :exc:`pytvdbapi.error.TVDBValueError`, :exc:`pytvdbapi.error.BadData` Retrieves a single episode. Depending on what method is specified different criteria can be used to retrieve the episode. Examples: Load an episode using the episode id >>> from pytvdbapi import api >>> db = api.TVDB("B43FF87DE395DF56") >>> ep = db.get_episode("en", episodeid=308834) # id is the default method >>> print(ep.EpisodeName) Crocodile Load an episode using dvd and default sort order >>> ep = db.get_episode("en", "dvd", seasonnumber=2, episodenumber=5, seriesid=79349) >>> print(ep.EpisodeName) The Dark Defender >>> ep = db.get_episode("en", "default", seasonnumber=2, episodenumber=6, seriesid=79349) >>> print(ep.EpisodeName) Dex, Lies, and Videotape Load an episode using the absolute number >>> ep = db.get_episode("en", "absolute", absolutenumber=19, seriesid=79349) >>> print(ep.EpisodeName) That Night, A Forest Grew Under some circumstances the backend server fails to return a proper **404** file not found error response when the requested episode can not be found, but instead returns a valid HTML file with the content *404 file not found*. For this reason it is required to check for both :exc:`pytvdbapi.error.TVDBValueError` and :exc:`pytvdbapi.error.BadData` to detect an issue downloading the episode. >>> from pytvdbapi.error import BadData >>> from pytvdbapi.error import TVDBNotFoundError >>> try: ... ep = db.get_episode("en", episodeid=308834) ... except TVDBNotFoundError: ... # this is the standard 404 error code returned from the server ... pass ... except BadData: ... # This is when the server returns a 200 code but with a HTML page saying 404 Nothing found ... pass .. Note:: When the :class:`Episode()` is loaded using :func:`get_episode()` the *season* attribute used to link the episode with a season will be None. """ methods = { "default": default_order, "dvd": dvd_order, "absolute": absolute_order, "id": episode } if language != 'all' and language not in __LANGUAGES__: raise error.TVDBValueError( u"{0} is not a valid language".format(language)) context = { "language": language, 'mirror': self.mirrors.get_mirror(TypeMask.XML).url, 'api_key': self.config['api_key'] } kwargs.update(context) try: url = methods[method] except KeyError: raise error.TVDBValueError( u"{0} is not a valid get method".format(method)) try: url = url.format(**kwargs) except KeyError: raise error.TVDBValueError( "Missing arguments for method {0}".format(method)) logger.debug(u'Getting episode from {0}'.format(url)) data = self.loader.load(url, cache) data = generate_tree(data) episodes = parse_xml(data, "Episode") if len(episodes) == 0: raise error.BadData("Bad data received") else: return Episode(episodes[0], None, self.config) @unicode_arguments def get_episode_by_air_date(self, series_id, language, air_date, cache=True): """ .. versionadded:: 0.5 :param series_id: The TVDB series id of the episode :param language: The language to search for. Should be a two letter abbreviation e.g. *en*. :param air_date: The air date to search for. Should be of type :class:`datetime.date` :type air_date: datetime.date :param cache: If False, the local cache will not be used and the resources will be reloaded from server. :return: If found, an :class:`Episode` instance :raise: :exc:`pytvdbapi.error.TVDBValueError` .. Note:: When the :class:`Episode()` is loaded using :func:`get_episode_by_air_date` the *season* attribute used to link the episode with a season will be None. """ if type(air_date) not in (datetime.date, ): raise error.TVDBValueError( "air_date should be of type datetime.date") elif language != 'all' and language not in __LANGUAGES__: raise error.TVDBValueError( u"{0} is not a valid language".format(language)) context = { 'seriesid': series_id, 'airdate': air_date, "language": language, 'mirror': self.mirrors.get_mirror(TypeMask.XML).url, 'api_key': self.config['api_key'] } url = airdate.format(**context) logger.debug(u'Getting episode from {0}'.format(url)) data = self.loader.load(url, cache) data = generate_tree(data) # The xml has an "Error" element in it if no episode was found if has_element(data, 'Error'): raise error.TVDBNotFoundError(u"".format()) episodes = parse_xml(data, "Episode") if len(episodes) == 0: raise error.BadData("Bad data received") else: return Episode(episodes[0], None, self.config)
class TVDB(object): """ :param api_key: The API key to use to communicate with the server :param kwargs: This is the main entry point for the API. The functionality of the API is controlled by configuring the key word arguments. The supported key word arguments are: * *force_lang* (default=False). If set to True, the API will reload the language list from the server. If False, the local preloaded file will be used. The language list is relative stable but if there are changes it could be useful to set this to True to obtain a new version from the server. It is only necessary to do this once since the API stores the reloaded data for future use. * *cache_dir* (default=/<system tmp dir>/pytvdbapi/). Specifies the directory to use for caching the server requests. .. versionadded:: 0.3 * *actors* (default=False) The extended actor information is stored in a separate XML file and would require an additional request to the server to obtain. To limit the resource usage, the actor information will only be loaded when explicitly requested. .. note:: The :class:`Show()` object always contain a list of actor names. * *banners* (default=False) The extended banner information is stored in a separate XML file and would require an additional request to the server to obtain. To limit the resource usage, the banner information will only be loaded when explicitly requested. """ def __init__(self, api_key, **kwargs): self.config = dict() #cache old searches to avoid hitting the server self.search_buffer = dict() #Store the path to where we are self.path = os.path.abspath(os.path.dirname(__file__)) #extract all argument and store for later use self.config['force_lang'] = kwargs.get("force_lang", False) self.config['api_key'] = api_key self.config['cache_dir'] = kwargs.get("cache_dir", os.path.join(tempfile.gettempdir(), name)) self.config['actors'] = kwargs.get('actors', False) self.config['banners'] = kwargs.get('banners', False) #Create the loader object to use self.loader = Loader(self.config['cache_dir']) language_file = resource_filename(__name__, 'data/languages.xml') #If requested, update the local language file from the server if self.config['force_lang']: logger.debug("updating Language file from server") with open(language_file, "wt") as languages: url = config.get("urls", "languages", raw=True) language_data = self.loader.load(url % self.config) languages.write(language_data) #Setup the list of supported languages with open(language_file, "rt") as languages: self.languages = LanguageList(generate_tree(languages.read())) #Create the list of available mirrors url = config.get("urls", "mirrors", raw=True) tree = generate_tree(self.loader.load(url % self.config)) self.mirrors = MirrorList(tree) def search(self, show, language, cache=True): """ :param show: The show name to search for :param language: The language abbreviation to search for. E.g. "en" :param cache: If False, the local cache will not be used and the resources will be reloaded from server. :return: A :class:`Search()` instance :raise: TVDBValueError Searches the server for a show with the provided show name in the provided language. The language should be one of the supported language abbreviations or it could be set to *all* to search all languages. It will raise :class:`TVDBValueError` if an invalid language is provided. Searches are always cached within a session to make subsequent searches with the same parameters really cheap and fast. If *cache* is set to True searches will also be cached across sessions, this is recommended to increase speed and to reduce the workload of the servers. Example:: >>> from pytvdbapi import api >>> db = api.TVDB("B43FF87DE395DF56") >>> search = db.search("Dexter", "en") >>> for s in search: ... print(s) ... <Show - Dexter> <Show - Cliff Dexter> """ logger.debug("Searching for {0} using language {1}" .format(show, language)) if language != 'all' and language not in self.languages: raise error.TVDBValueError( "{0} is not a valid language".format(language)) if (show, language) not in self.search_buffer: if sys.version_info < (3, 0): show = str(show.encode('utf-8')) context = {'series': quote(show), "language": language} url = config.get("urls", "search", raw=True) data = generate_tree(self.loader.load(url % context, cache)) shows = [Show(d, self, language) for d in parse_xml(data, "Series")] self.search_buffer[(show, language)] = shows return Search(self.search_buffer[(show, language)], show, language) def get(self, series_id, language, cache=True): """ .. versionadded:: 0.3 :param series_id: The Show Id to fetch :param language: The language abbreviation to search for. E.g. "en" :param cache: If False, the local cache will not be used and the resources will be reloaded from server. :return: A :class:`Show()` instance :raise: TVDBValueError, TVDBIdError Provided a valid Show ID, the data for the show is fetched and a corresponding :class:`Show()` object is returned. Example:: >>> from pytvdbapi import api >>> db = api.TVDB("B43FF87DE395DF56") >>> show = db.get( 79349, "en" ) >>> show.id 79349 >>> show.SeriesName 'Dexter' """ logger.debug("Getting show with id {0} with language {1}".format( series_id, language)) if language != 'all' and language not in self.languages: raise error.TVDBValueError("{0} is not a valid language".format( language)) context = {'seriesid': series_id, "language": language, 'mirror': self.mirrors.get_mirror(TypeMask.XML).url, 'api_key': self.config['api_key']} url = config.get("urls", "series", raw=True) % context try: data = self.loader.load(url, cache) except error.ConnectionError as _error: logger.debug("Unable to connect to URL: {0}. {1}".format(url, _error)) raise error.TVDBIdError("No Show with id {0} found".format( series_id)) if data.strip(): data = generate_tree(data) else: logger.debug("Empty data received for id {0}".format(series_id)) raise error.TVDBIdError("No Show with id {0} found".format( series_id)) series = parse_xml(data, "Series") assert len(series) <= 1, "Should not find more than one series" if len(series) >= 1: return Show(series[0], self, language) else: raise error.TVDBIdError("No Show with id {0} found".format( series_id)) def get_episode(self, episode_id, language, cache=True): logger.debug("Getting episode with id {0} with language {1}".format( episode_id, language)) if language != 'all' and language not in self.languages: raise error.TVDBValueError("{0} is not a valid language".format( language)) context = {'episodeid': episode_id, "language": language, 'mirror': self.mirrors.get_mirror(TypeMask.XML).url, 'api_key': self.config['api_key']} url = config.get("urls", "episode", raw=True) % context try: data = self.loader.load(url, cache) except error.ConnectionError as _error: logger.debug("Unable to connect to URL: {0}. {1}".format(url, _error)) raise error.TVDBIdError("No Episode with id {0} found".format( episode_id)) if data.strip(): data = generate_tree(data) else: logger.debug("Empty data received for id {0}".format(episode_id)) raise error.TVDBIdError("No Episode with id {0} found".format( episode_id)) episodes = parse_xml(data, "Episode") assert len(episodes) <= 1, "Should not find more than one episodes" if len(episodes) >= 1: return Episode(episodes[0], None) else: raise error.TVDBIdError("No Episode with id {0} found".format( episode_id))