Merge pull request #73 from l3uddz/develop

Develop
pull/74/head
desimaniac 6 years ago committed by GitHub
commit 31e229d963
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -2,7 +2,8 @@
[![License: GPL 3](https://img.shields.io/badge/License-GPL%203-blue.svg)](https://github.com/l3uddz/traktarr/blob/master/LICENSE.md) [![License: GPL 3](https://img.shields.io/badge/License-GPL%203-blue.svg)](https://github.com/l3uddz/traktarr/blob/master/LICENSE.md)
[![Discord](https://img.shields.io/discord/381077432285003776.svg?colorB=177DC1&label=Discord)](https://discord.io/cloudbox) [![Discord](https://img.shields.io/discord/381077432285003776.svg?colorB=177DC1&label=Discord)](https://discord.io/cloudbox)
[![Feature Requests](https://img.shields.io/badge/Requests-Feathub-blue.svg)](http://feathub.com/l3uddz/traktarr) [![Feature Requests](https://img.shields.io/badge/Requests-Feathub-blue.svg)](http://feathub.com/l3uddz/traktarr)
[![Beerpay](https://beerpay.io/l3uddz/traktarr/badge.svg?style=flat)](https://beerpay.io/l3uddz/traktarr)
[![Beerpay](https://beerpay.io/l3uddz/traktarr/make-wish.svg?style=flat)](https://beerpay.io/l3uddz/traktarr)
# traktarr # traktarr

@ -1,7 +1,9 @@
import json
import time import time
import backoff import backoff
import requests import requests
from cashier import cache
from helpers.misc import backoff_handler, dict_merge from helpers.misc import backoff_handler, dict_merge
from helpers.trakt import extract_list_user_and_key_from_url from helpers.trakt import extract_list_user_and_key_from_url
@ -22,31 +24,40 @@ class Trakt:
def _make_request(self, url, payload={}, authenticate_user=None, request_type='get'): def _make_request(self, url, payload={}, authenticate_user=None, request_type='get'):
headers, authenticate_user = self._headers(authenticate_user) headers, authenticate_user = self._headers(authenticate_user)
headers['User-Agent'] = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ' \
'(KHTML, like Gecko) Chrome/71.0.3578.80 Safari/537.36'
if authenticate_user: if authenticate_user:
url = url.replace('{authenticate_user}', authenticate_user) url = url.replace('{authenticate_user}', authenticate_user)
# make request # make request
resp_data = ''
if request_type == 'delete': if request_type == 'delete':
req = requests.delete(url, headers=headers, params=payload, timeout=30) with requests.delete(url, headers=headers, params=payload, timeout=30, stream=True) as req:
for chunk in req.iter_content(chunk_size=250000, decode_unicode=True):
if chunk:
resp_data += chunk
else: else:
req = requests.get(url, headers=headers, params=payload, timeout=30) with requests.get(url, headers=headers, params=payload, timeout=30, stream=True) as req:
for chunk in req.iter_content(chunk_size=250000, decode_unicode=True):
if chunk:
resp_data += chunk
log.debug("Request URL: %s", req.url) log.debug("Request URL: %s", req.url)
log.debug("Request Payload: %s", payload) log.debug("Request Payload: %s", payload)
log.debug("Request User: %s", authenticate_user) log.debug("Request User: %s", authenticate_user)
log.debug("Response Code: %d", req.status_code) log.debug("Response Code: %d", req.status_code)
return req, resp_data
return req
@backoff.on_predicate(backoff.expo, lambda x: x is None, max_tries=4, on_backoff=backoff_handler) @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={}): def _make_item_request(self, url, object_name, payload={}):
payload = dict_merge(payload, {'extended': 'full'}) payload = dict_merge(payload, {'extended': 'full'})
try: try:
req = self._make_request(url, payload) req, resp_data = self._make_request(url, payload)
if req.status_code == 200: if req.status_code == 200 and len(resp_data):
resp_json = req.json() resp_json = json.loads(resp_data)
return resp_json return resp_json
elif req.status_code == 401: elif req.status_code == 401:
log.error("The authentication to Trakt is revoked. Please re-authenticate.") log.error("The authentication to Trakt is revoked. Please re-authenticate.")
@ -58,7 +69,7 @@ class Trakt:
log.exception("Exception retrieving %s: ", object_name) log.exception("Exception retrieving %s: ", object_name)
return None return None
@backoff.on_predicate(backoff.expo, lambda x: x is None, max_tries=4, on_backoff=backoff_handler) @backoff.on_predicate(backoff.expo, lambda x: x is None, max_tries=6, on_backoff=backoff_handler)
def _make_items_request(self, url, limit, languages, type_name, object_name, authenticate_user=None, payload={}, def _make_items_request(self, url, limit, languages, type_name, object_name, authenticate_user=None, payload={},
sleep_between=5, genres=None): sleep_between=5, genres=None):
if not languages: if not languages:
@ -74,8 +85,34 @@ class Trakt:
type_name = type_name.replace('{authenticate_user}', self._user_used_for_authentication(authenticate_user)) type_name = type_name.replace('{authenticate_user}', self._user_used_for_authentication(authenticate_user))
try: try:
resp_data = ''
while True: while True:
req = self._make_request(url, payload, authenticate_user) attempts = 0
max_attempts = 6
retrieve_error = False
while attempts <= max_attempts:
try:
req, resp_data = self._make_request(url, payload, authenticate_user)
if resp_data is not None:
retrieve_error = False
break
else:
log.warning("Failed to retrieve valid response for Trakt %s %s from _make_item_request",
type_name, object_name)
except Exception:
log.exception("Exception retrieving %s %s in _make_item_request: ", type_name, object_name)
retrieve_error = True
attempts += 1
log.info("Sleeping for %d seconds before making attempt %d/%d", 3 * attempts, attempts + 1,
max_attempts)
time.sleep(3 * attempts)
if retrieve_error or not resp_data or not len(resp_data):
log.error("Failed retrieving %s %s from _make_item_request %d times, aborting...", type_name,
object_name, attempts)
return None
current_page = payload['page'] current_page = payload['page']
total_pages = 0 if 'X-Pagination-Page-Count' not in req.headers else int( total_pages = 0 if 'X-Pagination-Page-Count' not in req.headers else int(
@ -83,23 +120,28 @@ class Trakt:
log.debug("Response Page: %d of %d", current_page, total_pages) log.debug("Response Page: %d of %d", current_page, total_pages)
if req.status_code == 200: if req.status_code == 200 and len(resp_data):
resp_json = req.json() if resp_data.startswith("[{") and resp_data.endswith("}]"):
if type_name == 'person' and 'cast' in resp_json: resp_json = json.loads(resp_data)
# handle person results
for item in resp_json['cast']: if type_name == 'person' and 'cast' in resp_json:
if item not in processed: # handle person results
if object_name.rstrip('s') not in item and 'title' in item: for item in resp_json['cast']:
processed.append({object_name.rstrip('s'): item}) if item not in processed:
else: if object_name.rstrip('s') not in item and 'title' in item:
processed.append(item) processed.append({object_name.rstrip('s'): item})
else:
processed.append(item)
else:
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)
else: else:
for item in resp_json: log.warning("Received malformed JSON response for page: %d of %d", current_page, total_pages)
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 # check if we have fetched the last page, break if so
if total_pages == 0: if total_pages == 0:
@ -112,6 +154,7 @@ class Trakt:
log.info("There are %d pages left to retrieve results from", total_pages - current_page) log.info("There are %d pages left to retrieve results from", total_pages - current_page)
payload['page'] += 1 payload['page'] += 1
time.sleep(sleep_between) time.sleep(sleep_between)
elif req.status_code == 401: elif req.status_code == 401:
log.error("The authentication to Trakt is revoked. Please re-authenticate.") log.error("The authentication to Trakt is revoked. Please re-authenticate.")
exit() exit()
@ -122,6 +165,7 @@ class Trakt:
if len(processed): if len(processed):
log.debug("Found %d %s %s", len(processed), type_name, object_name) log.debug("Found %d %s %s", len(processed), type_name, object_name)
return processed return processed
return None return None
except Exception: except Exception:
log.exception("Exception retrieving %s %s: ", type_name, object_name) log.exception("Exception retrieving %s %s: ", type_name, object_name)
@ -130,7 +174,7 @@ class Trakt:
def validate_client_id(self): def validate_client_id(self):
try: try:
# request anticipated shows to validate client_id # request anticipated shows to validate client_id
req = self._make_request( req, req_data = self._make_request(
url='https://api.trakt.tv/shows/anticipated', url='https://api.trakt.tv/shows/anticipated',
) )
@ -142,7 +186,7 @@ class Trakt:
return False return False
def remove_recommended_item(self, item_type, trakt_id, authenticate_user=None): def remove_recommended_item(self, item_type, trakt_id, authenticate_user=None):
ret = self._make_request( ret, ret_data = self._make_request(
url='https://api.trakt.tv/recommendations/%ss/%s' % (item_type, str(trakt_id)), url='https://api.trakt.tv/recommendations/%ss/%s' % (item_type, str(trakt_id)),
authenticate_user=authenticate_user, authenticate_user=authenticate_user,
request_type='delete' request_type='delete'
@ -334,6 +378,7 @@ class Trakt:
object_name='show', object_name='show',
) )
@cache(cache_file='cache.db', retry_if_blank=True)
def get_trending_shows(self, limit=1000, languages=None, genres=None): def get_trending_shows(self, limit=1000, languages=None, genres=None):
return self._make_items_request( return self._make_items_request(
url='https://api.trakt.tv/shows/trending', url='https://api.trakt.tv/shows/trending',
@ -344,6 +389,7 @@ class Trakt:
genres=genres genres=genres
) )
@cache(cache_file='cache.db', retry_if_blank=True)
def get_popular_shows(self, limit=1000, languages=None, genres=None): def get_popular_shows(self, limit=1000, languages=None, genres=None):
return self._make_items_request( return self._make_items_request(
url='https://api.trakt.tv/shows/popular', url='https://api.trakt.tv/shows/popular',
@ -354,6 +400,7 @@ class Trakt:
genres=genres genres=genres
) )
@cache(cache_file='cache.db', retry_if_blank=True)
def get_anticipated_shows(self, limit=1000, languages=None, genres=None): def get_anticipated_shows(self, limit=1000, languages=None, genres=None):
return self._make_items_request( return self._make_items_request(
url='https://api.trakt.tv/shows/anticipated', url='https://api.trakt.tv/shows/anticipated',
@ -364,6 +411,7 @@ class Trakt:
genres=genres genres=genres
) )
@cache(cache_file='cache.db', retry_if_blank=True)
def get_person_shows(self, person, limit=1000, languages=None, genres=None): def get_person_shows(self, person, limit=1000, languages=None, genres=None):
return self._make_items_request( return self._make_items_request(
url='https://api.trakt.tv/people/%s/shows' % person, url='https://api.trakt.tv/people/%s/shows' % person,
@ -374,6 +422,7 @@ class Trakt:
genres=genres genres=genres
) )
@cache(cache_file='cache.db', retry_if_blank=True)
def get_most_played_shows(self, limit=1000, languages=None, genres=None, most_type=None): def get_most_played_shows(self, limit=1000, languages=None, genres=None, most_type=None):
return self._make_items_request( return self._make_items_request(
url='https://api.trakt.tv/shows/played/%s' % ('weekly' if not most_type else most_type), url='https://api.trakt.tv/shows/played/%s' % ('weekly' if not most_type else most_type),
@ -384,6 +433,7 @@ class Trakt:
genres=genres genres=genres
) )
@cache(cache_file='cache.db', retry_if_blank=True)
def get_most_watched_shows(self, limit=1000, languages=None, genres=None, most_type=None): def get_most_watched_shows(self, limit=1000, languages=None, genres=None, most_type=None):
return self._make_items_request( return self._make_items_request(
url='https://api.trakt.tv/shows/watched/%s' % ('weekly' if not most_type else most_type), url='https://api.trakt.tv/shows/watched/%s' % ('weekly' if not most_type else most_type),
@ -394,6 +444,7 @@ class Trakt:
genres=genres genres=genres
) )
@cache(cache_file='cache.db', retry_if_blank=True)
def get_recommended_shows(self, authenticate_user=None, limit=1000, languages=None, genres=None): def get_recommended_shows(self, authenticate_user=None, limit=1000, languages=None, genres=None):
return self._make_items_request( return self._make_items_request(
url='https://api.trakt.tv/recommendations/shows', url='https://api.trakt.tv/recommendations/shows',
@ -439,6 +490,7 @@ class Trakt:
object_name='movie', object_name='movie',
) )
@cache(cache_file='cache.db', retry_if_blank=True)
def get_trending_movies(self, limit=1000, languages=None, genres=None): def get_trending_movies(self, limit=1000, languages=None, genres=None):
return self._make_items_request( return self._make_items_request(
url='https://api.trakt.tv/movies/trending', url='https://api.trakt.tv/movies/trending',
@ -449,6 +501,7 @@ class Trakt:
genres=genres genres=genres
) )
@cache(cache_file='cache.db', retry_if_blank=True)
def get_popular_movies(self, limit=1000, languages=None, genres=None): def get_popular_movies(self, limit=1000, languages=None, genres=None):
return self._make_items_request( return self._make_items_request(
url='https://api.trakt.tv/movies/popular', url='https://api.trakt.tv/movies/popular',
@ -459,6 +512,7 @@ class Trakt:
genres=genres genres=genres
) )
@cache(cache_file='cache.db', retry_if_blank=True)
def get_anticipated_movies(self, limit=1000, languages=None, genres=None): def get_anticipated_movies(self, limit=1000, languages=None, genres=None):
return self._make_items_request( return self._make_items_request(
url='https://api.trakt.tv/movies/anticipated', url='https://api.trakt.tv/movies/anticipated',
@ -469,6 +523,7 @@ class Trakt:
genres=genres genres=genres
) )
@cache(cache_file='cache.db', retry_if_blank=True)
def get_person_movies(self, person, limit=1000, languages=None, genres=None): def get_person_movies(self, person, limit=1000, languages=None, genres=None):
return self._make_items_request( return self._make_items_request(
url='https://api.trakt.tv/people/%s/movies' % person, url='https://api.trakt.tv/people/%s/movies' % person,
@ -479,6 +534,7 @@ class Trakt:
genres=genres genres=genres
) )
@cache(cache_file='cache.db', retry_if_blank=True)
def get_most_played_movies(self, limit=1000, languages=None, genres=None, most_type=None): def get_most_played_movies(self, limit=1000, languages=None, genres=None, most_type=None):
return self._make_items_request( return self._make_items_request(
url='https://api.trakt.tv/movies/played/%s' % ('weekly' if not most_type else most_type), url='https://api.trakt.tv/movies/played/%s' % ('weekly' if not most_type else most_type),
@ -489,6 +545,7 @@ class Trakt:
genres=genres genres=genres
) )
@cache(cache_file='cache.db', retry_if_blank=True)
def get_most_watched_movies(self, limit=1000, languages=None, genres=None, most_type=None): def get_most_watched_movies(self, limit=1000, languages=None, genres=None, most_type=None):
return self._make_items_request( return self._make_items_request(
url='https://api.trakt.tv/movies/watched/%s' % ('weekly' if not most_type else most_type), url='https://api.trakt.tv/movies/watched/%s' % ('weekly' if not most_type else most_type),

@ -4,3 +4,4 @@ attrdict==2.0.0
click==6.7 click==6.7
requests~=2.20.0 requests~=2.20.0
pyfiglet pyfiglet
cashier~=1.3

@ -6,7 +6,6 @@ import time
import click import click
import schedule import schedule
from pyfiglet import Figlet from pyfiglet import Figlet
############################################################ ############################################################
@ -394,7 +393,8 @@ def movie(movie_id, folder=None, no_search=False):
@click.option('--add-delay', '-d', default=2.5, help='Seconds between each add request to Radarr.', show_default=True) @click.option('--add-delay', '-d', default=2.5, help='Seconds between each add request to Radarr.', show_default=True)
@click.option('--sort', '-s', default='votes', type=click.Choice(['votes', 'rating', 'release']), @click.option('--sort', '-s', default='votes', type=click.Choice(['votes', 'rating', 'release']),
help='Sort list to process.') help='Sort list to process.')
@click.option('--rating','-r',default=None,type=(int),help='Set a minimum rating threshold (according to Rotten Tomatoes)') @click.option('--rating', '-r', default=None, type=(int),
help='Set a minimum rating threshold (according to Rotten Tomatoes)')
@click.option('--genre', '-g', default=None, help='Only add movies from this genre to Radarr.') @click.option('--genre', '-g', default=None, help='Only add movies from this genre to Radarr.')
@click.option('--folder', '-f', default=None, help='Add movies with this root folder to Radarr.') @click.option('--folder', '-f', default=None, help='Add movies with this root folder to Radarr.')
@click.option('--actor', '-a', default=None, help='Only add movies from this actor to Radarr.') @click.option('--actor', '-a', default=None, help='Only add movies from this actor to Radarr.')
@ -405,7 +405,8 @@ def movie(movie_id, folder=None, no_search=False):
@click.option('--ignore-blacklist', is_flag=True, help='Ignores the blacklist when running the command.') @click.option('--ignore-blacklist', is_flag=True, help='Ignores the blacklist when running the command.')
@click.option('--remove-rejected-from-recommended', is_flag=True, @click.option('--remove-rejected-from-recommended', is_flag=True,
help='Removes rejected/existing movies from recommended.') help='Removes rejected/existing movies from recommended.')
def movies(list_type, add_limit=0, add_delay=2.5, sort='votes', rating=None, genre=None, folder=None, actor=None, no_search=False, def movies(list_type, add_limit=0, add_delay=2.5, sort='votes', rating=None, genre=None, folder=None, actor=None,
no_search=False,
notifications=False, authenticate_user=None, ignore_blacklist=False, remove_rejected_from_recommended=False): notifications=False, authenticate_user=None, ignore_blacklist=False, remove_rejected_from_recommended=False):
from media.radarr import Radarr from media.radarr import Radarr
from media.trakt import Trakt from media.trakt import Trakt
@ -522,18 +523,20 @@ def movies(list_type, add_limit=0, add_delay=2.5, sort='votes', rating=None, gen
else None): else None):
# Assuming the movie is not blacklisted, proceed to pull RT score if the user wishes to restrict # Assuming the movie is not blacklisted, proceed to pull RT score if the user wishes to restrict
movieRating = None movieRating = None
if (rating != None and cfg['omdb']['api_key'] != ''): if rating is not None and 'omdb' in cfg and 'api_key' in cfg['omdb'] and cfg['omdb']['api_key']:
movieRating = rating_helper.get_rating(cfg['omdb']['api_key'],movie) movieRating = rating_helper.get_rating(cfg['omdb']['api_key'], movie)
if (movieRating == -1): if movieRating == -1:
log.debug("Skipping: %s because it did not have a rating/lacked imdbID", log.debug("Skipping: %s because it did not have a rating/lacked imdbID",
movie['movie']['title']) movie['movie']['title'])
continue continue
if (rating == None or movieRating >= rating): if (rating is None or movieRating is None) or movieRating >= rating:
log.info("Adding: %s (%d) | Genres: %s | Country: %s", movie['movie']['title'], movie['movie']['year'], log.info("Adding: %s (%d) | Genres: %s | Country: %s", movie['movie']['title'],
movie['movie']['year'],
', '.join(movie['movie']['genres']), movie['movie']['country'].upper()) ', '.join(movie['movie']['genres']), movie['movie']['country'].upper())
# add movie to radarr # add movie to radarr
if radarr.add_movie(movie['movie']['ids']['tmdb'], movie['movie']['title'], movie['movie']['year'], if radarr.add_movie(movie['movie']['ids']['tmdb'], movie['movie']['title'], movie['movie']['year'],
movie['movie']['ids']['slug'], profile_id, cfg.radarr.root_folder, not no_search): movie['movie']['ids']['slug'], profile_id, cfg.radarr.root_folder,
not no_search):
log.info("ADDED %s (%d)", movie['movie']['title'], movie['movie']['year']) log.info("ADDED %s (%d)", movie['movie']['title'], movie['movie']['year'])
if notifications: if notifications:
callback_notify({'event': 'add_movie', 'list_type': list_type, 'movie': movie['movie']}) callback_notify({'event': 'add_movie', 'list_type': list_type, 'movie': movie['movie']})
@ -705,7 +708,8 @@ def automatic_shows(add_delay=2.5, sort='votes', no_search=False, notifications=
return return
def automatic_movies(add_delay=2.5, sort='votes', no_search=False, notifications=False, ignore_blacklist=False,rating_limit=None): def automatic_movies(add_delay=2.5, sort='votes', no_search=False, notifications=False, ignore_blacklist=False,
rating_limit=None):
from media.trakt import Trakt from media.trakt import Trakt
total_movies_added = 0 total_movies_added = 0
@ -736,7 +740,7 @@ def automatic_movies(add_delay=2.5, sort='votes', no_search=False, notifications
# run movies # run movies
added_movies = movies.callback(list_type=list_type, add_limit=limit, added_movies = movies.callback(list_type=list_type, add_limit=limit,
add_delay=add_delay, sort=sort, no_search=no_search, add_delay=add_delay, sort=sort, no_search=no_search,
notifications=notifications,rating=rating_limit) notifications=notifications, rating=rating_limit)
elif list_type.lower() == 'watchlist': elif list_type.lower() == 'watchlist':
for authenticate_user, limit in value.items(): for authenticate_user, limit in value.items():
if limit <= 0: if limit <= 0:
@ -754,7 +758,7 @@ def automatic_movies(add_delay=2.5, sort='votes', no_search=False, notifications
added_movies = movies.callback(list_type=list_type, add_limit=limit, added_movies = movies.callback(list_type=list_type, add_limit=limit,
add_delay=add_delay, sort=sort, no_search=no_search, add_delay=add_delay, sort=sort, no_search=no_search,
notifications=notifications, authenticate_user=authenticate_user, notifications=notifications, authenticate_user=authenticate_user,
ignore_blacklist=local_ignore_blacklist,rating=rating_limit) ignore_blacklist=local_ignore_blacklist, rating=rating_limit)
elif list_type.lower() == 'lists': elif list_type.lower() == 'lists':
for list, v in value.items(): for list, v in value.items():
if isinstance(v, dict): if isinstance(v, dict):
@ -773,7 +777,7 @@ def automatic_movies(add_delay=2.5, sort='votes', no_search=False, notifications
added_movies = movies.callback(list_type=list, add_limit=limit, added_movies = movies.callback(list_type=list, add_limit=limit,
add_delay=add_delay, sort=sort, no_search=no_search, add_delay=add_delay, sort=sort, no_search=no_search,
notifications=notifications, authenticate_user=authenticate_user, notifications=notifications, authenticate_user=authenticate_user,
ignore_blacklist=local_ignore_blacklist,rating=rating_limit) ignore_blacklist=local_ignore_blacklist, rating=rating_limit)
if added_movies is None: if added_movies is None:
log.error("Failed adding movies from Trakt's %s list", list_type) log.error("Failed adding movies from Trakt's %s list", list_type)
@ -880,7 +884,6 @@ def exit_handler(signum, frame):
############################################################ ############################################################
if __name__ == "__main__": if __name__ == "__main__":
print("") print("")
f = Figlet(font='graffiti') f = Figlet(font='graffiti')

Loading…
Cancel
Save