You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
traktarr/media/trakt.py

482 lines
18 KiB

import time
import backoff
import requests
from helpers.misc import backoff_handler, dict_merge
from helpers.trakt import extract_list_user_and_key_from_url
from misc.log import logger
log = logger.get_logger(__name__)
class Trakt:
non_user_lists = ['anticipated', 'trending', 'popular', 'boxoffice', 'watched', 'played']
def __init__(self, cfg):
self.cfg = cfg
############################################################
# Requests
############################################################
def _make_request(self, url, payload={}, authenticate_user=None):
headers, authenticate_user = self._headers(authenticate_user)
if authenticate_user:
url = url.replace('{authenticate_user}', authenticate_user)
# make request
req = requests.get(url, headers=headers, params=payload, timeout=30)
log.debug("Request URL: %s", req.url)
log.debug("Request Payload: %s", payload)
log.debug("Request User: %s", authenticate_user)
log.debug("Response Code: %d", req.status_code)
return req
@backoff.on_predicate(backoff.expo, lambda x: x is None, max_tries=4, on_backoff=backoff_handler)
def _make_item_request(self, url, object_name, payload={}):
payload = dict_merge(payload, {'extended': 'full'})
try:
req = self._make_request(url, payload)
if req.status_code == 200:
resp_json = req.json()
return resp_json
elif req.status_code == 401:
log.error("The authentication to Trakt is revoked. Please re-authenticate.")
exit()
else:
log.error("Failed to retrieve %s, request response: %d", object_name, req.status_code)
return None
except Exception:
log.exception("Exception retrieving %s: ", object_name)
return None
@backoff.on_predicate(backoff.expo, lambda x: x is None, max_tries=4, on_backoff=backoff_handler)
def _make_items_request(self, url, limit, languages, type_name, object_name, authenticate_user=None, payload={},
sleep_between=5, genres=None):
if not languages:
languages = ['en']
payload = dict_merge(payload, {'extended': 'full', 'limit': limit, 'page': 1, 'languages': ','.join(languages)})
if genres:
payload['genres'] = genres
processed = []
if authenticate_user:
type_name = type_name.replace('{authenticate_user}', self._user_used_for_authentication(authenticate_user))
try:
while True:
req = self._make_request(url, payload, authenticate_user)
current_page = payload['page']
total_pages = 0 if 'X-Pagination-Page-Count' not in req.headers else int(
req.headers['X-Pagination-Page-Count'])
log.debug("Response Page: %d of %d", current_page, total_pages)
if req.status_code == 200:
resp_json = req.json()
for item in resp_json:
if item not in processed:
if object_name.rstrip('s') not in item and 'title' in item:
processed.append({object_name.rstrip('s'): item})
else:
processed.append(item)
# check if we have fetched the last page, break if so
if total_pages == 0:
log.debug("There were no more pages to retrieve")
break
elif current_page >= total_pages:
log.debug("There are no more pages to retrieve results from")
break
else:
log.info("There are %d pages left to retrieve results from", total_pages - current_page)
payload['page'] += 1
time.sleep(sleep_between)
elif req.status_code == 401:
log.error("The authentication to Trakt is revoked. Please re-authenticate.")
exit()
else:
log.error("Failed to retrieve %s %s, request response: %d", type_name, object_name, req.status_code)
break
if len(processed):
log.debug("Found %d %s %s", len(processed), type_name, object_name)
return processed
return None
except Exception:
log.exception("Exception retrieving %s %s: ", type_name, object_name)
return None
def validate_client_id(self):
try:
# request anticipated shows to validate client_id
req = self._make_request(
url='https://api.trakt.tv/shows/anticipated',
)
if req.status_code == 200:
return True
return False
except Exception:
log.exception("Exception validating client_id: ")
return False
############################################################
# OAuth Authentication
############################################################
def __oauth_request_device_code(self):
log.info("We're talking to Trakt to get your verification code. Please wait a moment...")
payload = {'client_id': self.cfg.trakt.client_id}
print(self._headers_without_authentication())
# Request device code
req = requests.post('https://api.trakt.tv/oauth/device/code', params=payload,
headers=self._headers_without_authentication())
device_code_response = req.json()
# Display needed information to the user
log.info('Go to: %s on any device and enter %s. We\'ll be polling Trakt every %s seconds for a reply',
device_code_response['verification_url'], device_code_response['user_code'],
device_code_response['interval'])
return device_code_response
def __oauth_process_token_request(self, req):
success = False
if req.status_code == 200:
# Success; saving the access token
access_token_response = req.json()
access_token = access_token_response['access_token']
# But first we need to find out what user this token belongs to
temp_headers = self._headers_without_authentication()
temp_headers['Authorization'] = 'Bearer ' + access_token
req = requests.get('https://api.trakt.tv/users/me', headers=temp_headers)
from misc.config import Config
new_config = Config()
new_config.merge_settings({
"trakt": {
req.json()['username']: access_token_response
}
})
success = True
elif req.status_code == 404:
log.debug('The device code was wrong')
log.error('Whoops, something went wrong; aborting the authentication process')
elif req.status_code == 409:
log.error('You\'ve already authenticated this application; aborting the authentication process')
elif req.status_code == 410:
log.error('The authentication process has expired; please start again')
elif req.status_code == 418:
log.error('You\'ve denied the authentication; are you sure? Please try again')
elif req.status_code == 429:
log.debug('We\'re polling too quickly.')
return success, req.status_code
def __oauth_poll_for_access_token(self, device_code, polling_interval=5, polling_expire=600):
polling_start = time.time()
time.sleep(polling_interval)
tries = 0
while time.time() - polling_start < polling_expire:
tries += 1
log.debug('Polling Trakt for the %sth time; %s seconds left', tries,
polling_expire - round(time.time() - polling_start))
payload = {'code': device_code, 'client_id': self.cfg.trakt.client_id,
'client_secret': self.cfg.trakt.client_secret, 'grant_type': 'authorization_code'}
# Poll Trakt for access token
req = requests.post('https://api.trakt.tv/oauth/device/token', params=payload,
headers=self._headers_without_authentication())
success, status_code = self.__oauth_process_token_request(req)
if success:
break
elif status_code == 426:
log.debug('Increasing the interval by one second')
polling_interval += 1
time.sleep(polling_interval)
return False
def __oauth_refresh_access_token(self, refresh_token):
payload = {'refresh_token': refresh_token, 'client_id': self.cfg.trakt.client_id,
'client_secret': self.cfg.trakt.client_secret, 'grant_type': 'refresh_token'}
req = requests.post('https://api.trakt.tv/oauth/token', params=payload,
headers=self._headers_without_authentication())
success, status_code = self.__oauth_process_token_request(req)
return success
def oauth_authentication(self):
try:
device_code_response = self.__oauth_request_device_code()
if self.__oauth_poll_for_access_token(device_code_response['device_code'],
device_code_response['interval'],
device_code_response['expires_in']):
return True
except Exception:
log.exception("Exception occurred when authenticating user")
return False
def _get_first_authenticated_user(self):
import copy
users = copy.copy(self.cfg.trakt)
if 'client_id' in users.keys():
users.pop('client_id')
if 'client_secret' in users.keys():
users.pop('client_secret')
if len(users) > 0:
return list(users.keys())[0]
def _user_is_authenticated(self, user):
return user in self.cfg['trakt'].keys()
def _renew_oauth_token_if_expired(self, user):
token_information = self.cfg['trakt'][user]
# Check if the acces_token for the user is expired
expires_at = token_information['created_at'] + token_information['expires_in']
if expires_at < round(time.time()):
log.info("The access token for the user %s has expired. We're requesting a new one; please wait a moment.",
user)
if self.__oauth_refresh_access_token(token_information["refresh_token"]):
log.info("The access token for the user %s has been refreshed. Please restart the application.", user)
def _user_used_for_authentication(self, user=None):
if user is None:
user = self._get_first_authenticated_user()
elif not self._user_is_authenticated(user):
log.error('The user %s you specified to use for authentication is not authenticated yet. ' +
'Authenticate the user first, before you use it to retrieve lists.', user)
exit()
return user
def _headers_without_authentication(self):
return {
'Content-Type': 'application/json',
'trakt-api-version': '2',
'trakt-api-key': self.cfg.trakt.client_id
}
def _headers(self, user=None):
headers = self._headers_without_authentication()
user = self._user_used_for_authentication(user)
if user is not None:
self._renew_oauth_token_if_expired(user)
headers['Authorization'] = 'Bearer ' + self.cfg['trakt'][user]['access_token']
else:
log.info('No user')
return headers, user
############################################################
# Shows
############################################################
def get_show(self, show_id):
return self._make_item_request(
url='https://api.trakt.tv/shows/%s' % str(show_id),
object_name='show',
)
def get_trending_shows(self, limit=1000, languages=None, genres=None):
return self._make_items_request(
url='https://api.trakt.tv/shows/trending',
limit=limit,
languages=languages,
object_name='shows',
type_name='trending',
genres=genres
)
def get_popular_shows(self, limit=1000, languages=None, genres=None):
return self._make_items_request(
url='https://api.trakt.tv/shows/popular',
limit=limit,
languages=languages,
object_name='shows',
type_name='popular',
genres=genres
)
def get_anticipated_shows(self, limit=1000, languages=None, genres=None):
return self._make_items_request(
url='https://api.trakt.tv/shows/anticipated',
limit=limit,
languages=languages,
object_name='shows',
type_name='anticipated',
genres=genres
)
def get_most_played_shows(self, limit=1000, languages=None, genres=None, most_type=None):
return self._make_items_request(
url='https://api.trakt.tv/shows/played/%s' % ('weekly' if not most_type else most_type),
limit=limit,
languages=languages,
object_name='shows',
type_name='played',
genres=genres
)
def get_most_watched_shows(self, limit=1000, languages=None, genres=None, most_type=None):
return self._make_items_request(
url='https://api.trakt.tv/shows/watched/%s' % ('weekly' if not most_type else most_type),
limit=limit,
languages=languages,
object_name='shows',
type_name='watched',
genres=genres
)
def get_watchlist_shows(self, authenticate_user=None, limit=1000, languages=None):
return self._make_items_request(
url='https://api.trakt.tv/users/{authenticate_user}/watchlist/shows',
authenticate_user=authenticate_user,
limit=limit,
languages=languages,
object_name='shows',
type_name='watchlist from {authenticate_user}',
)
def get_user_list_shows(self, list_url, authenticate_user=None, limit=1000, languages=None):
list_user, list_key = extract_list_user_and_key_from_url(list_url)
log.debug('Fetching %s from %s', list_key, list_user)
return self._make_items_request(
url='https://api.trakt.tv/users/' + list_user + '/lists/' + list_key + '/items/shows',
authenticate_user=authenticate_user,
limit=limit,
languages=languages,
object_name='shows',
type_name=(list_key + ' from ' + list_user),
)
############################################################
# Movies
############################################################
def get_movie(self, movie_id):
return self._make_item_request(
url='https://api.trakt.tv/movies/%s' % str(movie_id),
object_name='movie',
)
def get_trending_movies(self, limit=1000, languages=None, genres=None):
return self._make_items_request(
url='https://api.trakt.tv/movies/trending',
limit=limit,
languages=languages,
object_name='movies',
type_name='trending',
genres=genres
)
def get_popular_movies(self, limit=1000, languages=None, genres=None):
return self._make_items_request(
url='https://api.trakt.tv/movies/popular',
limit=limit,
languages=languages,
object_name='movies',
type_name='popular',
genres=genres
)
def get_anticipated_movies(self, limit=1000, languages=None, genres=None):
return self._make_items_request(
url='https://api.trakt.tv/movies/anticipated',
limit=limit,
languages=languages,
object_name='movies',
type_name='anticipated',
genres=genres
)
def get_most_played_movies(self, limit=1000, languages=None, genres=None, most_type=None):
return self._make_items_request(
url='https://api.trakt.tv/movies/played/%s' % ('weekly' if not most_type else most_type),
limit=limit,
languages=languages,
object_name='movies',
type_name='played',
genres=genres
)
def get_most_watched_movies(self, limit=1000, languages=None, genres=None, most_type=None):
return self._make_items_request(
url='https://api.trakt.tv/movies/watched/%s' % ('weekly' if not most_type else most_type),
limit=limit,
languages=languages,
object_name='movies',
type_name='watched',
genres=genres
)
def get_boxoffice_movies(self, limit=1000, languages=None):
return self._make_items_request(
url='https://api.trakt.tv/movies/boxoffice',
limit=limit,
languages=languages,
object_name='movies',
type_name='anticipated',
)
def get_watchlist_movies(self, authenticate_user=None, limit=1000, languages=None):
return self._make_items_request(
url='https://api.trakt.tv/users/{authenticate_user}/watchlist/movies',
authenticate_user=authenticate_user,
limit=limit,
languages=languages,
object_name='movies',
type_name='watchlist from {authenticate_user}',
)
def get_user_list_movies(self, list_url, authenticate_user=None, limit=1000, languages=None):
list_user, list_key = extract_list_user_and_key_from_url(list_url)
log.debug('Fetching %s from %s', list_key, list_user)
return self._make_items_request(
url='https://api.trakt.tv/users/' + list_user + '/lists/' + list_key + '/items/movies',
authenticate_user=authenticate_user,
limit=limit,
languages=languages,
object_name='movies',
type_name=(list_key + ' from ' + list_user),
)