mirror of
https://github.com/SickGear/SickGear.git
synced 2024-12-25 12:13:38 +00:00
Add TVDb v4.
Add person search and get_person. Add id search. Add show characters. Add episodes. Add basic show info. Add TVDB_API_CONFIG to tvdb_api_v4 and assign it in indexer_config. Add auth to tvdb_api_v4. For sorting networks use '0000-00-00' instead of activeDate if not set. Add language support. Add new get_languages TVInfo Interface method that returns a list of dicts by the indexer supported languages and the sg_lang map code [{'id': 'lang code', 'name': 'english name', 'nativeName': 'native name', 'sg_lang': 'sg lang code'}]. Add all returned languages to webserve method. Use new interface parameter language for get_show. Add episode overview Add fallback to 'id' field which is str now for search tvdb_id. Add missing alias parsing. Filter out episode characters. Add IMDb and TMDB id search. Add IMDb search to person search. Add missing data to person and character objects. Add include error description if an error is raised in tvdb_api_v4. Add error handling for creating episode thumbs and nfo's, and on show level. Add absolute numbering. Add ids to person data. new mock data
This commit is contained in:
parent
9410f3f219
commit
79f0c829a7
253 changed files with 755 additions and 4 deletions
746
lib/api_tvdb/tvdb_api_v4.py
Normal file
746
lib/api_tvdb/tvdb_api_v4.py
Normal file
|
@ -0,0 +1,746 @@
|
||||||
|
# encoding:utf-8
|
||||||
|
# author:Prinz23
|
||||||
|
# project:tvdb_api_v4
|
||||||
|
|
||||||
|
__author__ = 'Prinz23'
|
||||||
|
__version__ = '1.0'
|
||||||
|
__api_version__ = '1.0.0'
|
||||||
|
|
||||||
|
import base64
|
||||||
|
import datetime
|
||||||
|
import logging
|
||||||
|
import re
|
||||||
|
|
||||||
|
import requests
|
||||||
|
from urllib3.util.retry import Retry
|
||||||
|
from requests.adapters import HTTPAdapter
|
||||||
|
|
||||||
|
from _23 import filter_iter
|
||||||
|
from exceptions_helper import ex
|
||||||
|
from six import integer_types, iteritems, PY3, string_types
|
||||||
|
from sg_helpers import clean_data, get_url, try_int
|
||||||
|
from lib.dateutil.parser import parser
|
||||||
|
# noinspection PyProtectedMember
|
||||||
|
from lib.exceptions_helper import ConnectionSkipException, ex
|
||||||
|
from lib.tvinfo_base import TVInfoBase, TVInfoImage, TVInfoImageSize, TVInfoImageType, Character, \
|
||||||
|
Person, RoleTypes, TVInfoShow, TVInfoEpisode, TVInfoIDs, TVInfoSeason, PersonGenders, \
|
||||||
|
TVINFO_FACEBOOK, TVINFO_TWITTER, TVINFO_INSTAGRAM, TVINFO_REDDIT, TVINFO_YOUTUBE, \
|
||||||
|
TVINFO_TVDB, TVInfoNetwork, TVInfoSocialIDs, CastList, TVINFO_TVDB_SLUG
|
||||||
|
from .tvdb_exceptions import TvdbTokenFailre, TvdbError
|
||||||
|
|
||||||
|
# noinspection PyUnreachableCode
|
||||||
|
if False:
|
||||||
|
from typing import Any, AnyStr, Dict, List, Optional, Tuple, Union
|
||||||
|
|
||||||
|
log = logging.getLogger('tvdb_v4.api')
|
||||||
|
log.addHandler(logging.NullHandler())
|
||||||
|
|
||||||
|
TVDB_API_CONFIG = {}
|
||||||
|
|
||||||
|
|
||||||
|
# always use https in cases of redirects
|
||||||
|
def _record_hook(r, *args, **kwargs):
|
||||||
|
r.hook_called = True
|
||||||
|
if r.status_code in (301, 302, 303, 307, 308) and isinstance(r.headers.get('Location'), string_types) \
|
||||||
|
and r.headers.get('Location').startswith('http://'):
|
||||||
|
r.headers['Location'] = r.headers['Location'].replace('http://', 'https://')
|
||||||
|
return r
|
||||||
|
|
||||||
|
|
||||||
|
class TvdbAuth(requests.auth.AuthBase):
|
||||||
|
_token = None
|
||||||
|
_auth_time = None
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def apikey():
|
||||||
|
string = TVDB_API_CONFIG['api_params']['apikey_v4']
|
||||||
|
key = TVDB_API_CONFIG['api_params']['apikey']
|
||||||
|
string = base64.urlsafe_b64decode(string + b'===')
|
||||||
|
string = string.decode('latin') if PY3 else string
|
||||||
|
encoded_chars = []
|
||||||
|
for i in range(len(string)):
|
||||||
|
key_c = key[i % len(key)]
|
||||||
|
encoded_c = chr((ord(string[i]) - ord(key_c) + 256) % 256)
|
||||||
|
encoded_chars.append(encoded_c)
|
||||||
|
encoded_string = ''.join(encoded_chars)
|
||||||
|
return encoded_string
|
||||||
|
|
||||||
|
def get_token(self):
|
||||||
|
url = '%s%s' % (Tvdb_API_V4.base_url, 'login')
|
||||||
|
params = {'apikey': self.apikey()}
|
||||||
|
resp = get_url(url, post_json=params, parse_json=True, raise_skip_exception=True)
|
||||||
|
if resp and isinstance(resp, dict):
|
||||||
|
if 'status' in resp:
|
||||||
|
if 'failure' == resp['status']:
|
||||||
|
raise TvdbTokenFailre('Failed to Authenticate. %s' % resp.get('message', ''))
|
||||||
|
if 'success' == resp['status'] and 'data' in resp and isinstance(resp['data'], dict) \
|
||||||
|
and 'token' in resp['data']:
|
||||||
|
self._token = resp['data']['token']
|
||||||
|
self._auth_time = datetime.datetime.now()
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
raise TvdbTokenFailre('Failed to get Tvdb Token')
|
||||||
|
|
||||||
|
@property
|
||||||
|
def token(self):
|
||||||
|
if not self._token or not self._auth_time:
|
||||||
|
self.get_token()
|
||||||
|
return self._token
|
||||||
|
|
||||||
|
def __call__(self, r):
|
||||||
|
r.headers["Authorization"] = "Bearer %s" % self.token
|
||||||
|
return r
|
||||||
|
|
||||||
|
|
||||||
|
DEFAULT_TIMEOUT = 30 # seconds
|
||||||
|
|
||||||
|
|
||||||
|
class TimeoutHTTPAdapter(HTTPAdapter):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
self.timeout = DEFAULT_TIMEOUT
|
||||||
|
if "timeout" in kwargs:
|
||||||
|
self.timeout = kwargs["timeout"]
|
||||||
|
del kwargs["timeout"]
|
||||||
|
super(TimeoutHTTPAdapter, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def send(self, request, **kwargs):
|
||||||
|
timeout = kwargs.get("timeout")
|
||||||
|
if timeout is None:
|
||||||
|
kwargs["timeout"] = self.timeout
|
||||||
|
return super(TimeoutHTTPAdapter, self).send(request, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
s = requests.Session()
|
||||||
|
retries = Retry(total=3,
|
||||||
|
backoff_factor=1,
|
||||||
|
status_forcelist=[429, 500, 502, 503, 504],
|
||||||
|
method_whitelist=['HEAD', 'GET', 'PUT', 'DELETE', 'OPTIONS', 'TRACE', 'POST'])
|
||||||
|
# noinspection HttpUrlsUsage
|
||||||
|
s.mount('http://', HTTPAdapter(TimeoutHTTPAdapter(max_retries=retries)))
|
||||||
|
s.mount('https://', HTTPAdapter(TimeoutHTTPAdapter(max_retries=retries)))
|
||||||
|
base_request_para = dict(session=s, hooks={'response': _record_hook}, raise_skip_exception=True, auth=TvdbAuth())
|
||||||
|
|
||||||
|
|
||||||
|
# Query TVdb endpoints
|
||||||
|
def tvdb_endpoint_get(*args, **kwargs):
|
||||||
|
kwargs.update(base_request_para)
|
||||||
|
return get_url(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
img_type_map = {
|
||||||
|
1: TVInfoImageType.banner, # series
|
||||||
|
2: TVInfoImageType.poster, # series
|
||||||
|
3: TVInfoImageType.fanart, # series
|
||||||
|
6: TVInfoImageType.season_banner, # season
|
||||||
|
7: TVInfoImageType.season_poster, # season
|
||||||
|
8: TVInfoImageType.season_fanart, # season
|
||||||
|
13: TVInfoImageType.person_poster, # person
|
||||||
|
}
|
||||||
|
|
||||||
|
empty_ep = TVInfoEpisode()
|
||||||
|
tz_p = parser()
|
||||||
|
|
||||||
|
|
||||||
|
class Tvdb_API_V4(TVInfoBase):
|
||||||
|
supported_id_searches = [TVINFO_TVDB, TVINFO_TVDB_SLUG, TVINFO_IMDB, TVINFO_TMDB]
|
||||||
|
supported_person_id_searches = [TVINFO_TVDB, TVINFO_IMDB]
|
||||||
|
base_url = 'https://api4.thetvdb.com/v4/'
|
||||||
|
|
||||||
|
def __init__(self, banners=False, posters=False, seasons=False, seasonwides=False, fanart=False, actors=False,
|
||||||
|
*args, **kwargs):
|
||||||
|
super(Tvdb_API_V4, self).__init__(banners, posters, seasons, seasonwides, fanart, actors, *args, **kwargs)
|
||||||
|
|
||||||
|
def _get_data(self, endpoint, **kwargs):
|
||||||
|
# type: (string_types, Any) -> Any
|
||||||
|
is_series_info, retry = endpoint.startswith('/series/'), kwargs.pop('token_retry', 1)
|
||||||
|
if retry > 3:
|
||||||
|
raise TvdbTokenFailre('Failed to get new token')
|
||||||
|
if is_series_info:
|
||||||
|
self.show_not_found = False
|
||||||
|
try:
|
||||||
|
return tvdb_endpoint_get(url='%s%s' % (self.base_url, endpoint), params=kwargs, parse_json=True,
|
||||||
|
raise_status_code=True, raise_exceptions=True)
|
||||||
|
except ConnectionSkipException as e:
|
||||||
|
raise e
|
||||||
|
except requests.exceptions.HTTPError as e:
|
||||||
|
if 401 == e.response.status_code:
|
||||||
|
# get new token
|
||||||
|
try:
|
||||||
|
if base_request_para['auth'].get_token():
|
||||||
|
retry += 1
|
||||||
|
kwargs['token_retry'] = retry
|
||||||
|
return self._get_data(endpoint, **kwargs)
|
||||||
|
except (BaseException, Exception):
|
||||||
|
pass
|
||||||
|
raise e
|
||||||
|
elif 404 == e.response.status_code:
|
||||||
|
if is_series_info:
|
||||||
|
self.show_not_found = True
|
||||||
|
self.not_found = True
|
||||||
|
elif 404 != e.response.status_code:
|
||||||
|
raise TvdbError(ex(e))
|
||||||
|
except (BaseException, Exception) as e:
|
||||||
|
raise TvdbError(ex(e))
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _convert_person(p):
|
||||||
|
# type: (Dict) -> List[Person]
|
||||||
|
ch = []
|
||||||
|
for c in sorted(filter_iter(lambda a: (3 == a['type'] or 'Actor' == a['peopleType']) and a['name']
|
||||||
|
and a['seriesId'],
|
||||||
|
p.get('characters') or []), key=lambda a: (not a['isFeatured'], a['sort'])):
|
||||||
|
show = TVInfoShow()
|
||||||
|
show.id = clean_data(c['seriesId'])
|
||||||
|
show.ids = TVInfoIDs(ids={TVINFO_TVDB: show.id})
|
||||||
|
ch.append(Character(id=c['id'], name=clean_data(c['name']), regular=c['isFeatured'],
|
||||||
|
ids={TVINFO_TVDB: c['id']}, image=c.get('image'), show=show))
|
||||||
|
try:
|
||||||
|
b_date = clean_data(p.get('birth'))
|
||||||
|
birthdate = (b_date and '0000-00-00' != b_date and tz_p.parse(b_date).date()) or None
|
||||||
|
except (BaseException, Exception):
|
||||||
|
birthdate = None
|
||||||
|
try:
|
||||||
|
d_date = clean_data(p.get('death'))
|
||||||
|
deathdate = (d_date and '0000-00-00' != d_date and tz_p.parse(d_date).date()) or None
|
||||||
|
except (BaseException, Exception):
|
||||||
|
deathdate = None
|
||||||
|
try:
|
||||||
|
p_tvdb_id = try_int(p.get('tvdb_id'), None) or try_int(re.sub(r'^.+-(\d+)$', r'\1', p['id']), None)
|
||||||
|
except (BaseException, Exception):
|
||||||
|
p_tvdb_id = None
|
||||||
|
|
||||||
|
ids, social_ids, official_site = {TVINFO_TVDB: p_tvdb_id}, {}, None
|
||||||
|
|
||||||
|
if 'remote_ids' in p and isinstance(p['remote_ids'], list):
|
||||||
|
for r_id in p['remote_ids']:
|
||||||
|
src_name = r_id['sourceName'].lower()
|
||||||
|
src_value = clean_data(r_id['id'])
|
||||||
|
if not src_value:
|
||||||
|
continue
|
||||||
|
if 'imdb' in src_name:
|
||||||
|
try:
|
||||||
|
imdb_id = try_int(('%s' % src_value).replace('nm', ''), None)
|
||||||
|
ids[TVINFO_IMDB] = imdb_id
|
||||||
|
except (BaseException, Exception):
|
||||||
|
pass
|
||||||
|
elif 'themoviedb' in src_name:
|
||||||
|
ids[TVINFO_TMDB] = try_int(src_value, None)
|
||||||
|
elif 'official website' in src_name:
|
||||||
|
official_site = src_value
|
||||||
|
elif 'facebook' in src_name:
|
||||||
|
social_ids[TVINFO_FACEBOOK] = src_value
|
||||||
|
elif 'twitter' in src_name:
|
||||||
|
social_ids[TVINFO_TWITTER] = src_value
|
||||||
|
elif 'instagram' in src_name:
|
||||||
|
social_ids[TVINFO_INSTAGRAM] = src_value
|
||||||
|
elif 'reddit' in src_name:
|
||||||
|
social_ids[TVINFO_REDDIT] = src_value
|
||||||
|
elif 'youtube' in src_name:
|
||||||
|
social_ids[TVINFO_YOUTUBE] = src_value
|
||||||
|
|
||||||
|
return [Person(p_id=p_tvdb_id, name=clean_data(p['name']),
|
||||||
|
image=p.get('image') or p.get('image_url'),
|
||||||
|
gender=PersonGenders.tvdb_map.get(p.get('gender'), PersonGenders.unknown),
|
||||||
|
birthdate=birthdate, deathdate=deathdate, birthplace=clean_data(p.get('birthPlace')),
|
||||||
|
akas=set(clean_data(a['name']) for a in p.get('aliases') or []),
|
||||||
|
ids=ids, social_ids=social_ids, homepage=official_site, characters=ch
|
||||||
|
)]
|
||||||
|
|
||||||
|
def get_person(self, p_id, get_show_credits=False, get_images=False, **kwargs):
|
||||||
|
# type: (integer_types, bool, bool, Any) -> Optional[Person]
|
||||||
|
"""
|
||||||
|
get person's data for id or list of matching persons for name
|
||||||
|
|
||||||
|
:param p_id: persons id
|
||||||
|
:param get_show_credits: get show credits
|
||||||
|
:param get_images: get images for person
|
||||||
|
:return: person object
|
||||||
|
"""
|
||||||
|
if not p_id:
|
||||||
|
return
|
||||||
|
cache_key_name = 'p-v4-%s' % p_id
|
||||||
|
is_none, people_obj = self._get_cache_entry(cache_key_name)
|
||||||
|
if None is people_obj and not is_none:
|
||||||
|
resp = self._get_data('/people/%s/extended' % p_id)
|
||||||
|
self._set_cache_entry(cache_key_name, resp)
|
||||||
|
else:
|
||||||
|
resp = people_obj
|
||||||
|
if isinstance(resp, dict) and all(t in resp for t in ('data', 'status')) and 'success' == resp['status'] \
|
||||||
|
and isinstance(resp['data'], dict):
|
||||||
|
return self._convert_person(resp['data'])[0]
|
||||||
|
|
||||||
|
def _search_person(self, name=None, ids=None):
|
||||||
|
# type: (AnyStr, Dict[integer_types, integer_types]) -> List[Person]
|
||||||
|
"""
|
||||||
|
search for person by name
|
||||||
|
:param name: name to search for
|
||||||
|
:param ids: dict of ids to search
|
||||||
|
:return: list of found person's
|
||||||
|
"""
|
||||||
|
urls, result, ids = [], [], ids or {}
|
||||||
|
for tv_src in self.supported_person_id_searches:
|
||||||
|
if tv_src in ids:
|
||||||
|
if TVINFO_TVDB == tv_src:
|
||||||
|
r = self.get_person(ids[tv_src])
|
||||||
|
if r:
|
||||||
|
result.append(r)
|
||||||
|
if TVINFO_IMDB == tv_src:
|
||||||
|
cache_id_key = 'p-v4-id-%s-%s' % (TVINFO_IMDB, ids[TVINFO_IMDB])
|
||||||
|
is_none, shows = self._get_cache_entry(cache_id_key)
|
||||||
|
if not self.config.get('cache_search') or (None is shows and not is_none):
|
||||||
|
try:
|
||||||
|
d_m = self._get_data('search', remote_id='nm%07d' % ids.get(TVINFO_IMDB),
|
||||||
|
q='nm%07d' % ids.get(TVINFO_IMDB), type='people')
|
||||||
|
self._set_cache_entry(cache_id_key, d_m, expire=self.search_cache_expire)
|
||||||
|
except (BaseException, Exception):
|
||||||
|
d_m = None
|
||||||
|
else:
|
||||||
|
d_m = shows
|
||||||
|
if isinstance(d_m, dict) and all(t in d_m for t in ('data', 'status')) and 'success' == d_m[
|
||||||
|
'status'] \
|
||||||
|
and isinstance(d_m['data'], list):
|
||||||
|
for r in d_m['data']:
|
||||||
|
try:
|
||||||
|
if 'nm%07d' % ids[TVINFO_IMDB] == \
|
||||||
|
next(filter_iter(lambda b: 'imdb' in b['sourceName'].lower(),
|
||||||
|
r.get('remote_ids', []) or []), {}).get('id'):
|
||||||
|
result.extend(self._convert_person(r))
|
||||||
|
break
|
||||||
|
except (BaseException, Exception):
|
||||||
|
pass
|
||||||
|
if name:
|
||||||
|
cache_key_name = 'p-v4-src-text-%s' % name
|
||||||
|
is_none, people_objs = self._get_cache_entry(cache_key_name)
|
||||||
|
if None is people_objs and not is_none:
|
||||||
|
resp = self._get_data('/search', q=name, type='people')
|
||||||
|
self._set_cache_entry(cache_key_name, resp)
|
||||||
|
else:
|
||||||
|
resp = people_objs
|
||||||
|
if isinstance(resp, dict) and all(t in resp for t in ('data', 'status')) and 'success' == resp['status'] \
|
||||||
|
and isinstance(resp['data'], list):
|
||||||
|
for r in resp['data']:
|
||||||
|
result.extend(self._convert_person(r))
|
||||||
|
seen = set()
|
||||||
|
result = [seen.add(r.id) or r for r in result if r.id not in seen]
|
||||||
|
return result
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _get_overview(show_data, language='eng'):
|
||||||
|
# type: (Dict, AnyStr) -> Optional[AnyStr]
|
||||||
|
"""
|
||||||
|
internal helper to get english overview
|
||||||
|
:param show_data:
|
||||||
|
:param language:
|
||||||
|
"""
|
||||||
|
if isinstance(show_data.get('translations'), dict) and 'overviewTranslations' in show_data['translations']:
|
||||||
|
try:
|
||||||
|
trans = next(filter_iter(lambda show: language == show['language'],
|
||||||
|
show_data['translations']['overviewTranslations']),
|
||||||
|
next(filter_iter(lambda show: 'eng' == show['language'],
|
||||||
|
show_data['translations']['overviewTranslations']), None)
|
||||||
|
)
|
||||||
|
if trans:
|
||||||
|
return clean_data(trans['overview'])
|
||||||
|
except (BaseException, Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _get_series_name(self, show_data, language=None):
|
||||||
|
# type: (Dict, AnyStr) -> Tuple[Optional[AnyStr], List]
|
||||||
|
series_name = clean_data(
|
||||||
|
next(filter_iter(lambda l: language and language == l['language'],
|
||||||
|
show_data.get('translations', {}).get('nameTranslations', [])),
|
||||||
|
{'name': show_data['name']})['name'])
|
||||||
|
series_aliases = self._get_aliases(show_data)
|
||||||
|
if not series_name:
|
||||||
|
if isinstance(series_aliases, list) and 0 < len(series_aliases):
|
||||||
|
series_name = series_aliases.pop(0)
|
||||||
|
return series_name, series_aliases
|
||||||
|
|
||||||
|
def _get_show_data(
|
||||||
|
self,
|
||||||
|
sid, # type: integer_types
|
||||||
|
language, # type: AnyStr
|
||||||
|
get_ep_info=False, # type: bool
|
||||||
|
banners=False, # type: bool
|
||||||
|
posters=False, # type: bool
|
||||||
|
seasons=False, # type: bool
|
||||||
|
seasonwides=False, # type: bool
|
||||||
|
fanart=False, # type: bool
|
||||||
|
actors=False, # type: bool
|
||||||
|
direct_data=False, # type: bool
|
||||||
|
**kwargs # type: Optional[Any]
|
||||||
|
):
|
||||||
|
# type: (...) -> Optional[bool, dict]
|
||||||
|
"""
|
||||||
|
internal function that should be overwritten in class to get data for given show id
|
||||||
|
:param sid: show id
|
||||||
|
:param language: language
|
||||||
|
:param get_ep_info: get episodes
|
||||||
|
:param banners: load banners
|
||||||
|
:param posters: load posters
|
||||||
|
:param seasons: load seasons
|
||||||
|
:param seasonwides: load seasonwides
|
||||||
|
:param fanart: load fanard
|
||||||
|
:param actors: load actors
|
||||||
|
:param direct_data: return pure data
|
||||||
|
"""
|
||||||
|
if not sid:
|
||||||
|
return False
|
||||||
|
resp = self._get_data('/series/%s/extended' % sid)
|
||||||
|
if direct_data:
|
||||||
|
return resp
|
||||||
|
if isinstance(resp, dict) and all(f in resp for f in ('status', 'data')) and 'success' == resp['status'] \
|
||||||
|
and isinstance(resp['data'], dict):
|
||||||
|
show_data = resp['data']
|
||||||
|
series_name, series_aliases = self._get_series_name(show_data, language)
|
||||||
|
if not series_name:
|
||||||
|
return False
|
||||||
|
show_obj = self.shows[sid] # type: TVInfoShow
|
||||||
|
show_obj.banner_loaded = show_obj.poster_loaded = show_obj.fanart_loaded = True
|
||||||
|
show_obj.id = show_data['id']
|
||||||
|
show_obj.seriesname = series_name
|
||||||
|
show_obj.slug = clean_data(show_data.get('slug'))
|
||||||
|
show_obj.poster = clean_data(show_data.get('image'))
|
||||||
|
show_obj.firstaired = clean_data(show_data.get('firstAired'))
|
||||||
|
show_obj.rating = show_data.get('score')
|
||||||
|
show_obj.aliases = series_aliases
|
||||||
|
show_obj.status = clean_data(show_data['status']['name'])
|
||||||
|
show_obj.network_country = clean_data(show_data.get('originalCountry'))
|
||||||
|
show_obj.lastupdated = clean_data(show_data.get('lastUpdated'))
|
||||||
|
if 'companies' in show_data and isinstance(show_data['companies'], list):
|
||||||
|
# filter networks
|
||||||
|
networks = sorted([n for n in show_data['companies'] if 1 == n['companyType']['companyTypeId']],
|
||||||
|
key=lambda a: a['activeDate'] or '0000-00-00')
|
||||||
|
if networks:
|
||||||
|
show_obj.networks = [TVInfoNetwork(name=clean_data(n['name']), country=clean_data(n['country']))
|
||||||
|
for n in networks]
|
||||||
|
show_obj.network = clean_data(networks[-1]['name'])
|
||||||
|
show_obj.network_country = clean_data(networks[-1]['country'])
|
||||||
|
show_obj.language = clean_data(show_data.get('originalLanguage'))
|
||||||
|
show_obj.runtime = show_data.get('averageRuntime')
|
||||||
|
show_obj.airs_time = clean_data(show_data.get('airsTime'))
|
||||||
|
show_obj.airs_dayofweek = ', '.join([k.capitalize() for k, v in iteritems(show_data.get('airsDays')) if v])
|
||||||
|
show_obj.genre_list = 'genres' in show_data and show_data['genres'] \
|
||||||
|
and [clean_data(g['name']) for g in show_data['genres']]
|
||||||
|
if show_obj.genre_list:
|
||||||
|
show_obj.genre = '|'.join(show_obj.genre_list)
|
||||||
|
|
||||||
|
ids, social_ids = {}, {}
|
||||||
|
if 'remoteIds' in show_data and isinstance(show_data['remoteIds'], list):
|
||||||
|
for r_id in show_data['remoteIds']:
|
||||||
|
src_name = r_id['sourceName'].lower()
|
||||||
|
src_value = clean_data(r_id['id'])
|
||||||
|
if 'imdb' in src_name:
|
||||||
|
try:
|
||||||
|
imdb_id = try_int(src_value.replace('tt', ''), None)
|
||||||
|
ids['imdb'] = imdb_id
|
||||||
|
except (BaseException, Exception):
|
||||||
|
pass
|
||||||
|
show_obj.imdb_id = src_value
|
||||||
|
elif 'themoviedb' in src_name:
|
||||||
|
ids['tmdb'] = try_int(src_value, None)
|
||||||
|
elif 'official website' in src_name:
|
||||||
|
show_obj.official_site = src_value
|
||||||
|
elif 'facebook' in src_name:
|
||||||
|
social_ids['facebook'] = src_value
|
||||||
|
elif 'twitter' in src_name:
|
||||||
|
social_ids['twitter'] = src_value
|
||||||
|
elif 'instagram' in src_name:
|
||||||
|
social_ids['instagram'] = src_value
|
||||||
|
elif 'reddit' in src_name:
|
||||||
|
social_ids['reddit'] = src_value
|
||||||
|
elif 'youtube' in src_name:
|
||||||
|
social_ids['youtube'] = src_value
|
||||||
|
|
||||||
|
show_obj.ids = TVInfoIDs(tvdb=show_data['id'], **ids)
|
||||||
|
if social_ids:
|
||||||
|
show_obj.social_ids = TVInfoSocialIDs(**social_ids)
|
||||||
|
|
||||||
|
show_obj.overview = self._get_overview(show_data)
|
||||||
|
|
||||||
|
if 'artworks' in show_data and isinstance(show_data['artworks'], list):
|
||||||
|
poster = banner = fanart_url = False
|
||||||
|
for artwork in sorted(show_data['artworks'], key=lambda a: a['score'], reverse=True):
|
||||||
|
img_type = img_type_map.get(artwork['type'], TVInfoImageType.other)
|
||||||
|
if False is poster and img_type == TVInfoImageType.poster:
|
||||||
|
show_obj.poster, show_obj.poster_thumb, poster = artwork['image'], artwork['thumbnail'], True
|
||||||
|
elif False is banner and img_type == TVInfoImageType.banner:
|
||||||
|
show_obj.banner, show_obj.banner_thumb, banner = artwork['image'], artwork['thumbnail'], True
|
||||||
|
elif False is fanart_url and img_type == TVInfoImageType.fanart:
|
||||||
|
show_obj.fanart, fanart_url = artwork['image'], True
|
||||||
|
show_obj['images'].setdefault(img_type, []).append(
|
||||||
|
TVInfoImage(
|
||||||
|
image_type=img_type,
|
||||||
|
sizes={TVInfoImageSize.original: artwork['image'],
|
||||||
|
TVInfoImageSize.small: artwork['thumbnail']},
|
||||||
|
img_id=artwork['id'],
|
||||||
|
lang=artwork['language'],
|
||||||
|
rating=artwork['score']
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if (actors or self.config['actors_enabled']) and not getattr(self.shows.get(sid), 'actors_loaded', False):
|
||||||
|
cast, show_obj.actors_loaded = CastList(), True
|
||||||
|
if isinstance(show_data.get('characters'), list):
|
||||||
|
for character in sorted(filter_iter(lambda a: (3 == a['type'] or 'Actor' == a['peopleType'])
|
||||||
|
and not a['episodeId'],
|
||||||
|
show_data.get('characters')) or [],
|
||||||
|
key=lambda c: (not c['isFeatured'], c['sort'])):
|
||||||
|
cast[RoleTypes.ActorMain].append(
|
||||||
|
Character(p_id=character['id'], name=clean_data(character['name']),
|
||||||
|
regular=character['isFeatured'], ids={TVINFO_TVDB: character['id']},
|
||||||
|
person=[Person(p_id=character['peopleId'],
|
||||||
|
name=clean_data(character['personName']),
|
||||||
|
ids={TVINFO_TVDB: character['peopleId']})],
|
||||||
|
image=character['image']))
|
||||||
|
show_obj.cast = cast
|
||||||
|
show_obj.actors = [
|
||||||
|
{'character': {'id': ch.id,
|
||||||
|
'name': ch.name,
|
||||||
|
'url': 'https://www.thetvdb.com/series/%s/people/%s' % (show_data['slug'], ch.id),
|
||||||
|
'image': ch.image,
|
||||||
|
},
|
||||||
|
'person': {'id': ch.person and ch.person[0].id,
|
||||||
|
'name': ch.person and ch.person[0].name,
|
||||||
|
'url': ch.person and 'https://www.thetvdb.com/people/%s' % ch.person[0].id,
|
||||||
|
'image': ch.person and ch.person[0].image,
|
||||||
|
'birthday': None, # not sure about format
|
||||||
|
'deathday': None, # not sure about format
|
||||||
|
'gender': None,
|
||||||
|
'country': None,
|
||||||
|
},
|
||||||
|
} for ch in cast[RoleTypes.ActorMain]]
|
||||||
|
|
||||||
|
if get_ep_info and not getattr(self.shows.get(sid), 'ep_loaded', False):
|
||||||
|
# fetch absolute numbers
|
||||||
|
eps_abs_nums = {}
|
||||||
|
if any(1 for s in show_data.get('seasons', []) or [] if 'absolute' == s.get('type', {}).get('type')):
|
||||||
|
page = 0
|
||||||
|
while 100 >= page:
|
||||||
|
abs_ep_data = self._get_data('/series/%s/episodes/absolute?page=%d' % (sid, page))
|
||||||
|
page += 1
|
||||||
|
if isinstance(abs_ep_data, dict):
|
||||||
|
valid_data = 'data' in abs_ep_data and isinstance(abs_ep_data['data'], dict) \
|
||||||
|
and 'episodes' in abs_ep_data['data'] \
|
||||||
|
and isinstance(abs_ep_data['data']['episodes'], list)
|
||||||
|
links = 'links' in abs_ep_data and isinstance(abs_ep_data['links'], dict) \
|
||||||
|
and 'next' in abs_ep_data['links']
|
||||||
|
more = (links and isinstance(abs_ep_data['links']['next'], string_types)
|
||||||
|
and '?page=%d' % page in abs_ep_data['links']['next'])
|
||||||
|
if valid_data:
|
||||||
|
eps_abs_nums.update({_e['id']: _e['number'] for _e in abs_ep_data['data']['episodes']
|
||||||
|
if None is _e['seasons'] and _e['number']})
|
||||||
|
if more:
|
||||||
|
continue
|
||||||
|
break
|
||||||
|
|
||||||
|
ep_lang = (language in (show_data.get('overviewTranslations', []) or []) and language) or 'eng'
|
||||||
|
page, more_eps, show_obj.ep_loaded = 0, True, True
|
||||||
|
while more_eps and 100 >= page:
|
||||||
|
ep_data = self._get_data('/series/%s/episodes/default/%s?page=%d' % (sid, ep_lang, page))
|
||||||
|
page += 1
|
||||||
|
if isinstance(ep_data, dict):
|
||||||
|
valid_data = 'data' in ep_data and isinstance(ep_data['data'], dict) \
|
||||||
|
and 'episodes' in ep_data['data'] and isinstance(ep_data['data']['episodes'], list)
|
||||||
|
full_page = valid_data and 500 <= len(ep_data['data']['episodes'])
|
||||||
|
links = 'links' in ep_data and isinstance(ep_data['links'], dict) \
|
||||||
|
and 'next' in ep_data['links']
|
||||||
|
more = links and isinstance(ep_data['links']['next'], string_types) \
|
||||||
|
and '?page=%d' % page in ep_data['links']['next']
|
||||||
|
alt_page = (full_page and not links)
|
||||||
|
if not alt_page and valid_data:
|
||||||
|
self._set_episodes(show_obj, ep_data, eps_abs_nums)
|
||||||
|
if 'links' in ep_data and isinstance(ep_data['links'], dict) and 'next' in ep_data['links'] \
|
||||||
|
and isinstance(ep_data['links']['next'], string_types):
|
||||||
|
if '?page=%d' % page in ep_data['links']['next']:
|
||||||
|
continue
|
||||||
|
break
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _set_episodes(s_ref, ep_data, eps_abs_nums):
|
||||||
|
# type: (TVInfoShow, Dict, Dict) -> None
|
||||||
|
"""
|
||||||
|
populates the show with episode objects
|
||||||
|
"""
|
||||||
|
for ep_obj in ep_data['data']['episodes']:
|
||||||
|
for _k, _s in (
|
||||||
|
('seasonnumber', 'seasonNumber'), ('episodenumber', 'number'),
|
||||||
|
('episodename', 'name'), ('firstaired', 'aired'), ('runtime', 'runtime'),
|
||||||
|
('seriesid', 'seriesId'), ('id', 'id'), ('filename', 'image'), ('overview', 'overview'),
|
||||||
|
('absolute_number', 'abs')):
|
||||||
|
seas, ep = ep_obj['seasonNumber'], ep_obj['number']
|
||||||
|
if 'abs' == _s:
|
||||||
|
value = eps_abs_nums.get(ep_obj['id'])
|
||||||
|
else:
|
||||||
|
value = clean_data(ep_obj.get(_s, getattr(empty_ep, _k)))
|
||||||
|
|
||||||
|
if seas not in s_ref:
|
||||||
|
s_ref[seas] = TVInfoSeason(show=s_ref)
|
||||||
|
s_ref[seas].number = seas
|
||||||
|
if ep not in s_ref[seas]:
|
||||||
|
s_ref[seas][ep] = TVInfoEpisode(season=s_ref[seas])
|
||||||
|
if _k not in ('cast', 'crew'):
|
||||||
|
s_ref[seas][ep][_k] = value
|
||||||
|
s_ref[seas][ep].__dict__[_k] = value
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _get_network(show):
|
||||||
|
# type: (Dict) -> Optional[AnyStr]
|
||||||
|
if show.get('companies'):
|
||||||
|
if isinstance(show['companies'][0], dict):
|
||||||
|
return clean_data(next(filter_iter(lambda a: 1 == a['companyType']['companyTypeId'], show['companies']),
|
||||||
|
{}).get('name'))
|
||||||
|
else:
|
||||||
|
return clean_data(show['companies'][0])
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _get_aliases(show):
|
||||||
|
if show.get('aliases') and isinstance(show['aliases'][0], dict):
|
||||||
|
return [clean_data(a['name']) for a in show['aliases']]
|
||||||
|
return clean_data(show.get('aliases', []))
|
||||||
|
|
||||||
|
def _search_show(self, name=None, ids=None, **kwargs):
|
||||||
|
# type: (Union[AnyStr, List[AnyStr]], Dict[integer_types, integer_types], Optional[Any]) -> List[Dict]
|
||||||
|
"""
|
||||||
|
internal search function to find shows, should be overwritten in class
|
||||||
|
:param name: name to search for
|
||||||
|
:param ids: dict of ids {tvid: prodid} to search for
|
||||||
|
"""
|
||||||
|
def _make_result_dict(show_data):
|
||||||
|
tvdb_id = self._get_tvdb_id(show_data)
|
||||||
|
series_name, series_aliases = self._get_series_name(show_data)
|
||||||
|
if not series_name:
|
||||||
|
return []
|
||||||
|
|
||||||
|
return [{'seriesname': series_name, 'id': tvdb_id,
|
||||||
|
'firstaired': clean_data(show_data.get('year') or show_data.get('firstAired')),
|
||||||
|
'network': self._get_network(show_data),
|
||||||
|
'overview': clean_data(show_data.get('overview')) or self._get_overview(show_data),
|
||||||
|
'poster': show_data.get('image_url') or show_data.get('image'),
|
||||||
|
'status': clean_data(isinstance(show_data['status'], dict) and
|
||||||
|
show_data['status']['name'] or show_data['status']),
|
||||||
|
'language': clean_data(show_data.get('primary_language')), 'country':
|
||||||
|
clean_data(show_data.get('country')),
|
||||||
|
'aliases': series_aliases, 'slug': clean_data(show_data.get('slug')),
|
||||||
|
'ids': TVInfoIDs(tvdb=tvdb_id)}]
|
||||||
|
results = []
|
||||||
|
if ids:
|
||||||
|
if ids.get(TVINFO_TVDB):
|
||||||
|
cache_id_key = 's-v4-id-%s-%s' % (TVINFO_TVDB, ids[TVINFO_TVDB])
|
||||||
|
is_none, shows = self._get_cache_entry(cache_id_key)
|
||||||
|
if not self.config.get('cache_search') or (None is shows and not is_none):
|
||||||
|
try:
|
||||||
|
d_m = self._get_show_data(ids.get(TVINFO_TVDB), self.config['language'], direct_data=True)
|
||||||
|
self._set_cache_entry(cache_id_key, d_m, expire=self.search_cache_expire)
|
||||||
|
except (BaseException, Exception):
|
||||||
|
d_m = None
|
||||||
|
else:
|
||||||
|
d_m = shows
|
||||||
|
if isinstance(d_m, dict) and all(t in d_m for t in ('data', 'status')) and 'success' == d_m['status'] \
|
||||||
|
and isinstance(d_m['data'], dict):
|
||||||
|
results.extend(_make_result_dict(d_m['data']))
|
||||||
|
|
||||||
|
if ids.get(TVINFO_IMDB):
|
||||||
|
cache_id_key = 's-v4-id-%s-%s' % (TVINFO_IMDB, ids[TVINFO_IMDB])
|
||||||
|
is_none, shows = self._get_cache_entry(cache_id_key)
|
||||||
|
if not self.config.get('cache_search') or (None is shows and not is_none):
|
||||||
|
try:
|
||||||
|
d_m = self._get_data('search', remote_id='tt%07d' % ids.get(TVINFO_IMDB),
|
||||||
|
q='tt%07d' % ids.get(TVINFO_IMDB), type='series')
|
||||||
|
self._set_cache_entry(cache_id_key, d_m, expire=self.search_cache_expire)
|
||||||
|
except (BaseException, Exception):
|
||||||
|
d_m = None
|
||||||
|
else:
|
||||||
|
d_m = shows
|
||||||
|
if isinstance(d_m, dict) and all(t in d_m for t in ('data', 'status')) and 'success' == d_m['status'] \
|
||||||
|
and isinstance(d_m['data'], list):
|
||||||
|
for r in d_m['data']:
|
||||||
|
try:
|
||||||
|
if 'tt%07d' % ids[TVINFO_IMDB] == \
|
||||||
|
next(filter_iter(lambda b: 'imdb' in b['sourceName'].lower(),
|
||||||
|
r.get('remote_ids', []) or []), {}).get('id'):
|
||||||
|
results.extend(_make_result_dict(r))
|
||||||
|
break
|
||||||
|
except (BaseException, Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
if ids.get(TVINFO_TMDB):
|
||||||
|
cache_id_key = 's-v4-id-%s-%s' % (TVINFO_TMDB, ids[TVINFO_TMDB])
|
||||||
|
is_none, shows = self._get_cache_entry(cache_id_key)
|
||||||
|
if not self.config.get('cache_search') or (None is shows and not is_none):
|
||||||
|
try:
|
||||||
|
d_m = self._get_data('search', remote_id='%s' % ids.get(TVINFO_TMDB),
|
||||||
|
q='%s' % ids.get(TVINFO_TMDB), type='series')
|
||||||
|
self._set_cache_entry(cache_id_key, d_m, expire=self.search_cache_expire)
|
||||||
|
except (BaseException, Exception):
|
||||||
|
d_m = None
|
||||||
|
else:
|
||||||
|
d_m = shows
|
||||||
|
if isinstance(d_m, dict) and all(t in d_m for t in ('data', 'status')) and 'success' == d_m['status'] \
|
||||||
|
and isinstance(d_m['data'], list):
|
||||||
|
for r in d_m['data']:
|
||||||
|
try:
|
||||||
|
if '%s' % ids[TVINFO_TMDB] == \
|
||||||
|
next(filter_iter(lambda b: 'themoviedb' in b['sourceName'].lower(),
|
||||||
|
r.get('remote_ids', []) or []), {}).get('id'):
|
||||||
|
results.extend(_make_result_dict(r))
|
||||||
|
break
|
||||||
|
except (BaseException, Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
if ids.get(TVINFO_TVDB_SLUG):
|
||||||
|
cache_id_key = 's-id-%s-%s' % (TVINFO_TVDB, ids[TVINFO_TVDB_SLUG])
|
||||||
|
is_none, shows = self._get_cache_entry(cache_id_key)
|
||||||
|
if not self.config.get('cache_search') or (None is shows and not is_none):
|
||||||
|
try:
|
||||||
|
d_m = self._get_data('search', q=ids.get(TVINFO_TVDB_SLUG).replace('-', ' '), type='series')
|
||||||
|
self._set_cache_entry(cache_id_key, d_m, expire=self.search_cache_expire)
|
||||||
|
except (BaseException, Exception):
|
||||||
|
d_m = None
|
||||||
|
else:
|
||||||
|
d_m = shows
|
||||||
|
if d_m and isinstance(d_m, dict) and 'data' in d_m and 'success' == d_m.get('status') \
|
||||||
|
and isinstance(d_m['data'], list):
|
||||||
|
for r in d_m['data']:
|
||||||
|
if ids.get(TVINFO_TVDB_SLUG) == r['slug']:
|
||||||
|
results.extend(_make_result_dict(r))
|
||||||
|
break
|
||||||
|
|
||||||
|
if name:
|
||||||
|
for n in ([name], name)[isinstance(name, list)]:
|
||||||
|
cache_name_key = 's-v4-name-%s' % n
|
||||||
|
is_none, shows = self._get_cache_entry(cache_name_key)
|
||||||
|
if not self.config.get('cache_search') or (None is shows and not is_none):
|
||||||
|
resp = self._get_data('search', q=n, type='series')
|
||||||
|
self._set_cache_entry(cache_name_key, resp, expire=self.search_cache_expire)
|
||||||
|
else:
|
||||||
|
resp = shows
|
||||||
|
|
||||||
|
if resp and isinstance(resp, dict) and 'data' in resp and 'success' == resp.get('status') \
|
||||||
|
and isinstance(resp['data'], list):
|
||||||
|
for show in resp['data']:
|
||||||
|
results.extend(_make_result_dict(show))
|
||||||
|
|
||||||
|
seen = set()
|
||||||
|
results = [seen.add(r['id']) or r for r in results if r['id'] not in seen]
|
||||||
|
return results
|
||||||
|
|
||||||
|
def _get_languages(self):
|
||||||
|
# type: (...) -> None
|
||||||
|
langs = self._get_data('/languages')
|
||||||
|
if isinstance(langs, dict) and 'status' in langs and 'success' == langs['status'] \
|
||||||
|
and isinstance(langs.get('data'), list):
|
||||||
|
self._supported_languages = [{'id': clean_data(a['id']), 'name': clean_data(a['name']),
|
||||||
|
'nativeName': clean_data(a['nativeName']),
|
||||||
|
'shortCode': clean_data(a['shortCode']),
|
||||||
|
'sg_lang': self.reverse_map_languages.get(a['id'], a['id'])}
|
||||||
|
for a in langs['data']]
|
||||||
|
else:
|
||||||
|
self._supported_languages = []
|
|
@ -1,4 +1,6 @@
|
||||||
from lib.api_tvdb.tvdb_api import Tvdb
|
from lib.api_tvdb.tvdb_api import Tvdb
|
||||||
|
from lib.api_tvdb.tvdb_api_v4 import Tvdb_API_V4
|
||||||
|
import lib.api_tvdb.tvdb_api_v4
|
||||||
from lib.api_trakt.indexerapiinterface import TraktIndexer
|
from lib.api_trakt.indexerapiinterface import TraktIndexer
|
||||||
from lib.api_tvmaze.tvmaze_api import TvMaze
|
from lib.api_tvmaze.tvmaze_api import TvMaze
|
||||||
from lib.api_tmdb.tmdb_api import TmdbIndexer
|
from lib.api_tmdb.tmdb_api import TmdbIndexer
|
||||||
|
@ -23,8 +25,9 @@ tvinfo_config = {
|
||||||
api_url='https://api.thetvdb.com/',
|
api_url='https://api.thetvdb.com/',
|
||||||
id=TVINFO_TVDB,
|
id=TVINFO_TVDB,
|
||||||
name='TheTVDB', slug='tvdb', kodi_slug='tvdb',
|
name='TheTVDB', slug='tvdb', kodi_slug='tvdb',
|
||||||
module=Tvdb,
|
module=Tvdb_API_V4,
|
||||||
api_params=dict(apikey='6cfd6399fd2bee018a8793da976f6522', language='en'),
|
api_params=dict(apikey='6cfd6399fd2bee018a8793da976f6522',
|
||||||
|
apikey_v4=b'm5uaxWhrm56TlWTGm5Jkk5uYZW-ea5uOnmqcmWmXZmVtxp2a', language='en'),
|
||||||
active=True,
|
active=True,
|
||||||
dupekey='',
|
dupekey='',
|
||||||
mapped_only=False,
|
mapped_only=False,
|
||||||
|
@ -260,3 +263,5 @@ tvinfo_config[src].update(dict(
|
||||||
show_url='%stv/%%d' % tvinfo_config[src]['main_url'],
|
show_url='%stv/%%d' % tvinfo_config[src]['main_url'],
|
||||||
finder='%ssearch/tv?query=%s' % (tvinfo_config[src]['main_url'], '%s'),
|
finder='%ssearch/tv?query=%s' % (tvinfo_config[src]['main_url'], '%s'),
|
||||||
))
|
))
|
||||||
|
|
||||||
|
lib.api_tvdb.tvdb_api_v4.TVDB_API_CONFIG = tvinfo_config[TVINFO_TVDB]
|
||||||
|
|
|
@ -73,7 +73,7 @@ class SBRotatingLogHandler(object):
|
||||||
self.console_logging = False # type: bool
|
self.console_logging = False # type: bool
|
||||||
self.log_lock = threading.Lock()
|
self.log_lock = threading.Lock()
|
||||||
self.log_types = ['sickgear', 'tornado.application', 'tornado.general', 'subliminal', 'adba', 'encodingKludge',
|
self.log_types = ['sickgear', 'tornado.application', 'tornado.general', 'subliminal', 'adba', 'encodingKludge',
|
||||||
'tvdb.api', 'TVInfo']
|
'tvdb.api', 'TVInfo', 'tvdb_v4.api']
|
||||||
self.external_loggers = ['sg.helper', 'api_trakt', 'api_trakt.api']
|
self.external_loggers = ['sg.helper', 'api_trakt', 'api_trakt.api']
|
||||||
self.log_types_null = ['tornado.access']
|
self.log_types_null = ['tornado.access']
|
||||||
|
|
||||||
|
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue