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

# !/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()