From 9cdf57f98975aaa363002639897b6352b1a165d0 Mon Sep 17 00:00:00 2001 From: Prinz23 Date: Sun, 23 Apr 2017 22:34:17 +0100 Subject: [PATCH] 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. --- .../interfaces/default/home_browseShows.tmpl | 12 ++ lib/libtrakt/trakt.py | 149 ++++++++++++------ sickbeard/webserve.py | 16 +- 3 files changed, 126 insertions(+), 51 deletions(-) diff --git a/gui/slick/interfaces/default/home_browseShows.tmpl b/gui/slick/interfaces/default/home_browseShows.tmpl index a780c583..a11a2a73 100644 --- a/gui/slick/interfaces/default/home_browseShows.tmpl +++ b/gui/slick/interfaces/default/home_browseShows.tmpl @@ -196,6 +196,18 @@ $(document).ready(function(){ #else + #end if + + #if any($sg_var('TRAKT_ACCOUNTS', [])) + + #for $account in $sg_var('TRAKT_ACCOUNTS') + #if $sg_var('TRAKT_ACCOUNTS').get($account).active and $sg_var('TRAKT_ACCOUNTS').get($account).name + + #end if + #end for + #else + + #end if #elif 'IMDb' == $browse_type diff --git a/lib/libtrakt/trakt.py b/lib/libtrakt/trakt.py index 07dafa35..4096067d 100644 --- a/lib/libtrakt/trakt.py +++ b/lib/libtrakt/trakt.py @@ -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 + 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: - 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'] - except TraktAuthException: - self.inc_auth_failure() - self._name = '' - except TraktException: - pass + 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: - return self.add_account(resp['access_token'], resp['refresh_token'], token_valid_date) + 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 + # It means there basically was no response at all else: - logger.log(u'Could not connect to Trakt. Error: {0}'.format(e), logger.WARNING) + 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: - 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 + 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() - 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: - raise TraktAuthException() + 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) + 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') + 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 diff --git a/sickbeard/webserve.py b/sickbeard/webserve.py index c061740e..7baa3109 100644 --- a/sickbeard/webserve.py +++ b/sickbeard/webserve.py @@ -3321,6 +3321,20 @@ class NewHomeAddShows(Home): 'recommendations/shows?limit=%s&' % 100, 'Recommended for %s 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 %s 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)