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 00d80616..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
@@ -592,12 +611,10 @@ class GenericMetadata():
show_obj: a TVShow object for which to save the season thumbs
- Cycles through all seasons and saves the season posters if possible. This
- method should not need to be overridden by implementing classes, changing
- _season_posters_dict and get_season_poster_path should be good enough.
+ Cycles through all seasons and saves the season posters if possible.
"""
- season_dict = self._season_posters_dict(show_obj, season)
+ season_dict = self._season_image_dict(show_obj, season, 'seasons')
result = []
# Returns a nested dictionary of season art with the season
@@ -607,7 +624,7 @@ class GenericMetadata():
cur_season_art = season_dict[cur_season]
- if len(cur_season_art) == 0:
+ if 0 == len(cur_season_art):
continue
# Just grab whatever's there for now
@@ -616,23 +633,21 @@ class GenericMetadata():
season_poster_file_path = self.get_season_poster_path(show_obj, cur_season)
if not season_poster_file_path:
- logger.log(u"Path for season " + str(cur_season) + " came back blank, skipping this season",
+ logger.log(u'Path for season ' + str(cur_season) + ' came back blank, skipping this season',
logger.DEBUG)
continue
- seasonData = metadata_helpers.getShowImage(season_url, showName=show_obj.name)
+ season_data = metadata_helpers.getShowImage(season_url, showName=show_obj.name)
- if not seasonData:
- logger.log(u"No season poster data available, skipping this season", logger.DEBUG)
+ if not season_data:
+ logger.log(u'No season poster data available, skipping this season', logger.DEBUG)
continue
- result = result + [self._write_image(seasonData, season_poster_file_path)]
+ result = result + [self._write_image(season_data, season_poster_file_path)]
+
if result:
return all(result)
- else:
- return False
-
- return True
+ return False
def save_season_banners(self, show_obj, season):
"""
@@ -640,12 +655,10 @@ class GenericMetadata():
show_obj: a TVShow object for which to save the season thumbs
- Cycles through all seasons and saves the season banners if possible. This
- method should not need to be overridden by implementing classes, changing
- _season_banners_dict and get_season_banner_path should be good enough.
+ Cycles through all seasons and saves the season banners if possible.
"""
- season_dict = self._season_banners_dict(show_obj, season)
+ season_dict = self._season_image_dict(show_obj, season, 'seasonwides')
result = []
# Returns a nested dictionary of season art with the season
@@ -655,7 +668,7 @@ class GenericMetadata():
cur_season_art = season_dict[cur_season]
- if len(cur_season_art) == 0:
+ if 0 == len(cur_season_art):
continue
# Just grab whatever's there for now
@@ -664,23 +677,21 @@ class GenericMetadata():
season_banner_file_path = self.get_season_banner_path(show_obj, cur_season)
if not season_banner_file_path:
- logger.log(u"Path for season " + str(cur_season) + " came back blank, skipping this season",
+ logger.log(u'Path for season ' + str(cur_season) + ' came back blank, skipping this season',
logger.DEBUG)
continue
- seasonData = metadata_helpers.getShowImage(season_url, showName=show_obj.name)
+ season_data = metadata_helpers.getShowImage(season_url, showName=show_obj.name)
- if not seasonData:
- logger.log(u"No season banner data available, skipping this season", logger.DEBUG)
+ if not season_data:
+ logger.log(u'No season banner data available, skipping this season', logger.DEBUG)
continue
- result = result + [self._write_image(seasonData, season_banner_file_path)]
+ result = result + [self._write_image(season_data, season_banner_file_path)]
+
if result:
return all(result)
- else:
- return False
-
- return True
+ return False
def save_season_all_poster(self, show_obj, which=None):
# use the default season all poster name
@@ -836,110 +847,42 @@ class GenericMetadata():
return None
- def _season_posters_dict(self, show_obj, season):
+ @staticmethod
+ def _season_image_dict(show_obj, season, image_type):
"""
+ image_type : Type of image to fetch, 'seasons' or 'seasonwides'
+ image_type type : String
+
Should return a dict like:
result = {:
{1: '', 2: , ...},}
"""
-
- # This holds our resulting dictionary of season art
result = {}
- indexer_lang = show_obj.lang
-
try:
# There's gotta be a better way of doing this but we don't wanna
# change the language value elsewhere
lINDEXER_API_PARMS = sickbeard.indexerApi(show_obj.indexer).api_params.copy()
- lINDEXER_API_PARMS['banners'] = True
+ lINDEXER_API_PARMS[image_type] = True
lINDEXER_API_PARMS['dvdorder'] = 0 != show_obj.dvdorder
- if indexer_lang and not indexer_lang == 'en':
- lINDEXER_API_PARMS['language'] = indexer_lang
+ if 'en' != getattr(show_obj, 'lang', None):
+ lINDEXER_API_PARMS['language'] = show_obj.lang
t = sickbeard.indexerApi(show_obj.indexer).indexer(**lINDEXER_API_PARMS)
indexer_show_obj = t[show_obj.indexerid]
except (sickbeard.indexer_error, IOError) as e:
- logger.log(u"Unable to look up show on " + sickbeard.indexerApi(
- show_obj.indexer).name + ", not downloading images: " + ex(e), logger.ERROR)
+ logger.log(u'Unable to look up show on ' + sickbeard.indexerApi(
+ show_obj.indexer).name + ', not downloading images: ' + ex(e), logger.ERROR)
return result
- # if we have no season banners then just finish
- if getattr(indexer_show_obj, '_banners', None) is None:
- return result
-
- if 'season' not in indexer_show_obj['_banners'] or 'season' not in indexer_show_obj['_banners']['season']:
- return result
-
- # Give us just the normal poster-style season graphics
- seasonsArtObj = indexer_show_obj['_banners']['season']['season']
-
- # Returns a nested dictionary of season art with the season
- # number as primary key. It's really overkill but gives the option
- # to present to user via ui to pick down the road.
-
- result[season] = {}
-
- # find the correct season in the TVDB and TVRAGE object and just copy the dict into our result dict
- for seasonArtID in seasonsArtObj.keys():
- if int(seasonsArtObj[seasonArtID]['season']) == season and seasonsArtObj[seasonArtID]['language'] == 'en':
- result[season][seasonArtID] = seasonsArtObj[seasonArtID]['_bannerpath']
-
- return result
-
- def _season_banners_dict(self, show_obj, season):
- """
- Should return a dict like:
-
- result = {:
- {1: '', 2: , ...},}
- """
-
- # This holds our resulting dictionary of season art
- result = {}
-
- indexer_lang = show_obj.lang
-
- try:
- # There's gotta be a better way of doing this but we don't wanna
- # change the language value elsewhere
- lINDEXER_API_PARMS = sickbeard.indexerApi(show_obj.indexer).api_params.copy()
- lINDEXER_API_PARMS['banners'] = True
- lINDEXER_API_PARMS['dvdorder'] = 0 != show_obj.dvdorder
-
- if indexer_lang and not indexer_lang == 'en':
- lINDEXER_API_PARMS['language'] = indexer_lang
-
- t = sickbeard.indexerApi(show_obj.indexer).indexer(**lINDEXER_API_PARMS)
- indexer_show_obj = t[show_obj.indexerid]
- except (sickbeard.indexer_error, IOError) as e:
- logger.log(u"Unable to look up show on " + sickbeard.indexerApi(
- show_obj.indexer).name + ", not downloading images: " + ex(e), logger.ERROR)
- return result
-
- # if we have no season banners then just finish
- if getattr(indexer_show_obj, '_banners', None) is None:
- return result
-
- # if we have no season banners then just finish
- if 'season' not in indexer_show_obj['_banners'] or 'seasonwide' not in indexer_show_obj['_banners']['season']:
- return result
-
- # Give us just the normal season graphics
- seasonsArtObj = indexer_show_obj['_banners']['season']['seasonwide']
-
- # Returns a nested dictionary of season art with the season
- # number as primary key. It's really overkill but gives the option
- # to present to user via ui to pick down the road.
-
- result[season] = {}
-
- # find the correct season in the TVDB and TVRAGE object and just copy the dict into our result dict
- for seasonArtID in seasonsArtObj.keys():
- if int(seasonsArtObj[seasonArtID]['season']) == season and seasonsArtObj[seasonArtID]['language'] == 'en':
- result[season][seasonArtID] = seasonsArtObj[seasonArtID]['_bannerpath']
+ season_images = getattr(indexer_show_obj, '_banners', {}).get(
+ ('season', 'seasonwide')['seasonwides' == image_type], {}).get(season, {})
+ for image_id in season_images.keys():
+ if season not in result:
+ result[season] = {}
+ result[season][image_id] = season_images[image_id]['_bannerpath']
return result