diff --git a/CHANGES.md b/CHANGES.md index 790c915f..5497a753 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,4 +1,10 @@ -### 0.13.2 (2017-12-08 19:00:00 UTC) +### 0.13.3 (2017-12-10 20:30:00 UTC) + +* Fix metadata Season Posters and Banners +* Change restore fetching metadata episode thumbs + + +### 0.13.2 (2017-12-08 19:00:00 UTC) * Fix tools menu on Chrome mobile browser diff --git a/lib/tvdb_api/tvdb_api.py b/lib/tvdb_api/tvdb_api.py index 91b7e741..d4830abc 100644 --- a/lib/tvdb_api/tvdb_api.py +++ b/lib/tvdb_api/tvdb_api.py @@ -517,6 +517,7 @@ class Tvdb: self.config['url_epInfo'] = '%(base_url)sseries/%%s/episodes?page=%%s' % self.config self.config['url_seriesInfo'] = '%(base_url)sseries/%%s' % self.config + self.config['url_episodeInfo'] = '%(base_url)sepisodes/%%s' % self.config self.config['url_actorsInfo'] = '%(base_url)sseries/%%s/actors' % self.config self.config['url_seriesBanner'] = '%(base_url)sseries/%%s/images/query?keyType=%%s' % self.config @@ -795,6 +796,28 @@ class Tvdb: self._set_show_data(sid, '_actors', cur_actors) + def get_episode_data(self, epid): + # Parse episode information + data = None + log().debug('Getting all episode data for %s' % epid) + url = self.config['url_episodeInfo'] % epid + episode_data = self._getetsrc(url, language=self.config['language']) + + if isinstance(episode_data, dict) and 'data' in episode_data: + data = episode_data['data'] + if isinstance(data, dict): + for k, v in data.iteritems(): + k = k.lower() + + if None is not v: + if 'filename' == k and v: + v = self.config['url_artworkPrefix'] % v + else: + v = self._clean_data(v) + data[k] = v + + return data + def _get_show_data(self, sid, language, get_ep_info=False): """Takes a series ID, gets the epInfo URL and parses the TVDB XML file into the shows dict in layout: diff --git a/lib/tvdb_api_v1/UNLICENSE b/lib/tvdb_api_v1/UNLICENSE new file mode 100644 index 00000000..c4205d41 --- /dev/null +++ b/lib/tvdb_api_v1/UNLICENSE @@ -0,0 +1,26 @@ +Copyright 2011-2012 Ben Dickson (dbr) + +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or +distribute this software, either in source code form or as a compiled +binary, for any purpose, commercial or non-commercial, and by any +means. + +In jurisdictions that recognize copyright laws, the author or authors +of this software dedicate any and all copyright interest in the +software to the public domain. We make this dedication for the benefit +of the public at large and to the detriment of our heirs and +successors. We intend this dedication to be an overt act of +relinquishment in perpetuity of all present and future rights to this +software under copyright law. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR +OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + +For more information, please refer to diff --git a/lib/tvdb_api_v1/__init__.py b/lib/tvdb_api_v1/__init__.py new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/lib/tvdb_api_v1/__init__.py @@ -0,0 +1 @@ + diff --git a/lib/tvdb_api_v1/tvdb_api.py b/lib/tvdb_api_v1/tvdb_api.py new file mode 100644 index 00000000..b70ab366 --- /dev/null +++ b/lib/tvdb_api_v1/tvdb_api.py @@ -0,0 +1,950 @@ +# !/usr/bin/env python2 +# encoding:utf-8 +# author:dbr/Ben +# project:tvdb_api +# repository:http://github.com/dbr/tvdb_api +# license:unlicense (http://unlicense.org/) + +import traceback +from functools import wraps + +__author__ = 'dbr/Ben' +__version__ = '1.9' + +import os +import time +import getpass +import StringIO +import tempfile +import warnings +import logging +import zipfile +import requests +import requests.exceptions + +try: + import gzip +except ImportError: + gzip = None + +from lib.dateutil.parser import parse +from lib.cachecontrol import CacheControl, caches + +from lib.etreetodict import ConvertXmlToDict +from tvdb_ui import BaseUI, ConsoleUI +from tvdb_exceptions import (tvdb_error_v1, tvdb_shownotfound_v1, + tvdb_seasonnotfound_v1, tvdb_episodenotfound_v1, tvdb_attributenotfound_v1) + +from sickbeard import logger + + +def retry(ExceptionToCheck, tries=4, delay=3, backoff=2, logr=None): + """Retry calling the decorated function using an exponential backoff. + + http://www.saltycrane.com/blog/2009/11/trying-out-retry-decorator-python/ + original from: http://wiki.python.org/moin/PythonDecoratorLibrary#Retry + + :param ExceptionToCheck: the exception to check. may be a tuple of + exceptions to check + :type ExceptionToCheck: Exception or tuple + :param tries: number of times to try (not retry) before giving up + :type tries: int + :param delay: initial delay between retries in seconds + :type delay: int + :param backoff: backoff multiplier e.g. value of 2 will double the delay + each retry + :type backoff: int + :param logr: logger to use. If None, print + :type logr: logging.Logger instance + """ + + def deco_retry(f): + + @wraps(f) + def f_retry(*args, **kwargs): + mtries, mdelay = tries, delay + while mtries > 1: + try: + return f(*args, **kwargs) + except ExceptionToCheck, e: + msg = 'TVDB_API :: %s, Retrying in %d seconds...' % (str(e), mdelay) + if logr: + logger.log(msg, logger.WARNING) + else: + print msg + time.sleep(mdelay) + mtries -= 1 + mdelay *= backoff + return f(*args, **kwargs) + + return f_retry # true decorator + + return deco_retry + + +class ShowContainer(dict): + """Simple dict that holds a series of Show instances + """ + + def __init__(self): + self._stack = [] + self._lastgc = time.time() + + def __set_item__(self, key, value): + self._stack.append(key) + + # keep only the 100th latest results + if time.time() - self._lastgc > 20: + for o in self._stack[:-100]: + del self[o] + + self._stack = self._stack[-100:] + + self._lastgc = time.time() + + super(ShowContainer, self).__set_item__(key, value) + + +class Show(dict): + """Holds a dict of seasons, and show data. + """ + + def __init__(self): + dict.__init__(self) + self.data = {} + + def __repr__(self): + return '' % (self.data.get(u'seriesname', 'instance'), len(self)) + + def __getattr__(self, key): + if key in self: + # Key is an episode, return it + return self[key] + + if key in self.data: + # Non-numeric request is for show-data + return self.data[key] + + raise AttributeError + + def __getitem__(self, key): + if key in self: + # Key is an episode, return it + return dict.__getitem__(self, key) + + if key in self.data: + # Non-numeric request is for show-data + return dict.__getitem__(self.data, key) + + # Data wasn't found, raise appropriate error + if isinstance(key, int) or key.isdigit(): + # Episode number x was not found + raise tvdb_seasonnotfound_v1('Could not find season %s' % (repr(key))) + else: + # If it's not numeric, it must be an attribute name, which + # doesn't exist, so attribute error. + raise tvdb_attributenotfound_v1('Cannot find attribute %s' % (repr(key))) + + def airedOn(self, date): + ret = self.search(str(date), 'firstaired') + if 0 == len(ret): + raise tvdb_episodenotfound_v1('Could not find any episodes that aired on %s' % date) + return ret + + def search(self, term=None, key=None): + """ + Search all episodes in show. Can search all data, or a specific key (for + example, episodename) + + Always returns an array (can be empty). First index contains the first + match, and so on. + + Each array index is an Episode() instance, so doing + search_results[0]['episodename'] will retrieve the episode name of the + first match. + + Search terms are converted to lower case (unicode) strings. + + # Examples + + These examples assume t is an instance of Tvdb(): + + >> t = Tvdb() + >> + + To search for all episodes of Scrubs with a bit of data + containing "my first day": + + >> t['Scrubs'].search("my first day") + [] + >> + + Search for "My Name Is Earl" episode named "Faked His Own Death": + + >> t['My Name Is Earl'].search('Faked His Own Death', key = 'episodename') + [] + >> + + To search Scrubs for all episodes with "mentor" in the episode name: + + >> t['scrubs'].search('mentor', key = 'episodename') + [, ] + >> + + # Using search results + + >> results = t['Scrubs'].search("my first") + >> print results[0]['episodename'] + My First Day + >> for x in results: print x['episodename'] + My First Day + My First Step + My First Kill + >> + """ + results = [] + for cur_season in self.values(): + searchresult = cur_season.search(term=term, key=key) + if 0 != len(searchresult): + results.extend(searchresult) + + return results + + +class Season(dict): + def __init__(self, show=None): + """The show attribute points to the parent show + """ + self.show = show + + def __repr__(self): + return '' % (len(self.keys())) + + def __getattr__(self, episode_number): + if episode_number in self: + return self[episode_number] + raise AttributeError + + def __getitem__(self, episode_number): + if episode_number not in self: + raise tvdb_episodenotfound_v1('Could not find episode %s' % (repr(episode_number))) + else: + return dict.__getitem__(self, episode_number) + + def search(self, term=None, key=None): + """Search all episodes in season, returns a list of matching Episode + instances. + + >> t = Tvdb() + >> t['scrubs'][1].search('first day') + [] + >> + + See Show.search documentation for further information on search + """ + results = [] + for ep in self.values(): + searchresult = ep.search(term=term, key=key) + if None is not searchresult: + results.append(searchresult) + return results + + +class Episode(dict): + def __init__(self, season=None): + """The season attribute points to the parent season + """ + self.season = season + + def __repr__(self): + seasno, epno = int(self.get(u'seasonnumber', 0)), int(self.get(u'episodenumber', 0)) + epname = self.get(u'episodename') + if None is not epname: + return '' % (seasno, epno, epname) + else: + return '' % (seasno, epno) + + def __getattr__(self, key): + if key in self: + return self[key] + raise AttributeError + + def __getitem__(self, key): + try: + return dict.__getitem__(self, key) + except KeyError: + raise tvdb_attributenotfound_v1('Cannot find attribute %s' % (repr(key))) + + def search(self, term=None, key=None): + """Search episode data for term, if it matches, return the Episode (self). + The key parameter can be used to limit the search to a specific element, + for example, episodename. + + This primarily for use use by Show.search and Season.search. See + Show.search for further information on search + + Simple example: + + >> e = Episode() + >> e['episodename'] = "An Example" + >> e.search("examp") + + >> + + Limiting by key: + + >> e.search("examp", key = "episodename") + + >> + """ + if None is term: + raise TypeError('must supply string to search for (contents)') + + term = unicode(term).lower() + for cur_key, cur_value in self.items(): + cur_key, cur_value = unicode(cur_key).lower(), unicode(cur_value).lower() + if None is not key and cur_key != key: + # Do not search this key + continue + if cur_value.find(unicode(term).lower()) > -1: + return self + + +class Actors(list): + """Holds all Actor instances for a show + """ + pass + + +class Actor(dict): + """Represents a single actor. Should contain.. + + id, + image, + name, + role, + sortorder + """ + + def __repr__(self): + return '' % self.get('name') + + +class TvdbV1: + """Create easy-to-use interface to name of season/episode name + >> t = Tvdb() + >> t['Scrubs'][1][24]['episodename'] + u'My Last Day' + """ + + def __init__(self, + interactive=False, + select_first=False, + debug=False, + cache=True, + banners=False, + actors=False, + custom_ui=None, + language=None, + search_all_languages=False, + apikey=None, + forceConnect=False, + useZip=False, + dvdorder=False, + proxy=None): + + """interactive (True/False): + When True, uses built-in console UI is used to select the correct show. + When False, the first search result is used. + + select_first (True/False): + Automatically selects the first series search result (rather + than showing the user a list of more than one series). + Is overridden by interactive = False, or specifying a custom_ui + + debug (True/False) DEPRECATED: + Replaced with proper use of logging module. To show debug messages: + + >> import logging + >> logging.basicConfig(level = logging.DEBUG) + + cache (True/False/str/unicode/urllib2 opener): + Retrieved XML are persisted to to disc. If true, stores in + tvdb_api folder under your systems TEMP_DIR, if set to + str/unicode instance it will use this as the cache + location. If False, disables caching. Can also be passed + an arbitrary Python object, which is used as a urllib2 + opener, which should be created by urllib2.build_opener + + banners (True/False): + Retrieves the banners for a show. These are accessed + via the _banners key of a Show(), for example: + + >> Tvdb(banners=True)['scrubs']['_banners'].keys() + ['fanart', 'poster', 'series', 'season'] + + actors (True/False): + Retrieves a list of the actors for a show. These are accessed + via the _actors key of a Show(), for example: + + >> t = Tvdb(actors=True) + >> t['scrubs']['_actors'][0]['name'] + u'Zach Braff' + + custom_ui (tvdb_ui.BaseUI subclass): + A callable subclass of tvdb_ui.BaseUI (overrides interactive option) + + language (2 character language abbreviation): + The language of the returned data. Is also the language search + uses. Default is "en" (English). For full list, run.. + + >> Tvdb().config['valid_languages'] #doctest: +ELLIPSIS + ['da', 'fi', 'nl', ...] + + search_all_languages (True/False): + By default, Tvdb will only search in the language specified using + the language option. When this is True, it will search for the + show in and language + + apikey (str/unicode): + Override the default thetvdb.com API key. By default it will use + tvdb_api's own key (fine for small scripts), but you can use your + own key if desired - this is recommended if you are embedding + tvdb_api in a larger application) + See http://thetvdb.com/?tab=apiregister to get your own key + + forceConnect (bool): + If true it will always try to connect to theTVDB.com even if we + recently timed out. By default it will wait one minute before + trying again, and any requests within that one minute window will + return an exception immediately. + + useZip (bool): + Download the zip archive where possibale, instead of the xml. + This is only used when all episodes are pulled. + And only the main language xml is used, the actor and banner xml are lost. + """ + + self.shows = ShowContainer() # Holds all Show classes + self.corrections = {} # Holds show-name to show_id mapping + + self.config = {} + + if None is not apikey: + self.config['apikey'] = apikey + else: + self.config['apikey'] = '0629B785CE550C8D' # tvdb_api's API key + + self.config['debug_enabled'] = debug # show debugging messages + + self.config['custom_ui'] = custom_ui + + self.config['interactive'] = interactive # prompt for correct series? + + self.config['select_first'] = select_first + + self.config['search_all_languages'] = search_all_languages + + self.config['useZip'] = useZip + + self.config['dvdorder'] = dvdorder + + self.config['proxy'] = proxy + + if cache is True: + self.config['cache_enabled'] = True + self.config['cache_location'] = self._get_temp_dir() + elif cache is False: + self.config['cache_enabled'] = False + elif isinstance(cache, basestring): + self.config['cache_enabled'] = True + self.config['cache_location'] = cache + else: + raise ValueError('Invalid value for Cache %r (type was %s)' % (cache, type(cache))) + + self.config['banners_enabled'] = banners + self.config['actors_enabled'] = actors + + if self.config['debug_enabled']: + warnings.warn('The debug argument to tvdb_api.__init__ will be removed in the next version. ' + + 'To enable debug messages, use the following code before importing: ' + + 'import logging; logging.basicConfig(level=logging.DEBUG)') + logging.basicConfig(level=logging.DEBUG) + + # List of language from http://thetvdb.com/api/0629B785CE550C8D/languages.xml + # Hard-coded here as it is realtively static, and saves another HTTP request, as + # recommended on http://thetvdb.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} + + if None is language: + self.config['language'] = 'en' + else: + if language not in self.config['valid_languages']: + raise ValueError('Invalid language %s, options are: %s' % (language, self.config['valid_languages'])) + else: + self.config['language'] = language + + # The following url_ configs are based of the + # http://thetvdb.com/wiki/index.php/Programmers_API + self.config['base_url'] = 'http://thetvdb.com' + + if self.config['search_all_languages']: + self.config['url_get_series'] = u'%(base_url)s/api/GetSeries.php' % self.config + self.config['params_get_series'] = {'seriesname': '', 'language': 'all'} + else: + self.config['url_get_series'] = u'%(base_url)s/api/GetSeries.php' % self.config + self.config['params_get_series'] = {'seriesname': '', 'language': self.config['language']} + + self.config['url_epInfo'] = u'%(base_url)s/api/%(apikey)s/series/%%s/all/%%s.xml' % self.config + self.config['url_epInfo_zip'] = u'%(base_url)s/api/%(apikey)s/series/%%s/all/%%s.zip' % self.config + + self.config['url_seriesInfo'] = u'%(base_url)s/api/%(apikey)s/series/%%s/%%s.xml' % self.config + self.config['url_actorsInfo'] = u'%(base_url)s/api/%(apikey)s/series/%%s/actors.xml' % self.config + + self.config['url_seriesBanner'] = u'%(base_url)s/api/%(apikey)s/series/%%s/banners.xml' % self.config + self.config['url_artworkPrefix'] = u'%(base_url)s/banners/%%s' % self.config + + def log(self, msg, log_level=logger.DEBUG): + logger.log('TVDB_API :: %s' % (msg.replace(self.config['apikey'], '')), log_level=log_level) + + @staticmethod + def _get_temp_dir(): + """Returns the [system temp dir]/tvdb_api-u501 (or + tvdb_api-myuser) + """ + if hasattr(os, 'getuid'): + uid = 'u%d' % (os.getuid()) + else: + # For Windows + try: + uid = getpass.getuser() + except ImportError: + return os.path.join(tempfile.gettempdir(), 'tvdb_api') + + return os.path.join(tempfile.gettempdir(), 'tvdb_api-%s' % uid) + + @retry(tvdb_error_v1) + def _load_url(self, url, params=None, language=None): + self.log('Retrieving URL %s' % url) + + session = requests.session() + + if self.config['cache_enabled']: + session = CacheControl(session, cache=caches.FileCache(self.config['cache_location'])) + + if self.config['proxy']: + self.log('Using proxy for URL: %s' % url) + session.proxies = {'http': self.config['proxy'], 'https': self.config['proxy']} + + session.headers.update({'Accept-Encoding': 'gzip,deflate'}) + + try: + resp = session.get(url.strip(), params=params) + except requests.exceptions.HTTPError, e: + raise tvdb_error_v1('HTTP error %s while loading URL %s' % (e.errno, url)) + except requests.exceptions.ConnectionError, e: + raise tvdb_error_v1('Connection error %s while loading URL %s' % (e.message, url)) + except requests.exceptions.Timeout, e: + raise tvdb_error_v1('Connection timed out %s while loading URL %s' % (e.message, url)) + except Exception: + raise tvdb_error_v1('Unknown exception while loading URL %s: %s' % (url, traceback.format_exc())) + + def process_data(data): + te = ConvertXmlToDict(data) + if isinstance(te, dict) and 'Data' in te and isinstance(te['Data'], dict) \ + and 'Series' in te['Data'] and isinstance(te['Data']['Series'], dict) \ + and 'FirstAired' in te['Data']['Series']: + try: + value = parse(te['Data']['Series']['FirstAired'], fuzzy=True).strftime('%Y-%m-%d') + except (StandardError, Exception): + value = None + te['Data']['Series']['firstaired'] = value + return te + + if resp.ok: + if 'application/zip' in resp.headers.get('Content-Type', ''): + try: + # TODO: The zip contains actors.xml and banners.xml, which are currently ignored [GH-20] + self.log('We received a zip file unpacking now ...') + zipdata = StringIO.StringIO() + zipdata.write(resp.content) + myzipfile = zipfile.ZipFile(zipdata) + return process_data(myzipfile.read('%s.xml' % language)) + except zipfile.BadZipfile: + raise tvdb_error_v1('Bad zip file received from thetvdb.com, could not read it') + else: + try: + return process_data(resp.content.strip()) + except (StandardError, Exception): + return dict([(u'data', None)]) + + def _getetsrc(self, url, params=None, language=None): + """Loads a URL using caching, returns an ElementTree of the source + """ + try: + src = self._load_url(url, params=params, language=language).values()[0] + return src + except (StandardError, Exception): + return [] + + def _set_item(self, sid, seas, ep, attrib, value): + """Creates a new episode, creating Show(), Season() and + Episode()s as required. Called by _get_show_data to populate show + + Since the nice-to-use tvdb[1][24]['name] interface + makes it impossible to do tvdb[1][24]['name] = "name" + and still be capable of checking if an episode exists + so we can raise tvdb_shownotfound_v1, we have a slightly + less pretty method of setting items.. but since the API + is supposed to be read-only, this is the best way to + do it! + The problem is that calling tvdb[1][24]['episodename'] = "name" + calls __getitem__ on tvdb[1], there is no way to check if + tvdb.__dict__ should have a key "1" before we auto-create it + """ + if sid not in self.shows: + self.shows[sid] = Show() + if seas not in self.shows[sid]: + self.shows[sid][seas] = Season(show=self.shows[sid]) + if ep not in self.shows[sid][seas]: + self.shows[sid][seas][ep] = Episode(season=self.shows[sid][seas]) + self.shows[sid][seas][ep][attrib] = value + + def _set_show_data(self, sid, key, value): + """Sets self.shows[sid] to a new Show instance, or sets the data + """ + if sid not in self.shows: + self.shows[sid] = Show() + self.shows[sid].data[key] = value + + @staticmethod + def _clean_data(data): + """Cleans up strings returned by TheTVDB.com + + Issues corrected: + - Replaces & with & + - Trailing whitespace + """ + return data if not isinstance(data, basestring) else data.strip().replace(u'&', u'&') + + def _get_url_artwork(self, image): + return image and (self.config['url_artworkPrefix'] % image) or image + + def search(self, series): + """This searches TheTVDB.com for the series name + and returns the result list + """ + series = series.encode('utf-8') + self.log('Searching for show %s' % series) + self.config['params_get_series']['seriesname'] = series + + try: + series_found = self._getetsrc(self.config['url_get_series'], self.config['params_get_series']) + if series_found: + if not isinstance(series_found['Series'], list): + series_found['Series'] = [series_found['Series']] + series_found['Series'] = [{k.lower(): v for k, v in s.iteritems()} for s in series_found['Series']] + return series_found.values()[0] + except (StandardError, Exception): + pass + + return [] + + def _get_series(self, series): + """This searches TheTVDB.com for the series name, + If a custom_ui UI is configured, it uses this to select the correct + series. If not, and interactive == True, ConsoleUI is used, if not + BaseUI is used to select the first result. + """ + all_series = self.search(series) + if not isinstance(all_series, list): + all_series = [all_series] + + if 0 == len(all_series): + self.log('Series result returned zero') + raise tvdb_shownotfound_v1('Show-name search returned zero results (cannot find show on TVDB)') + + if None is not self.config['custom_ui']: + self.log('Using custom UI %s' % (repr(self.config['custom_ui']))) + custom_ui = self.config['custom_ui'] + ui = custom_ui(config=self.config) + else: + if not self.config['interactive']: + self.log('Auto-selecting first search result using BaseUI') + ui = BaseUI(config=self.config) + else: + self.log('Interactively selecting show using ConsoleUI') + ui = ConsoleUI(config=self.config) + + return ui.selectSeries(all_series) + + def _parse_banners(self, sid): + """Parses banners XML, from + http://thetvdb.com/api/[APIKEY]/series/[SERIES ID]/banners.xml + + Banners are retrieved using t['show name]['_banners'], for example: + + >> t = Tvdb(banners = True) + >> t['scrubs']['_banners'].keys() + ['fanart', 'poster', 'series', 'season'] + >> t['scrubs']['_banners']['poster']['680x1000']['35308']['_bannerpath'] + u'http://thetvdb.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. + """ + self.log('Getting season banners for %s' % sid) + banners_et = self._getetsrc(self.config['url_seriesBanner'] % sid) + banners = {} + + try: + for cur_banner in banners_et['banner']: + bid = cur_banner['id'] + btype = cur_banner['bannertype'] + btype2 = cur_banner['bannertype2'] + if None is btype or None is btype2: + continue + if btype not in banners: + banners[btype] = {} + if btype2 not in banners[btype]: + banners[btype][btype2] = {} + if bid not in banners[btype][btype2]: + banners[btype][btype2][bid] = {} + + for k, v in cur_banner.items(): + if None is k or None is v: + continue + + k, v = k.lower(), v.lower() + banners[btype][btype2][bid][k] = v + + for k, v in banners[btype][btype2][bid].items(): + if k.endswith('path'): + new_key = '_%s' % k + self.log('Transforming %s to %s' % (k, new_key)) + new_url = self._get_url_artwork(v) + banners[btype][btype2][bid][new_key] = new_url + except (StandardError, Exception): + pass + + self._set_show_data(sid, '_banners', banners) + + def _parse_actors(self, sid): + """Parsers actors XML, from + http://thetvdb.com/api/[APIKEY]/series/[SERIES ID]/actors.xml + + Actors are retrieved using t['show name]['_actors'], for example: + + >> t = Tvdb(actors = True) + >> actors = t['scrubs']['_actors'] + >> type(actors) + + >> type(actors[0]) + + >> actors[0] + + >> sorted(actors[0].keys()) + ['id', 'image', 'name', 'role', 'sortorder'] + >> actors[0]['name'] + u'Zach Braff' + >> actors[0]['image'] + u'http://thetvdb.com/banners/actors/43640.jpg' + + Any key starting with an underscore has been processed (not the raw + data from the XML) + """ + self.log('Getting actors for %s' % sid) + actors_et = self._getetsrc(self.config['url_actorsInfo'] % sid) + + cur_actors = Actors() + try: + for curActorItem in actors_et['actor']: + cur_actor = Actor() + for k, v in curActorItem.items(): + k = k.lower() + if None is not v: + if 'image' == k: + v = self._get_url_artwork(v) + else: + v = self._clean_data(v) + cur_actor[k] = v + cur_actors.append(cur_actor) + except (StandardError, Exception): + pass + + self._set_show_data(sid, '_actors', cur_actors) + + def _get_show_data(self, sid, language, get_ep_info=False): + """Takes a series ID, gets the epInfo URL and parses the TVDB + XML file into the shows dict in layout: + shows[series_id][season_number][episode_number] + """ + + if None is self.config['language']: + self.log('Config language is none, using show language') + if None is language: + raise tvdb_error_v1('config[\'language\'] was None, this should not happen') + get_show_in_language = language + else: + self.log('Configured language %s override show language of %s' % (self.config['language'], language)) + get_show_in_language = self.config['language'] + + # Parse show information + self.log('Getting all series data for %s' % sid) + url = (self.config['url_seriesInfo'] % (sid, language), self.config['url_epInfo%s' % ('', '_zip')[self.config['useZip']]] % (sid, language))[get_ep_info] + show_data = self._getetsrc(url, language=get_show_in_language) + + # check and make sure we have data to process and that it contains a series name + if not len(show_data) or (isinstance(show_data, dict) and 'SeriesName' not in show_data['Series']): + return False + + for k, v in show_data['Series'].iteritems(): + if None is not v: + if k in ['banner', 'fanart', 'poster']: + v = self._get_url_artwork(v) + else: + v = self._clean_data(v) + + self._set_show_data(sid, k.lower(), v) + + if get_ep_info: + # Parse banners + if self.config['banners_enabled']: + self._parse_banners(sid) + + # Parse actors + if self.config['actors_enabled']: + self._parse_actors(sid) + + # Parse episode data + self.log('Getting all episodes of %s' % sid) + + if 'Episode' not in show_data: + return False + + episodes = show_data['Episode'] + if not isinstance(episodes, list): + episodes = [episodes] + + dvd_order = {'dvd': [], 'network': []} + for cur_ep in episodes: + if self.config['dvdorder']: + use_dvd = cur_ep['DVD_season'] not in (None, '') and cur_ep['DVD_episodenumber'] not in (None, '') + else: + use_dvd = False + + if use_dvd: + elem_seasnum, elem_epno = cur_ep['DVD_season'], cur_ep['DVD_episodenumber'] + else: + elem_seasnum, elem_epno = cur_ep['SeasonNumber'], cur_ep['EpisodeNumber'] + + if None is elem_seasnum or None is elem_epno: + self.log('An episode has incomplete season/episode number (season: %r, episode: %r)' % ( + elem_seasnum, elem_epno), logger.WARNING) + continue # Skip to next episode + + # float() is because https://github.com/dbr/tvnamer/issues/95 - should probably be fixed in TVDB data + seas_no = int(float(elem_seasnum)) + ep_no = int(float(elem_epno)) + + if self.config['dvdorder']: + dvd_order[('network', 'dvd')[use_dvd]] += ['S%02dE%02d' % (seas_no, ep_no)] + + for k, v in cur_ep.items(): + k = k.lower() + + if None is not v: + if 'filename' == k: + v = self._get_url_artwork(v) + else: + v = self._clean_data(v) + + self._set_item(sid, seas_no, ep_no, k, v) + + if self.config['dvdorder']: + num_dvd, num_network = [len(dvd_order[x]) for x in 'dvd', 'network'] + num_all = num_dvd + num_network + if num_all: + self.log('Of %s episodes, %s use the DVD order, and %s use the network aired order' % ( + num_all, num_dvd, num_network)) + for ep_numbers in [', '.join(dvd_order['dvd'][i:i + 5]) for i in xrange(0, num_dvd, 5)]: + self.log('Using DVD order: %s' % ep_numbers) + + return True + + def _name_to_sid(self, name): + """Takes show name, returns the correct series ID (if the show has + already been grabbed), or grabs all episodes and returns + the correct SID. + """ + if name in self.corrections: + self.log('Correcting %s to %s' % (name, self.corrections[name])) + return self.corrections[name] + else: + self.log('Getting show %s' % name) + selected_series = self._get_series(name) + if isinstance(selected_series, dict): + selected_series = [selected_series] + sids = list(int(x['id']) for x in selected_series if + self._get_show_data(int(x['id']), self.config['language'])) + self.corrections.update(dict((x['seriesname'], int(x['id'])) for x in selected_series)) + return sids + + def __getitem__(self, key): + """Handles tvdb_instance['seriesname'] calls. + The dict index should be the show id + """ + arg = None + if isinstance(key, tuple) and 2 == len(key): + key, arg = key + if not isinstance(arg, bool): + arg = None + + if isinstance(key, (int, long)): + # Item is integer, treat as show id + if key not in self.shows: + self._get_show_data(key, self.config['language'], (True, arg)[arg is not None]) + return None if key not in self.shows else self.shows[key] + + key = str(key).lower() + self.config['searchterm'] = key + selected_series = self._get_series(key) + if isinstance(selected_series, dict): + selected_series = [selected_series] + [[self._set_show_data(show['id'], k, v) for k, v in show.items()] for show in selected_series] + return selected_series + + def __repr__(self): + return str(self.shows) + + +def main(): + """Simple example of using tvdb_api - it just + grabs an episode name interactively. + """ + import logging + + logging.basicConfig(level=logging.DEBUG) + + tvdb_instance = Tvdb(interactive=True, cache=False) + print tvdb_instance['Lost']['seriesname'] + print tvdb_instance['Lost'][1][4]['episodename'] + + +if '__main__' == __name__: + main() diff --git a/lib/tvdb_api_v1/tvdb_cache.py b/lib/tvdb_api_v1/tvdb_cache.py new file mode 100644 index 00000000..50c198d4 --- /dev/null +++ b/lib/tvdb_api_v1/tvdb_cache.py @@ -0,0 +1,251 @@ +#!/usr/bin/env python2 +#encoding:utf-8 +#author:dbr/Ben +#project:tvdb_api +#repository:http://github.com/dbr/tvdb_api +#license:unlicense (http://unlicense.org/) + +""" +urllib2 caching handler +Modified from http://code.activestate.com/recipes/491261/ +""" +from __future__ import with_statement + +__author__ = "dbr/Ben" +__version__ = "1.9" + +import os +import time +import errno +import httplib +import urllib2 +import StringIO +from hashlib import md5 +from threading import RLock + +cache_lock = RLock() + +def locked_function(origfunc): + """Decorator to execute function under lock""" + def wrapped(*args, **kwargs): + cache_lock.acquire() + try: + return origfunc(*args, **kwargs) + finally: + cache_lock.release() + return wrapped + +def calculate_cache_path(cache_location, url): + """Checks if [cache_location]/[hash_of_url].headers and .body exist + """ + thumb = md5(url).hexdigest() + header = os.path.join(cache_location, thumb + ".headers") + body = os.path.join(cache_location, thumb + ".body") + return header, body + +def check_cache_time(path, max_age): + """Checks if a file has been created/modified in the [last max_age] seconds. + False means the file is too old (or doesn't exist), True means it is + up-to-date and valid""" + if not os.path.isfile(path): + return False + cache_modified_time = os.stat(path).st_mtime + time_now = time.time() + if cache_modified_time < time_now - max_age: + # Cache is old + return False + else: + return True + +@locked_function +def exists_in_cache(cache_location, url, max_age): + """Returns if header AND body cache file exist (and are up-to-date)""" + hpath, bpath = calculate_cache_path(cache_location, url) + if os.path.exists(hpath) and os.path.exists(bpath): + return( + check_cache_time(hpath, max_age) + and check_cache_time(bpath, max_age) + ) + else: + # File does not exist + return False + +@locked_function +def store_in_cache(cache_location, url, response): + """Tries to store response in cache.""" + hpath, bpath = calculate_cache_path(cache_location, url) + try: + outf = open(hpath, "wb") + headers = str(response.info()) + outf.write(headers) + outf.close() + + outf = open(bpath, "wb") + outf.write(response.read()) + outf.close() + except IOError: + return True + else: + return False + +@locked_function +def delete_from_cache(cache_location, url): + """Deletes a response in cache.""" + hpath, bpath = calculate_cache_path(cache_location, url) + try: + if os.path.exists(hpath): + os.remove(hpath) + if os.path.exists(bpath): + os.remove(bpath) + except IOError: + return True + else: + return False + +class CacheHandler(urllib2.BaseHandler): + """Stores responses in a persistant on-disk cache. + + If a subsequent GET request is made for the same URL, the stored + response is returned, saving time, resources and bandwidth + """ + @locked_function + def __init__(self, cache_location, max_age = 21600): + """The location of the cache directory""" + self.max_age = max_age + self.cache_location = cache_location + if not os.path.exists(self.cache_location): + try: + os.mkdir(self.cache_location) + except OSError, e: + if e.errno == errno.EEXIST and os.path.isdir(self.cache_location): + # File exists, and it's a directory, + # another process beat us to creating this dir, that's OK. + pass + else: + # Our target dir is already a file, or different error, + # relay the error! + raise + + def default_open(self, request): + """Handles GET requests, if the response is cached it returns it + """ + if request.get_method() != "GET": + return None # let the next handler try to handle the request + + if exists_in_cache( + self.cache_location, request.get_full_url(), self.max_age + ): + return CachedResponse( + self.cache_location, + request.get_full_url(), + set_cache_header = True + ) + else: + return None + + def http_response(self, request, response): + """Gets a HTTP response, if it was a GET request and the status code + starts with 2 (200 OK etc) it caches it and returns a CachedResponse + """ + if (request.get_method() == "GET" + and str(response.code).startswith("2") + ): + if 'x-local-cache' not in response.info(): + # Response is not cached + set_cache_header = store_in_cache( + self.cache_location, + request.get_full_url(), + response + ) + else: + set_cache_header = True + + return CachedResponse( + self.cache_location, + request.get_full_url(), + set_cache_header = set_cache_header + ) + else: + return response + +class CachedResponse(StringIO.StringIO): + """An urllib2.response-like object for cached responses. + + To determine if a response is cached or coming directly from + the network, check the x-local-cache header rather than the object type. + """ + + @locked_function + def __init__(self, cache_location, url, set_cache_header=True): + self.cache_location = cache_location + hpath, bpath = calculate_cache_path(cache_location, url) + + StringIO.StringIO.__init__(self, file(bpath, "rb").read()) + + self.url = url + self.code = 200 + self.msg = "OK" + headerbuf = file(hpath, "rb").read() + if set_cache_header: + headerbuf += "x-local-cache: %s\r\n" % (bpath) + self.headers = httplib.HTTPMessage(StringIO.StringIO(headerbuf)) + + def info(self): + """Returns headers + """ + return self.headers + + def geturl(self): + """Returns original URL + """ + return self.url + + @locked_function + def recache(self): + new_request = urllib2.urlopen(self.url) + set_cache_header = store_in_cache( + self.cache_location, + new_request.url, + new_request + ) + CachedResponse.__init__(self, self.cache_location, self.url, True) + + @locked_function + def delete_cache(self): + delete_from_cache( + self.cache_location, + self.url + ) + + +if __name__ == "__main__": + def main(): + """Quick test/example of CacheHandler""" + opener = urllib2.build_opener(CacheHandler("/tmp/")) + response = opener.open("http://google.com") + print response.headers + print "Response:", response.read() + + response.recache() + print response.headers + print "After recache:", response.read() + + # Test usage in threads + from threading import Thread + class CacheThreadTest(Thread): + lastdata = None + def run(self): + req = opener.open("http://google.com") + newdata = req.read() + if self.lastdata is None: + self.lastdata = newdata + assert self.lastdata == newdata, "Data was not consistent, uhoh" + req.recache() + threads = [CacheThreadTest() for x in range(50)] + print "Starting threads" + [t.start() for t in threads] + print "..done" + print "Joining threads" + [t.join() for t in threads] + print "..done" + main() diff --git a/lib/tvdb_api_v1/tvdb_exceptions.py b/lib/tvdb_api_v1/tvdb_exceptions.py new file mode 100644 index 00000000..e44afb81 --- /dev/null +++ b/lib/tvdb_api_v1/tvdb_exceptions.py @@ -0,0 +1,52 @@ +#!/usr/bin/env python2 +#encoding:utf-8 +#author:dbr/Ben +#project:tvdb_api +#repository:http://github.com/dbr/tvdb_api +#license:unlicense (http://unlicense.org/) + +"""Custom exceptions used or raised by tvdb_api +""" + +__author__ = "dbr/Ben" +__version__ = "1.9" + +__all__ = ["tvdb_error_v1", "tvdb_userabort_v1", "tvdb_shownotfound_v1", +"tvdb_seasonnotfound_v1", "tvdb_episodenotfound_v1", "tvdb_attributenotfound_v1"] + +class tvdb_exception_v1(Exception): + """Any exception generated by tvdb_api + """ + pass + +class tvdb_error_v1(tvdb_exception_v1): + """An error with thetvdb.com (Cannot connect, for example) + """ + pass + +class tvdb_userabort_v1(tvdb_exception_v1): + """User aborted the interactive selection (via + the q command, ^c etc) + """ + pass + +class tvdb_shownotfound_v1(tvdb_exception_v1): + """Show cannot be found on thetvdb.com (non-existant show) + """ + pass + +class tvdb_seasonnotfound_v1(tvdb_exception_v1): + """Season cannot be found on thetvdb.com + """ + pass + +class tvdb_episodenotfound_v1(tvdb_exception_v1): + """Episode cannot be found on thetvdb.com + """ + pass + +class tvdb_attributenotfound_v1(tvdb_exception_v1): + """Raised if an episode does not have the requested + attribute (such as a episode name) + """ + pass diff --git a/lib/tvdb_api_v1/tvdb_ui.py b/lib/tvdb_api_v1/tvdb_ui.py new file mode 100644 index 00000000..96819186 --- /dev/null +++ b/lib/tvdb_api_v1/tvdb_ui.py @@ -0,0 +1,153 @@ +#!/usr/bin/env python2 +#encoding:utf-8 +#author:dbr/Ben +#project:tvdb_api +#repository:http://github.com/dbr/tvdb_api +#license:unlicense (http://unlicense.org/) + +"""Contains included user interfaces for Tvdb show selection. + +A UI is a callback. A class, it's __init__ function takes two arguments: + +- config, which is the Tvdb config dict, setup in tvdb_api.py +- log, which is Tvdb's logger instance (which uses the logging module). You can +call log.info() log.warning() etc + +It must have a method "selectSeries", this is passed a list of dicts, each dict +contains the the keys "name" (human readable show name), and "sid" (the shows +ID as on thetvdb.com). For example: + +[{'name': u'Lost', 'sid': u'73739'}, + {'name': u'Lost Universe', 'sid': u'73181'}] + +The "selectSeries" method must return the appropriate dict, or it can raise +tvdb_userabort (if the selection is aborted), tvdb_shownotfound (if the show +cannot be found). + +A simple example callback, which returns a random series: + +>>> import random +>>> from tvdb_ui import BaseUI +>>> class RandomUI(BaseUI): +... def selectSeries(self, allSeries): +... import random +... return random.choice(allSeries) + +Then to use it.. + +>>> from tvdb_api import Tvdb +>>> t = Tvdb(custom_ui = RandomUI) +>>> random_matching_series = t['Lost'] +>>> type(random_matching_series) + +""" + +__author__ = "dbr/Ben" +__version__ = "1.9" + +import logging +import warnings + +from tvdb_exceptions import tvdb_userabort_v1 + +def log(): + return logging.getLogger(__name__) + +class BaseUI: + """Default non-interactive UI, which auto-selects first results + """ + def __init__(self, config, log = None): + self.config = config + if log is not None: + warnings.warn("the UI's log parameter is deprecated, instead use\n" + "use import logging; logging.getLogger('ui').info('blah')\n" + "The self.log attribute will be removed in the next version") + self.log = logging.getLogger(__name__) + + def selectSeries(self, allSeries): + return allSeries[0] + + +class ConsoleUI(BaseUI): + """Interactively allows the user to select a show from a console based UI + """ + + def _displaySeries(self, allSeries, limit = 6): + """Helper function, lists series with corresponding ID + """ + if limit is not None: + toshow = allSeries[:limit] + else: + toshow = allSeries + + print "TVDB Search Results:" + for i, cshow in enumerate(toshow): + i_show = i + 1 # Start at more human readable number 1 (not 0) + log().debug('Showing allSeries[%s], series %s)' % (i_show, allSeries[i]['seriesname'])) + if i == 0: + extra = " (default)" + else: + extra = "" + + print "%s -> %s [%s] # http://thetvdb.com/?tab=series&id=%s&lid=%s%s" % ( + i_show, + cshow['seriesname'].encode("UTF-8", "ignore"), + cshow['language'].encode("UTF-8", "ignore"), + str(cshow['id']), + cshow['lid'], + extra + ) + + def selectSeries(self, allSeries): + self._displaySeries(allSeries) + + if len(allSeries) == 1: + # Single result, return it! + print "Automatically selecting only result" + return allSeries[0] + + if self.config['select_first'] is True: + print "Automatically returning first search result" + return allSeries[0] + + while True: # return breaks this loop + try: + print "Enter choice (first number, return for default, 'all', ? for help):" + ans = raw_input() + except KeyboardInterrupt: + raise tvdb_userabort("User aborted (^c keyboard interupt)") + except EOFError: + raise tvdb_userabort("User aborted (EOF received)") + + log().debug('Got choice of: %s' % (ans)) + try: + selected_id = int(ans) - 1 # The human entered 1 as first result, not zero + except ValueError: # Input was not number + if len(ans.strip()) == 0: + # Default option + log().debug('Default option, returning first series') + return allSeries[0] + if ans == "q": + log().debug('Got quit command (q)') + raise tvdb_userabort("User aborted ('q' quit command)") + elif ans == "?": + print "## Help" + print "# Enter the number that corresponds to the correct show." + print "# a - display all results" + print "# all - display all results" + print "# ? - this help" + print "# q - abort tvnamer" + print "# Press return with no input to select first result" + elif ans.lower() in ["a", "all"]: + self._displaySeries(allSeries, limit = None) + else: + log().debug('Unknown keypress %s' % (ans)) + else: + log().debug('Trying to return ID: %d' % (selected_id)) + try: + return allSeries[selected_id] + except IndexError: + log().debug('Invalid show number entered!') + print "Invalid number (%s) selected!" + self._displaySeries(allSeries) + diff --git a/sickbeard/indexers/indexer_api.py b/sickbeard/indexers/indexer_api.py index 007bee87..b1057514 100644 --- a/sickbeard/indexers/indexer_api.py +++ b/sickbeard/indexers/indexer_api.py @@ -83,7 +83,9 @@ class indexerApi(object): def indexer(self, *args, **kwargs): if self.indexerID: - if indexerConfig[self.indexerID]['active']: + if indexerConfig[self.indexerID]['active'] or ('no_dummy' in kwargs and True is kwargs['no_dummy']): + if 'no_dummy' in kwargs: + kwargs.pop('no_dummy') return indexerConfig[self.indexerID]['module'](*args, **kwargs) else: return DummyIndexer(*args, **kwargs) diff --git a/sickbeard/indexers/indexer_config.py b/sickbeard/indexers/indexer_config.py index 2f508499..6c23fede 100644 --- a/sickbeard/indexers/indexer_config.py +++ b/sickbeard/indexers/indexer_config.py @@ -1,10 +1,14 @@ from lib.tvdb_api.tvdb_api import Tvdb +from lib.tvdb_api_v1.tvdb_api import TvdbV1 from lib.libtrakt.indexerapiinterface import TraktIndexer INDEXER_TVDB = 1 INDEXER_TVRAGE = 2 INDEXER_TVMAZE = 3 +# old tvdb api - version 1 +INDEXER_TVDB_V1 = 10001 + # mapped only indexer INDEXER_IMDB = 100 INDEXER_TRAKT = 101 @@ -29,6 +33,17 @@ indexerConfig = { mapped_only=False, icon='thetvdb16.png', ), + INDEXER_TVDB_V1: dict( + main_url='https://thetvdb.com/', + id=INDEXER_TVDB_V1, + name='TheTVDBV1', + module=TvdbV1, + api_params=dict(apikey='F9C450E78D99172E', language='en'), + active=False, + dupekey='', + mapped_only=True, + icon='thetvdb16.png', + ), INDEXER_TVRAGE: dict( main_url='http://tvrage.com/', id=INDEXER_TVRAGE, @@ -97,6 +112,15 @@ indexerConfig[info_src].update(dict( xem_origin='tvdb', )) +info_src = INDEXER_TVDB_V1 +indexerConfig[info_src].update(dict( + base_url=(indexerConfig[info_src]['main_url'] + + 'api/%(apikey)s/series/' % indexerConfig[info_src]['api_params']), + show_url='%s?tab=series&id=' % indexerConfig[info_src]['main_url'], + finder=(indexerConfig[info_src]['main_url'] + + 'index.php?fieldlocation=2&language=7&order=translation&searching=Search&tab=advancedsearch&seriesname=%s'), +)) + info_src = INDEXER_TVRAGE indexerConfig[info_src].update(dict( base_url=(indexerConfig[info_src]['main_url'] + diff --git a/sickbeard/indexers/indexer_exceptions.py b/sickbeard/indexers/indexer_exceptions.py index 173c9422..7587b9a9 100644 --- a/sickbeard/indexers/indexer_exceptions.py +++ b/sickbeard/indexers/indexer_exceptions.py @@ -9,6 +9,10 @@ from lib.tvdb_api.tvdb_exceptions import \ tvdb_exception, tvdb_attributenotfound, tvdb_episodenotfound, tvdb_error, \ tvdb_seasonnotfound, tvdb_shownotfound, tvdb_userabort, tvdb_tokenexpired +from lib.tvdb_api_v1.tvdb_exceptions import \ + tvdb_exception_v1, tvdb_attributenotfound_v1, tvdb_episodenotfound_v1, tvdb_error_v1, \ + tvdb_seasonnotfound_v1, tvdb_shownotfound_v1, tvdb_userabort_v1 + indexerExcepts = [ 'indexer_exception', 'indexer_error', 'indexer_userabort', 'indexer_shownotfound', 'indexer_seasonnotfound', 'indexer_episodenotfound', @@ -19,12 +23,16 @@ tvdbExcepts = [ 'tvdb_seasonnotfound', 'tvdb_episodenotfound', 'tvdb_attributenotfound', 'tvdb_tokenexpired'] +tvdbV1Excepts = [ + 'tvdb_exception_v1', 'tvdb_error_v1', 'tvdb_userabort_v1', 'tvdb_shownotfound_v1', + 'tvdb_seasonnotfound_v1', 'tvdb_episodenotfound_v1', 'tvdb_attributenotfound_v1'] + # link API exceptions to our exception handler -indexer_exception = tvdb_exception -indexer_error = tvdb_error +indexer_exception = tvdb_exception, tvdb_exception_v1 +indexer_error = tvdb_error, tvdb_error_v1 indexer_authenticationerror = tvdb_tokenexpired -indexer_userabort = tvdb_userabort -indexer_attributenotfound = tvdb_attributenotfound -indexer_episodenotfound = tvdb_episodenotfound -indexer_seasonnotfound = tvdb_seasonnotfound -indexer_shownotfound = tvdb_shownotfound \ No newline at end of file +indexer_userabort = tvdb_userabort, tvdb_userabort_v1 +indexer_attributenotfound = tvdb_attributenotfound, tvdb_attributenotfound_v1 +indexer_episodenotfound = tvdb_episodenotfound, tvdb_episodenotfound_v1 +indexer_seasonnotfound = tvdb_seasonnotfound, tvdb_seasonnotfound_v1 +indexer_shownotfound = tvdb_shownotfound, tvdb_shownotfound_v1 \ No newline at end of file diff --git a/sickbeard/metadata/generic.py b/sickbeard/metadata/generic.py index 7d69ecb5..08883cd9 100644 --- a/sickbeard/metadata/generic.py +++ b/sickbeard/metadata/generic.py @@ -39,6 +39,7 @@ from sickbeard import encodingKludge as ek from sickbeard.exceptions import ex from sickbeard.show_name_helpers import allPossibleShowNames from sickbeard.indexers import indexer_config +from sickbeard.indexers.indexer_config import INDEXER_TVDB, INDEXER_TVDB_V1 from six import iteritems @@ -397,12 +398,30 @@ class GenericMetadata(): # try all included episodes in case some have thumbs and others don't for cur_ep in all_eps: - myEp = helpers.validateShow(cur_ep.show, cur_ep.season, cur_ep.episode) + if INDEXER_TVDB == cur_ep.show.indexer: + indexer_lang = cur_ep.show.lang + + try: + lINDEXER_API_PARMS = sickbeard.indexerApi(INDEXER_TVDB_V1).api_params.copy() + lINDEXER_API_PARMS['dvdorder'] = 0 != cur_ep.show.dvdorder + lINDEXER_API_PARMS['no_dummy'] = True + + if indexer_lang and not indexer_lang == 'en': + lINDEXER_API_PARMS['language'] = indexer_lang + + t = sickbeard.indexerApi(INDEXER_TVDB_V1).indexer(**lINDEXER_API_PARMS) + + myEp = t[cur_ep.show.indexerid][cur_ep.season][cur_ep.episode] + except (sickbeard.indexer_episodenotfound, sickbeard.indexer_seasonnotfound, TypeError): + myEp = None + else: + myEp = helpers.validateShow(cur_ep.show, cur_ep.season, cur_ep.episode) + if not myEp: continue - thumb_url = getattr(myEp, 'filename', None) - if thumb_url is not None: + thumb_url = getattr(myEp, 'filename', None) or (isinstance(myEp, dict) and myEp.get('filename', None)) + if thumb_url not in (None, False, ''): return thumb_url return None