usenetbinary-newsreaderquickboxtraktkodistabletvshowsqnaptautullifanartsickbeardtvseriesplexswizzinembyseedboxtvdbnzbgetsubtitlewebui
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
832 lines
34 KiB
832 lines
34 KiB
# !/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/)
|
|
|
|
from functools import wraps
|
|
|
|
__author__ = 'dbr/Ben'
|
|
__version__ = '2.0'
|
|
__api_version__ = '3.0.0'
|
|
|
|
import os
|
|
import time
|
|
import getpass
|
|
import tempfile
|
|
import warnings
|
|
import logging
|
|
import requests
|
|
import requests.exceptions
|
|
import datetime
|
|
import re
|
|
|
|
from six import integer_types, string_types, iteritems, PY2
|
|
from _23 import list_values, map_list
|
|
from sg_helpers import clean_data, try_int, get_url
|
|
from collections import OrderedDict
|
|
from tvinfo_base import TVInfoBase, CastList, Character, CrewList, Person, RoleTypes
|
|
|
|
from lib.dateutil.parser import parse
|
|
from lib.cachecontrol import CacheControl, caches
|
|
from lib.exceptions_helper import ConnectionSkipException
|
|
|
|
from .tvdb_ui import BaseUI, ConsoleUI
|
|
from .tvdb_exceptions import TvdbError, TvdbShownotfound, TvdbTokenexpired
|
|
|
|
# noinspection PyUnreachableCode
|
|
if False:
|
|
# noinspection PyUnresolvedReferences
|
|
from typing import Any, AnyStr, Dict, List, Optional
|
|
from tvinfo_base import TVInfoShow
|
|
|
|
|
|
THETVDB_V2_API_TOKEN = {'token': None, 'datetime': datetime.datetime.fromordinal(1)}
|
|
log = logging.getLogger('tvdb.api')
|
|
log.addHandler(logging.NullHandler())
|
|
|
|
|
|
# noinspection PyUnusedLocal
|
|
def _record_hook(r, *args, **kwargs):
|
|
r.hook_called = True
|
|
if 301 == r.status_code and isinstance(r.headers.get('Location'), string_types) \
|
|
and r.headers.get('Location').startswith('http://api.thetvdb.com/'):
|
|
r.headers['Location'] = r.headers['Location'].replace('http://', 'https://')
|
|
return r
|
|
|
|
|
|
def retry(exception_to_check, tries=4, delay=3, backoff=2):
|
|
"""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 exception_to_check: the exception to check. may be a tuple of
|
|
exceptions to check
|
|
:type exception_to_check: 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
|
|
"""
|
|
|
|
def deco_retry(f):
|
|
|
|
@wraps(f)
|
|
def f_retry(*args, **kwargs):
|
|
mtries, mdelay = tries, delay
|
|
auth_error = 0
|
|
while 1 < mtries:
|
|
try:
|
|
return f(*args, **kwargs)
|
|
except exception_to_check as e:
|
|
msg = '%s, Retrying in %d seconds...' % (str(e), mdelay)
|
|
log.warning(msg)
|
|
time.sleep(mdelay)
|
|
if isinstance(e, TvdbTokenexpired) and not auth_error:
|
|
auth_error += 1
|
|
else:
|
|
mtries -= 1
|
|
mdelay *= backoff
|
|
except ConnectionSkipException as e:
|
|
raise e
|
|
try:
|
|
return f(*args, **kwargs)
|
|
except TvdbTokenexpired:
|
|
if not auth_error:
|
|
return f(*args, **kwargs)
|
|
raise TvdbTokenexpired
|
|
except ConnectionSkipException as e:
|
|
raise e
|
|
|
|
return f_retry # true decorator
|
|
|
|
return deco_retry
|
|
|
|
|
|
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 '<Actor "%r">' % self.get('name')
|
|
|
|
|
|
class Tvdb(TVInfoBase):
|
|
"""Create easy-to-use interface to name of season/episode name
|
|
>> t = Tvdb()
|
|
>> t['Scrubs'][1][24]['episodename']
|
|
u'My Last Day'
|
|
"""
|
|
|
|
# noinspection PyUnusedLocal
|
|
def __init__(self,
|
|
interactive=False,
|
|
select_first=False,
|
|
debug=False,
|
|
cache=True,
|
|
banners=False,
|
|
fanart=False,
|
|
posters=False,
|
|
seasons=False,
|
|
seasonwides=False,
|
|
actors=False,
|
|
custom_ui=None,
|
|
language=None,
|
|
search_all_languages=False,
|
|
apikey=None,
|
|
dvdorder=False,
|
|
proxy=None,
|
|
*args,
|
|
**kwargs):
|
|
|
|
"""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
|
|
|
|
"""
|
|
|
|
super(Tvdb, self).__init__(*args, **kwargs)
|
|
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['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, string_types):
|
|
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['posters_enabled'] = posters
|
|
self.config['seasons_enabled'] = seasons
|
|
self.config['seasonwides_enabled'] = seasonwides
|
|
self.config['fanart_enabled'] = fanart
|
|
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 not 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'] = 'https://api.thetvdb.com/'
|
|
|
|
self.config['url_search_series'] = '%(base_url)s/search/series' % self.config
|
|
self.config['params_search_series'] = {'name': ''}
|
|
|
|
self.config['url_series_episodes_info'] = '%(base_url)sseries/%%s/episodes?page=%%s' % self.config
|
|
|
|
self.config['url_series_info'] = '%(base_url)sseries/%%s' % self.config
|
|
self.config['url_episodes_info'] = '%(base_url)sepisodes/%%s' % self.config
|
|
self.config['url_actors_info'] = '%(base_url)sseries/%%s/actors' % self.config
|
|
|
|
self.config['url_series_images'] = '%(base_url)sseries/%%s/images/query?keyType=%%s' % self.config
|
|
self.config['url_artworks'] = 'https://artworks.thetvdb.com/banners/%s'
|
|
|
|
def _search_show(self, name, **kwargs):
|
|
# type: (AnyStr, Optional[Any]) -> List[TVInfoShow]
|
|
def map_data(data):
|
|
data['poster'] = data.get('image')
|
|
return data
|
|
|
|
return map_list(map_data, self.get_series(name))
|
|
|
|
def get_new_token(self):
|
|
global THETVDB_V2_API_TOKEN
|
|
token = THETVDB_V2_API_TOKEN.get('token', None)
|
|
dt = THETVDB_V2_API_TOKEN.get('datetime', datetime.datetime.fromordinal(1))
|
|
url = '%s%s' % (self.config['base_url'], 'login')
|
|
params = {'apikey': self.config['apikey']}
|
|
resp = get_url(url.strip(), post_json=params, parse_json=True, raise_skip_exception=True)
|
|
if resp:
|
|
if 'token' in resp:
|
|
token = resp['token']
|
|
dt = datetime.datetime.now()
|
|
|
|
return {'token': token, 'datetime': dt}
|
|
|
|
def get_token(self):
|
|
global THETVDB_V2_API_TOKEN
|
|
if None is THETVDB_V2_API_TOKEN.get(
|
|
'token') or datetime.datetime.now() - THETVDB_V2_API_TOKEN.get(
|
|
'datetime', datetime.datetime.fromordinal(1)) > datetime.timedelta(hours=23):
|
|
THETVDB_V2_API_TOKEN = self.get_new_token()
|
|
if not THETVDB_V2_API_TOKEN.get('token'):
|
|
raise TvdbError('Could not get Authentification Token')
|
|
return THETVDB_V2_API_TOKEN.get('token')
|
|
|
|
@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)
|
|
|
|
def _match_url_pattern(self, pattern, url):
|
|
if pattern in self.config:
|
|
try:
|
|
if PY2:
|
|
return None is not re.search('^%s$' % re.escape(self.config[pattern]).replace('\\%s', '[^/]+'), url)
|
|
else:
|
|
return None is not re.search('^%s$' % re.escape(self.config[pattern]).replace(r'%s', '[^/]+'), url)
|
|
except (BaseException, Exception):
|
|
pass
|
|
return False
|
|
|
|
@retry((TvdbError, TvdbTokenexpired))
|
|
def _load_url(self, url, params=None, language=None):
|
|
log.debug('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']:
|
|
log.debug('Using proxy for URL: %s' % url)
|
|
session.proxies = {'http': self.config['proxy'], 'https': self.config['proxy']}
|
|
|
|
headers = {'Accept-Encoding': 'gzip,deflate', 'Authorization': 'Bearer %s' % self.get_token(),
|
|
'Accept': 'application/vnd.thetvdb.v%s' % __api_version__}
|
|
|
|
if None is not language and language in self.config['valid_languages']:
|
|
headers.update({'Accept-Language': language})
|
|
|
|
resp = None
|
|
is_series_info = self._match_url_pattern('url_series_info', url)
|
|
if is_series_info:
|
|
self.show_not_found = False
|
|
self.not_found = False
|
|
try:
|
|
resp = get_url(url.strip(), params=params, session=session, headers=headers, parse_json=True,
|
|
raise_status_code=True, raise_exceptions=True, raise_skip_exception=True)
|
|
except ConnectionSkipException as e:
|
|
raise e
|
|
except requests.exceptions.HTTPError as e:
|
|
if 401 == e.response.status_code:
|
|
# token expired, get new token, raise error to retry
|
|
global THETVDB_V2_API_TOKEN
|
|
THETVDB_V2_API_TOKEN = self.get_new_token()
|
|
raise TvdbTokenexpired
|
|
elif 404 == e.response.status_code:
|
|
if is_series_info:
|
|
self.show_not_found = True
|
|
elif self._match_url_pattern('url_series_episodes_info', url):
|
|
resp = {'data': []}
|
|
self.not_found = True
|
|
elif 404 != e.response.status_code:
|
|
raise TvdbError
|
|
except (BaseException, Exception):
|
|
raise TvdbError
|
|
|
|
if is_series_info and isinstance(resp, dict) and isinstance(resp.get('data'), dict) and \
|
|
isinstance(resp['data'].get('seriesName'), string_types) and \
|
|
re.search(r'^[*]\s*[*]\s*[*]', resp['data'].get('seriesName', ''), flags=re.I):
|
|
self.show_not_found = True
|
|
self.not_found = True
|
|
|
|
map_show = {'airstime': 'airs_time', 'airsdayofweek': 'airs_dayofweek', 'imdbid': 'imdb_id',
|
|
'writers': 'writer', 'siterating': 'rating'}
|
|
|
|
def map_show_keys(data):
|
|
keep_data = {}
|
|
del_keys = []
|
|
new_data = {}
|
|
for k, v in iteritems(data):
|
|
k_org = k
|
|
k = k.lower()
|
|
if None is not v:
|
|
if k in ['banner', 'fanart', 'poster'] and v:
|
|
v = self.config['url_artworks'] % v
|
|
elif 'genre' == k:
|
|
keep_data['genre_list'] = v
|
|
v = '|%s|' % '|'.join([clean_data(c) for c in v if isinstance(c, string_types)])
|
|
elif 'gueststars' == k:
|
|
keep_data['gueststars_list'] = v
|
|
v = '|%s|' % '|'.join([clean_data(c) for c in v if isinstance(c, string_types)])
|
|
elif 'writers' == k:
|
|
keep_data[k] = v
|
|
v = '|%s|' % '|'.join([clean_data(c) for c in v if isinstance(c, string_types)])
|
|
elif 'rating' == k:
|
|
new_data['contentrating'] = v
|
|
elif 'firstaired' == k:
|
|
if v:
|
|
try:
|
|
v = parse(v, fuzzy=True).strftime('%Y-%m-%d')
|
|
except (BaseException, Exception):
|
|
v = None
|
|
else:
|
|
v = None
|
|
elif 'imdbid' == k:
|
|
if v:
|
|
if re.search(r'^(tt)?\d{1,9}$', v, flags=re.I):
|
|
v = clean_data(v)
|
|
else:
|
|
v = ''
|
|
else:
|
|
v = clean_data(v)
|
|
|
|
if not v and 'seriesname' == k:
|
|
if isinstance(data.get('aliases'), list) and 0 < len(data.get('aliases')):
|
|
v = data['aliases'].pop(0)
|
|
# this is a invalid show, it has no Name
|
|
if not v:
|
|
return None
|
|
|
|
if k in map_show:
|
|
k = map_show[k]
|
|
if k_org is not k:
|
|
del_keys.append(k_org)
|
|
new_data[k] = v
|
|
else:
|
|
data[k] = v
|
|
for d in del_keys:
|
|
del (data[d])
|
|
if isinstance(data, dict):
|
|
data.update(new_data)
|
|
data.update(keep_data)
|
|
return data
|
|
|
|
if resp:
|
|
if isinstance(resp['data'], dict):
|
|
resp['data'] = map_show_keys(resp['data'])
|
|
elif isinstance(resp['data'], list):
|
|
data_list = []
|
|
for idx, row in enumerate(resp['data']):
|
|
if isinstance(row, dict):
|
|
cr = map_show_keys(row)
|
|
if None is not cr:
|
|
data_list.append(cr)
|
|
resp['data'] = data_list
|
|
return resp
|
|
return dict([(u'data', None)])
|
|
|
|
def _getetsrc(self, url, params=None, language=None):
|
|
"""Loads a URL using caching
|
|
"""
|
|
try:
|
|
src = self._load_url(url, params=params, language=language)
|
|
if isinstance(src, dict):
|
|
if None is not src['data']:
|
|
data = src['data']
|
|
else:
|
|
data = {}
|
|
# data = src['data'] or {}
|
|
if isinstance(data, list):
|
|
if 0 < len(data):
|
|
data = data[0]
|
|
# data = data[0] or {}
|
|
if None is data or (isinstance(data, dict) and 1 > len(data.keys())):
|
|
raise ValueError
|
|
return src
|
|
except (KeyError, IndexError, Exception):
|
|
pass
|
|
|
|
def search(self, series):
|
|
# type: (AnyStr) -> List
|
|
"""This searches TheTVDB.com for the series name
|
|
and returns the result list
|
|
"""
|
|
if PY2:
|
|
series = series.encode('utf-8')
|
|
self.config['params_search_series']['name'] = series
|
|
log.debug('Searching for show %s' % series)
|
|
|
|
try:
|
|
series_found = self._getetsrc(self.config['url_search_series'], params=self.config['params_search_series'],
|
|
language=self.config['language'])
|
|
if series_found:
|
|
return list_values(series_found)[0]
|
|
except (BaseException, 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):
|
|
log.debug('Series result returned zero')
|
|
raise TvdbShownotfound('Show-name search returned zero results (cannot find show on TVDB)')
|
|
|
|
if None is not self.config['custom_ui']:
|
|
log.debug('Using custom UI %s' % self.config['custom_ui'].__name__)
|
|
custom_ui = self.config['custom_ui']
|
|
ui = custom_ui(config=self.config)
|
|
else:
|
|
if not self.config['interactive']:
|
|
log.debug('Auto-selecting first search result using BaseUI')
|
|
ui = BaseUI(config=self.config)
|
|
else:
|
|
log.debug('Interactively selecting show using ConsoleUI')
|
|
ui = ConsoleUI(config=self.config)
|
|
|
|
return ui.select_series(all_series)
|
|
|
|
def _parse_banners(self, sid, img_list):
|
|
banners = {}
|
|
|
|
try:
|
|
for cur_banner in img_list:
|
|
bid = cur_banner['id']
|
|
btype = (cur_banner['keytype'], 'banner')['series' == cur_banner['keytype']]
|
|
btype2 = (cur_banner['resolution'], try_int(cur_banner['subkey'], cur_banner['subkey']))[
|
|
btype in ('season', 'seasonwide')]
|
|
if None is btype or None is btype2:
|
|
continue
|
|
|
|
for k, v in iteritems(cur_banner):
|
|
if None is k or None is v:
|
|
continue
|
|
|
|
k, v = k.lower(), v.lower() if isinstance(v, string_types) else v
|
|
if 'filename' == k:
|
|
k = 'bannerpath'
|
|
v = self.config['url_artworks'] % v
|
|
elif 'thumbnail' == k:
|
|
k = 'thumbnailpath'
|
|
v = self.config['url_artworks'] % v
|
|
elif 'keytype' == k:
|
|
k = 'bannertype'
|
|
banners.setdefault(btype, OrderedDict()).setdefault(btype2, OrderedDict()).setdefault(bid, {})[
|
|
k] = v
|
|
|
|
except (BaseException, Exception):
|
|
pass
|
|
|
|
self._set_show_data(sid, '_banners', banners, add=True)
|
|
|
|
def _parse_actors(self, sid, actor_list):
|
|
|
|
a = []
|
|
cast = CastList()
|
|
try:
|
|
for n in sorted(actor_list, key=lambda x: x['sortorder']):
|
|
role_image = (None, self.config['url_artworks'] % n.get('image'))[any([n.get('image')])]
|
|
character_name = n.get('role', '').strip()
|
|
person_name = n.get('name', '').strip()
|
|
character_id = n.get('id', None)
|
|
a.append({'character': {'id': character_id,
|
|
'name': character_name,
|
|
'url': None, # not supported by tvdb
|
|
'image': role_image,
|
|
},
|
|
'person': {'id': None,
|
|
'name': person_name,
|
|
'url': None, # not supported by tvdb
|
|
'image': None, # not supported by tvdb
|
|
'birthday': None, # not supported by tvdb
|
|
'deathday': None, # not supported by tvdb
|
|
'gender': None, # not supported by tvdb
|
|
'country': None, # not supported by tvdb
|
|
},
|
|
})
|
|
cast[RoleTypes.ActorMain].append(
|
|
Character(p_id=character_id, name=character_name,
|
|
person=Person(name=person_name), image=role_image))
|
|
except (BaseException, Exception):
|
|
pass
|
|
self._set_show_data(sid, 'actors', a)
|
|
self._set_show_data(sid, 'cast', cast)
|
|
self.shows[sid].actors_loaded = True
|
|
|
|
def get_episode_data(self, epid):
|
|
# Parse episode information
|
|
data = None
|
|
log.debug('Getting all episode data for %s' % epid)
|
|
url = self.config['url_episodes_info'] % epid
|
|
episode_data = self._getetsrc(url, language=self.config['language'])
|
|
|
|
if episode_data and 'data' in episode_data:
|
|
data = episode_data['data']
|
|
if isinstance(data, dict):
|
|
for k, v in iteritems(data):
|
|
k = k.lower()
|
|
|
|
if None is not v:
|
|
if 'filename' == k and v:
|
|
v = self.config['url_artworks'] % v
|
|
else:
|
|
v = clean_data(v)
|
|
data[k] = v
|
|
|
|
return data
|
|
|
|
def _parse_images(self, sid, language, show_data, image_type, enabled_type):
|
|
mapped_img_types = {'banner': 'series'}
|
|
excluded_main_data = enabled_type in ['seasons_enabled', 'seasonwides_enabled']
|
|
loaded_name = '%s_loaded' % image_type
|
|
if self.config[enabled_type] and not getattr(self.shows.get(sid), loaded_name, False):
|
|
image_data = self._getetsrc(self.config['url_series_images'] %
|
|
(sid, mapped_img_types.get(image_type, image_type)), language=language)
|
|
if image_data and 0 < len(image_data.get('data', '') or ''):
|
|
image_data['data'] = sorted(image_data['data'], reverse=True,
|
|
key=lambda x: (x['ratingsinfo']['average'], x['ratingsinfo']['count']))
|
|
if not excluded_main_data:
|
|
url_image = self.config['url_artworks'] % image_data['data'][0]['filename']
|
|
url_thumb = self.config['url_artworks'] % image_data['data'][0]['thumbnail']
|
|
self._set_show_data(sid, image_type, url_image)
|
|
self._set_show_data(sid, u'%s_thumb' % image_type, url_thumb)
|
|
excluded_main_data = True # artwork found so prevent fallback
|
|
self._parse_banners(sid, image_data['data'])
|
|
self.shows[sid].__dict__[loaded_name] = True
|
|
|
|
# fallback image thumbnail for none excluded_main_data if artwork is not found
|
|
if not excluded_main_data and show_data['data'].get(image_type):
|
|
self._set_show_data(sid, u'%s_thumb' % image_type,
|
|
re.sub(r'\.jpg$', '_t.jpg', show_data['data'][image_type], flags=re.I))
|
|
|
|
def _get_show_data(self, sid, language, get_ep_info=False, **kwargs):
|
|
# type: (integer_types, AnyStr, bool, Optional[Any]) -> bool
|
|
"""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]
|
|
"""
|
|
|
|
# Parse show information
|
|
url = self.config['url_series_info'] % sid
|
|
if sid not in self.shows or None is self.shows[sid].id:
|
|
log.debug('Getting all series data for %s' % sid)
|
|
show_data = self._getetsrc(url, language=language)
|
|
|
|
# check and make sure we have data to process and that it contains a series name
|
|
if not (show_data and 'seriesname' in show_data.get('data', {}) or {}):
|
|
return False
|
|
|
|
for k, v in iteritems(show_data['data']):
|
|
self._set_show_data(sid, k, v)
|
|
else:
|
|
show_data = {'data': {}}
|
|
|
|
for img_type, en_type in [(u'poster', 'posters_enabled'), (u'banner', 'banners_enabled'),
|
|
(u'fanart', 'fanart_enabled'), (u'season', 'seasons_enabled'),
|
|
(u'seasonwide', 'seasonwides_enabled')]:
|
|
self._parse_images(sid, language, show_data, img_type, en_type)
|
|
|
|
if self.config['actors_enabled'] and not getattr(self.shows.get(sid), 'actors_loaded', False):
|
|
actor_data = self._getetsrc(self.config['url_actors_info'] % sid, language=language)
|
|
if actor_data and 0 < len(actor_data.get('data', '') or ''):
|
|
self._parse_actors(sid, actor_data['data'])
|
|
|
|
if get_ep_info and not getattr(self.shows.get(sid), 'ep_loaded', False):
|
|
# Parse episode data
|
|
log.debug('Getting all episodes of %s' % sid)
|
|
|
|
page = 1
|
|
episodes = []
|
|
while page <= 400:
|
|
episode_data = self._getetsrc(self.config['url_series_episodes_info'] % (sid, page), language=language)
|
|
if None is episode_data:
|
|
raise TvdbError('Exception retrieving episodes for show')
|
|
if isinstance(episode_data, dict) and not episode_data.get('data', []):
|
|
if 1 != page:
|
|
self.not_found = False
|
|
break
|
|
if not getattr(self, 'not_found', False) and None is not episode_data.get('data'):
|
|
episodes.extend(episode_data['data'])
|
|
next_link = episode_data.get('links', {}).get('next', None)
|
|
# check if page is a valid following page
|
|
if not isinstance(next_link, integer_types) or next_link <= page:
|
|
next_link = None
|
|
if not next_link and isinstance(episode_data, dict) \
|
|
and isinstance(episode_data.get('data', []), list) and 100 > len(episode_data.get('data', [])):
|
|
break
|
|
if next_link:
|
|
page = next_link
|
|
else:
|
|
page += 1
|
|
|
|
ep_map_keys = {'absolutenumber': u'absolute_number', 'airedepisodenumber': u'episodenumber',
|
|
'airedseason': u'seasonnumber', 'airedseasonid': u'seasonid',
|
|
'dvdepisodenumber': u'dvd_episodenumber', 'dvdseason': u'dvd_season'}
|
|
|
|
for cur_ep in episodes:
|
|
if self.config['dvdorder']:
|
|
log.debug('Using DVD ordering.')
|
|
use_dvd = None is not cur_ep.get('dvdseason') and None is not cur_ep.get('dvdepisodenumber')
|
|
else:
|
|
use_dvd = False
|
|
|
|
if use_dvd:
|
|
elem_seasnum, elem_epno = cur_ep.get('dvdseason'), cur_ep.get('dvdepisodenumber')
|
|
else:
|
|
elem_seasnum, elem_epno = cur_ep.get('airedseason'), cur_ep.get('airedepisodenumber')
|
|
|
|
if None is elem_seasnum or None is elem_epno:
|
|
log.warning('An episode has incomplete season/episode number (season: %r, episode: %r)' % (
|
|
elem_seasnum, elem_epno))
|
|
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))
|
|
|
|
for k, v in iteritems(cur_ep):
|
|
k = k.lower()
|
|
|
|
if None is not v:
|
|
if 'filename' == k and v:
|
|
v = self.config['url_artworks'] % v
|
|
else:
|
|
v = clean_data(v)
|
|
|
|
if k in ep_map_keys:
|
|
k = ep_map_keys[k]
|
|
self._set_item(sid, seas_no, ep_no, k, v)
|
|
|
|
crew = CrewList()
|
|
cast = CastList()
|
|
try:
|
|
for director in cur_ep.get('directors', []):
|
|
crew[RoleTypes.CrewDirector].append(Person(name=director))
|
|
except (BaseException, Exception):
|
|
pass
|
|
try:
|
|
for guest in cur_ep.get('gueststars_list', []):
|
|
cast[RoleTypes.ActorGuest].append(Character(person=Person(name=guest)))
|
|
except (BaseException, Exception):
|
|
pass
|
|
try:
|
|
for writers in cur_ep.get('writers', []):
|
|
crew[RoleTypes.CrewWriter].append(Person(name=writers))
|
|
except (BaseException, Exception):
|
|
pass
|
|
self._set_item(sid, seas_no, ep_no, 'crew', crew)
|
|
self._set_item(sid, seas_no, ep_no, 'cast', cast)
|
|
|
|
self.shows[sid].ep_loaded = True
|
|
|
|
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:
|
|
log.debug('Correcting %s to %s' % (name, self.corrections[name]))
|
|
return self.corrections[name]
|
|
else:
|
|
log.debug('Getting show %s' % name)
|
|
selected_series = self.get_series(name)
|
|
if isinstance(selected_series, dict):
|
|
selected_series = [selected_series]
|
|
sids = [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 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()
|
|
|