From 6e1934ae9a829cc86dfab390e9cc19e44bd965be Mon Sep 17 00:00:00 2001 From: Mitchell Klijs Date: Thu, 26 Apr 2018 16:16:50 +0200 Subject: [PATCH 01/16] Add ability to fetch from watchlists and other private lists --- README.md | 112 +++++++++++--- media/trakt.py | 398 ++++++++++++++++++++++++++++++++++++++++++++++++- misc/config.py | 82 ++++++---- traktarr.py | 132 +++++++++++----- 4 files changed, 633 insertions(+), 91 deletions(-) diff --git a/README.md b/README.md index 1b05a17..02fdcb3 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,8 @@ Trakt lists currently supported: - popular - trending +Furthermore, watchlist and other types of list from multiple users are supported. + # Requirements 1. Python 3.5 or higher (`sudo apt install python3 python3-pip`). @@ -28,7 +30,38 @@ Install Traktarr to be run with `traktarr` command. 7. `traktarr` - run once to generate a default a config.json file. 8. `nano config.json` - edit preferences. -## 2. Setup Schedule +## 2. Authenticate Trakt (optional) + +If you want to acces private lists (watchlists or other user lists), +you'll need to authenticate Traktarr to access your personal lists. + +1. Create an Trakt application by going [here](https://trakt.tv/oauth/applications/new) +2. Enter a name for your application; for example `Traktarr` +3. Enter `urn:ietf:wg:oauth:2.0:oob` in the Redirecht uri field. +4. Click "Save app" +5. Open the Traktarr configuration file `config.json` and insert the +Client ID in the api_key and the Client Secret in the api_secret. Like this: +``` + { + "trakt": { + "api_key": "my_client_id", + "api_secret": "my_client_secret_key" + } + } +``` + +Repeat the following steps for every user you want to authenticate: +1. Run `traktarr trakt_authenticate` (from the installation path) +2. Go to https://trakt.tv/activate. +3. Enter the code you see in your terminal +4. Click continue +5. If you're not loggedin to Trakt, login noe +6. Accept + +You've now authenticated the user. +You can repeat this process for as many users as you want. + +## 3. Setup Schedule To have Traktarr get Movies and Shows for you automatically, on set interval. @@ -52,13 +85,25 @@ To have Traktarr get Movies and Shows for you automatically, on set interval. "boxoffice": 10, "interval": 24, "popular": 3, - "trending": 2 + "trending": 2, + "watchlist": { + "username": 10 + }, + "my-custom-list" { + "username": 10 + } }, "shows": { "anticipated": 10, "interval": 48, "popular": 1, - "trending": 2 + "trending": 2, + "watchlist": { + "username": 10 + }, + "my-custom-list" { + "username": 10 + } } }, "filters": { @@ -149,7 +194,8 @@ To have Traktarr get Movies and Shows for you automatically, on set interval. "url": "http://localhost:8989/" }, "trakt": { - "api_key": "" + "api_key": "", + "api_secret": "" } } ``` @@ -179,13 +225,33 @@ Movies can be run on a separate schedule from Shows. "boxoffice": 10, "interval": 24, "popular": 3, - "trending": 2 + "trending": 2, + "watchlist": { + "user1": 10 + "user2": 10 + }, + "my-custom-list": { + "user1": 10 + }, + "another-custom-list": { + "user2": 10 + } }, "shows": { "anticipated": 10, "interval": 48, "popular": 1, - "trending": 2 + "trending": 2, + "watchlist": { + "user1": 10 + "user2": 10 + }, + "my-custom-list": { + "user1": 10 + }, + "another-custom-list": { + "user2": 10 + } } }, ``` @@ -194,6 +260,10 @@ Movies can be run on a separate schedule from Shows. `anticipated`, `popular`, `trending`, `boxoffice` (movies only) - specify how many items from each Trakt list to find. +`watchlist` - add every user you want to fetch items from and specify how many items to fetch + +You can add every (private) list you want by adding the list key. + ## Filters Use filters to specify the movie/shows's country of origin or blacklist (i.e. filter-out) certain keywords, genres, years, runtime, or specific movies/shows. @@ -437,14 +507,7 @@ Finally, we will edit the Traktarr config and assign the `AMZN` tag to certain n `api_key` - Fill in your Trakt API key (_Client ID_). - -How to get a Trakt API Key: - - Go to https://trakt.tv/oauth/applications/new - - Fill in: - - Name: `Traktarr` - - Redirect uri: `https://google.com` - - Click `Save App` - - Retrieve the _Client ID_. +`api_secret` - Fill in your Trakt Secret key (_Client Scret_) # Usage @@ -467,9 +530,10 @@ Options: --help Show this message and exit. Commands: - movies Add new movies to Radarr. - run Run in automatic mode. - shows Add new shows to Sonarr. + movies Add new movies to Radarr. + run Run in automatic mode. + shows Add new shows to Sonarr. + trakt_authentication Authenticate Traktrarr to index your personal... ``` @@ -486,8 +550,9 @@ Usage: traktarr movies [OPTIONS] Add new movies to Radarr. Options: - -t, --list-type [anticipated|trending|popular|boxoffice] - Trakt list to process. [required] + -t, --list-type TEXT Trakt list to process. For example, anticipated, + trending, popular, boxoffice, watchlist or any + other user list [required] -l, --add-limit INTEGER Limit number of movies added to Radarr. [default: 0] -d, --add-delay FLOAT Seconds between each add request to Radarr. @@ -496,6 +561,8 @@ Options: -f, --folder TEXT Add movies with this root folder to Radarr. --no-search Disable search when adding movies to Radarr. --notifications Send notifications. + --user TEXT Specify which user to use for the personal Trakt + lists. Default: first user in the config --help Show this message and exit. ``` @@ -508,8 +575,9 @@ Usage: traktarr shows [OPTIONS] Add new shows to Sonarr. Options: - -t, --list-type [anticipated|trending|popular] - Trakt list to process. [required] + -t, --list-type TEXT Trakt list to process. For example, anticipated, + trending, popular, watchlist or any other user + list [required] -l, --add-limit INTEGER Limit number of shows added to Sonarr. [default: 0] -d, --add-delay FLOAT Seconds between each add request to Sonarr. @@ -518,6 +586,8 @@ Options: -f, --folder TEXT Add shows with this root folder to Sonarr. --no-search Disable search when adding shows to Sonarr. --notifications Send notifications. + --user TEXT Specify which user to use for the personal Trakt + lists. Default: first user in the config --help Show this message and exit. ``` diff --git a/media/trakt.py b/media/trakt.py index b2cf3c4..15a8670 100644 --- a/media/trakt.py +++ b/media/trakt.py @@ -15,8 +15,12 @@ def backoff_handler(details): class Trakt: - def __init__(self, api_key): - self.api_key = api_key + non_user_lists = ['anticipated', 'trending', 'popular', 'boxoffice'] + + def __init__(self, cfg): + self.cfg = cfg + self.api_key = cfg.trakt.api_key + self.api_secret = cfg.trakt.api_secret self.headers = { 'Content-Type': 'application/json', 'trakt-api-version': '2', @@ -44,6 +48,142 @@ class Trakt: log.exception("Exception validating api_key: ") return False + ############################################################ + # OAuth Authentication Initialisation + ############################################################ + + 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.api_key} + + # Request device code + req = requests.post('https://api.trakt.tv/oauth/device/code', params=payload, headers=self.headers) + 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 + 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.api_key, 'client_secret': self.api_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) + + 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): + # TODO Doesn't work + + payload = {'refresh_token': refresh_token, 'client_id': self.api_key, 'client_secret': self.api_secret, + 'grant_type': 'refresh_token'} + + req = requests.post('https://api.trakt.tv/oauth/token', params=payload, headers=self.headers) + + 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 oauth_headers(self, user): + headers = self.headers + + if user is None: + users = self.cfg['trakt'] + users.pop('api_key') + users.pop('api_secret') + + user = list(users.keys())[0] + + 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) + + headers['Authorization'] = 'Bearer ' + token_information['access_token'] + + return headers + ############################################################ # Shows ############################################################ @@ -108,6 +248,133 @@ class Trakt: log.exception("Exception retrieving anticipated shows: ") return None + @backoff.on_predicate(backoff.expo, lambda x: x is None, max_tries=4, on_backoff=backoff_handler) + def get_watchlist_shows(self, user=None, limit=1000, languages=None): + try: + processed_shows = [] + + if languages is None: + languages = ['en'] + + # generate payload + payload = {'extended': 'full', 'limit': limit, 'page': 1} + if languages: + payload['languages'] = ','.join(languages) + + # make request + while True: + req = requests.get('https://api.trakt.tv/sync/watchlist/shows', params=payload, + headers=self.oauth_headers(user), + timeout=30) + log.debug("Request User: %s", user) + log.debug("Request URL: %s", req.url) + log.debug("Request Payload: %s", payload) + log.debug("Response Code: %d", req.status_code) + log.debug("Response Page: %d of %d", payload['page'], + 0 if 'X-Pagination-Page-Count' not in req.headers else int( + req.headers['X-Pagination-Page-Count'])) + + if req.status_code == 200: + resp_json = req.json() + + for show in resp_json: + if show not in processed_shows: + processed_shows.append(show) + + # check if we have fetched the last page, break if so + if 'X-Pagination-Page-Count' not in req.headers or not int(req.headers['X-Pagination-Page-Count']): + log.debug("There was no more pages to retrieve") + break + elif payload['page'] >= int(req.headers['X-Pagination-Page-Count']): + log.debug("There are no more pages to retrieve results from") + break + else: + log.info("There are %d pages left to retrieve results from", + int(req.headers['X-Pagination-Page-Count']) - payload['page']) + payload['page'] += 1 + time.sleep(5) + elif req.status_code == 401: + log.error("The authentication to Trakt is revoked. Please re-authenticate.") + + exit() + else: + log.error("Failed to retrieve shows on watchlist from %s, request response: %d", user, + req.status_code) + break + + if len(processed_shows): + log.debug("Found %d shows on watchlist from %s", len(processed_shows), user) + + return processed_shows + return None + except Exception: + log.exception("Exception retrieving shows on watchlist") + return None + + @backoff.on_predicate(backoff.expo, lambda x: x is None, max_tries=4, on_backoff=backoff_handler) + def get_user_list_shows(self, list_id, user=None, limit=1000, languages=None): + try: + processed_shows = [] + + if languages is None: + languages = ['en'] + + # generate payload + payload = {'extended': 'full', 'limit': limit, 'page': 1} + if languages: + payload['languages'] = ','.join(languages) + + # make request + while True: + req = requests.get('https://api.trakt.tv/users/' + user + '/lists/' + list_id + '/items/shows', + params=payload, + headers=self.oauth_headers(user), + timeout=30) + log.debug("Request User: %s", user) + log.debug("Request URL: %s", req.url) + log.debug("Request Payload: %s", payload) + log.debug("Response Code: %d", req.status_code) + log.debug("Response Page: %d of %d", payload['page'], + 0 if 'X-Pagination-Page-Count' not in req.headers else int( + req.headers['X-Pagination-Page-Count'])) + + if req.status_code == 200: + resp_json = req.json() + + for show in resp_json: + if show not in processed_shows: + processed_shows.append(show) + + # check if we have fetched the last page, break if so + if 'X-Pagination-Page-Count' not in req.headers or not int(req.headers['X-Pagination-Page-Count']): + log.debug("There was no more pages to retrieve") + break + elif payload['page'] >= int(req.headers['X-Pagination-Page-Count']): + log.debug("There are no more pages to retrieve results from") + break + else: + log.info("There are %d pages left to retrieve results from", + int(req.headers['X-Pagination-Page-Count']) - payload['page']) + payload['page'] += 1 + time.sleep(5) + elif req.status_code == 401: + log.error("The authentication to Trakt is revoked. Please re-authenticate.") + + exit() + else: + log.error("Failed to retrieve shows on watchlist from %s, request response: %d", user, + req.status_code) + break + + if len(processed_shows): + log.debug("Found %d shows on watchlist from %s", len(processed_shows), user) + + return processed_shows + return None + except Exception: + log.exception("Exception retrieving shows on watchlist") + return None + @backoff.on_predicate(backoff.expo, lambda x: x is None, max_tries=4, on_backoff=backoff_handler) def get_trending_shows(self, limit=1000, languages=None): try: @@ -473,3 +740,130 @@ class Trakt: except Exception: log.exception("Exception retrieving boxoffice movies: ") return None + + @backoff.on_predicate(backoff.expo, lambda x: x is None, max_tries=4, on_backoff=backoff_handler) + def get_watchlist_movies(self, user=None, limit=1000, languages=None): + try: + processed_movies = [] + + if languages is None: + languages = ['en'] + + # generate payload + payload = {'extended': 'full', 'limit': limit, 'page': 1} + if languages: + payload['languages'] = ','.join(languages) + + # make request + while True: + req = requests.get('https://api.trakt.tv/sync/watchlist/movies', params=payload, + headers=self.oauth_headers(user), + timeout=30) + log.debug("Request User: %s", user) + log.debug("Request URL: %s", req.url) + log.debug("Request Payload: %s", payload) + log.debug("Response Code: %d", req.status_code) + log.debug("Response Page: %d of %d", payload['page'], + 0 if 'X-Pagination-Page-Count' not in req.headers else int( + req.headers['X-Pagination-Page-Count'])) + + if req.status_code == 200: + resp_json = req.json() + + for show in resp_json: + if show not in processed_movies: + processed_movies.append(show) + + # check if we have fetched the last page, break if so + if 'X-Pagination-Page-Count' not in req.headers or not int(req.headers['X-Pagination-Page-Count']): + log.debug("There was no more pages to retrieve") + break + elif payload['page'] >= int(req.headers['X-Pagination-Page-Count']): + log.debug("There are no more pages to retrieve results from") + break + else: + log.info("There are %d pages left to retrieve results from", + int(req.headers['X-Pagination-Page-Count']) - payload['page']) + payload['page'] += 1 + time.sleep(5) + elif req.status_code == 401: + log.error("The authentication to Trakt is revoked. Please re-authenticate.") + + exit() + else: + log.error("Failed to retrieve movies on watchlist from %s, request response: %d", user, + req.status_code) + break + + if len(processed_movies): + log.debug("Found %d movies on watchlist from %s", len(processed_movies), user) + + return processed_movies + return None + except Exception: + log.exception("Exception retrieving movies on watchlist") + return None + + @backoff.on_predicate(backoff.expo, lambda x: x is None, max_tries=4, on_backoff=backoff_handler) + def get_user_list_movies(self, list_id, user=None, limit=1000, languages=None): + try: + processed_movies = [] + + if languages is None: + languages = ['en'] + + # generate payload + payload = {'extended': 'full', 'limit': limit, 'page': 1} + if languages: + payload['languages'] = ','.join(languages) + + # make request + while True: + req = requests.get('https://api.trakt.tv/users/' + user + '/lists/' + list_id + '/items/movies', + params=payload, + headers=self.oauth_headers(user), + timeout=30) + log.debug("Request User: %s", user) + log.debug("Request URL: %s", req.url) + log.debug("Request Payload: %s", payload) + log.debug("Response Code: %d", req.status_code) + log.debug("Response Page: %d of %d", payload['page'], + 0 if 'X-Pagination-Page-Count' not in req.headers else int( + req.headers['X-Pagination-Page-Count'])) + + if req.status_code == 200: + resp_json = req.json() + + for show in resp_json: + if show not in processed_movies: + processed_movies.append(show) + + # check if we have fetched the last page, break if so + if 'X-Pagination-Page-Count' not in req.headers or not int(req.headers['X-Pagination-Page-Count']): + log.debug("There was no more pages to retrieve") + break + elif payload['page'] >= int(req.headers['X-Pagination-Page-Count']): + log.debug("There are no more pages to retrieve results from") + break + else: + log.info("There are %d pages left to retrieve results from", + int(req.headers['X-Pagination-Page-Count']) - payload['page']) + payload['page'] += 1 + time.sleep(5) + elif req.status_code == 401: + log.error("The authentication to Trakt is revoked. Please re-authenticate.") + + exit() + else: + log.error("Failed to retrieve movies on watchlist from %s, request response: %d", user, + req.status_code) + break + + if len(processed_movies): + log.debug("Found %d movies on watchlist from %s", len(processed_movies), user) + + return processed_movies + return None + except Exception: + log.exception("Exception retrieving movies on watchlist") + return None diff --git a/misc/config.py b/misc/config.py index 37d00ca..e1b1982 100644 --- a/misc/config.py +++ b/misc/config.py @@ -33,7 +33,6 @@ class AttrConfig(AttrDict): class Config(object, metaclass=Singleton): - base_config = { 'core': { 'debug': False @@ -150,36 +149,55 @@ class Config(object, metaclass=Singleton): with open(self.config_path, 'r') as fp: return AttrConfig(json.load(fp)) + def __inner_upgrade(self, settings1, settings2, key=None, overwrite=False): + sub_upgraded = False + merged = settings2.copy() + + # print(settings1) + # print(settings2) + # print(overwrite) + # print("_______________") + + if isinstance(settings1, dict): + for k, v in settings1.items(): + # missing k + if k not in settings2: + merged[k] = v + sub_upgraded = True + if not key: + print("Added %r config option: %s" % (str(k), str(v))) + else: + print("Added %r to config option %r: %s" % (str(k), str(key), str(v))) + continue + + # iterate children + if isinstance(v, dict) or isinstance(v, list): + merged[k], did_upgrade = self.__inner_upgrade(settings1[k], settings2[k], key=k, + overwrite=overwrite) + sub_upgraded = did_upgrade if did_upgrade else sub_upgraded + elif settings1[k] != settings2[k] and overwrite: + merged = settings1 + sub_upgraded = True + elif isinstance(settings1, list) and key: + for v in settings1: + if v not in settings2: + merged.append(v) + sub_upgraded = True + print("Added to config option %r: %s" % (str(key), str(v))) + continue + + return merged, sub_upgraded + def upgrade_settings(self, currents): - upgraded = False - - def inner_upgrade(default, current, key=None): - sub_upgraded = False - merged = current.copy() - if isinstance(default, dict): - for k, v in default.items(): - # missing k - if k not in current: - merged[k] = v - sub_upgraded = True - if not key: - print("Added %r config option: %s" % (str(k), str(v))) - else: - print("Added %r to config option %r: %s" % (str(k), str(key), str(v))) - continue - # iterate children - if isinstance(v, dict) or isinstance(v, list): - merged[k], did_upgrade = inner_upgrade(default[k], current[k], key=k) - sub_upgraded = did_upgrade if did_upgrade else sub_upgraded - - elif isinstance(default, list) and key: - for v in default: - if v not in current: - merged.append(v) - sub_upgraded = True - print("Added to config option %r: %s" % (str(key), str(v))) - continue - return merged, sub_upgraded - - upgraded_settings, upgraded = inner_upgrade(self.base_config, currents) + upgraded_settings, upgraded = self.__inner_upgrade(self.base_config, currents) + return AttrConfig(upgraded_settings), upgraded + + def merge_settings(self, settings_to_merge): + upgraded_settings, upgraded = self.__inner_upgrade(settings_to_merge, self.conf, overwrite=True) + + self.conf = upgraded_settings + + if upgraded: + self.dump_config() + return AttrConfig(upgraded_settings), upgraded diff --git a/traktarr.py b/traktarr.py index bfd5d0b..4115f47 100755 --- a/traktarr.py +++ b/traktarr.py @@ -53,20 +53,40 @@ def app(config, logfile): init_notifications() +############################################################ +# Trakt OAuth +############################################################ + +@app.command(help='Authenticate Traktrarr to index your personal lists') +def trakt_authentication(): + from media.trakt import Trakt + trakt = Trakt(cfg) + + response = trakt.oauth_authentication() + + if response: + log.info("Authentication information saved; please restart the application") + exit() + + ############################################################ # SHOWS ############################################################ @app.command(help='Add new shows to Sonarr.') -@click.option('--list-type', '-t', type=click.Choice(['anticipated', 'trending', 'popular']), - help='Trakt list to process.', required=True) +@click.option('--list-type', '-t', + help='Trakt list to process. For example, anticipated, trending, popular, watchlist or any other user list', + required=True) @click.option('--add-limit', '-l', default=0, help='Limit number of shows added to Sonarr.', show_default=True) @click.option('--add-delay', '-d', default=2.5, help='Seconds between each add request to Sonarr.', show_default=True) @click.option('--genre', '-g', default=None, help='Only add shows from this genre to Sonarr.') @click.option('--folder', '-f', default=None, help='Add shows with this root folder to Sonarr.') @click.option('--no-search', is_flag=True, help='Disable search when adding shows to Sonarr.') @click.option('--notifications', is_flag=True, help='Send notifications.') -def shows(list_type, add_limit=0, add_delay=2.5, genre=None, folder=None, no_search=False, notifications=False): +@click.option('--user', + help='Specify which user to use for the personal Trakt lists. Default: first user in the config') +def shows(list_type, add_limit=0, add_delay=2.5, genre=None, folder=None, no_search=False, notifications=False, + user=None): from media.sonarr import Sonarr from media.trakt import Trakt from misc import helpers @@ -82,7 +102,7 @@ def shows(list_type, add_limit=0, add_delay=2.5, genre=None, folder=None, no_sea cfg['sonarr']['root_folder'] = folder # validate trakt api_key - trakt = Trakt(cfg.trakt.api_key) + trakt = Trakt(cfg) if not trakt.validate_api_key(): log.error("Aborting due to failure to validate Trakt API Key") if notifications: @@ -144,12 +164,11 @@ def shows(list_type, add_limit=0, add_delay=2.5, genre=None, folder=None, no_sea trakt_series_list = trakt.get_trending_shows() elif list_type.lower() == 'popular': trakt_series_list = trakt.get_popular_shows() + elif list_type.lower() == 'watchlist': + trakt_series_list = trakt.get_watchlist_shows(user) else: - log.error("Aborting due to unknown Trakt list type") - if notifications: - callback_notify({'event': 'abort', 'type': 'shows', 'list_type': list_type, - 'reason': 'Failure to determine Trakt list type'}) - return None + trakt_series_list = trakt.get_user_list_shows(list_type, user) + if not trakt_series_list: log.error("Aborting due to failure to retrieve Trakt %s shows list", list_type) if notifications: @@ -232,15 +251,19 @@ def shows(list_type, add_limit=0, add_delay=2.5, genre=None, folder=None, no_sea ############################################################ @app.command(help='Add new movies to Radarr.') -@click.option('--list-type', '-t', type=click.Choice(['anticipated', 'trending', 'popular', 'boxoffice']), - help='Trakt list to process.', required=True) +@click.option('--list-type', '-t', + help='Trakt list to process. For example, anticipated, trending, popular, boxoffice, watchlist or any other user list', + required=True) @click.option('--add-limit', '-l', default=0, help='Limit number of movies added 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('--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('--no-search', is_flag=True, help='Disable search when adding movies to Radarr.') @click.option('--notifications', is_flag=True, help='Send notifications.') -def movies(list_type, add_limit=0, add_delay=2.5, genre=None, folder=None, no_search=False, notifications=False): +@click.option('--user', + help='Specify which user to use for the personal Trakt lists. Default: first user in the config') +def movies(list_type, add_limit=0, add_delay=2.5, genre=None, folder=None, no_search=False, notifications=False, + user=None): from media.radarr import Radarr from media.trakt import Trakt from misc import helpers @@ -256,7 +279,7 @@ def movies(list_type, add_limit=0, add_delay=2.5, genre=None, folder=None, no_se cfg['radarr']['root_folder'] = folder # validate trakt api_key - trakt = Trakt(cfg.trakt.api_key) + trakt = Trakt(cfg) if not trakt.validate_api_key(): log.error("Aborting due to failure to validate Trakt API Key") if notifications: @@ -310,12 +333,11 @@ def movies(list_type, add_limit=0, add_delay=2.5, genre=None, folder=None, no_se trakt_movies_list = trakt.get_popular_movies() elif list_type.lower() == 'boxoffice': trakt_movies_list = trakt.get_boxoffice_movies() + elif list_type.lower() == 'watchlist': + trakt_movies_list = trakt.get_watchlist_shows(user) else: - log.error("Aborting due to unknown Trakt list type") - if notifications: - callback_notify({'event': 'abort', 'type': 'movies', 'list_type': list_type, - 'reason': 'Failure to determine Trakt list type'}) - return None + trakt_movies_list = trakt.get_user_list_shows(list_type, user) + if not trakt_movies_list: log.error("Aborting due to failure to retrieve Trakt %s movies list", list_type) if notifications: @@ -412,23 +434,42 @@ def callback_notify(data): def automatic_shows(add_delay=2.5, no_search=False, notifications=False): + from media.trakt import Trakt + total_shows_added = 0 try: log.info("Started") - for list_type, type_amount in cfg.automatic.shows.items(): + for list_type, value in cfg.automatic.shows.items(): if list_type.lower() == 'interval': continue - elif type_amount <= 0: - log.info("Skipped Trakt's %s shows list", list_type) - continue + + if list_type.lower() not in Trakt.non_user_lists: + type_amount = value + + if type_amount <= 0: + log.info("Skipped Trakt's %s shows list", list_type) + continue + else: + log.info("Adding %d shows from Trakt's %s list", type_amount, list_type) + + # run shows + added_shows = shows.callback(list_type=list_type, add_limit=type_amount, + add_delay=add_delay, no_search=no_search, + notifications=notifications) else: - log.info("Adding %d shows from Trakt's %s list", type_amount, list_type) + for user, type_amount in value: + if type_amount <= 0: + log.info("Skipped Trakt's %s for &s", list_type, user) + continue + else: + log.info("Adding %d shows from the %s from &s", type_amount, list_type, user) + + # run shows + added_shows = shows.callback(list_type=list_type, add_limit=type_amount, + add_delay=add_delay, no_search=no_search, + notifications=notifications, user=user) - # run shows - added_shows = shows.callback(list_type=list_type, add_limit=type_amount, - add_delay=add_delay, no_search=no_search, - notifications=notifications) if added_shows is None: log.error("Failed adding shows from Trakt's %s list", list_type) time.sleep(10) @@ -449,23 +490,42 @@ def automatic_shows(add_delay=2.5, no_search=False, notifications=False): def automatic_movies(add_delay=2.5, no_search=False, notifications=False): + from media.trakt import Trakt + total_movies_added = 0 try: log.info("Started") - for list_type, type_amount in cfg.automatic.movies.items(): + for list_type, value in cfg.automatic.movies.items(): if list_type.lower() == 'interval': continue - elif type_amount <= 0: - log.info("Skipped Trakt's %s movies list", list_type) - continue + + if list_type.lower() not in Trakt.non_user_lists: + type_amount = value + + if type_amount <= 0: + log.info("Skipped Trakt's %s movies list", list_type) + continue + else: + log.info("Adding %d movies from Trakt's %s list", type_amount, list_type) + + # run movies + added_movies = movies.callback(list_type=list_type, add_limit=type_amount, + add_delay=add_delay, no_search=no_search, + notifications=notifications) else: - log.info("Adding %d movies from Trakt's %s list", type_amount, list_type) + for user, type_amount in value: + if type_amount <= 0: + log.info("Skipped Trakt's %s for &s", list_type, user) + continue + else: + log.info("Adding %d movies from the %s from &s", type_amount, list_type, user) + + # run movies + added_movies = movies.callback(list_type=list_type, add_limit=type_amount, + add_delay=add_delay, no_search=no_search, + notifications=notifications, user=user) - # run movies - added_movies = movies.callback(list_type=list_type, add_limit=type_amount, - add_delay=add_delay, no_search=no_search, - notifications=notifications) if added_movies is None: log.error("Failed adding movies from Trakt's %s list", list_type) time.sleep(10) From 3dae1c3ddea1dfc465c35796822d107a928f097e Mon Sep 17 00:00:00 2001 From: Mitchell Klijs Date: Fri, 27 Apr 2018 09:55:40 +0200 Subject: [PATCH 02/16] Fix bug when no api_secret is set --- media/trakt.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/media/trakt.py b/media/trakt.py index 15a8670..832c89d 100644 --- a/media/trakt.py +++ b/media/trakt.py @@ -163,8 +163,12 @@ class Trakt: if user is None: users = self.cfg['trakt'] - users.pop('api_key') - users.pop('api_secret') + + if 'api_key' in users.keys(): + users.pop('api_key') + + if 'api_secret' in users.keys(): + users.pop('api_secret') user = list(users.keys())[0] From 3d7c1e8703924363c76984bca644a5a068d7b88e Mon Sep 17 00:00:00 2001 From: Mitchell Klijs Date: Fri, 27 Apr 2018 10:02:33 +0200 Subject: [PATCH 03/16] Add api_secret to default config --- misc/config.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/misc/config.py b/misc/config.py index e1b1982..3e3ba09 100644 --- a/misc/config.py +++ b/misc/config.py @@ -38,7 +38,8 @@ class Config(object, metaclass=Singleton): 'debug': False }, 'trakt': { - 'api_key': '' + 'api_key': '', + 'api_secret': '' }, 'sonarr': { 'url': 'http://localhost:8989/', From 8ab620d9dd1690369fc3f7f1b9190630a07ff857 Mon Sep 17 00:00:00 2001 From: Mitchell Klijs Date: Fri, 27 Apr 2018 10:03:32 +0200 Subject: [PATCH 04/16] Default to the first authenticated user if the list's user is not found --- media/trakt.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/media/trakt.py b/media/trakt.py index 832c89d..3dc24b8 100644 --- a/media/trakt.py +++ b/media/trakt.py @@ -161,9 +161,11 @@ class Trakt: def oauth_headers(self, user): headers = self.headers - if user is None: - users = self.cfg['trakt'] + if user is None or user not in self.cfg['trakt'].keys(): + log.debug('No authenticated user corresponds to "%s", so the first user in the config to authenticated.', user) + users = self.cfg['trakt'] + if 'api_key' in users.keys(): users.pop('api_key') From 1c5e68e43857e9804c72fc7ac89c7f46191d54a0 Mon Sep 17 00:00:00 2001 From: Mitchell Klijs Date: Fri, 27 Apr 2018 10:34:03 +0200 Subject: [PATCH 05/16] Fix log messages --- media/trakt.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/media/trakt.py b/media/trakt.py index 3dc24b8..8d74e2e 100644 --- a/media/trakt.py +++ b/media/trakt.py @@ -368,12 +368,12 @@ class Trakt: exit() else: - log.error("Failed to retrieve shows on watchlist from %s, request response: %d", user, + log.error("Failed to retrieve shows on %s from %s, request response: %d", list_key, list_user, req.status_code) break if len(processed_shows): - log.debug("Found %d shows on watchlist from %s", len(processed_shows), user) + log.debug("Found %d shows on %s from %s", len(processed_shows), list_key, list_user) return processed_shows return None From 8e641d6690e58cd69d130c8e553591900ddce951 Mon Sep 17 00:00:00 2001 From: Mitchell Klijs Date: Fri, 27 Apr 2018 12:11:11 +0200 Subject: [PATCH 06/16] Refactor the way custom lists are fetched Allow public custom lists to be fetched without an authenticated user and add the ability to fetch a list from a specific user by authenticating as another user. --- README.md | 216 +++++++++++++++++++++++++++++++++++++------------ media/trakt.py | 89 ++++++++++++++------ traktarr.py | 120 ++++++++++++++++++--------- 3 files changed, 309 insertions(+), 116 deletions(-) diff --git a/README.md b/README.md index 02fdcb3..dc3ae2e 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ Trakt lists currently supported: - popular - trending -Furthermore, watchlist and other types of list from multiple users are supported. +Furthermore, watchlists and custom list from multiple users are supported. # Requirements @@ -30,10 +30,7 @@ Install Traktarr to be run with `traktarr` command. 7. `traktarr` - run once to generate a default a config.json file. 8. `nano config.json` - edit preferences. -## 2. Authenticate Trakt (optional) - -If you want to acces private lists (watchlists or other user lists), -you'll need to authenticate Traktarr to access your personal lists. +## 2. Create app authentication 1. Create an Trakt application by going [here](https://trakt.tv/oauth/applications/new) 2. Enter a name for your application; for example `Traktarr` @@ -50,6 +47,10 @@ Client ID in the api_key and the Client Secret in the api_secret. Like this: } ``` +## 3. Authenticate users (optional) + +If you want to be able to access private lists, you have to authentcate that user. + Repeat the following steps for every user you want to authenticate: 1. Run `traktarr trakt_authenticate` (from the installation path) 2. Go to https://trakt.tv/activate. @@ -61,7 +62,7 @@ Repeat the following steps for every user you want to authenticate: You've now authenticated the user. You can repeat this process for as many users as you want. -## 3. Setup Schedule +## 4. Setup Schedule To have Traktarr get Movies and Shows for you automatically, on set interval. @@ -74,6 +75,8 @@ To have Traktarr get Movies and Shows for you automatically, on set interval. # Configuration +Here is some default configuration you can use. + ```json { "core": { @@ -85,25 +88,13 @@ To have Traktarr get Movies and Shows for you automatically, on set interval. "boxoffice": 10, "interval": 24, "popular": 3, - "trending": 2, - "watchlist": { - "username": 10 - }, - "my-custom-list" { - "username": 10 - } + "trending": 2 }, "shows": { "anticipated": 10, "interval": 48, "popular": 1, - "trending": 2, - "watchlist": { - "username": 10 - }, - "my-custom-list" { - "username": 10 - } + "trending": 2 } }, "filters": { @@ -201,6 +192,22 @@ To have Traktarr get Movies and Shows for you automatically, on set interval. ``` +## Watchlist + +Traktarr can fetch the watchlist for as many users as you like. +You'll have to authenticate every user from whome you want to fetch the watchlist, +by following the steps described [here]((#authenticate-users-optional)). + +When all users are authenticated you can fetch their watchlist either +with the automatic task or with the manual commands (see examples below). + +## Other custom user lists + +Traktarr can also fetch any number of other custom lists. + +If the custom list is private, you'll have to authenticate a user that is allowed to +access that list by following the steps described [here]((#authenticate-users-optional)). + ## Core ```json @@ -226,32 +233,16 @@ Movies can be run on a separate schedule from Shows. "interval": 24, "popular": 3, "trending": 2, - "watchlist": { - "user1": 10 - "user2": 10 - }, - "my-custom-list": { - "user1": 10 - }, - "another-custom-list": { - "user2": 10 - } + "watchlist": {}, + "lists": {} }, "shows": { "anticipated": 10, "interval": 48, "popular": 1, "trending": 2, - "watchlist": { - "user1": 10 - "user2": 10 - }, - "my-custom-list": { - "user1": 10 - }, - "another-custom-list": { - "user2": 10 - } + "watchlist": {}, + "lists": {} } }, ``` @@ -260,9 +251,106 @@ Movies can be run on a separate schedule from Shows. `anticipated`, `popular`, `trending`, `boxoffice` (movies only) - specify how many items from each Trakt list to find. -`watchlist` - add every user you want to fetch items from and specify how many items to fetch +`watchlist` - specify which watchlists to fetch (see explanation below) + +`lists` - specify which custom lists to fetch (see explanation below) + +### Watchlist + +The watchlist task can be scheduled with a differtent item limit for every user. +For every user you've to add: `"username": limit` to the watchlist key. For example: + +```json +"automatic": { + "movies": { + "watchlist": { + "user1": 10, + "user2": 5 + } + }, + "shows": { + "watchlist": { + "user1": 2, + "user3": 1 + } + } +}, +``` + +Of course you can combine this with running the other list types as well. + +### Custom lists + +You can also schedule any number of public or private custom lists. +For both public and private lsits you'll need the url to that list. +You can copy this url from the address bar in you browser when viewing +the list on Trakt. + +Public lists can be added by specifying the url and the item limit like this: + +```json +"automatic": { + "movies": { + "lists": { + "https://trakt.tv/users/rkerwin/lists/top-100-movies": 10 + } + }, + "shows": { + "lists": { + "https://trakt.tv/users/claireaa/lists/top-100-tv-shows-of-all-time-ign": 10 + } + } +}, +``` + +Private lists can be added in two ways: + +1. If there is only one authenticated user to Traktarr, you can add +the private list just like any other public list: + +```json +"automatic": { + "movies": { + "lists": { + "https://trakt.tv/users/user/lists/my-private-movies-list": 10 + } + }, + "shows": { + "lists": { + "https://trakt.tv/users/user/lists/my-private-shows-list": 10 + } + } +}, +``` + +2. If there are multiple authenticated users to Traktarr, you'll need +to specify with which user Traktarr should authenticate when fetching +the list. The user should have acces to the list (either own the list, +or friends with the owner of the list and the list is specified to be +shared with friends) +_Note that the specified user has to be authenticated_ + +```json +"automatic": { + "movies": { + "lists": { + "https://trakt.tv/users/user/lists/my-private-movies-list": { + "authenticate_as": "user2", + "limit": 10 + } + } + }, + "shows": { + "lists": { + "https://trakt.tv/users/user/lists/my-private-shows-list": { + "authenticate_as": "user2", + "limit": 10 + } + } + } +}, +``` -You can add every (private) list you want by adding the list key. ## Filters @@ -501,7 +589,8 @@ Finally, we will edit the Traktarr config and assign the `AMZN` tag to certain n ```json "trakt": { - "api_key": "" + "api_key": "", + "api_scret": "" } ``` @@ -509,6 +598,9 @@ Finally, we will edit the Traktarr config and assign the `AMZN` tag to certain n `api_secret` - Fill in your Trakt Secret key (_Client Scret_) +_Note that when users authenticate to Traktarr, their token information +will be added to this._ + # Usage @@ -552,7 +644,7 @@ Usage: traktarr movies [OPTIONS] Options: -t, --list-type TEXT Trakt list to process. For example, anticipated, trending, popular, boxoffice, watchlist or any - other user list [required] + URL to a list [required] -l, --add-limit INTEGER Limit number of movies added to Radarr. [default: 0] -d, --add-delay FLOAT Seconds between each add request to Radarr. @@ -561,9 +653,9 @@ Options: -f, --folder TEXT Add movies with this root folder to Radarr. --no-search Disable search when adding movies to Radarr. --notifications Send notifications. - --user TEXT Specify which user to use for the personal Trakt - lists. Default: first user in the config - --help Show this message and exit. + --authencate-user TEXT Specify which user to authenticate with to + retrieve Trakt lists. Default: first user in the + config ``` @@ -576,7 +668,7 @@ Usage: traktarr shows [OPTIONS] Options: -t, --list-type TEXT Trakt list to process. For example, anticipated, - trending, popular, watchlist or any other user + trending, popular, watchlist or any URL to a list [required] -l, --add-limit INTEGER Limit number of shows added to Sonarr. [default: 0] @@ -586,19 +678,39 @@ Options: -f, --folder TEXT Add shows with this root folder to Sonarr. --no-search Disable search when adding shows to Sonarr. --notifications Send notifications. - --user TEXT Specify which user to use for the personal Trakt - lists. Default: first user in the config + --authencate-user TEXT Specify which user to authenticate with to + retrieve Trakt lists. Default: first user in the + config --help Show this message and exit. ``` ## Examples - +- Fetch boxoffice movies labeld with the comedy genere, limit to 10 items and send notifications ``` traktarr movies -t boxoffice -g comedy -l 10 --notifications - ``` + +- Fetch popular shows, limit to 2 items and don't start the search in Sonarr ``` traktarr shows -t popular -l 2 --no-search ``` + +- Fetch all shows from the watchlist from user1 + +``` +traktarr shows -t watchlist --authenticate-user user1 +``` + +- Fetch all movies from the public https://trakt.tv/users/rkerwin/lists/top-100-movies list + +``` +traktarr shows -t https://trakt.tv/users/rkerwin/lists/top-100-movies +``` + +- Fetch all movies from the private https://trakt.tv/users/user1/lists/private-movies-list list + +``` +traktarr shows -t https://trakt.tv/users/user1/lists/private-movies-list --authenticate-user=user1 +``` \ No newline at end of file diff --git a/media/trakt.py b/media/trakt.py index 8d74e2e..d0b7fdc 100644 --- a/media/trakt.py +++ b/media/trakt.py @@ -161,9 +161,7 @@ class Trakt: def oauth_headers(self, user): headers = self.headers - if user is None or user not in self.cfg['trakt'].keys(): - log.debug('No authenticated user corresponds to "%s", so the first user in the config to authenticated.', user) - + if user is None: users = self.cfg['trakt'] if 'api_key' in users.keys(): @@ -172,7 +170,20 @@ class Trakt: if 'api_secret' in users.keys(): users.pop('api_secret') - user = list(users.keys())[0] + if len(users) > 0: + user = list(users.keys())[0] + + log.debug('No user provided, so default to the first user in the config (%s)', user) + elif user not in self.cfg['trakt'].keys(): + 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() + + # If there is no default user, try without authentication + if user is None: + log.info('Using no authentication') + + return headers, user token_information = self.cfg['trakt'][user] # Check if the acces_token for the user is expired @@ -188,7 +199,7 @@ class Trakt: headers['Authorization'] = 'Bearer ' + token_information['access_token'] - return headers + return headers, user ############################################################ # Shows @@ -255,7 +266,7 @@ class Trakt: return None @backoff.on_predicate(backoff.expo, lambda x: x is None, max_tries=4, on_backoff=backoff_handler) - def get_watchlist_shows(self, user=None, limit=1000, languages=None): + def get_watchlist_shows(self, authenticate_user=None, limit=1000, languages=None): try: processed_shows = [] @@ -269,10 +280,12 @@ class Trakt: # make request while True: + headers, authenticate_user = self.oauth_headers(authenticate_user) + req = requests.get('https://api.trakt.tv/sync/watchlist/shows', params=payload, - headers=self.oauth_headers(user), + headers=headers, timeout=30) - log.debug("Request User: %s", user) + log.debug("Request User: %s", authenticate_user) log.debug("Request URL: %s", req.url) log.debug("Request Payload: %s", payload) log.debug("Response Code: %d", req.status_code) @@ -304,12 +317,12 @@ class Trakt: exit() else: - log.error("Failed to retrieve shows on watchlist from %s, request response: %d", user, + log.error("Failed to retrieve shows on watchlist from %s, request response: %d", authenticate_user, req.status_code) break if len(processed_shows): - log.debug("Found %d shows on watchlist from %s", len(processed_shows), user) + log.debug("Found %d shows on watchlist from %s", len(processed_shows), authenticate_user) return processed_shows return None @@ -318,7 +331,7 @@ class Trakt: return None @backoff.on_predicate(backoff.expo, lambda x: x is None, max_tries=4, on_backoff=backoff_handler) - def get_user_list_shows(self, list_id, user=None, limit=1000, languages=None): + def get_user_list_shows(self, list_url, authenticate_user=None, limit=1000, languages=None): try: processed_shows = [] @@ -330,13 +343,26 @@ class Trakt: if languages: payload['languages'] = ','.join(languages) + try: + import re + list_user = re.search('\/users\/([^/]*)', list_url).group(1) + list_key = re.search('\/lists\/([^/]*)', list_url).group(1) + except: + log.error('The URL "%s" is not in the correct format', list_url) + + exit() + + log.debug('Fetching %s from %s', list_key, list_user) + # make request while True: - req = requests.get('https://api.trakt.tv/users/' + user + '/lists/' + list_id + '/items/shows', + headers, authenticate_user = self.oauth_headers(authenticate_user) + + req = requests.get('https://api.trakt.tv/users/' + list_user + '/lists/' + list_key + '/items/shows', params=payload, - headers=self.oauth_headers(user), + headers=headers, timeout=30) - log.debug("Request User: %s", user) + log.debug("Request User: %s", authenticate_user) log.debug("Request URL: %s", req.url) log.debug("Request Payload: %s", payload) log.debug("Response Code: %d", req.status_code) @@ -748,7 +774,7 @@ class Trakt: return None @backoff.on_predicate(backoff.expo, lambda x: x is None, max_tries=4, on_backoff=backoff_handler) - def get_watchlist_movies(self, user=None, limit=1000, languages=None): + def get_watchlist_movies(self, authenticate_user=None, limit=1000, languages=None): try: processed_movies = [] @@ -762,10 +788,12 @@ class Trakt: # make request while True: + headers, authenticate_user = self.oauth_headers(authenticate_user) + req = requests.get('https://api.trakt.tv/sync/watchlist/movies', params=payload, - headers=self.oauth_headers(user), + headers=headers, timeout=30) - log.debug("Request User: %s", user) + log.debug("Request User: %s", authenticate_user) log.debug("Request URL: %s", req.url) log.debug("Request Payload: %s", payload) log.debug("Response Code: %d", req.status_code) @@ -797,12 +825,12 @@ class Trakt: exit() else: - log.error("Failed to retrieve movies on watchlist from %s, request response: %d", user, + log.error("Failed to retrieve movies on watchlist from %s, request response: %d", authenticate_user, req.status_code) break if len(processed_movies): - log.debug("Found %d movies on watchlist from %s", len(processed_movies), user) + log.debug("Found %d movies on watchlist from %s", len(processed_movies), authenticate_user) return processed_movies return None @@ -811,7 +839,7 @@ class Trakt: return None @backoff.on_predicate(backoff.expo, lambda x: x is None, max_tries=4, on_backoff=backoff_handler) - def get_user_list_movies(self, list_id, user=None, limit=1000, languages=None): + def get_user_list_movies(self, list_url, authenticate_user=None, limit=1000, languages=None): try: processed_movies = [] @@ -823,13 +851,24 @@ class Trakt: if languages: payload['languages'] = ','.join(languages) + try: + import re + list_user = re.search('\/users\/([^/]*)', list_url).group(1) + list_key = re.search('\/lists\/([^/]*)', list_url).group(1) + except: + log.error('The URL "%s" is not in the correct format', list_url) + + log.debug('Fetching %s from %s', list_key, list_user) + # make request while True: - req = requests.get('https://api.trakt.tv/users/' + user + '/lists/' + list_id + '/items/movies', + headers, authenticate_user = self.oauth_headers(authenticate_user) + + req = requests.get('https://api.trakt.tv/users/' + list_user + '/lists/' + list_key + '/items/movies', params=payload, - headers=self.oauth_headers(user), + headers=headers, timeout=30) - log.debug("Request User: %s", user) + log.debug("Request User: %s", authenticate_user) log.debug("Request URL: %s", req.url) log.debug("Request Payload: %s", payload) log.debug("Response Code: %d", req.status_code) @@ -861,12 +900,12 @@ class Trakt: exit() else: - log.error("Failed to retrieve movies on watchlist from %s, request response: %d", user, + log.error("Failed to retrieve movies on watchlist from %s, request response: %d", authenticate_user, req.status_code) break if len(processed_movies): - log.debug("Found %d movies on watchlist from %s", len(processed_movies), user) + log.debug("Found %d movies on watchlist from %s", len(processed_movies), authenticate_user) return processed_movies return None diff --git a/traktarr.py b/traktarr.py index 4115f47..4c6a08d 100755 --- a/traktarr.py +++ b/traktarr.py @@ -75,7 +75,7 @@ def trakt_authentication(): @app.command(help='Add new shows to Sonarr.') @click.option('--list-type', '-t', - help='Trakt list to process. For example, anticipated, trending, popular, watchlist or any other user list', + help='Trakt list to process. For example, anticipated, trending, popular, watchlist or any URL to a list', required=True) @click.option('--add-limit', '-l', default=0, help='Limit number of shows added to Sonarr.', show_default=True) @click.option('--add-delay', '-d', default=2.5, help='Seconds between each add request to Sonarr.', show_default=True) @@ -83,10 +83,10 @@ def trakt_authentication(): @click.option('--folder', '-f', default=None, help='Add shows with this root folder to Sonarr.') @click.option('--no-search', is_flag=True, help='Disable search when adding shows to Sonarr.') @click.option('--notifications', is_flag=True, help='Send notifications.') -@click.option('--user', - help='Specify which user to use for the personal Trakt lists. Default: first user in the config') +@click.option('--authenticate-user', + help='Specify which user to authenticate with to retrieve Trakt lists. Default: first user in the config') def shows(list_type, add_limit=0, add_delay=2.5, genre=None, folder=None, no_search=False, notifications=False, - user=None): + authenticate_user=None): from media.sonarr import Sonarr from media.trakt import Trakt from misc import helpers @@ -165,9 +165,15 @@ def shows(list_type, add_limit=0, add_delay=2.5, genre=None, folder=None, no_sea elif list_type.lower() == 'popular': trakt_series_list = trakt.get_popular_shows() elif list_type.lower() == 'watchlist': - trakt_series_list = trakt.get_watchlist_shows(user) + trakt_series_list = trakt.get_watchlist_shows(authenticate_user) + elif list_type.lower() == 'lists': + trakt_series_list = trakt.get_user_list_shows(list_type, authenticate_user) else: - trakt_series_list = trakt.get_user_list_shows(list_type, user) + log.error("Aborting due to unknown Trakt list type") + if notifications: + callback_notify({'event': 'abort', 'type': 'shows', 'list_type': list_type, + 'reason': 'Failure to determine Trakt list type'}) + return None if not trakt_series_list: log.error("Aborting due to failure to retrieve Trakt %s shows list", list_type) @@ -252,7 +258,7 @@ def shows(list_type, add_limit=0, add_delay=2.5, genre=None, folder=None, no_sea @app.command(help='Add new movies to Radarr.') @click.option('--list-type', '-t', - help='Trakt list to process. For example, anticipated, trending, popular, boxoffice, watchlist or any other user list', + help='Trakt list to process. For example, anticipated, trending, popular, boxoffice, watchlist or any URL to a list', required=True) @click.option('--add-limit', '-l', default=0, help='Limit number of movies added to Radarr.', show_default=True) @click.option('--add-delay', '-d', default=2.5, help='Seconds between each add request to Radarr.', show_default=True) @@ -260,10 +266,10 @@ def shows(list_type, add_limit=0, add_delay=2.5, genre=None, folder=None, no_sea @click.option('--folder', '-f', default=None, help='Add movies with this root folder to Radarr.') @click.option('--no-search', is_flag=True, help='Disable search when adding movies to Radarr.') @click.option('--notifications', is_flag=True, help='Send notifications.') -@click.option('--user', - help='Specify which user to use for the personal Trakt lists. Default: first user in the config') +@click.option('--authenticate-user', + help='Specify which user to authenticate with to retrieve Trakt lists. Default: first user in the config') def movies(list_type, add_limit=0, add_delay=2.5, genre=None, folder=None, no_search=False, notifications=False, - user=None): + authenticate_user=None): from media.radarr import Radarr from media.trakt import Trakt from misc import helpers @@ -334,9 +340,15 @@ def movies(list_type, add_limit=0, add_delay=2.5, genre=None, folder=None, no_se elif list_type.lower() == 'boxoffice': trakt_movies_list = trakt.get_boxoffice_movies() elif list_type.lower() == 'watchlist': - trakt_movies_list = trakt.get_watchlist_shows(user) + trakt_movies_list = trakt.get_watchlist_movies(authenticate_user) + elif list_type.lower() == 'lists': + trakt_movies_list = trakt.get_user_list_movies(list_type, authenticate_user) else: - trakt_movies_list = trakt.get_user_list_shows(list_type, user) + log.error("Aborting due to unknown Trakt list type") + if notifications: + callback_notify({'event': 'abort', 'type': 'movies', 'list_type': list_type, + 'reason': 'Failure to determine Trakt list type'}) + return None if not trakt_movies_list: log.error("Aborting due to failure to retrieve Trakt %s movies list", list_type) @@ -441,34 +453,49 @@ def automatic_shows(add_delay=2.5, no_search=False, notifications=False): log.info("Started") for list_type, value in cfg.automatic.shows.items(): + added_shows = None + if list_type.lower() == 'interval': continue - if list_type.lower() not in Trakt.non_user_lists: - type_amount = value + if list_type.lower() in Trakt.non_user_lists: + limit = value - if type_amount <= 0: + if limit <= 0: log.info("Skipped Trakt's %s shows list", list_type) continue else: - log.info("Adding %d shows from Trakt's %s list", type_amount, list_type) + log.info("Adding %d shows from Trakt's %s list", limit, list_type) # run shows - added_shows = shows.callback(list_type=list_type, add_limit=type_amount, + added_shows = shows.callback(list_type=list_type, add_limit=limit, add_delay=add_delay, no_search=no_search, notifications=notifications) - else: - for user, type_amount in value: - if type_amount <= 0: - log.info("Skipped Trakt's %s for &s", list_type, user) + elif list_type.lower() == 'watchlist': + for authenticate_user, limit in value.items(): + if limit <= 0: + log.info("Skipped Trakt's %s for %s", list_type, authenticate_user) continue else: - log.info("Adding %d shows from the %s from &s", type_amount, list_type, user) + log.info("Adding %d shows from the %s from %s", limit, list_type, authenticate_user) + + # run shows + added_shows = shows.callback(list_type=list_type, add_limit=limit, + add_delay=add_delay, no_search=no_search, + notifications=notifications, authenticate_user=authenticate_user) + elif list_type.lower() == 'lists': + for list, v in value.items(): + if isinstance(v, dict): + authenticate_user = v['authenticate_user'] + limit = v['limit'] + else: + authenticate_user = None + limit = v # run shows - added_shows = shows.callback(list_type=list_type, add_limit=type_amount, + added_shows = shows.callback(list_type=list_type, add_limit=limit, add_delay=add_delay, no_search=no_search, - notifications=notifications, user=user) + notifications=notifications, authenticate_user=authenticate_user) if added_shows is None: log.error("Failed adding shows from Trakt's %s list", list_type) @@ -497,34 +524,49 @@ def automatic_movies(add_delay=2.5, no_search=False, notifications=False): log.info("Started") for list_type, value in cfg.automatic.movies.items(): + added_movies = None + if list_type.lower() == 'interval': continue - if list_type.lower() not in Trakt.non_user_lists: - type_amount = value + if list_type.lower() in Trakt.non_user_lists: + limit = value - if type_amount <= 0: + if limit <= 0: log.info("Skipped Trakt's %s movies list", list_type) continue else: - log.info("Adding %d movies from Trakt's %s list", type_amount, list_type) + log.info("Adding %d movies from Trakt's %s list", limit, list_type) # run movies - added_movies = movies.callback(list_type=list_type, add_limit=type_amount, - add_delay=add_delay, no_search=no_search, - notifications=notifications) - else: - for user, type_amount in value: - if type_amount <= 0: - log.info("Skipped Trakt's %s for &s", list_type, user) + added_movies = movies.callback(list_type=list_type, add_limit=limit, + add_delay=add_delay, no_search=no_search, + notifications=notifications) + elif list_type.lower() == 'watchlist': + for authenticate_user, limit in value.items(): + if limit <= 0: + log.info("Skipped Trakt's %s for %s", list_type, authenticate_user) continue else: - log.info("Adding %d movies from the %s from &s", type_amount, list_type, user) + log.info("Adding %d movies from the %s from %s", limit, list_type, authenticate_user) # run movies - added_movies = movies.callback(list_type=list_type, add_limit=type_amount, - add_delay=add_delay, no_search=no_search, - notifications=notifications, user=user) + added_movies = movies.callback(list_type=list_type, add_limit=limit, + add_delay=add_delay, no_search=no_search, + notifications=notifications, authenticate_user=authenticate_user) + elif list_type.lower() == 'lists': + for list, v in value.items(): + if isinstance(v, dict): + authenticate_user = v['authenticate_user'] + limit = v['limit'] + else: + authenticate_user = None + limit = v + + # run shows + added_movies = movies.callback(list_type=list, add_limit=limit, + add_delay=add_delay, no_search=no_search, + notifications=notifications, authenticate_user=authenticate_user) if added_movies is None: log.error("Failed adding movies from Trakt's %s list", list_type) From d90eb11bf754cb89120d2542f345b6ec3ffe05c3 Mon Sep 17 00:00:00 2001 From: Mitchell Klijs Date: Fri, 27 Apr 2018 12:33:32 +0200 Subject: [PATCH 07/16] Add notice to readme --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index dc3ae2e..e5688f7 100644 --- a/README.md +++ b/README.md @@ -279,6 +279,8 @@ For every user you've to add: `"username": limit` to the watchlist key. For exam Of course you can combine this with running the other list types as well. +_Please note that every user from whome you fetch the watchlist should be authenticated._ + ### Custom lists You can also schedule any number of public or private custom lists. From e03654393a06e0d0e6411b452cf54c115bbe9b14 Mon Sep 17 00:00:00 2001 From: desimaniac Date: Sun, 29 Apr 2018 17:56:08 -0500 Subject: [PATCH 08/16] Readme: Updated with minor tweaks. --- README.md | 179 ++++++++++++++++++++++++++++-------------------------- 1 file changed, 94 insertions(+), 85 deletions(-) diff --git a/README.md b/README.md index e5688f7..c5a51b7 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,27 @@ # Traktarr Script to add new shows & movies to Sonarr/Radarr based on Trakt lists. -Trakt lists currently supported: -- anticipated -- boxoffice -- interval -- popular -- trending +Types of Trakt lists supported: -Furthermore, watchlists and custom list from multiple users are supported. +- Official Trakt lists + + - Trending + + - Popular + + - Anticipated + + - boxoffice + +- Public lists + +- Private lists* + + - Watchlist + + - Custom list(s) + +\* Support for multiple (authenticated) users. # Requirements @@ -32,35 +45,43 @@ Install Traktarr to be run with `traktarr` command. ## 2. Create app authentication -1. Create an Trakt application by going [here](https://trakt.tv/oauth/applications/new) +1. Create a Trakt application by going [here](https://trakt.tv/oauth/applications/new) 2. Enter a name for your application; for example `Traktarr` -3. Enter `urn:ietf:wg:oauth:2.0:oob` in the Redirecht uri field. -4. Click "Save app" -5. Open the Traktarr configuration file `config.json` and insert the -Client ID in the api_key and the Client Secret in the api_secret. Like this: -``` - { - "trakt": { - "api_key": "my_client_id", - "api_secret": "my_client_secret_key" - } - } -``` +3. Enter `urn:ietf:wg:oauth:2.0:oob` in the `Redirect uri` field. +4. Click "SAVE APP". +5. Open the Traktarr configuration file `config.json` and insert the Client ID in the `api_key` and the Client Secret in the `api_secret`, like this: + + ``` + { + "trakt": { + "api_key": "my_client_id", + "api_secret": "my_client_secret_key" + } + } + ``` -## 3. Authenticate users (optional) +## 3. Authenticate User(s) (optional) + +For reach user you want to access the private lists for (i.e. watchlist and/or custom lists), you will need to to authenticate that user. -If you want to be able to access private lists, you have to authentcate that user. Repeat the following steps for every user you want to authenticate: -1. Run `traktarr trakt_authenticate` (from the installation path) -2. Go to https://trakt.tv/activate. -3. Enter the code you see in your terminal -4. Click continue -5. If you're not loggedin to Trakt, login noe -6. Accept +1. Run `traktarr trakt_authentication` +1. You wil get the following prompt: + + ``` + 2018-04-29 23:52:43,455 - INFO - media.trakt - __oauth_request_device_code - We're talking to Trakt to get your verification code. Please wait a moment... + 2018-04-29 23:52:43,772 - INFO - media.trakt - __oauth_request_device_code - Go to: https://trakt.tv/activate on any device and enter A0XXXXXX. We'll be polling Trakt every 5 seconds for a reply + ``` +1. Go to https://trakt.tv/activate. +1. Enter the code you see in your terminal. +1. Click continue. +1. If you are not logged in to Trakt, login now. +1. Click "Accept". +1. You will get the message: "Woohoo! Your device is now connected and will automatically refresh in a few seconds.". You've now authenticated the user. -You can repeat this process for as many users as you want. +You can repeat this process for as many users as you like. ## 4. Setup Schedule @@ -75,7 +96,7 @@ To have Traktarr get Movies and Shows for you automatically, on set interval. # Configuration -Here is some default configuration you can use. +## Sample Configuration ```json { @@ -192,22 +213,6 @@ Here is some default configuration you can use. ``` -## Watchlist - -Traktarr can fetch the watchlist for as many users as you like. -You'll have to authenticate every user from whome you want to fetch the watchlist, -by following the steps described [here]((#authenticate-users-optional)). - -When all users are authenticated you can fetch their watchlist either -with the automatic task or with the manual commands (see examples below). - -## Other custom user lists - -Traktarr can also fetch any number of other custom lists. - -If the custom list is private, you'll have to authenticate a user that is allowed to -access that list by following the steps described [here]((#authenticate-users-optional)). - ## Core ```json @@ -255,10 +260,11 @@ Movies can be run on a separate schedule from Shows. `lists` - specify which custom lists to fetch (see explanation below) -### Watchlist +### Personal Watchlists + +The watchlist task can be scheduled with a differtent item limit for every (authenticated) user.* -The watchlist task can be scheduled with a differtent item limit for every user. -For every user you've to add: `"username": limit` to the watchlist key. For example: +So for every user, you will add: `"username": limit` to the watchlist key. For example: ```json "automatic": { @@ -279,14 +285,15 @@ For every user you've to add: `"username": limit` to the watchlist key. For exam Of course you can combine this with running the other list types as well. -_Please note that every user from whome you fetch the watchlist should be authenticated._ +\*_Please note that every user, from whom you fetch the watchlist from, will need to be authenticated._ -### Custom lists +### Custom Lists You can also schedule any number of public or private custom lists. -For both public and private lsits you'll need the url to that list. -You can copy this url from the address bar in you browser when viewing -the list on Trakt. + +For both public and private lists you'll need the url to that list. When viewing the list on Trakt, simply copy the url from the address bar of the your browser. + +#### Public Lists Public lists can be added by specifying the url and the item limit like this: @@ -305,10 +312,12 @@ Public lists can be added by specifying the url and the item limit like this: }, ``` + +#### Private Lists + Private lists can be added in two ways: -1. If there is only one authenticated user to Traktarr, you can add -the private list just like any other public list: +1. If there is only one authenticated user, you can add the private list just like any other public list: ```json "automatic": { @@ -325,12 +334,9 @@ the private list just like any other public list: }, ``` -2. If there are multiple authenticated users to Traktarr, you'll need -to specify with which user Traktarr should authenticate when fetching -the list. The user should have acces to the list (either own the list, -or friends with the owner of the list and the list is specified to be -shared with friends) -_Note that the specified user has to be authenticated_ +2. If there are multiple authenticated users you want to fetch the lists from, you'll need to specify the username under `authenticate_as`. + +_Note: The user should have access to the list (either own the list or a list that was shared to them by a friend)._ ```json "automatic": { @@ -547,6 +553,8 @@ Sonarr configuration. To show how tags work, we will create a sample tag `AMZN` and assign it to certain networks. +_Note: These are optional._ + ### Sonarr First, we will create a tag in Sonarr (Settings > Indexers > Restrictions). @@ -559,7 +567,7 @@ Tags: AMZN ### Traktarr -Finally, we will edit the Traktarr config and assign the `AMZN` tag to certain networks. +Finally, we will edit the Traktarr config and assign the `AMZN` tag to certain networks. ```json "tags": { @@ -589,6 +597,8 @@ Finally, we will edit the Traktarr config and assign the `AMZN` tag to certain n ## Trakt +Trakt Authentication info: + ```json "trakt": { "api_key": "", @@ -600,8 +610,6 @@ Finally, we will edit the Traktarr config and assign the `AMZN` tag to certain n `api_secret` - Fill in your Trakt Secret key (_Client Scret_) -_Note that when users authenticate to Traktarr, their token information -will be added to this._ # Usage @@ -688,31 +696,32 @@ Options: ## Examples -- Fetch boxoffice movies labeld with the comedy genere, limit to 10 items and send notifications -``` -traktarr movies -t boxoffice -g comedy -l 10 --notifications -``` +- Fetch boxoffice movies, labeled with the comedy genre, limited to 10 items, and send notifications: + ``` + traktarr movies -t boxoffice -g comedy -l 10 --notifications + ``` -- Fetch popular shows, limit to 2 items and don't start the search in Sonarr -``` -traktarr shows -t popular -l 2 --no-search -``` -- Fetch all shows from the watchlist from user1 +- Fetch popular shows, limited to 2 items, and don't start the search in Sonarr: -``` -traktarr shows -t watchlist --authenticate-user user1 -``` + ``` + traktarr shows -t popular -l 2 --no-search + ``` -- Fetch all movies from the public https://trakt.tv/users/rkerwin/lists/top-100-movies list +- Fetch all shows from the watchlist of `user1`: -``` -traktarr shows -t https://trakt.tv/users/rkerwin/lists/top-100-movies -``` + ``` + traktarr shows -t watchlist --authenticate-user user1 + ``` +- Fetch all movies from the public list `https://trakt.tv/users/rkerwin/lists/top-100-movies`: -- Fetch all movies from the private https://trakt.tv/users/user1/lists/private-movies-list list + ``` + traktarr shows -t https://trakt.tv/users/rkerwin/lists/top-100-movies + ``` -``` -traktarr shows -t https://trakt.tv/users/user1/lists/private-movies-list --authenticate-user=user1 -``` \ No newline at end of file +- Fetch all movies from the private list `https://trakt.tv/users/user1/lists/private-movies-list` of `user1`: + + ``` + traktarr shows -t https://trakt.tv/users/user1/lists/private-movies-list --authenticate-user=user1 + ``` From 08c4638fba013fb7501caceb6c6a8426a731fc46 Mon Sep 17 00:00:00 2001 From: desimaniac Date: Mon, 30 Apr 2018 14:19:09 -0500 Subject: [PATCH 09/16] Readme: Tweaked examples. --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index c5a51b7..a64e632 100644 --- a/README.md +++ b/README.md @@ -702,7 +702,6 @@ Options: traktarr movies -t boxoffice -g comedy -l 10 --notifications ``` - - Fetch popular shows, limited to 2 items, and don't start the search in Sonarr: ``` @@ -714,14 +713,15 @@ Options: ``` traktarr shows -t watchlist --authenticate-user user1 ``` + - Fetch all movies from the public list `https://trakt.tv/users/rkerwin/lists/top-100-movies`: ``` - traktarr shows -t https://trakt.tv/users/rkerwin/lists/top-100-movies + traktarr movies -t https://trakt.tv/users/rkerwin/lists/top-100-movies ``` - Fetch all movies from the private list `https://trakt.tv/users/user1/lists/private-movies-list` of `user1`: ``` - traktarr shows -t https://trakt.tv/users/user1/lists/private-movies-list --authenticate-user=user1 + traktarr movies -t https://trakt.tv/users/user1/lists/private-movies-list --authenticate-user=user1 ``` From 5d3f34f5d9de9337840ab077672383b3396ad457 Mon Sep 17 00:00:00 2001 From: Mitchell Klijs Date: Mon, 30 Apr 2018 23:52:26 +0200 Subject: [PATCH 10/16] Fix bug where list url's weren't working in console command --- traktarr.py | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/traktarr.py b/traktarr.py index 4c6a08d..ec73b0d 100755 --- a/traktarr.py +++ b/traktarr.py @@ -166,14 +166,8 @@ def shows(list_type, add_limit=0, add_delay=2.5, genre=None, folder=None, no_sea trakt_series_list = trakt.get_popular_shows() elif list_type.lower() == 'watchlist': trakt_series_list = trakt.get_watchlist_shows(authenticate_user) - elif list_type.lower() == 'lists': - trakt_series_list = trakt.get_user_list_shows(list_type, authenticate_user) else: - log.error("Aborting due to unknown Trakt list type") - if notifications: - callback_notify({'event': 'abort', 'type': 'shows', 'list_type': list_type, - 'reason': 'Failure to determine Trakt list type'}) - return None + trakt_series_list = trakt.get_user_list_shows(list_type, authenticate_user) if not trakt_series_list: log.error("Aborting due to failure to retrieve Trakt %s shows list", list_type) @@ -341,14 +335,8 @@ def movies(list_type, add_limit=0, add_delay=2.5, genre=None, folder=None, no_se trakt_movies_list = trakt.get_boxoffice_movies() elif list_type.lower() == 'watchlist': trakt_movies_list = trakt.get_watchlist_movies(authenticate_user) - elif list_type.lower() == 'lists': - trakt_movies_list = trakt.get_user_list_movies(list_type, authenticate_user) else: - log.error("Aborting due to unknown Trakt list type") - if notifications: - callback_notify({'event': 'abort', 'type': 'movies', 'list_type': list_type, - 'reason': 'Failure to determine Trakt list type'}) - return None + trakt_movies_list = trakt.get_user_list_movies(list_type, authenticate_user) if not trakt_movies_list: log.error("Aborting due to failure to retrieve Trakt %s movies list", list_type) @@ -493,7 +481,7 @@ def automatic_shows(add_delay=2.5, no_search=False, notifications=False): limit = v # run shows - added_shows = shows.callback(list_type=list_type, add_limit=limit, + added_shows = shows.callback(list_type=list, add_limit=limit, add_delay=add_delay, no_search=no_search, notifications=notifications, authenticate_user=authenticate_user) From bdf3218bd86d20d5c705ed2e9477c5d17c0f95a4 Mon Sep 17 00:00:00 2001 From: Mitchell Klijs Date: Mon, 30 Apr 2018 23:58:55 +0200 Subject: [PATCH 11/16] Readme: fix typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a64e632..7cdd664 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,7 @@ Install Traktarr to be run with `traktarr` command. ## 3. Authenticate User(s) (optional) -For reach user you want to access the private lists for (i.e. watchlist and/or custom lists), you will need to to authenticate that user. +For each user you want to access the private lists for (i.e. watchlist and/or custom lists), you will need to to authenticate that user. Repeat the following steps for every user you want to authenticate: From 00a12ae49e2fd79a70abd2a3ad7f50f603e1a947 Mon Sep 17 00:00:00 2001 From: Mitchell Klijs Date: Mon, 30 Apr 2018 23:59:51 +0200 Subject: [PATCH 12/16] Readme: fix numbering of list --- README.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 7cdd664..aa57242 100644 --- a/README.md +++ b/README.md @@ -67,18 +67,18 @@ For each user you want to access the private lists for (i.e. watchlist and/or cu Repeat the following steps for every user you want to authenticate: 1. Run `traktarr trakt_authentication` -1. You wil get the following prompt: +2. You wil get the following prompt: ``` 2018-04-29 23:52:43,455 - INFO - media.trakt - __oauth_request_device_code - We're talking to Trakt to get your verification code. Please wait a moment... 2018-04-29 23:52:43,772 - INFO - media.trakt - __oauth_request_device_code - Go to: https://trakt.tv/activate on any device and enter A0XXXXXX. We'll be polling Trakt every 5 seconds for a reply ``` -1. Go to https://trakt.tv/activate. -1. Enter the code you see in your terminal. -1. Click continue. -1. If you are not logged in to Trakt, login now. -1. Click "Accept". -1. You will get the message: "Woohoo! Your device is now connected and will automatically refresh in a few seconds.". +3. Go to https://trakt.tv/activate. +4. Enter the code you see in your terminal. +5. Click continue. +6. If you are not logged in to Trakt, login now. +7. Click "Accept". +8. You will get the message: "Woohoo! Your device is now connected and will automatically refresh in a few seconds.". You've now authenticated the user. You can repeat this process for as many users as you like. From d285ccf6dbeb53405143e13670feab0bdf83f74b Mon Sep 17 00:00:00 2001 From: Mitchell Klijs Date: Tue, 1 May 2018 00:00:11 +0200 Subject: [PATCH 13/16] Readme: remove timestamps from log messages --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index aa57242..441a136 100644 --- a/README.md +++ b/README.md @@ -70,8 +70,8 @@ Repeat the following steps for every user you want to authenticate: 2. You wil get the following prompt: ``` - 2018-04-29 23:52:43,455 - INFO - media.trakt - __oauth_request_device_code - We're talking to Trakt to get your verification code. Please wait a moment... - 2018-04-29 23:52:43,772 - INFO - media.trakt - __oauth_request_device_code - Go to: https://trakt.tv/activate on any device and enter A0XXXXXX. We'll be polling Trakt every 5 seconds for a reply + INFO - media.trakt - __oauth_request_device_code - We're talking to Trakt to get your verification code. Please wait a moment... + INFO - media.trakt - __oauth_request_device_code - Go to: https://trakt.tv/activate on any device and enter A0XXXXXX. We'll be polling Trakt every 5 seconds for a reply ``` 3. Go to https://trakt.tv/activate. 4. Enter the code you see in your terminal. From d69f1454ea9e9894f41af4f6c9aa4b188830a0ef Mon Sep 17 00:00:00 2001 From: Mitchell Klijs Date: Wed, 2 May 2018 09:50:13 +0200 Subject: [PATCH 14/16] Add ability to fetch other users' watchlists --- media/trakt.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/media/trakt.py b/media/trakt.py index d0b7fdc..d1e8163 100644 --- a/media/trakt.py +++ b/media/trakt.py @@ -282,7 +282,8 @@ class Trakt: while True: headers, authenticate_user = self.oauth_headers(authenticate_user) - req = requests.get('https://api.trakt.tv/sync/watchlist/shows', params=payload, + req = requests.get('https://api.trakt.tv/users/' + authenticate_user + '/watchlist/movies', + params=payload, headers=headers, timeout=30) log.debug("Request User: %s", authenticate_user) @@ -790,7 +791,8 @@ class Trakt: while True: headers, authenticate_user = self.oauth_headers(authenticate_user) - req = requests.get('https://api.trakt.tv/sync/watchlist/movies', params=payload, + req = requests.get('https://api.trakt.tv/users/' + authenticate_user + '/watchlist/movies', + params=payload, headers=headers, timeout=30) log.debug("Request User: %s", authenticate_user) From 9ad8253edbcf6b1307a6982161cf9664c06e706f Mon Sep 17 00:00:00 2001 From: Mitchell Klijs Date: Wed, 2 May 2018 09:50:21 +0200 Subject: [PATCH 15/16] Fix log messages --- media/trakt.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/media/trakt.py b/media/trakt.py index d1e8163..bd141b9 100644 --- a/media/trakt.py +++ b/media/trakt.py @@ -175,7 +175,9 @@ class Trakt: log.debug('No user provided, so default to the first user in the config (%s)', user) elif user not in self.cfg['trakt'].keys(): - 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) + 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() @@ -405,7 +407,7 @@ class Trakt: return processed_shows return None except Exception: - log.exception("Exception retrieving shows on watchlist") + log.exception("Exception retrieving shows on user list") return None @backoff.on_predicate(backoff.expo, lambda x: x is None, max_tries=4, on_backoff=backoff_handler) @@ -902,15 +904,16 @@ class Trakt: exit() else: - log.error("Failed to retrieve movies on watchlist from %s, request response: %d", authenticate_user, + log.error("Failed to retrieve movies on %s from %s, request response: %d", list_key, + authenticate_user, req.status_code) break if len(processed_movies): - log.debug("Found %d movies on watchlist from %s", len(processed_movies), authenticate_user) + log.debug("Found %d movies on %s from %s", len(processed_movies), list_key, authenticate_user) return processed_movies return None except Exception: - log.exception("Exception retrieving movies on watchlist") + log.exception("Exception retrieving movies on user list") return None From 976da5922f3c8bab70aef377d0e15c5030720319 Mon Sep 17 00:00:00 2001 From: Mitchell Klijs Date: Wed, 2 May 2018 09:52:32 +0200 Subject: [PATCH 16/16] Readme: adapt for new ability for fetching other users' watchlists --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index 441a136..d90ae7a 100644 --- a/README.md +++ b/README.md @@ -285,8 +285,6 @@ So for every user, you will add: `"username": limit` to the watchlist key. For e Of course you can combine this with running the other list types as well. -\*_Please note that every user, from whom you fetch the watchlist from, will need to be authenticated._ - ### Custom Lists You can also schedule any number of public or private custom lists.