From c6b7348c3750730c6f697e16678df8fd7110b28f Mon Sep 17 00:00:00 2001 From: echel0n Date: Fri, 28 Nov 2014 14:07:26 -0800 Subject: [PATCH] Overhaul of Trakt.TV API handler --- lib/trakt/__init__.py | 22 +------ lib/trakt/exceptions.py | 8 +++ lib/trakt/trakt.py | 53 +++++++++++++++ sickbeard/notifiers/trakt.py | 121 +++++++++++++++++------------------ sickbeard/traktChecker.py | 56 ++++++++++------ sickbeard/webserve.py | 55 ++++++++++------ 6 files changed, 193 insertions(+), 122 deletions(-) create mode 100644 lib/trakt/exceptions.py create mode 100644 lib/trakt/trakt.py diff --git a/lib/trakt/__init__.py b/lib/trakt/__init__.py index 501370ae..bfcea869 100644 --- a/lib/trakt/__init__.py +++ b/lib/trakt/__init__.py @@ -1,20 +1,2 @@ -import hashlib -import requests - -def TraktCall(method, api, username=None, password=None, data={}): - base_url = 'http://api.trakt.tv/' - - # if username and password given then encode password with sha1 - auth = None - if username and password: - auth = (username, hashlib.sha1(password.encode('utf-8')).hexdigest()) - - # request the URL from trakt and parse the result as json - try: - resp = requests.get(base_url + method.replace("%API%", api), auth=auth, data=data).json() - if isinstance(resp, dict) and resp.get('status', False) == 'failure': - raise Exception(resp.get('error', 'Unknown Error')) - except: - return None - - return resp \ No newline at end of file +from trakt import TraktAPI +from trakt.exceptions import traktException, traktAuthException, traktServerBusy \ No newline at end of file diff --git a/lib/trakt/exceptions.py b/lib/trakt/exceptions.py new file mode 100644 index 00000000..f82e88e5 --- /dev/null +++ b/lib/trakt/exceptions.py @@ -0,0 +1,8 @@ +class traktException(Exception): + pass + +class traktAuthException(traktException): + pass + +class traktServerBusy(traktException): + pass \ No newline at end of file diff --git a/lib/trakt/trakt.py b/lib/trakt/trakt.py new file mode 100644 index 00000000..766f9f64 --- /dev/null +++ b/lib/trakt/trakt.py @@ -0,0 +1,53 @@ +import hashlib +import requests + +from . import traktException, traktAuthException, traktServerBusy + +class TraktAPI(): + def __init__(self, apikey, username=None, password=None, use_https=False, timeout=5): + self.apikey = apikey + + self.username = username + self.password = password + if password: hashlib.sha1(password.encode('utf-8')).hexdigest() + + self.protocol = 'https://' if use_https else 'http://' + self.timeout = timeout + + def validateAccount(self): + url = '/account/test/%APIKEY%' + return self.traktRequest(url) + + def traktRequest(self, url, data=None): + base_url = self.protocol + 'api.trakt.tv/%s' % url.replace('%APIKEY%', self.apikey).replace('%USER%', + self.username) + + # request the URL from trakt and parse the result as json + try: + resp = requests.get(base_url, + auth=(self.username, self.password) if self.username and self.password else None, + data=data if data else []) + + # check for http errors and raise if any are present + resp.raise_for_status() + + # convert response to json + resp = resp.json() + except (requests.HTTPError, requests.ConnectionError) as e: + if e.code == 401: + raise traktAuthException(e.message, e.code) + elif e.code == 503: + raise traktServerBusy(e.message, e.code) + else: + raise traktException(e.message, e.code) + + # check and confirm trakt call did not fail + if isinstance(resp, dict) and resp.get('status', False) == 'failure': + if 'message' in resp: + raise traktException(resp['message']) + if 'error' in resp: + raise traktException(resp['error']) + else: + raise traktException('Unknown Error') + + return resp \ No newline at end of file diff --git a/sickbeard/notifiers/trakt.py b/sickbeard/notifiers/trakt.py index 2c2de376..6f9b3857 100644 --- a/sickbeard/notifiers/trakt.py +++ b/sickbeard/notifiers/trakt.py @@ -18,8 +18,11 @@ import sickbeard from sickbeard import logger -from lib.trakt import * +from lib.trakt import TraktAPI +from lib.trakt.exceptions import traktException, traktServerBusy, traktAuthException + +trakt_api = TraktAPI(sickbeard.TRAKT_API, sickbeard.TRAKT_USERNAME, sickbeard.TRAKT_USERNAME) class TraktNotifier: """ @@ -48,63 +51,64 @@ class TraktNotifier: trakt_id = sickbeard.indexerApi(ep_obj.show.indexer).config['trakt_id'] if sickbeard.USE_TRAKT: - # URL parameters - data = { - 'title': ep_obj.show.name, - 'year': ep_obj.show.startyear, - 'episodes': [{ - 'season': ep_obj.season, - 'episode': ep_obj.episode - }] - } - - if trakt_id == 'tvdb_id': - data[trakt_id] = ep_obj.show.indexerid - - # update library - TraktCall("show/episode/library/%API%", self._api(), self._username(), self._password(), data) - - # remove from watchlist - if sickbeard.TRAKT_REMOVE_WATCHLIST: - TraktCall("show/episode/unwatchlist/%API%", self._api(), self._username(), self._password(), data) - - if sickbeard.TRAKT_REMOVE_SERIESLIST: + try: + # URL parameters data = { - 'shows': [ - { - 'title': ep_obj.show.name, - 'year': ep_obj.show.startyear - } - ] + 'title': ep_obj.show.name, + 'year': ep_obj.show.startyear, + 'episodes': [{ + 'season': ep_obj.season, + 'episode': ep_obj.episode + }] } if trakt_id == 'tvdb_id': - data['shows'][trakt_id] = ep_obj.show.indexerid + data[trakt_id] = ep_obj.show.indexerid - TraktCall("show/unwatchlist/%API%", self._api(), self._username(), self._password(), data) + # update library + trakt_api.traktRequest("show/episode/library/%APIKEY%", data) - # Remove all episodes from episode watchlist - # Start by getting all episodes in the watchlist - watchlist = TraktCall("user/watchlist/episodes.json/%API%/" + sickbeard.TRAKT_USERNAME, - sickbeard.TRAKT_API, sickbeard.TRAKT_USERNAME, sickbeard.TRAKT_PASSWORD) + # remove from watchlist + if sickbeard.TRAKT_REMOVE_WATCHLIST: + trakt_api.traktRequest("show/episode/unwatchlist/%APIKEY%", data) - # Convert watchlist to only contain current show - if watchlist: - for show in watchlist: - if show[trakt_id] == ep_obj.show.indexerid: - data_show = { - 'title': show['title'], - trakt_id: show[trakt_id], - 'episodes': [] + if sickbeard.TRAKT_REMOVE_SERIESLIST: + data = { + 'shows': [ + { + 'title': ep_obj.show.name, + 'year': ep_obj.show.startyear } + ] + } - # Add series and episode (number) to the array - for episodes in show['episodes']: - ep = {'season': episodes['season'], 'episode': episodes['number']} - data_show['episodes'].append(ep) + if trakt_id == 'tvdb_id': + data['shows'][trakt_id] = ep_obj.show.indexerid - TraktCall("show/episode/unwatchlist/%API%", sickbeard.TRAKT_API, sickbeard.TRAKT_USERNAME, - sickbeard.TRAKT_PASSWORD, data_show) + trakt_api.traktRequest("show/unwatchlist/%APIKEY%", data) + + # Remove all episodes from episode watchlist + # Start by getting all episodes in the watchlist + watchlist = trakt_api.traktRequest("user/watchlist/episodes.json/%APIKEY%/%USER%") + + # Convert watchlist to only contain current show + if watchlist: + for show in watchlist: + if show[trakt_id] == ep_obj.show.indexerid: + data_show = { + 'title': show['title'], + trakt_id: show[trakt_id], + 'episodes': [] + } + + # Add series and episode (number) to the array + for episodes in show['episodes']: + ep = {'season': episodes['season'], 'episode': episodes['number']} + data_show['episodes'].append(ep) + + trakt_api.traktRequest("show/episode/unwatchlist/%APIKEY%", data_show) + except (traktException, traktAuthException, traktServerBusy) as e: + logger.log(u"Could not connect to Trakt service: %s" % e.message, logger.ERROR) def test_notify(self, api, username, password): """ @@ -118,21 +122,10 @@ class TraktNotifier: Returns: True if the request succeeded, False otherwise """ - data = TraktCall("account/test/%API%", api, username, password) - if data and data["status"] == "success": - return True - - def _username(self): - return sickbeard.TRAKT_USERNAME - - def _password(self): - return sickbeard.TRAKT_PASSWORD - - def _api(self): - return sickbeard.TRAKT_API - - def _use_me(self): - return sickbeard.USE_TRAKT - + try: + if trakt_api.validateAccount(): + return True + except (traktException, traktAuthException, traktServerBusy) as e: + logger.log(u"Could not connect to Trakt service: %s" % e.message, logger.ERROR) notifier = TraktNotifier diff --git a/sickbeard/traktChecker.py b/sickbeard/traktChecker.py index 332028d7..54ceceee 100644 --- a/sickbeard/traktChecker.py +++ b/sickbeard/traktChecker.py @@ -27,9 +27,13 @@ from sickbeard import helpers from sickbeard import search_queue from sickbeard.common import SKIPPED, WANTED from lib.trakt import * +from trakt.exceptions import traktException +trakt_api = TraktAPI(sickbeard.TRAKT_API, sickbeard.TRAKT_USERNAME, sickbeard.TRAKT_USERNAME) + class TraktChecker(): + def __init__(self): self.todoWanted = [] @@ -51,17 +55,24 @@ class TraktChecker(): logger.log(traceback.format_exc(), logger.DEBUG) def findShow(self, indexer, indexerid): - library = TraktCall("user/library/shows/all.json/%API%/" + sickbeard.TRAKT_USERNAME, sickbeard.TRAKT_API, sickbeard.TRAKT_USERNAME, sickbeard.TRAKT_PASSWORD) + traktShow = None - if not library: - logger.log(u"Could not connect to trakt service, aborting library check", logger.ERROR) - return + try: + library = trakt_api.traktRequest("user/library/shows/all.json/%APIKEY%/%USER%") - if not len(library): - logger.log(u"No shows found in your library, aborting library update", logger.DEBUG) - return + if not library: + logger.log(u"Could not connect to trakt service, aborting library check", logger.ERROR) + return - return filter(lambda x: int(indexerid) in [int(x['tvdb_id']) or 0, int(x['tvrage_id'])] or 0, library) + if not len(library): + logger.log(u"No shows found in your library, aborting library update", logger.DEBUG) + return + + traktShow = filter(lambda x: int(indexerid) in [int(x['tvdb_id']) or 0, int(x['tvrage_id'])] or 0, library) + except (traktException, traktAuthException, traktServerBusy) as e: + logger.log(u"Could not connect to Trakt service: %s" % e.message, logger.ERROR) + + return traktShow def syncLibrary(self): logger.log(u"Syncing Trakt.tv show library", logger.DEBUG) @@ -71,6 +82,7 @@ class TraktChecker(): def removeShowFromTraktLibrary(self, show_obj): data = {} + if self.findShow(show_obj.indexer, show_obj.indexerid): # URL parameters data['tvdb_id'] = helpers.mapIndexersToShow(show_obj)[1] @@ -79,8 +91,11 @@ class TraktChecker(): if len(data): logger.log(u"Removing " + show_obj.name + " from trakt.tv library", logger.DEBUG) - TraktCall("show/unlibrary/%API%", sickbeard.TRAKT_API, sickbeard.TRAKT_USERNAME, sickbeard.TRAKT_PASSWORD, - data) + + try: + trakt_api.traktRequest("show/unlibrary/%APIKEY%", data) + except (traktException, traktAuthException, traktServerBusy) as e: + logger.log(u"Could not connect to Trakt service: %s" % e.message, logger.ERROR) def addShowToTraktLibrary(self, show_obj): """ @@ -99,15 +114,19 @@ class TraktChecker(): if len(data): logger.log(u"Adding " + show_obj.name + " to trakt.tv library", logger.DEBUG) - TraktCall("show/library/%API%", sickbeard.TRAKT_API, sickbeard.TRAKT_USERNAME, sickbeard.TRAKT_PASSWORD, - data) + + try: + trakt_api.traktRequest("show/library/%APIKEY%", data) + except (traktException, traktAuthException, traktServerBusy) as e: + logger.log(u"Could not connect to Trakt service: %s" % e.message, logger.ERROR) def updateShows(self): logger.log(u"Starting trakt show watchlist check", logger.DEBUG) - watchlist = TraktCall("user/watchlist/shows.json/%API%/" + sickbeard.TRAKT_USERNAME, sickbeard.TRAKT_API, sickbeard.TRAKT_USERNAME, sickbeard.TRAKT_PASSWORD) - if not watchlist: - logger.log(u"Could not connect to trakt service, aborting watchlist update", logger.ERROR) + try: + watchlist = trakt_api.traktRequest("user/watchlist/shows.json/%APIKEY%/%USER%") + except (traktException, traktAuthException, traktServerBusy) as e: + logger.log(u"Could not connect to Trakt service: %s" % e.message, logger.ERROR) return if not len(watchlist): @@ -138,10 +157,11 @@ class TraktChecker(): Sets episodes to wanted that are in trakt watchlist """ logger.log(u"Starting trakt episode watchlist check", logger.DEBUG) - watchlist = TraktCall("user/watchlist/episodes.json/%API%/" + sickbeard.TRAKT_USERNAME, sickbeard.TRAKT_API, sickbeard.TRAKT_USERNAME, sickbeard.TRAKT_PASSWORD) - if not watchlist: - logger.log(u"Could not connect to trakt service, aborting watchlist update", logger.ERROR) + try: + watchlist = trakt_api.traktRequest("user/watchlist/episodes.json/%APIKEY%/%USER%") + except (traktException, traktAuthException, traktServerBusy) as e: + logger.log(u"Could not connect to Trakt service: %s" % e.message, logger.ERROR) return if not len(watchlist): diff --git a/sickbeard/webserve.py b/sickbeard/webserve.py index dd3593b7..718b67d7 100644 --- a/sickbeard/webserve.py +++ b/sickbeard/webserve.py @@ -65,7 +65,9 @@ from lib.dateutil import tz from lib.unrar2 import RarFile from lib import adba, subliminal -from lib.trakt import TraktCall + +from lib.trakt import TraktAPI +from lib.trakt.exceptions import traktException, traktAuthException, traktServerBusy try: import json @@ -77,7 +79,6 @@ try: except ImportError: import xml.etree.ElementTree as etree - from Cheetah.Template import Template from tornado.web import RequestHandler, HTTPError, asynchronous @@ -282,6 +283,7 @@ class MainHandler(RequestHandler): image_path = image_file_name from mimetypes import MimeTypes + mime_type, encoding = MimeTypes().guess_type(image_path) self.set_header('Content-Type', mime_type) with file(image_path, 'rb') as img: @@ -2934,7 +2936,8 @@ class NewHomeAddShows(MainHandler): if not show_dir: t.default_show_name = '' elif not show_name: - t.default_show_name = re.sub(' \(\d{4}\)','', ek.ek(os.path.basename, ek.ek(os.path.normpath, show_dir)).replace('.', ' ')) + t.default_show_name = re.sub(' \(\d{4}\)', '', + ek.ek(os.path.basename, ek.ek(os.path.normpath, show_dir)).replace('.', ' ')) else: t.default_show_name = show_name @@ -2969,16 +2972,22 @@ class NewHomeAddShows(MainHandler): final_results = [] logger.log(u"Getting recommended shows from Trakt.tv", logger.DEBUG) - recommendedlist = TraktCall("recommendations/shows.json/%API%", sickbeard.TRAKT_API, sickbeard.TRAKT_USERNAME, - sickbeard.TRAKT_PASSWORD) - if recommendedlist: - indexers = ['tvdb_id', 'tvrage_id'] - map(final_results.append, ( - [int(show[indexers[sickbeard.TRAKT_DEFAULT_INDEXER - 1]]), show['url'], show['title'], show['overview'], - datetime.date.fromtimestamp(int(show['first_aired']) / 1000.0).strftime('%Y%m%d')] - for show in recommendedlist if not helpers.findCertainShow(sickbeard.showList, [ - int(show[indexers[sickbeard.TRAKT_DEFAULT_INDEXER - 1]])]))) + trakt_api = TraktAPI(sickbeard.TRAKT_API, sickbeard.TRAKT_USERNAME, sickbeard.TRAKT_USERNAME) + + try: + recommendedlist = trakt_api.traktRequest("recommendations/shows.json/%APIKEY%") + + if recommendedlist: + indexers = ['tvdb_id', 'tvrage_id'] + map(final_results.append, ( + [int(show[indexers[sickbeard.TRAKT_DEFAULT_INDEXER - 1]]), show['url'], show['title'], + show['overview'], + datetime.date.fromtimestamp(int(show['first_aired']) / 1000.0).strftime('%Y%m%d')] + for show in recommendedlist if not helpers.findCertainShow(sickbeard.showList, [ + int(show[indexers[sickbeard.TRAKT_DEFAULT_INDEXER - 1]])]))) + except (traktException, traktAuthException, traktServerBusy) as e: + logger.log(u"Could not connect to Trakt service: %s" % e.message, logger.ERROR) return json.dumps({'results': final_results}) @@ -3009,14 +3018,20 @@ class NewHomeAddShows(MainHandler): t.trending_shows = [] - trending_shows = TraktCall("shows/trending.json/%API%", sickbeard.TRAKT_API_KEY) - if trending_shows: - for show in trending_shows: - try: - if not helpers.findCertainShow(sickbeard.showList, [int(show['tvdb_id']), int(show['tvrage_id'])]): - t.trending_shows += [show] - except exceptions.MultipleShowObjectsException: - continue + trakt_api = TraktAPI(sickbeard.TRAKT_API, sickbeard.TRAKT_USERNAME, sickbeard.TRAKT_USERNAME) + + try: + trending_shows = trakt_api.traktRequest("shows/trending.json/%APIKEY%") + + if trending_shows: + for show in trending_shows: + try: + if not helpers.findCertainShow(sickbeard.showList, [int(show['tvdb_id']), int(show['tvrage_id'])]): + t.trending_shows += [show] + except exceptions.MultipleShowObjectsException: + continue + except (traktException, traktAuthException, traktServerBusy) as e: + logger.log(u"Could not connect to Trakt service: %s" % e.message, logger.ERROR) return _munge(t)