From 96467210c8e933489af465e85ee4f58561e0f5c0 Mon Sep 17 00:00:00 2001 From: Prinz23 Date: Mon, 21 Sep 2015 17:46:04 +0200 Subject: [PATCH 1/2] Change replace trakt with libtrakt for API v2. --- CHANGES.md | 1 + .../default/config_notifications.tmpl | 33 +--- gui/slick/js/configNotifications.js | 38 ++--- lib/libtrakt/__init__.py | 1 + lib/libtrakt/exceptions.py | 10 ++ lib/libtrakt/trakt.py | 142 ++++++++++++++++++ lib/trakt/__init__.py | 67 --------- sickbeard/__init__.py | 38 +++-- sickbeard/notifiers/__init__.py | 6 +- sickbeard/notifiers/trakt.py | 2 +- sickbeard/traktChecker.py | 2 +- sickbeard/webserve.py | 68 +++++---- 12 files changed, 243 insertions(+), 165 deletions(-) create mode 100644 lib/libtrakt/__init__.py create mode 100644 lib/libtrakt/exceptions.py create mode 100644 lib/libtrakt/trakt.py delete mode 100644 lib/trakt/__init__.py diff --git a/CHANGES.md b/CHANGES.md index d537c527..df34c3d0 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -60,6 +60,7 @@ * Change to throttle connection rate on thread initiation for adba library * Change default manage episodes selector to Snatched episodes if items exist else Wanted on Episode Status Manage page * Change snatched row colour on Episode Status Manage page to match colour used on the show details page +* Change replace trakt with libtrakt for API v2 [develop changelog] Enable Alpha Ratio again now that the secure login page over https is fixed diff --git a/gui/slick/interfaces/default/config_notifications.tmpl b/gui/slick/interfaces/default/config_notifications.tmpl index 6515de23..c59eac9b 100644 --- a/gui/slick/interfaces/default/config_notifications.tmpl +++ b/gui/slick/interfaces/default/config_notifications.tmpl @@ -1,5 +1,6 @@ #import sickbeard #import re +#from lib.libtrakt import TraktAPI #from sickbeard.helpers import anon_url, starify ## #set global $title = 'Config - Notifications' @@ -1480,33 +1481,15 @@
-
-
- - -
-
- -
@@ -1582,8 +1565,6 @@
-
Click below to test.
- diff --git a/gui/slick/js/configNotifications.js b/gui/slick/js/configNotifications.js index 62dcd4e3..a3f777a6 100644 --- a/gui/slick/js/configNotifications.js +++ b/gui/slick/js/configNotifications.js @@ -352,36 +352,20 @@ $(document).ready(function(){ }); }); - $('#testTrakt').click(function () { - var trakt_api = $.trim($('#trakt_api').val()); - var trakt_username = $.trim($('#trakt_username').val()); - var trakt_password = $.trim($('#trakt_password').val()); - if (!trakt_api || !trakt_username || !trakt_password) { + $('#pinTrakt').click(function () { + var trakt_pin = $.trim($('#trakt_pin').val()); + if (!trakt_pin) { $('#testTrakt-result').html('Please fill out the necessary fields above.'); - if (!trakt_api) { - $('#trakt_api').addClass('warning'); - } else { - $('#trakt_api').removeClass('warning'); - } - if (!trakt_username) { - $('#trakt_username').addClass('warning'); - } else { - $('#trakt_username').removeClass('warning'); - } - if (!trakt_password) { - $('#trakt_password').addClass('warning'); - } else { - $('#trakt_password').removeClass('warning'); - } + $('#trakt_pin').addClass('warning'); return; } - $('#trakt_api,#trakt_username,#trakt_password').removeClass('warning'); + $('#trakt_pin').removeClass('warning'); $(this).prop('disabled', true); $('#testTrakt-result').html(loading); - $.get(sbRoot + '/home/testTrakt', {'api': trakt_api, 'username': trakt_username, 'password': trakt_password}) + $.get(sbRoot + '/home/pinTrakt', {'pin': trakt_pin}) .done(function (data) { - $('#testTrakt-result').html(data); - $('#testTrakt').prop('disabled', false); + $('#pinTrakt-result').html(data); + $('#pinTrakt').prop('disabled', false); }); }); @@ -568,4 +552,10 @@ $(document).ready(function(){ } }); if ($('input[id="use_plex"]').is(':checked')) {$('.plexinfo').removeClass('hide')} + + $('#testTrakt-result').html(loading); + $.get(sbRoot + '/home/get_connected_Trakt_Account') + .done(function (data) { + $('#pinTrakt-result').html(data); + }); }); diff --git a/lib/libtrakt/__init__.py b/lib/libtrakt/__init__.py new file mode 100644 index 00000000..efdcc682 --- /dev/null +++ b/lib/libtrakt/__init__.py @@ -0,0 +1 @@ +from trakt import TraktAPI \ No newline at end of file diff --git a/lib/libtrakt/exceptions.py b/lib/libtrakt/exceptions.py new file mode 100644 index 00000000..a52ef0a9 --- /dev/null +++ b/lib/libtrakt/exceptions.py @@ -0,0 +1,10 @@ +class traktException(Exception): + pass + + +class traktAuthException(traktException): + pass + + +class traktServerBusy(traktException): + pass diff --git a/lib/libtrakt/trakt.py b/lib/libtrakt/trakt.py new file mode 100644 index 00000000..31aa3057 --- /dev/null +++ b/lib/libtrakt/trakt.py @@ -0,0 +1,142 @@ +import requests +import certifi +import json +import sickbeard +import time +from sickbeard import logger + +from exceptions import traktException, traktAuthException # , traktServerBusy + + +class TraktAPI: + def __init__(self, ssl_verify=True, timeout=30): + self.session = requests.Session() + self.verify = certifi.where() if ssl_verify else False + self.timeout = timeout if timeout else None + 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 + } + + def trakt_token(self, trakt_pin=None, refresh=False, count=0): + + if count > 3: + sickbeard.TRAKT_ACCESS_TOKEN = '' + return False + elif count > 0: + time.sleep(2) + + data = { + 'client_id': sickbeard.TRAKT_CLIENT_ID, + 'client_secret': sickbeard.TRAKT_CLIENT_SECRET, + 'redirect_uri': 'urn:ietf:wg:oauth:2.0:oob' + } + + if refresh: + data['grant_type'] = 'refresh_token' + data['refresh_token'] = sickbeard.TRAKT_REFRESH_TOKEN + else: + data['grant_type'] = 'authorization_code' + if None is not trakt_pin: + data['code'] = trakt_pin + + headers = { + 'Content-Type': 'application/json' + } + + resp = self.trakt_request('oauth/token', data=data, headers=headers, url=self.auth_url, method='POST', count=count) + + if 'access_token' in resp: + sickbeard.TRAKT_TOKEN = resp['access_token'] + if 'refresh_token' in resp: + sickbeard.TRAKT_REFRESH_TOKEN = resp['refresh_token'] + return True + return False + + def validate_account(self): + + resp = self.trakt_request('users/settings') + + if 'account' in resp: + return True + return False + + def get_connected_user(self): + + if sickbeard.TRAKT_TOKEN: + if sickbeard.TRAKT_CONNECTED_ACCOUNT and sickbeard.TRAKT_TOKEN == sickbeard.TRAKT_CONNECTED_ACCOUNT[1] and sickbeard.TRAKT_CONNECTED_ACCOUNT[0]: + return 'Connected to Trakt user account: ' + str(sickbeard.TRAKT_CONNECTED_ACCOUNT[0]) + + resp = self.trakt_request('users/settings') + + if 'user' in resp: + sickbeard.TRAKT_CONNECTED_ACCOUNT = [resp['user']['username'], sickbeard.TRAKT_TOKEN] + return 'Connected to Trakt user account: ' + str(sickbeard.TRAKT_CONNECTED_ACCOUNT[0]) + return 'Not Connected to Trakt' + + def trakt_request(self, path, data=None, headers=None, url=None, method='GET', count=0): + if None is url: + url = self.api_url + + count += 1 + + if None is headers: + headers = self.headers + + if None is sickbeard.TRAKT_TOKEN: + logger.log(u'You must get a Trakt TOKEN. Check your Trakt settings', logger.WARNING) + return {} + + headers['Authorization'] = 'Bearer ' + sickbeard.TRAKT_TOKEN + + try: + resp = self.session.request(method, url + path, headers=headers, timeout=self.timeout, + data=json.dumps(data) if data else [], verify=self.verify) + + # 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. Try to increase timeout value in Trakt settings', logger.WARNING) + # 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, method) + elif 401 == code: + if self.trakt_token(refresh=True, count=count): + sickbeard.save_config() + return self.trakt_request(path, data, headers, url, method) + else: + logger.log(u'Unauthorized. Please check your Trakt settings', logger.WARNING) + 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. Try again later please', logger.WARNING) + elif 404 == code: + logger.log(u'Trakt error (404) the resource does not exist: %s' % url + path, logger.WARNING) + else: + logger.log(u'Could not connect to Trakt. Code error: {0}'.format(code), logger.ERROR) + return {} + + # check and confirm trakt call did not fail + if isinstance(resp, dict) and 'failure' == resp.get('status', False): + if 'message' in resp: + raise traktException(resp['message']) + if 'error' in resp: + raise traktException(resp['error']) + else: + raise traktException('Unknown Error') + + return resp diff --git a/lib/trakt/__init__.py b/lib/trakt/__init__.py deleted file mode 100644 index 8fdc70e4..00000000 --- a/lib/trakt/__init__.py +++ /dev/null @@ -1,67 +0,0 @@ -from urllib2 import Request, urlopen, HTTPError, URLError - -import base64 -from sha import new as sha1 - -try: - import json -except ImportError: - from lib import simplejson as json - -def TraktCall(method, api, username=None, password=None, data={}): - """ - A generic method for communicating with trakt. Uses the method and data provided along - with the auth info to send the command. - - method: The URL to use at trakt, relative, no leading slash. - api: The API string to provide to trakt - username: The username to use when logging in - password: The unencrypted password to use when logging in - - Returns: A boolean representing success - """ - #logger.log("trakt: Call method " + method, logger.DEBUG) - - # if the API isn't given then it failed - if not api: - return None - - # replace the API string with what we found - method = method.replace("%API%", api) - - # make the full url - url = 'https://api-v2launch.trakt.tv/' + method - - # take the URL params and make a json object out of them - encoded_data = json.JSONEncoder().encode(data) - - request = Request(url, encoded_data) - - # if the username isn't given then it failed - if username and password: - pwdsha1 = sha1(password).hexdigest() - base64string = base64.encodestring('%s:%s' % (username, pwdsha1)).replace('\n', '') - request.add_header("Accept", "*/*") - request.add_header("User-Agent", "CPython/2.7.5 Unknown/Unknown") - request.add_header("Authorization", "Basic %s" % base64string) - - # request the URL from trakt and parse the result as json - try: - #logger.log("trakt: Calling method http://api.trakt.tv/" + method + ", with data" + encoded_data, logger.DEBUG) - stream = urlopen(request).read() - - # check if results are valid - if stream == '[]': - resp = 'NULL' - else: - resp = json.JSONDecoder().decode(stream) - - if ("error" in resp): - raise Exception(resp["error"]) - - except (IOError): - #logger.log("trakt: Failed calling method", logger.ERROR) - return None - - #logger.log("trakt: Failed calling method", logger.ERROR) - return resp \ No newline at end of file diff --git a/sickbeard/__init__.py b/sickbeard/__init__.py index 1118f245..167bf219 100755 --- a/sickbeard/__init__.py +++ b/sickbeard/__init__.py @@ -356,9 +356,8 @@ SYNOLOGYNOTIFIER_NOTIFY_ONDOWNLOAD = False SYNOLOGYNOTIFIER_NOTIFY_ONSUBTITLEDOWNLOAD = False USE_TRAKT = False -TRAKT_USERNAME = None -TRAKT_PASSWORD = None -TRAKT_API = '' +TRAKT_TOKEN = '' +TRAKT_REFRESH_TOKEN = '' TRAKT_REMOVE_WATCHLIST = False TRAKT_REMOVE_SERIESLIST = False TRAKT_USE_WATCHLIST = False @@ -449,7 +448,26 @@ REQUIRE_WORDS = '' CALENDAR_UNPROTECTED = False TMDB_API_KEY = 'edc5f123313769de83a71e157758030b' -TRAKT_API_KEY = 'abd806c54516240c76e4ebc9c5ccf394' + +# to switch between staging and production TRAKT environment +TRAKT_STAGING = False + +TRAKT_TIMEOUT = 60 +TRAKT_VERIFY = True +TRAKT_CONNECTED_ACCOUNT = None + +if TRAKT_STAGING: + # staging trakt values: + TRAKT_CLIENT_ID = '2aae3052f90b14235d184cc8f709b12b4fd8ae35f339a060a890c70db92be87a' + TRAKT_CLIENT_SECRET = '900e03471220503843d4a856bfbef17080cddb630f2b7df6a825e96e3ff3c39e' + TRAKT_PIN_URL = 'https://staging.trakt.tv/pin/638' + TRAKT_BASE_URL = 'http://api.staging.trakt.tv/' +else: + # production trakt values: + TRAKT_CLIENT_ID = 'f1c453c67d81f1307f9118172c408a883eb186b094d5ea33080d59ddedb7fc7c' + TRAKT_CLIENT_SECRET = '12efb6fb6e863a08934d9904032a90008325df7e23514650cade55e7e7c118c5' + TRAKT_PIN_URL = 'https://trakt.tv/pin/6314' + TRAKT_BASE_URL = 'https://api-v2launch.trakt.tv/' COOKIE_SECRET = base64.b64encode(uuid.uuid4().bytes + uuid.uuid4().bytes) @@ -472,7 +490,7 @@ def initialize(consoleLogging=True): USE_XBMC, XBMC_ALWAYS_ON, XBMC_NOTIFY_ONSNATCH, XBMC_NOTIFY_ONDOWNLOAD, XBMC_NOTIFY_ONSUBTITLEDOWNLOAD, XBMC_UPDATE_FULL, XBMC_UPDATE_ONLYFIRST, \ XBMC_UPDATE_LIBRARY, XBMC_HOST, XBMC_USERNAME, XBMC_PASSWORD, BACKLOG_FREQUENCY, \ USE_KODI, KODI_ALWAYS_ON, KODI_NOTIFY_ONSNATCH, KODI_NOTIFY_ONDOWNLOAD, KODI_NOTIFY_ONSUBTITLEDOWNLOAD, KODI_UPDATE_FULL, KODI_UPDATE_ONLYFIRST, KODI_UPDATE_LIBRARY, KODI_HOST, KODI_USERNAME, KODI_PASSWORD, \ - USE_TRAKT, TRAKT_USERNAME, TRAKT_PASSWORD, TRAKT_API, TRAKT_REMOVE_WATCHLIST, TRAKT_USE_WATCHLIST, TRAKT_METHOD_ADD, TRAKT_START_PAUSED, traktCheckerScheduler, TRAKT_USE_RECOMMENDED, TRAKT_SYNC, TRAKT_DEFAULT_INDEXER, TRAKT_REMOVE_SERIESLIST, \ + USE_TRAKT, TRAKT_CONNECTED_ACCOUNT, TRAKT_VERIFY, TRAKT_REMOVE_WATCHLIST, TRAKT_TOKEN, TRAKT_TIMEOUT, TRAKT_REFRESH_TOKEN, TRAKT_USE_WATCHLIST, TRAKT_METHOD_ADD, TRAKT_START_PAUSED, traktCheckerScheduler, TRAKT_USE_RECOMMENDED, TRAKT_SYNC, TRAKT_DEFAULT_INDEXER, TRAKT_REMOVE_SERIESLIST, \ USE_PLEX, PLEX_NOTIFY_ONSNATCH, PLEX_NOTIFY_ONDOWNLOAD, PLEX_NOTIFY_ONSUBTITLEDOWNLOAD, PLEX_UPDATE_LIBRARY, \ PLEX_SERVER_HOST, PLEX_HOST, PLEX_USERNAME, PLEX_PASSWORD, DEFAULT_BACKLOG_FREQUENCY, MIN_BACKLOG_FREQUENCY, MAX_BACKLOG_FREQUENCY, BACKLOG_STARTUP, SKIP_REMOVED_FILES, \ showUpdateScheduler, __INITIALIZED__, LAUNCH_BROWSER, TRASH_REMOVE_SHOW, TRASH_ROTATE_LOGS, HOME_SEARCH_FOCUS, SORT_ARTICLE, showList, loadingShowList, UPDATE_SHOWS_ON_START, SHOW_UPDATE_HOUR, ALLOW_INCOMPLETE_SHOWDATA, \ @@ -860,9 +878,8 @@ def initialize(consoleLogging=True): check_setting_int(CFG, 'SynologyNotifier', 'synologynotifier_notify_onsubtitledownload', 0)) USE_TRAKT = bool(check_setting_int(CFG, 'Trakt', 'use_trakt', 0)) - TRAKT_USERNAME = check_setting_str(CFG, 'Trakt', 'trakt_username', '') - TRAKT_PASSWORD = check_setting_str(CFG, 'Trakt', 'trakt_password', '') - TRAKT_API = check_setting_str(CFG, 'Trakt', 'trakt_api', '') + TRAKT_TOKEN = check_setting_str(CFG, 'Trakt', 'trakt_token', '') + TRAKT_REFRESH_TOKEN = check_setting_str(CFG, 'Trakt', 'trakt_refresh_token', '') TRAKT_REMOVE_WATCHLIST = bool(check_setting_int(CFG, 'Trakt', 'trakt_remove_watchlist', 0)) TRAKT_REMOVE_SERIESLIST = bool(check_setting_int(CFG, 'Trakt', 'trakt_remove_serieslist', 0)) TRAKT_USE_WATCHLIST = bool(check_setting_int(CFG, 'Trakt', 'trakt_use_watchlist', 0)) @@ -1673,9 +1690,8 @@ def save_config(): new_config['Trakt'] = {} new_config['Trakt']['use_trakt'] = int(USE_TRAKT) - new_config['Trakt']['trakt_username'] = TRAKT_USERNAME - new_config['Trakt']['trakt_password'] = helpers.encrypt(TRAKT_PASSWORD, ENCRYPTION_VERSION) - new_config['Trakt']['trakt_api'] = TRAKT_API + new_config['Trakt']['trakt_token'] = TRAKT_TOKEN + new_config['Trakt']['trakt_refresh_token'] = TRAKT_REFRESH_TOKEN new_config['Trakt']['trakt_remove_watchlist'] = int(TRAKT_REMOVE_WATCHLIST) new_config['Trakt']['trakt_remove_serieslist'] = int(TRAKT_REMOVE_SERIESLIST) new_config['Trakt']['trakt_use_watchlist'] = int(TRAKT_USE_WATCHLIST) diff --git a/sickbeard/notifiers/__init__.py b/sickbeard/notifiers/__init__.py index 83094dfd..c48228ef 100644 --- a/sickbeard/notifiers/__init__.py +++ b/sickbeard/notifiers/__init__.py @@ -37,7 +37,7 @@ import pushalot import pushbullet import tweet -import trakt +from lib import libtrakt import emailnotify from sickbeard.common import * @@ -62,7 +62,7 @@ pushalot_notifier = pushalot.PushalotNotifier() pushbullet_notifier = pushbullet.PushbulletNotifier() # social twitter_notifier = tweet.TwitterNotifier() -trakt_notifier = trakt.TraktNotifier() +#trakt_notifier = trakt.TraktNotifier() email_notifier = emailnotify.EmailNotifier() notifiers = [ @@ -83,7 +83,7 @@ notifiers = [ pushalot_notifier, pushbullet_notifier, twitter_notifier, - trakt_notifier, +# trakt_notifier, email_notifier, ] diff --git a/sickbeard/notifiers/trakt.py b/sickbeard/notifiers/trakt.py index de780ff5..75929f87 100644 --- a/sickbeard/notifiers/trakt.py +++ b/sickbeard/notifiers/trakt.py @@ -18,7 +18,7 @@ import sickbeard from sickbeard import logger -from lib.trakt import * +from lib.libtrakt import TraktAPI class TraktNotifier: diff --git a/sickbeard/traktChecker.py b/sickbeard/traktChecker.py index 0affba23..8b988c33 100644 --- a/sickbeard/traktChecker.py +++ b/sickbeard/traktChecker.py @@ -26,7 +26,7 @@ from sickbeard import logger from sickbeard import helpers from sickbeard import search_queue from sickbeard.common import SKIPPED, WANTED -from lib.trakt import * +from lib import libtrakt class TraktChecker(): diff --git a/sickbeard/webserve.py b/sickbeard/webserve.py index e9a8821b..fc728dcb 100644 --- a/sickbeard/webserve.py +++ b/sickbeard/webserve.py @@ -54,7 +54,8 @@ from lib import adba from lib import subliminal from lib.dateutil import tz from lib.unrar2 import RarFile -from lib.trakt import TraktCall +from lib.libtrakt import TraktAPI +from lib.libtrakt.exceptions import traktException try: import json @@ -879,19 +880,23 @@ class Home(MainHandler): return '{"message": "Unable to find NMJ Database at location: %(dbloc)s. Is the right location selected and PCH running?", "database": ""}' % { "dbloc": dbloc} - def testTrakt(self, api=None, username=None, password=None): + def pinTrakt(self, pin=None): self.set_header('Cache-Control', 'max-age=0,no-cache,no-store') - if None is not api and starify(api, True): - api = sickbeard.TRAKT_API - if None is not password and set('*') == set(password): - password = sickbeard.TRAKT_PASSWORD + if None is pin: + return 'Enter PIN' + + result = TraktAPI(ssl_verify=False, timeout=sickbeard.TRAKT_TIMEOUT).trakt_token(pin) - result = notifiers.trakt_notifier.test_notify(api, username, password) if result: - return 'Test notice sent successfully to Trakt' + sickbeard.USE_TRAKT = 1 + sickbeard.save_config() + return 'Trakt Authorized' else: - return 'Test notice failed to Trakt' + return 'Trakt NOT Authorized' + + def get_connected_Trakt_Account(self): + return TraktAPI(ssl_verify=sickbeard.TRAKT_VERIFY, timeout=sickbeard.TRAKT_TIMEOUT).get_connected_user() def loadShowNotifyLists(self, *args, **kwargs): self.set_header('Cache-Control', 'max-age=0,no-cache,no-store') @@ -2273,22 +2278,27 @@ class NewHomeAddShows(Home): t = PageTemplate(headers=self.request.headers, file='home_trendingShows.tmpl') t.submenu = self.HomeMenu() - t.trending_shows = TraktCall('shows/trending.json/%API%', sickbeard.TRAKT_API_KEY) - t.trending_inlibrary = 0 - if None is not t.trending_shows: - for item in t.trending_shows: - tvdbs = ['tvdb_id', 'tvrage_id'] - for index, tvdb in enumerate(tvdbs): - try: - item[u'show_id'] = str(item[tvdb]) - tvshow = helpers.findCertainShow(sickbeard.showList, int(item[tvdb])) - except: - continue - # check tvshow indexer is not using the same id from another indexer - if tvshow and (index + 1) == tvshow.indexer: - item[u'show_id'] = u'%s:%s' % (tvshow.indexer, item[tvdb]) - t.trending_inlibrary += 1 - break + trakt_api = TraktAPI(ssl_verify=sickbeard.TRAKT_VERIFY, timeout=sickbeard.TRAKT_TIMEOUT) + limit_show = 50 + try: + t.trending_shows = trakt_api.trakt_request("shows/trending?limit=" + str(limit_show) + "&extended=full,images") or [] + t.trending_inlibrary = 0 + if None is not t.trending_shows: + for item in t.trending_shows: + tvdbs = ['tvdb_id', 'tvrage_id'] + for index, tvdb in enumerate(tvdbs): + try: + item[u'show_id'] = str(item[tvdb]) + tvshow = helpers.findCertainShow(sickbeard.showList, int(item[tvdb])) + except: + continue + # check tvshow indexer is not using the same id from another indexer + if tvshow and (index + 1) == tvshow.indexer: + item[u'show_id'] = u'%s:%s' % (tvshow.indexer, item[tvdb]) + t.trending_inlibrary += 1 + break + except traktException as e: + logger.log(u"Could not connect to Trakt service: %s" % ex(e), logger.WARNING) return t.respond() @@ -4368,7 +4378,7 @@ class ConfigNotifications(Config): libnotify_notify_onsubtitledownload=None, use_nmj=None, nmj_host=None, nmj_database=None, nmj_mount=None, use_synoindex=None, use_nmjv2=None, nmjv2_host=None, nmjv2_dbloc=None, nmjv2_database=None, - use_trakt=None, trakt_username=None, trakt_password=None, trakt_api=None, + use_trakt=None, trakt_pin=None, trakt_remove_watchlist=None, trakt_use_watchlist=None, trakt_method_add=None, trakt_start_paused=None, trakt_use_recommended=None, trakt_sync=None, trakt_default_indexer=None, trakt_remove_serieslist=None, @@ -4497,12 +4507,6 @@ class ConfigNotifications(Config): synologynotifier_notify_onsubtitledownload) sickbeard.USE_TRAKT = config.checkbox_to_value(use_trakt) - sickbeard.TRAKT_USERNAME = trakt_username - if set('*') != set(trakt_password): - sickbeard.TRAKT_PASSWORD = trakt_password - key = trakt_api.strip() - if not starify(key, True): - sickbeard.TRAKT_API = key sickbeard.TRAKT_REMOVE_WATCHLIST = config.checkbox_to_value(trakt_remove_watchlist) sickbeard.TRAKT_REMOVE_SERIESLIST = config.checkbox_to_value(trakt_remove_serieslist) sickbeard.TRAKT_USE_WATCHLIST = config.checkbox_to_value(trakt_use_watchlist) From 74feb39eb666184e957e93c5883715c4d247a58e Mon Sep 17 00:00:00 2001 From: JackDandy Date: Mon, 19 Oct 2015 16:37:26 +0100 Subject: [PATCH 2/2] Change Trakt notification config to only handle PIN authentication with the service. Fix handling of an erroneous PIN input/authentication flow and clean-up the ui and results output. Remove all other Trakt deprecated API V1 service features pending reconsideration. --- CHANGES.md | 2 + .../default/config_notifications.tmpl | 19 +++--- gui/slick/js/configNotifications.js | 41 ++++++------- lib/libtrakt/trakt.py | 59 +++++++++---------- sickbeard/__init__.py | 5 +- sickbeard/postProcessor.py | 2 +- sickbeard/webserve.py | 51 ++++++++-------- 7 files changed, 88 insertions(+), 91 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index df34c3d0..70137352 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -61,6 +61,8 @@ * Change default manage episodes selector to Snatched episodes if items exist else Wanted on Episode Status Manage page * Change snatched row colour on Episode Status Manage page to match colour used on the show details page * Change replace trakt with libtrakt for API v2 +* Change Trakt notification config to only handle PIN authentication with the service +* Remove all other Trakt deprecated API V1 service features pending reconsideration [develop changelog] Enable Alpha Ratio again now that the secure login page over https is fixed diff --git a/gui/slick/interfaces/default/config_notifications.tmpl b/gui/slick/interfaces/default/config_notifications.tmpl index c59eac9b..86ce8193 100644 --- a/gui/slick/interfaces/default/config_notifications.tmpl +++ b/gui/slick/interfaces/default/config_notifications.tmpl @@ -1466,7 +1466,7 @@

