Add Trakt watchlist to Add show/Trakt Cards.

Change revoke application access at Trakt when account is deleted in SG.
Add support for 'DELETE' and 'PUT' requests in libtrakt.
Fix use 'GET', 'POST' method in Trakt lib upgrade.
This commit is contained in:
Prinz23 2017-04-23 22:34:17 +01:00 committed by JackDandy
parent 21c235e52c
commit 9cdf57f989
3 changed files with 126 additions and 51 deletions

View file

@ -196,6 +196,18 @@ $(document).ready(function(){
#else
<optgroup label="To get recommended">
<option value="trakt_recommended?action=add">Enable Trakt here</option>
#end if
</optgroup>
#if any($sg_var('TRAKT_ACCOUNTS', []))
<optgroup label="Trakt watchlisted">
#for $account in $sg_var('TRAKT_ACCOUNTS')
#if $sg_var('TRAKT_ACCOUNTS').get($account).active and $sg_var('TRAKT_ACCOUNTS').get($account).name
<option value="trakt_watchlist?account=$account"#echo ('', selected)[('watchlist-%s' % $account) == $mode]#>for $sg_var('TRAKT_ACCOUNTS').get($account).name</option>
#end if
#end for
#else
<optgroup label="To get watchlisted">
<option value="trakt_watchlist?action=add">Enable Trakt here</option>
#end if
</optgroup>
#elif 'IMDb' == $browse_type

View file

@ -15,26 +15,40 @@ class TraktAccount:
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
@property
def name(self):
if self.token and self.active:
if not self._name:
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 = ''
@ -53,10 +67,10 @@ class TraktAccount:
@property
def token_expired(self):
return self.token_valid_date and datetime.datetime.now() > self.token_valid_date
return self.token_valid_date and self.token_valid_date < datetime.datetime.now()
def reset_auth_failure(self):
if self.auth_fail != 0:
if 0 != self.auth_fail:
self.auth_fail = 0
self.last_fail = None
@ -68,13 +82,13 @@ class TraktAccount:
if self.auth_fail < self.max_auth_fail:
if self.last_fail:
time_diff = datetime.datetime.now() - self.last_fail
if self.auth_fail % 3 == 0:
if time_diff > datetime.timedelta(days=1):
if 0 == self.auth_fail % 3:
if datetime.timedelta(days=1) < time_diff:
self.inc_auth_failure()
sickbeard.save_config()
elif time_diff > datetime.timedelta(minutes=15):
elif datetime.timedelta(minutes=15) < time_diff:
self.inc_auth_failure()
if self.auth_fail == self.max_auth_fail or time_diff > datetime.timedelta(hours=6):
if self.auth_fail == self.max_auth_fail or datetime.timedelta(hours=6) < time_diff:
sickbeard.save_config()
else:
self.inc_auth_failure()
@ -99,19 +113,22 @@ class TraktAPI:
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())
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)
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.TRAKT_ACCOUNTS[k] = TraktAccount(account_id=k, token=token, refresh_token=refresh_token,
token_valid_date=token_valid_date)
sickbeard.save_config()
return k
@ -132,6 +149,7 @@ class TraktAPI:
@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
@ -149,7 +167,7 @@ class TraktAPI:
}
if refresh:
if account and account in sickbeard.TRAKT_ACCOUNTS:
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:
@ -170,13 +188,22 @@ class TraktAPI:
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 account and account in sickbeard.TRAKT_ACCOUNTS):
return self.replace_account(account, resp['access_token'], resp['refresh_token'], token_valid_date, refresh)
else:
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, **kwargs):
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:
@ -186,7 +213,7 @@ class TraktAPI:
count > 1 and time.sleep(sleep_retry)
headers = headers or self.headers
if send_oauth and send_oauth in sickbeard.TRAKT_ACCOUNTS:
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)
@ -202,54 +229,77 @@ class TraktAPI:
url = url or self.api_url
try:
resp = self.session.request(('GET', 'POST')['data' in kwargs.keys()],
url + path, **kwargs)
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)
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 send_oauth:
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)
else:
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()
else:
# 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()
else:
return self.trakt_request(path, data, headers, url, count=count, sleep_retry=sleep_retry, send_oauth=send_oauth)
else:
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)
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' % url + path, logger.WARNING)
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):
@ -257,9 +307,8 @@ class TraktAPI:
raise TraktException(resp['message'])
if 'error' in resp:
raise TraktException(resp['error'])
else:
raise TraktException('Unknown Error')
if send_oauth and send_oauth in sickbeard.TRAKT_ACCOUNTS:
if None is not send_oauth and send_oauth in sickbeard.TRAKT_ACCOUNTS:
sickbeard.TRAKT_ACCOUNTS[send_oauth].reset_auth_failure()
return resp

View file

@ -3321,6 +3321,20 @@ class NewHomeAddShows(Home):
'recommendations/shows?limit=%s&' % 100, 'Recommended for <b class="grey-text">%s</b> by Trakt' % name,
mode='recommended-%s' % account, send_oauth=account)
def trakt_watchlist(self, *args, **kwargs):
if 'add' == kwargs.get('action'):
return self.redirect('/config/notifications/#tabs-3')
account = sickbeard.helpers.tryInt(kwargs.get('account'), None)
try:
name = sickbeard.TRAKT_ACCOUNTS[account].name
except KeyError:
return self.trakt_default()
return self.browse_trakt(
'users/%s/watchlist/shows?limit=%s&' % (sickbeard.TRAKT_ACCOUNTS[account].slug, 100), 'WatchList for <b class="grey-text">%s</b> by Trakt' % name,
mode='watchlist-%s' % account, send_oauth=account)
def trakt_default(self):
return self.redirect('/home/addShows/%s' % ('trakt_trending', sickbeard.TRAKT_MRU)[any(sickbeard.TRAKT_MRU)])
@ -3330,7 +3344,7 @@ class NewHomeAddShows(Home):
browse_type = 'Trakt'
normalised, filtered = ([], [])
if not sickbeard.USE_TRAKT and 'recommended' in kwargs.get('mode', ''):
if not sickbeard.USE_TRAKT and ('recommended' in kwargs.get('mode', '') or 'watchlist' in kwargs.get('mode', '')):
error_msg = 'To browse personal recommendations, enable Trakt.tv in Config/Notifications/Social'
return self.browse_shows(browse_type, browse_title, filtered, error_msg=error_msg, show_header=1, **kwargs)