SickGear/lib/libtrakt/trakt.py
Prinz23 41206c2de7 Fix notifications Trakt "inactive" status not displayed when it should be.
Fix prevent waiting loops for deactivated accounts that can result in long loading times for the notification page on first time opened.
2018-03-05 15:37:17 +00:00

315 lines
13 KiB
Python

import requests
import certifi
import json
import sickbeard
import time
import datetime
from sickbeard import logger
from .exceptions import TraktException, TraktAuthException # , TraktServerBusy
class TraktAccount:
max_auth_fail = 9
def __init__(self, account_id=None, token='', refresh_token='', auth_fail=0, last_fail=None, token_valid_date=None):
self.account_id = account_id
self._name = ''
self._slug = ''
self.token = token
self.refresh_token = refresh_token
self.auth_fail = auth_fail
self.last_fail = last_fail
self.token_valid_date = token_valid_date
def get_name_slug(self):
try:
resp = TraktAPI().trakt_request('users/settings', send_oauth=self.account_id, sleep_retry=20)
self.reset_auth_failure()
if 'user' in resp:
self._name = resp['user']['username']
self._slug = resp['user']['ids']['slug']
except TraktAuthException:
self.inc_auth_failure()
self._name = ''
except TraktException:
pass
@property
def slug(self):
if self.token and self.active:
if not self._slug:
self.get_name_slug()
else:
self._slug = ''
return self._slug
@property
def name(self):
if self.token and self.active:
if not self._name:
self.get_name_slug()
else:
self._name = ''
return self._name
def reset_name(self):
self._name = ''
@property
def active(self):
return self.auth_fail < self.max_auth_fail and self.token
@property
def needs_refresh(self):
return not self.token_valid_date or self.token_valid_date - datetime.datetime.now() < datetime.timedelta(days=3)
@property
def token_expired(self):
return self.token_valid_date and self.token_valid_date < datetime.datetime.now()
def reset_auth_failure(self):
if 0 != self.auth_fail:
self.auth_fail = 0
self.last_fail = None
def inc_auth_failure(self):
self.auth_fail += 1
self.last_fail = datetime.datetime.now()
def auth_failure(self):
if self.auth_fail < self.max_auth_fail:
if self.last_fail:
time_diff = datetime.datetime.now() - self.last_fail
if 0 == self.auth_fail % 3:
if datetime.timedelta(days=1) < time_diff:
self.inc_auth_failure()
sickbeard.save_config()
elif datetime.timedelta(minutes=15) < time_diff:
self.inc_auth_failure()
if self.auth_fail == self.max_auth_fail or datetime.timedelta(hours=6) < time_diff:
sickbeard.save_config()
else:
self.inc_auth_failure()
class TraktAPI:
max_retrys = 3
def __init__(self, timeout=None):
self.session = requests.Session()
self.verify = sickbeard.TRAKT_VERIFY and certifi.where()
self.timeout = timeout or sickbeard.TRAKT_TIMEOUT
self.auth_url = sickbeard.TRAKT_BASE_URL
self.api_url = sickbeard.TRAKT_BASE_URL
self.headers = {'Content-Type': 'application/json',
'trakt-api-version': '2',
'trakt-api-key': sickbeard.TRAKT_CLIENT_ID}
@staticmethod
def build_config_string(data):
return '!!!'.join('%s|%s|%s|%s|%s|%s' % (
value.account_id, value.token, value.refresh_token, value.auth_fail,
value.last_fail.strftime('%Y%m%d%H%M') if value.last_fail else '0',
value.token_valid_date.strftime('%Y%m%d%H%M%S') if value.token_valid_date else '0')
for (key, value) in data.items())
@staticmethod
def read_config_string(data):
return dict((int(a.split('|')[0]), TraktAccount(
int(a.split('|')[0]), a.split('|')[1], a.split('|')[2], int(a.split('|')[3]),
datetime.datetime.strptime(a.split('|')[4], '%Y%m%d%H%M') if a.split('|')[4] != '0' else None,
datetime.datetime.strptime(a.split('|')[5], '%Y%m%d%H%M%S') if a.split('|')[5] != '0' else None))
for a in data.split('!!!') if data)
@staticmethod
def add_account(token, refresh_token, token_valid_date):
k = max(sickbeard.TRAKT_ACCOUNTS.keys() or [0]) + 1
sickbeard.TRAKT_ACCOUNTS[k] = TraktAccount(account_id=k, token=token, refresh_token=refresh_token,
token_valid_date=token_valid_date)
sickbeard.save_config()
return k
@staticmethod
def replace_account(account, token, refresh_token, token_valid_date, refresh):
if account in sickbeard.TRAKT_ACCOUNTS:
sickbeard.TRAKT_ACCOUNTS[account].token = token
sickbeard.TRAKT_ACCOUNTS[account].refresh_token = refresh_token
sickbeard.TRAKT_ACCOUNTS[account].token_valid_date = token_valid_date
if not refresh:
sickbeard.TRAKT_ACCOUNTS[account].reset_name()
sickbeard.TRAKT_ACCOUNTS[account].reset_auth_failure()
sickbeard.save_config()
return True
else:
return False
@staticmethod
def delete_account(account):
if account in sickbeard.TRAKT_ACCOUNTS:
TraktAPI().trakt_request('/oauth/revoke', send_oauth=account, method='POST')
sickbeard.TRAKT_ACCOUNTS.pop(account)
sickbeard.save_config()
return True
return False
def trakt_token(self, trakt_pin=None, refresh=False, count=0, account=None):
if self.max_retrys <= count:
return False
0 < count and time.sleep(3)
data = {
'client_id': sickbeard.TRAKT_CLIENT_ID,
'client_secret': sickbeard.TRAKT_CLIENT_SECRET,
'redirect_uri': 'urn:ietf:wg:oauth:2.0:oob'
}
if refresh:
if None is not account and account in sickbeard.TRAKT_ACCOUNTS:
data['grant_type'] = 'refresh_token'
data['refresh_token'] = sickbeard.TRAKT_ACCOUNTS[account].refresh_token
else:
return False
else:
data['grant_type'] = 'authorization_code'
if trakt_pin:
data['code'] = trakt_pin
headers = {'Content-Type': 'application/json'}
try:
now = datetime.datetime.now()
resp = self.trakt_request('oauth/token', data=data, headers=headers, url=self.auth_url,
count=count, sleep_retry=0)
except (TraktAuthException, TraktException):
return False
if 'access_token' in resp and 'refresh_token' in resp and 'expires_in' in resp:
token_valid_date = now + datetime.timedelta(seconds=sickbeard.helpers.tryInt(resp['expires_in']))
if refresh or (not refresh and None is not account and account in sickbeard.TRAKT_ACCOUNTS):
return self.replace_account(account, resp['access_token'], resp['refresh_token'],
token_valid_date, refresh)
return self.add_account(resp['access_token'], resp['refresh_token'], token_valid_date)
return False
def trakt_request(self, path, data=None, headers=None, url=None, count=0, sleep_retry=60,
send_oauth=None, method=None, **kwargs):
if method not in ['GET', 'POST', 'PUT', 'DELETE', None]:
return {}
if None is method:
method = ('GET', 'POST')['data' in kwargs.keys() or data is not None]
if path != 'oauth/token' and None is send_oauth and method in ['POST', 'PUT', 'DELETE']:
return {}
count += 1
if count > self.max_retrys:
return {}
# wait before retry
if 'users/settings' != path:
count > 1 and time.sleep(sleep_retry)
headers = headers or self.headers
if None is not send_oauth and send_oauth in sickbeard.TRAKT_ACCOUNTS:
if sickbeard.TRAKT_ACCOUNTS[send_oauth].active:
if sickbeard.TRAKT_ACCOUNTS[send_oauth].needs_refresh:
self.trakt_token(refresh=True, count=0, account=send_oauth)
if sickbeard.TRAKT_ACCOUNTS[send_oauth].token_expired:
return {}
headers['Authorization'] = 'Bearer %s' % sickbeard.TRAKT_ACCOUNTS[send_oauth].token
else:
return {}
kwargs = dict(headers=headers, timeout=self.timeout, verify=self.verify)
if data:
kwargs['data'] = json.dumps(data)
url = url or self.api_url
try:
resp = self.session.request(method, '%s%s' % (url, path), **kwargs)
if 'DELETE' == method:
result = None
if 204 == resp.status_code:
result = {'result': 'success'}
elif 404 == resp.status_code:
result = {'result': 'failed'}
if result and None is not send_oauth and send_oauth in sickbeard.TRAKT_ACCOUNTS:
sickbeard.TRAKT_ACCOUNTS[send_oauth].reset_auth_failure()
return result
resp.raise_for_status()
return {}
# check for http errors and raise if any are present
resp.raise_for_status()
# convert response to json
resp = resp.json()
except requests.RequestException as e:
code = getattr(e.response, 'status_code', None)
if not code:
if 'timed out' in e:
logger.log(u'Timeout connecting to Trakt', logger.WARNING)
return self.trakt_request(path, data, headers, url, count=count, sleep_retry=sleep_retry,
send_oauth=send_oauth, method=method)
# This is pretty much a fatal error if there is no status_code
# It means there basically was no response at all
else:
logger.log(u'Could not connect to Trakt. Error: {0}'.format(e), logger.WARNING)
elif 502 == code:
# Retry the request, Cloudflare had a proxying issue
logger.log(u'Retrying Trakt api request: %s' % path, logger.WARNING)
return self.trakt_request(path, data, headers, url, count=count, sleep_retry=sleep_retry,
send_oauth=send_oauth, method=method)
elif 401 == code and path != 'oauth/token':
if None is not send_oauth:
if sickbeard.TRAKT_ACCOUNTS[send_oauth].needs_refresh:
if self.trakt_token(refresh=True, count=count, account=send_oauth):
return self.trakt_request(path, data, headers, url, count=count, sleep_retry=sleep_retry,
send_oauth=send_oauth, method=method)
logger.log(u'Unauthorized. Please check your Trakt settings', logger.WARNING)
sickbeard.TRAKT_ACCOUNTS[send_oauth].auth_failure()
raise TraktAuthException()
# sometimes the trakt server sends invalid token error even if it isn't
sickbeard.TRAKT_ACCOUNTS[send_oauth].auth_failure()
if count >= self.max_retrys:
raise TraktAuthException()
return self.trakt_request(path, data, headers, url, count=count, sleep_retry=sleep_retry,
send_oauth=send_oauth, method=method)
raise TraktAuthException()
elif code in (500, 501, 503, 504, 520, 521, 522):
# http://docs.trakt.apiary.io/#introduction/status-codes
logger.log(u'Trakt may have some issues and it\'s unavailable. Trying again', logger.WARNING)
self.trakt_request(path, data, headers, url, count=count, sleep_retry=sleep_retry,
send_oauth=send_oauth, method=method)
elif 404 == code:
logger.log(u'Trakt error (404) the resource does not exist: %s%s' % (url, path), logger.WARNING)
else:
logger.log(u'Could not connect to Trakt. Code error: {0}'.format(code), logger.ERROR)
return {}
except ValueError as e:
logger.log(u'Value Error: {0}'.format(e), logger.ERROR)
return {}
# check and confirm Trakt call did not fail
if isinstance(resp, dict) and 'failure' == resp.get('status', None):
if 'message' in resp:
raise TraktException(resp['message'])
if 'error' in resp:
raise TraktException(resp['error'])
raise TraktException('Unknown Error')
if None is not send_oauth and send_oauth in sickbeard.TRAKT_ACCOUNTS:
sickbeard.TRAKT_ACCOUNTS[send_oauth].reset_auth_failure()
return resp