Trakt

-

trakt helps keep a record of what TV shows and movies you are watching. Based on your favorites, trakt recommends additional shows and movies you'll enjoy!

+

Trakt can keep a record of what TV shows you are watching and recommend additional shows based on your show data.

@@ -1474,7 +1474,7 @@ Enable -

should SickGear send Trakt.tv notifications ?

+

should SickGear use Trakt.tv ?

@@ -1483,15 +1483,15 @@
-
- +
+
diff --git a/gui/slick/js/configNotifications.js b/gui/slick/js/configNotifications.js index a3f777a6..1f47dcbd 100644 --- a/gui/slick/js/configNotifications.js +++ b/gui/slick/js/configNotifications.js @@ -352,23 +352,30 @@ $(document).ready(function(){ }); }); - $('#pinTrakt').click(function () { - var trakt_pin = $.trim($('#trakt_pin').val()); - if (!trakt_pin) { - $('#testTrakt-result').html('Please fill out the necessary fields above.'); - $('#trakt_pin').addClass('warning'); - return; + var elTraktAuth = $('#trakt-authenticate'), elTraktAuthResult = $('#trakt-authentication-result'); + elTraktAuth.click(function() { + var elTrakt = $('#trakt_pin'), traktPin = $.trim(elTrakt.val()); + if(!traktPin) { + elTrakt.addClass('warning'); + elTraktAuthResult.html('Please enter a required PIN above.'); + } else { + elTrakt.removeClass('warning'); + $(this).prop('disabled', true); + elTraktAuthResult.html(loading); + $.get(sbRoot + '/home/trakt_authenticate', {'pin': traktPin}) + .done(function(data) { + elTraktAuthResult.html(data); + elTraktAuth.prop('disabled', false); + }); } - $('#trakt_pin').removeClass('warning'); - $(this).prop('disabled', true); - $('#testTrakt-result').html(loading); - $.get(sbRoot + '/home/pinTrakt', {'pin': trakt_pin}) - .done(function (data) { - $('#pinTrakt-result').html(data); - $('#pinTrakt').prop('disabled', false); - }); }); + elTraktAuthResult.html(loading); + $.get(sbRoot + '/home/trakt_get_connected_account') + .done(function(data) { + elTraktAuthResult.html(data); + }); + $('#testEmail').click(function () { var status, host, port, tls, from, user, pwd, err, to; status = $('#testEmail-result'); @@ -552,10 +559,4 @@ $(document).ready(function(){ } }); if ($('input[id="use_plex"]').is(':checked')) {$('.plexinfo').removeClass('hide')} - - $('#testTrakt-result').html(loading); - $.get(sbRoot + '/home/get_connected_Trakt_Account') - .done(function (data) { - $('#pinTrakt-result').html(data); - }); }); diff --git a/lib/libtrakt/trakt.py b/lib/libtrakt/trakt.py index 31aa3057..5018719e 100644 --- a/lib/libtrakt/trakt.py +++ b/lib/libtrakt/trakt.py @@ -9,10 +9,12 @@ from exceptions import traktException, traktAuthException # , traktServerBusy class TraktAPI: - def __init__(self, ssl_verify=True, timeout=30): + + def __init__(self, ssl_verify=True, timeout=None): + self.session = requests.Session() - self.verify = certifi.where() if ssl_verify else False - self.timeout = timeout if timeout else None + self.verify = ssl_verify and 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 = { @@ -23,11 +25,11 @@ class TraktAPI: def trakt_token(self, trakt_pin=None, refresh=False, count=0): - if count > 3: + if 3 <= count: sickbeard.TRAKT_ACCESS_TOKEN = '' return False - elif count > 0: - time.sleep(2) + elif 0 < count: + time.sleep(3) data = { 'client_id': sickbeard.TRAKT_CLIENT_ID, @@ -40,9 +42,9 @@ class TraktAPI: data['refresh_token'] = sickbeard.TRAKT_REFRESH_TOKEN else: data['grant_type'] = 'authorization_code' - if None is not trakt_pin: + if trakt_pin: data['code'] = trakt_pin - + headers = { 'Content-Type': 'application/json' } @@ -57,39 +59,36 @@ class TraktAPI: return False def validate_account(self): - + resp = self.trakt_request('users/settings') - - if 'account' in resp: - return True - return False + + return 'account' in resp def get_connected_user(self): if sickbeard.TRAKT_TOKEN: + response = 'Connected to Trakt user account: %s' + if sickbeard.TRAKT_CONNECTED_ACCOUNT and sickbeard.TRAKT_TOKEN == sickbeard.TRAKT_CONNECTED_ACCOUNT[1] and sickbeard.TRAKT_CONNECTED_ACCOUNT[0]: - return 'Connected to Trakt user account: ' + str(sickbeard.TRAKT_CONNECTED_ACCOUNT[0]) + return response % sickbeard.TRAKT_CONNECTED_ACCOUNT[0] resp = self.trakt_request('users/settings') - if 'user' in resp: sickbeard.TRAKT_CONNECTED_ACCOUNT = [resp['user']['username'], sickbeard.TRAKT_TOKEN] - return 'Connected to Trakt user account: ' + str(sickbeard.TRAKT_CONNECTED_ACCOUNT[0]) - return 'Not Connected to Trakt' + return response % sickbeard.TRAKT_CONNECTED_ACCOUNT[0] + + return 'Not connected to Trakt' def trakt_request(self, path, data=None, headers=None, url=None, method='GET', count=0): - if None is url: - url = self.api_url - count += 1 - - if None is headers: - headers = self.headers - if None is sickbeard.TRAKT_TOKEN: - logger.log(u'You must get a Trakt TOKEN. Check your Trakt settings', logger.WARNING) + logger.log(u'You must get a Trakt token. Check your Trakt settings', logger.WARNING) return {} + headers = headers or self.headers + url = url or self.api_url + count += 1 + headers['Authorization'] = 'Bearer ' + sickbeard.TRAKT_TOKEN try: @@ -111,13 +110,13 @@ class TraktAPI: 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 + # 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, method) + return self.trakt_request(path, data, headers, url, method, count=count) elif 401 == code: if self.trakt_token(refresh=True, count=count): sickbeard.save_config() - return self.trakt_request(path, data, headers, url, method) + return self.trakt_request(path, data, headers, url, method, count=count) else: logger.log(u'Unauthorized. Please check your Trakt settings', logger.WARNING) raise traktAuthException() @@ -130,8 +129,8 @@ class TraktAPI: logger.log(u'Could not connect to Trakt. Code error: {0}'.format(code), logger.ERROR) return {} - # check and confirm trakt call did not fail - if isinstance(resp, dict) and 'failure' == resp.get('status', False): + # 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: diff --git a/sickbeard/__init__.py b/sickbeard/__init__.py index 167bf219..0ead3e49 100755 --- a/sickbeard/__init__.py +++ b/sickbeard/__init__.py @@ -363,7 +363,6 @@ TRAKT_REMOVE_SERIESLIST = False TRAKT_USE_WATCHLIST = False TRAKT_METHOD_ADD = 0 TRAKT_START_PAUSED = False -TRAKT_USE_RECOMMENDED = False TRAKT_SYNC = False TRAKT_DEFAULT_INDEXER = None @@ -490,7 +489,7 @@ def initialize(consoleLogging=True): USE_XBMC, XBMC_ALWAYS_ON, XBMC_NOTIFY_ONSNATCH, XBMC_NOTIFY_ONDOWNLOAD, XBMC_NOTIFY_ONSUBTITLEDOWNLOAD, XBMC_UPDATE_FULL, XBMC_UPDATE_ONLYFIRST, \ XBMC_UPDATE_LIBRARY, XBMC_HOST, XBMC_USERNAME, XBMC_PASSWORD, BACKLOG_FREQUENCY, \ USE_KODI, KODI_ALWAYS_ON, KODI_NOTIFY_ONSNATCH, KODI_NOTIFY_ONDOWNLOAD, KODI_NOTIFY_ONSUBTITLEDOWNLOAD, KODI_UPDATE_FULL, KODI_UPDATE_ONLYFIRST, KODI_UPDATE_LIBRARY, KODI_HOST, KODI_USERNAME, KODI_PASSWORD, \ - USE_TRAKT, TRAKT_CONNECTED_ACCOUNT, TRAKT_VERIFY, TRAKT_REMOVE_WATCHLIST, TRAKT_TOKEN, TRAKT_TIMEOUT, TRAKT_REFRESH_TOKEN, TRAKT_USE_WATCHLIST, TRAKT_METHOD_ADD, TRAKT_START_PAUSED, traktCheckerScheduler, TRAKT_USE_RECOMMENDED, TRAKT_SYNC, TRAKT_DEFAULT_INDEXER, TRAKT_REMOVE_SERIESLIST, \ + USE_TRAKT, TRAKT_CONNECTED_ACCOUNT, TRAKT_VERIFY, TRAKT_REMOVE_WATCHLIST, TRAKT_TOKEN, TRAKT_TIMEOUT, TRAKT_REFRESH_TOKEN, TRAKT_USE_WATCHLIST, TRAKT_METHOD_ADD, TRAKT_START_PAUSED, traktCheckerScheduler, TRAKT_SYNC, TRAKT_DEFAULT_INDEXER, TRAKT_REMOVE_SERIESLIST, \ USE_PLEX, PLEX_NOTIFY_ONSNATCH, PLEX_NOTIFY_ONDOWNLOAD, PLEX_NOTIFY_ONSUBTITLEDOWNLOAD, PLEX_UPDATE_LIBRARY, \ PLEX_SERVER_HOST, PLEX_HOST, PLEX_USERNAME, PLEX_PASSWORD, DEFAULT_BACKLOG_FREQUENCY, MIN_BACKLOG_FREQUENCY, MAX_BACKLOG_FREQUENCY, BACKLOG_STARTUP, SKIP_REMOVED_FILES, \ showUpdateScheduler, __INITIALIZED__, LAUNCH_BROWSER, TRASH_REMOVE_SHOW, TRASH_ROTATE_LOGS, HOME_SEARCH_FOCUS, SORT_ARTICLE, showList, loadingShowList, UPDATE_SHOWS_ON_START, SHOW_UPDATE_HOUR, ALLOW_INCOMPLETE_SHOWDATA, \ @@ -885,7 +884,6 @@ def initialize(consoleLogging=True): TRAKT_USE_WATCHLIST = bool(check_setting_int(CFG, 'Trakt', 'trakt_use_watchlist', 0)) TRAKT_METHOD_ADD = check_setting_int(CFG, 'Trakt', 'trakt_method_add', 0) TRAKT_START_PAUSED = bool(check_setting_int(CFG, 'Trakt', 'trakt_start_paused', 0)) - TRAKT_USE_RECOMMENDED = bool(check_setting_int(CFG, 'Trakt', 'trakt_use_recommended', 0)) TRAKT_SYNC = bool(check_setting_int(CFG, 'Trakt', 'trakt_sync', 0)) TRAKT_DEFAULT_INDEXER = check_setting_int(CFG, 'Trakt', 'trakt_default_indexer', 1) @@ -1697,7 +1695,6 @@ def save_config(): new_config['Trakt']['trakt_use_watchlist'] = int(TRAKT_USE_WATCHLIST) new_config['Trakt']['trakt_method_add'] = int(TRAKT_METHOD_ADD) new_config['Trakt']['trakt_start_paused'] = int(TRAKT_START_PAUSED) - new_config['Trakt']['trakt_use_recommended'] = int(TRAKT_USE_RECOMMENDED) new_config['Trakt']['trakt_sync'] = int(TRAKT_SYNC) new_config['Trakt']['trakt_default_indexer'] = int(TRAKT_DEFAULT_INDEXER) diff --git a/sickbeard/postProcessor.py b/sickbeard/postProcessor.py index 7583023c..44736222 100644 --- a/sickbeard/postProcessor.py +++ b/sickbeard/postProcessor.py @@ -1034,7 +1034,7 @@ class PostProcessor(object): notifiers.pytivo_notifier.update_library(ep_obj) # do the library update for Trakt - notifiers.trakt_notifier.update_library(ep_obj) + # notifiers.trakt_notifier.update_library(ep_obj) self._run_extra_scripts(ep_obj) diff --git a/sickbeard/webserve.py b/sickbeard/webserve.py index fc728dcb..a714e55f 100644 --- a/sickbeard/webserve.py +++ b/sickbeard/webserve.py @@ -55,7 +55,8 @@ from lib import subliminal from lib.dateutil import tz from lib.unrar2 import RarFile from lib.libtrakt import TraktAPI -from lib.libtrakt.exceptions import traktException +from lib.libtrakt.exceptions import traktException, traktAuthException + try: import json @@ -880,23 +881,24 @@ class Home(MainHandler): return '{"message": "Unable to find NMJ Database at location: %(dbloc)s. Is the right location selected and PCH running?", "database": ""}' % { "dbloc": dbloc} - def pinTrakt(self, pin=None): + def trakt_authenticate(self, pin=None): self.set_header('Cache-Control', 'max-age=0,no-cache,no-store') if None is pin: - return 'Enter PIN' + return 'Trakt PIN required for authentication' - result = TraktAPI(ssl_verify=False, timeout=sickbeard.TRAKT_TIMEOUT).trakt_token(pin) + try: + TraktAPI().trakt_token(pin) + except traktAuthException: + return 'Fail: Trakt NOT authenticated' - if result: - sickbeard.USE_TRAKT = 1 - sickbeard.save_config() - return 'Trakt Authorized' - else: - return 'Trakt NOT Authorized' + sickbeard.USE_TRAKT = True + sickbeard.save_config() + return '%s %s' % ('Success: Trakt authenticated.', self.trakt_get_connected_account()) - def get_connected_Trakt_Account(self): - return TraktAPI(ssl_verify=sickbeard.TRAKT_VERIFY, timeout=sickbeard.TRAKT_TIMEOUT).get_connected_user() + @staticmethod + def trakt_get_connected_account(): + return TraktAPI().get_connected_user() def loadShowNotifyLists(self, *args, **kwargs): self.set_header('Cache-Control', 'max-age=0,no-cache,no-store') @@ -2278,7 +2280,7 @@ class NewHomeAddShows(Home): t = PageTemplate(headers=self.request.headers, file='home_trendingShows.tmpl') t.submenu = self.HomeMenu() - trakt_api = TraktAPI(ssl_verify=sickbeard.TRAKT_VERIFY, timeout=sickbeard.TRAKT_TIMEOUT) + trakt_api = TraktAPI() limit_show = 50 try: t.trending_shows = trakt_api.trakt_request("shows/trending?limit=" + str(limit_show) + "&extended=full,images") or [] @@ -4380,7 +4382,7 @@ class ConfigNotifications(Config): use_nmjv2=None, nmjv2_host=None, nmjv2_dbloc=None, nmjv2_database=None, use_trakt=None, trakt_pin=None, trakt_remove_watchlist=None, trakt_use_watchlist=None, trakt_method_add=None, - trakt_start_paused=None, trakt_use_recommended=None, trakt_sync=None, + trakt_start_paused=None, trakt_sync=None, trakt_default_indexer=None, trakt_remove_serieslist=None, use_synologynotifier=None, synologynotifier_notify_onsnatch=None, synologynotifier_notify_ondownload=None, synologynotifier_notify_onsubtitledownload=None, @@ -4507,19 +4509,14 @@ class ConfigNotifications(Config): synologynotifier_notify_onsubtitledownload) sickbeard.USE_TRAKT = config.checkbox_to_value(use_trakt) - sickbeard.TRAKT_REMOVE_WATCHLIST = config.checkbox_to_value(trakt_remove_watchlist) - sickbeard.TRAKT_REMOVE_SERIESLIST = config.checkbox_to_value(trakt_remove_serieslist) - sickbeard.TRAKT_USE_WATCHLIST = config.checkbox_to_value(trakt_use_watchlist) - sickbeard.TRAKT_METHOD_ADD = int(trakt_method_add) - sickbeard.TRAKT_START_PAUSED = config.checkbox_to_value(trakt_start_paused) - sickbeard.TRAKT_USE_RECOMMENDED = config.checkbox_to_value(trakt_use_recommended) - sickbeard.TRAKT_SYNC = config.checkbox_to_value(trakt_sync) - sickbeard.TRAKT_DEFAULT_INDEXER = int(trakt_default_indexer) - - if sickbeard.USE_TRAKT: - sickbeard.traktCheckerScheduler.silent = False - else: - sickbeard.traktCheckerScheduler.silent = True + sickbeard.traktCheckerScheduler.silent = not sickbeard.USE_TRAKT + # sickbeard.TRAKT_DEFAULT_INDEXER = int(trakt_default_indexer) + # sickbeard.TRAKT_SYNC = config.checkbox_to_value(trakt_sync) + # sickbeard.TRAKT_USE_WATCHLIST = config.checkbox_to_value(trakt_use_watchlist) + # sickbeard.TRAKT_METHOD_ADD = int(trakt_method_add) + # sickbeard.TRAKT_REMOVE_WATCHLIST = config.checkbox_to_value(trakt_remove_watchlist) + # sickbeard.TRAKT_REMOVE_SERIESLIST = config.checkbox_to_value(trakt_remove_serieslist) + # sickbeard.TRAKT_START_PAUSED = config.checkbox_to_value(trakt_start_paused) sickbeard.USE_EMAIL = config.checkbox_to_value(use_email) sickbeard.EMAIL_NOTIFY_ONSNATCH = config.checkbox_to_value(email_notify_onsnatch)