From c7cfa180dd315fd0ffb97142a7162b130a41f9ce Mon Sep 17 00:00:00 2001 From: l3uddz Date: Thu, 8 Mar 2018 08:43:39 +0000 Subject: [PATCH 01/20] added --version option. begin addition of run mode/automatic mode. --- misc/config.py | 24 ++++++++++++++++++++++++ misc/log.py | 1 + requirements.txt | 3 ++- traktarr.py | 42 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 69 insertions(+), 1 deletion(-) diff --git a/misc/config.py b/misc/config.py index 46f6133..70d2b58 100644 --- a/misc/config.py +++ b/misc/config.py @@ -47,6 +47,30 @@ base_config = { 'blacklist_title_keywords': ['untitled', 'barbie'], 'allowed_countries': ['us', 'gb', 'ca'] } + }, + 'automatic': { + 'movies': { + 'interval': 24, + 'anticipated': 10, + 'trending': 2, + 'popular': 3 + }, + 'shows': { + 'interval': 72, + 'anticipated': 100, + 'trending': 2, + 'popular': 1 + }, + 'notifications': { + 'plex slack': { + 'type': 'slack', + 'webhook': 'http://' + }, + 'my pushover': { + 'client_id': '....', + 'client_secret': '....' + } + } } } cfg = None diff --git a/misc/log.py b/misc/log.py index 251724f..0abfbcd 100644 --- a/misc/log.py +++ b/misc/log.py @@ -18,6 +18,7 @@ class Logger: # disable bloat loggers logging.getLogger('urllib3').setLevel(logging.ERROR) + logging.getLogger('schedule').setLevel(logging.ERROR) # init console_logger self.console_handler = logging.StreamHandler(sys.stdout) diff --git a/requirements.txt b/requirements.txt index cd28427..27f2206 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ +backoff==1.4.3 +schedule==0.4.3 attrdict==2.0.0 click==6.7 -backoff==1.4.3 requests==2.18.4 diff --git a/traktarr.py b/traktarr.py index 92afffd..baa4681 100644 --- a/traktarr.py +++ b/traktarr.py @@ -2,6 +2,7 @@ import time import click +import schedule from media.radarr import Radarr from media.sonarr import Sonarr @@ -20,6 +21,7 @@ log = logger.get_logger('traktarr') # Click @click.group(help='Add new shows & movies to Sonarr/Radarr from Trakt lists.') +@click.version_option('1.0.0', prog_name='traktarr') def app(): pass @@ -143,6 +145,10 @@ def shows(list_type, add_limit=0, add_delay=2.5, no_search=False): log.info("Added %d new show(s) to Sonarr", added_shows) +############################################################ +# MOVIES +############################################################ + @app.command(help='Add new movies to Radarr.') @click.option('--list-type', '-t', type=click.Choice(['anticipated', 'trending', 'popular']), help='Trakt list to process.', required=True) @@ -243,6 +249,42 @@ def movies(list_type, add_limit=0, add_delay=2.5, no_search=False): log.info("Added %d new movie(s) to Radarr", added_movies) +############################################################ +# AUTOMATIC +############################################################ + +def automatic_shows(): + log.info("Running") + + +def automatic_movies(): + log.info("Running") + + +@app.command(help='Run in automatic mode.') +@click.option('--add-delay', '-d', default=2.5, help='Seconds between each add request to Sonarr / Radarr.', + show_default=True) +@click.option('--no-search', is_flag=True, help='Disable search when adding to Sonarr / Radarr.') +def run(add_delay=2.5, no_search=False): + # add tasks to repeat + schedule.every(1).minutes.do(automatic_movies) + schedule.every(2).minutes.do(automatic_shows) + + # run schedule + log.info("Automatic mode is now running...") + while True: + try: + schedule.run_pending() + except KeyboardInterrupt: + log.info("Scheduled tasks are no longer being processed due to Ctrl + C") + break + except Exception: + log.exception("Unhandled exception occurred while processing scheduled tasks: ") + else: + time.sleep(1) + log.info("Automatic mode is now finished!") + + ############################################################ # MAIN ############################################################ From 3bd59f3be2f9e33f209a1b7f25e6d63fd52842a7 Mon Sep 17 00:00:00 2001 From: l3uddz Date: Thu, 8 Mar 2018 08:51:04 +0000 Subject: [PATCH 02/20] remove keyboardinterrupt --- traktarr.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/traktarr.py b/traktarr.py index baa4681..0d65f75 100644 --- a/traktarr.py +++ b/traktarr.py @@ -275,14 +275,10 @@ def run(add_delay=2.5, no_search=False): while True: try: schedule.run_pending() - except KeyboardInterrupt: - log.info("Scheduled tasks are no longer being processed due to Ctrl + C") - break except Exception: log.exception("Unhandled exception occurred while processing scheduled tasks: ") else: time.sleep(1) - log.info("Automatic mode is now finished!") ############################################################ From de47161661e5ce577792a2d322919b0ad846075a Mon Sep 17 00:00:00 2001 From: l3uddz Date: Thu, 8 Mar 2018 10:03:20 +0000 Subject: [PATCH 03/20] more work on automatic mode --- traktarr.py | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/traktarr.py b/traktarr.py index 0d65f75..d24f645 100644 --- a/traktarr.py +++ b/traktarr.py @@ -253,11 +253,25 @@ def movies(list_type, add_limit=0, add_delay=2.5, no_search=False): # AUTOMATIC ############################################################ -def automatic_shows(): +def callback_automatic(data): + log.debug("Received callback data:\n%s", data) + + # handle event + if data['event'] == 'add_movie': + log.info("Added movie: %s (%d)", data['movie']['title'], data['movie']['year']) + elif data['event'] == 'add_show': + log.info("Added show: %s (%d)", data['show']['title'], data['show']['year']) + else: + log.error("Unexpected callback:\n%s", data) + + return + + +def automatic_shows(add_delay=2.5, no_search=False): log.info("Running") -def automatic_movies(): +def automatic_movies(add_delay=2.5, no_search=False): log.info("Running") @@ -267,8 +281,8 @@ def automatic_movies(): @click.option('--no-search', is_flag=True, help='Disable search when adding to Sonarr / Radarr.') def run(add_delay=2.5, no_search=False): # add tasks to repeat - schedule.every(1).minutes.do(automatic_movies) - schedule.every(2).minutes.do(automatic_shows) + schedule.every(cfg.automatic.movies.interval).minutes.do(automatic_movies, add_delay, no_search) + schedule.every(cfg.automatic.shows.interval).minutes.do(automatic_shows, add_delay, no_search) # run schedule log.info("Automatic mode is now running...") From 8748964a4e393ae1bea9fdfe06e8f79526220cc0 Mon Sep 17 00:00:00 2001 From: l3uddz Date: Thu, 8 Mar 2018 16:05:11 +0000 Subject: [PATCH 04/20] add bravo to default networks for amzn --- misc/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misc/config.py b/misc/config.py index 70d2b58..1f9b2c8 100644 --- a/misc/config.py +++ b/misc/config.py @@ -19,7 +19,7 @@ base_config = { 'root_folder': '/tv/', 'tags': { 'amzn': ['hbo', 'amc', 'usa network', 'tnt', 'starz', 'the cw', 'fx', 'fox', 'abc', 'nbc', 'cbs', 'tbs', - 'amazon', 'syfy', 'cinemax'] + 'amazon', 'syfy', 'cinemax', 'bravo'] } }, 'radarr': { From 0855bdb70e4bc5fdf943ee7135fc7f890ad58fc9 Mon Sep 17 00:00:00 2001 From: l3uddz Date: Thu, 8 Mar 2018 16:38:45 +0000 Subject: [PATCH 05/20] added boxoffice list to movies command --- media/trakt.py | 57 +++++++++++++++++++++++++++++++++++++++++++++++++- misc/str.py | 13 ++++++++++++ traktarr.py | 4 +++- 3 files changed, 72 insertions(+), 2 deletions(-) diff --git a/media/trakt.py b/media/trakt.py index a164352..ef7e856 100644 --- a/media/trakt.py +++ b/media/trakt.py @@ -349,7 +349,7 @@ class Trakt: if req.status_code == 200: resp_json = req.json() - # process list so it conforms to standard we expect ( e.g. {"show": {.....}} ) + # process list so it conforms to standard we expect ( e.g. {"movie": {.....}} ) for movie in resp_json: if movie not in processed_movies: processed_movies.append({'movie': movie}) @@ -377,3 +377,58 @@ class Trakt: except Exception: log.exception("Exception retrieving popular movies: ") return None + + @backoff.on_predicate(backoff.expo, lambda x: x is None, max_tries=4, on_backoff=backoff_handler) + def get_boxoffice_movies(self, 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/movies/boxoffice', params=payload, headers=self.headers, + timeout=30) + 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 movie in resp_json: + if movie not in processed_movies: + processed_movies.append(movie) + + # 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 + + else: + log.error("Failed to retrieve boxoffice movies, request response: %d", req.status_code) + break + + if len(processed_movies): + log.debug("Found %d boxoffice movies", len(processed_movies)) + return processed_movies + return None + except Exception: + log.exception("Exception retrieving boxoffice movies: ") + return None diff --git a/misc/str.py b/misc/str.py index f47425c..91c9cab 100644 --- a/misc/str.py +++ b/misc/str.py @@ -13,3 +13,16 @@ def get_year_from_timestamp(timestamp): except Exception: log.exception("Exception parsing year from %s: ", timestamp) return int(year) if str(year).isdigit() else 0 + + +def is_ascii(string): + try: + string.encode('ascii') + except UnicodeEncodeError: + return False + except UnicodeDecodeError: + return False + except Exception: + log.exception(u"Exception checking if %r was ascii: ", string) + return False + return True diff --git a/traktarr.py b/traktarr.py index d24f645..abb311b 100644 --- a/traktarr.py +++ b/traktarr.py @@ -150,7 +150,7 @@ def shows(list_type, add_limit=0, add_delay=2.5, no_search=False): ############################################################ @app.command(help='Add new movies to Radarr.') -@click.option('--list-type', '-t', type=click.Choice(['anticipated', 'trending', 'popular']), +@click.option('--list-type', '-t', type=click.Choice(['anticipated', 'trending', 'popular', 'boxoffice']), help='Trakt list to process.', 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) @@ -198,6 +198,8 @@ def movies(list_type, add_limit=0, add_delay=2.5, no_search=False): trakt_movies_list = trakt.get_trending_movies() elif list_type.lower() == 'popular': trakt_movies_list = trakt.get_popular_movies() + elif list_type.lower() == 'boxoffice': + trakt_movies_list = trakt.get_boxoffice_movies() else: log.error("Aborting due to unknown Trakt list type") return From 632e60311857670fc2bd633d351eb7b660bf6f01 Mon Sep 17 00:00:00 2001 From: l3uddz Date: Thu, 8 Mar 2018 16:40:45 +0000 Subject: [PATCH 06/20] add boxoffice to automatic movie config --- misc/config.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/misc/config.py b/misc/config.py index 1f9b2c8..0e7800d 100644 --- a/misc/config.py +++ b/misc/config.py @@ -53,7 +53,8 @@ base_config = { 'interval': 24, 'anticipated': 10, 'trending': 2, - 'popular': 3 + 'popular': 3, + 'boxoffice': 10 }, 'shows': { 'interval': 72, From 3e7fd6683e9c40159b3eb58bf3fce8cd77d6fc63 Mon Sep 17 00:00:00 2001 From: l3uddz Date: Fri, 9 Mar 2018 03:20:29 +0000 Subject: [PATCH 07/20] more boiler plate for automatic mode. fix abort when lists are empty after removing existing movies/shows. --- traktarr.py | 34 ++++++++++++++++++++++++++++++---- 1 file changed, 30 insertions(+), 4 deletions(-) diff --git a/traktarr.py b/traktarr.py index abb311b..1b1dd10 100644 --- a/traktarr.py +++ b/traktarr.py @@ -98,7 +98,7 @@ def shows(list_type, add_limit=0, add_delay=2.5, no_search=False): # build filtered series list without series that exist in sonarr processed_series_list = helpers.sonarr_remove_existing_series(sonarr_series_list, trakt_series_list) - if not processed_series_list: + if processed_series_list is None: log.error("Aborting due to failure to remove existing Sonarr shows from retrieved Trakt shows list") return else: @@ -211,7 +211,7 @@ def movies(list_type, add_limit=0, add_delay=2.5, no_search=False): # build filtered movie list without movies that exist in radarr processed_movies_list = helpers.radarr_remove_existing_movies(radarr_movie_list, trakt_movies_list) - if not processed_movies_list: + if processed_movies_list is None: log.error("Aborting due to failure to remove existing Radarr movies from retrieved Trakt movies list") return else: @@ -270,11 +270,37 @@ def callback_automatic(data): def automatic_shows(add_delay=2.5, no_search=False): - log.info("Running") + try: + for list_type, type_amount 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 + else: + log.info("Adding %d shows from Trakt's %s list", type_amount, list_type) + + + except Exception: + log.exception("Exception while automatically adding shows: ") + return def automatic_movies(add_delay=2.5, no_search=False): - log.info("Running") + try: + for list_type, type_amount 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 + else: + log.info("Adding %d movies from Trakt's %s list", type_amount, list_type) + + + except Exception: + log.exception("Exception while automatically adding movies: ") + return @app.command(help='Run in automatic mode.') From 0bc9977541000b4ad4527b4342b5062578f264ce Mon Sep 17 00:00:00 2001 From: l3uddz Date: Fri, 9 Mar 2018 11:59:34 +0000 Subject: [PATCH 08/20] should catch error reasons more regardless of status_code --- media/radarr.py | 5 +++-- media/sonarr.py | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/media/radarr.py b/media/radarr.py index a2dad65..eade2a0 100644 --- a/media/radarr.py +++ b/media/radarr.py @@ -98,8 +98,9 @@ class Radarr: if req.status_code == 201 and req.json()['tmdbId'] == movie_tmdbid: log.debug("Successfully added %s (%d)", movie_title, movie_tmdbid) return True - elif req.status_code == 409 and 'message' in req.text: - log.error("Failed to add %s (%d), reason: %s", movie_title, movie_tmdbid, req.json()['message']) + elif 'json' in req.headers['Content-Type'].lower() and 'message' in req.text: + log.error("Failed to add %s (%d) - status_code: %d, reason: %s", movie_title, movie_tmdbid, + req.status_code, req.json()['message']) return False else: log.error("Failed to add %s (%d), unexpected response:\n%s", movie_title, movie_tmdbid, req.text) diff --git a/media/sonarr.py b/media/sonarr.py index 7030f2f..22452d9 100644 --- a/media/sonarr.py +++ b/media/sonarr.py @@ -144,8 +144,9 @@ class Sonarr: if req.status_code == 201 and req.json()['tvdbId'] == series_tvdbid: log.debug("Successfully added %s (%d)", series_title, series_tvdbid) return True - elif req.status_code == 401 and 'errorMessage' in req.text: - log.error("Failed to add %s (%d), reason: %s", series_title, series_tvdbid, req.json()['errorMessage']) + elif 'json' in req.headers['Content-Type'].lower() and 'errorMessage' in req.text: + log.error("Failed to add %s (%d) - status_code: %d, reason: %s", series_title, series_tvdbid, + req.status_code, req.json()['errorMessage']) return False else: log.error("Failed to add %s (%d), unexpected response:\n%s", series_title, series_tvdbid, req.text) From 865134c26e50cc5422017a820ce584d1943879a5 Mon Sep 17 00:00:00 2001 From: l3uddz Date: Fri, 9 Mar 2018 16:55:52 +0000 Subject: [PATCH 09/20] more work on run mode and moved notifications into its own config section. --- misc/config.py | 18 +++--- traktarr.py | 145 ++++++++++++++++++++++++++++++++++++++++--------- 2 files changed, 128 insertions(+), 35 deletions(-) diff --git a/misc/config.py b/misc/config.py index 0e7800d..fe0e659 100644 --- a/misc/config.py +++ b/misc/config.py @@ -61,16 +61,16 @@ base_config = { 'anticipated': 100, 'trending': 2, 'popular': 1 + } + }, + 'notifications': { + 'plex slack': { + 'type': 'slack', + 'webhook': 'http://' }, - 'notifications': { - 'plex slack': { - 'type': 'slack', - 'webhook': 'http://' - }, - 'my pushover': { - 'client_id': '....', - 'client_secret': '....' - } + 'my pushover': { + 'client_id': '....', + 'client_secret': '....' } } } diff --git a/traktarr.py b/traktarr.py index 1b1dd10..1fad9f6 100644 --- a/traktarr.py +++ b/traktarr.py @@ -36,14 +36,17 @@ def app(): @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('--no-search', is_flag=True, help='Disable search when adding shows to Sonarr.') -def shows(list_type, add_limit=0, add_delay=2.5, no_search=False): +@click.option('--notifications', is_flag=True, help='Send notifications.') +def shows(list_type, add_limit=0, add_delay=2.5, no_search=False, notifications=False): added_shows = 0 # validate trakt api_key trakt = Trakt(cfg.trakt.api_key) if not trakt.validate_api_key(): log.error("Aborting due to failure to validate Trakt API Key") - return + if notifications: + callback_notify({'event': 'abort', 'type': 'shows', 'reason': 'Failure to validate Trakt API Key'}) + return None else: log.info("Validated Trakt API Key") @@ -51,7 +54,9 @@ def shows(list_type, add_limit=0, add_delay=2.5, no_search=False): sonarr = Sonarr(cfg.sonarr.url, cfg.sonarr.api_key) if not sonarr.validate_api_key(): log.error("Aborting due to failure to validate Sonarr URL / API Key") - return + if notifications: + callback_notify({'event': 'abort', 'type': 'shows', 'reason': 'Failure to validate Sonarr URL / API Key'}) + return None else: log.info("Validated Sonarr URL & API Key") @@ -59,7 +64,10 @@ def shows(list_type, add_limit=0, add_delay=2.5, no_search=False): profile_id = sonarr.get_profile_id(cfg.sonarr.profile) if not profile_id or not profile_id > 0: log.error("Aborting due to failure to retrieve Profile ID for: %s", cfg.sonarr.profile) - return + if notifications: + callback_notify({'event': 'abort', 'type': 'shows', + 'reason': 'Failure to retrieve Sonarr Profile ID of %s' % cfg.sonarr.profile}) + return None else: log.info("Retrieved Profile ID for %s: %d", cfg.sonarr.profile, profile_id) @@ -67,7 +75,9 @@ def shows(list_type, add_limit=0, add_delay=2.5, no_search=False): profile_tags = sonarr.get_tags() if profile_tags is None: log.error("Aborting due to failure to retrieve Tag ID's") - return + if notifications: + callback_notify({'event': 'abort', 'type': 'shows', 'reason': "Failure to retrieve Sonarr Tag ID's"}) + return None else: log.info("Retrieved %d Tag ID's", len(profile_tags)) @@ -75,7 +85,9 @@ def shows(list_type, add_limit=0, add_delay=2.5, no_search=False): sonarr_series_list = sonarr.get_series() if not sonarr_series_list: log.error("Aborting due to failure to retrieve Sonarr shows list") - return + if notifications: + callback_notify({'event': 'abort', 'type': 'shows', 'reason': 'Failure to retrieve Sonarr shows list'}) + return None else: log.info("Retrieved Sonarr shows list, shows found: %d", len(sonarr_series_list)) @@ -89,10 +101,15 @@ def shows(list_type, add_limit=0, add_delay=2.5, no_search=False): trakt_series_list = trakt.get_popular_shows() else: log.error("Aborting due to unknown Trakt list type") - return + if notifications: + callback_notify({'event': 'abort', 'type': 'shows', '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) - return + if notifications: + callback_notify( + {'event': 'abort', 'type': 'shows', 'reason': 'Failure to retrieve Trakt %s shows list' % list_type}) + return None else: log.info("Retrieved Trakt %s shows list, shows found: %d", list_type, len(trakt_series_list)) @@ -100,7 +117,11 @@ def shows(list_type, add_limit=0, add_delay=2.5, no_search=False): processed_series_list = helpers.sonarr_remove_existing_series(sonarr_series_list, trakt_series_list) if processed_series_list is None: log.error("Aborting due to failure to remove existing Sonarr shows from retrieved Trakt shows list") - return + if notifications: + callback_notify({'event': 'abort', 'type': 'shows', + 'reason': 'Failure to remove existing Sonarr shows from retrieved Trakt %s shows list' % list_type + }) + return None else: log.info("Removed existing Sonarr shows from Trakt shows list, shows left to process: %d", len(processed_series_list)) @@ -127,6 +148,8 @@ def shows(list_type, add_limit=0, add_delay=2.5, no_search=False): series['show']['ids']['slug'], profile_id, cfg.sonarr.root_folder, use_tags, not no_search): log.info("ADDED %s (%d) with tags: %s", series['show']['title'], series['show']['year'], use_tags) + if notifications: + callback_notify({'event': 'add_show', 'show': series['show']}) added_shows += 1 else: log.error("FAILED adding %s (%d) with tags: %s", series['show']['title'], series['show']['year'], @@ -143,6 +166,7 @@ def shows(list_type, add_limit=0, add_delay=2.5, no_search=False): log.exception("Exception while processing show %s: ", series['show']['title']) log.info("Added %d new show(s) to Sonarr", added_shows) + return added_shows ############################################################ @@ -155,14 +179,17 @@ def shows(list_type, add_limit=0, add_delay=2.5, no_search=False): @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('--no-search', is_flag=True, help='Disable search when adding movies to Radarr.') -def movies(list_type, add_limit=0, add_delay=2.5, no_search=False): +@click.option('--notifications', is_flag=True, help='Send notifications.') +def movies(list_type, add_limit=0, add_delay=2.5, no_search=False, notifications=False): added_movies = 0 # validate trakt api_key trakt = Trakt(cfg.trakt.api_key) if not trakt.validate_api_key(): log.error("Aborting due to failure to validate Trakt API Key") - return + if notifications: + callback_notify({'event': 'abort', 'type': 'movies', 'reason': 'Failure to validate Trakt API Key'}) + return None else: log.info("Validated Trakt API Key") @@ -170,7 +197,10 @@ def movies(list_type, add_limit=0, add_delay=2.5, no_search=False): radarr = Radarr(cfg.radarr.url, cfg.radarr.api_key) if not radarr.validate_api_key(): log.error("Aborting due to failure to validate Radarr URL / API Key") - return + if notifications: + callback_notify( + {'event': 'abort', 'type': 'movies', 'reason': 'Failure to validate Radarr URL / API Key'}) + return None else: log.info("Validated Radarr URL & API Key") @@ -178,7 +208,10 @@ def movies(list_type, add_limit=0, add_delay=2.5, no_search=False): profile_id = radarr.get_profile_id(cfg.radarr.profile) if not profile_id or not profile_id > 0: log.error("Aborting due to failure to retrieve Profile ID for: %s", cfg.radarr.profile) - return + if notifications: + callback_notify({'event': 'abort', 'type': 'movies', + 'reason': 'Failure to retrieve Radarr Profile ID of %s' % cfg.radarr.profile}) + return None else: log.info("Retrieved Profile ID for %s: %d", cfg.radarr.profile, profile_id) @@ -186,7 +219,9 @@ def movies(list_type, add_limit=0, add_delay=2.5, no_search=False): radarr_movie_list = radarr.get_movies() if not radarr_movie_list: log.error("Aborting due to failure to retrieve Radarr movies list") - return + if notifications: + callback_notify({'event': 'abort', 'type': 'movies', 'reason': 'Failure to retrieve Radarr movies list'}) + return None else: log.info("Retrieved Radarr movies list, movies found: %d", len(radarr_movie_list)) @@ -202,10 +237,15 @@ def movies(list_type, add_limit=0, add_delay=2.5, no_search=False): trakt_movies_list = trakt.get_boxoffice_movies() else: log.error("Aborting due to unknown Trakt list type") - return + if notifications: + callback_notify({'event': 'abort', 'type': 'movies', '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) - return + if notifications: + callback_notify( + {'event': 'abort', 'type': 'movies', 'reason': 'Failure to retrieve Trakt %s movies list' % list_type}) + return None else: log.info("Retrieved Trakt %s movies list, movies found: %d", list_type, len(trakt_movies_list)) @@ -213,7 +253,11 @@ def movies(list_type, add_limit=0, add_delay=2.5, no_search=False): processed_movies_list = helpers.radarr_remove_existing_movies(radarr_movie_list, trakt_movies_list) if processed_movies_list is None: log.error("Aborting due to failure to remove existing Radarr movies from retrieved Trakt movies list") - return + if notifications: + callback_notify({'event': 'abort', 'type': 'movies', + 'reason': 'Failure to remove existing Radarr movies from retrieved ' + 'Trakt %s movies list' % list_type}) + return None else: log.info("Removed existing Radarr movies from Trakt movies list, movies left to process: %d", len(processed_movies_list)) @@ -234,6 +278,8 @@ def movies(list_type, add_limit=0, add_delay=2.5, no_search=False): 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): log.info("ADDED %s (%d)", movie['movie']['title'], movie['movie']['year']) + if notifications: + callback_notify({'event': 'add_movie', 'movie': movie['movie']}) added_movies += 1 else: log.error("FAILED adding %s (%d)", movie['movie']['title'], movie['movie']['year']) @@ -249,28 +295,36 @@ def movies(list_type, add_limit=0, add_delay=2.5, no_search=False): log.exception("Exception while processing movie %s: ", movie['movie']['title']) log.info("Added %d new movie(s) to Radarr", added_movies) + return added_movies ############################################################ # AUTOMATIC ############################################################ -def callback_automatic(data): - log.debug("Received callback data:\n%s", data) +def callback_notify(data): + log.debug("Received callback data: %s", data) # handle event if data['event'] == 'add_movie': log.info("Added movie: %s (%d)", data['movie']['title'], data['movie']['year']) + return elif data['event'] == 'add_show': log.info("Added show: %s (%d)", data['show']['title'], data['show']['year']) + return + elif data['event'] == 'abort': + log.error("Error while adding %s due to: %s", data['type'], data['reason']) + return else: - log.error("Unexpected callback:\n%s", data) - + log.error("Unexpected callback: %s", data) return -def automatic_shows(add_delay=2.5, no_search=False): +def automatic_shows(add_delay=2.5, no_search=False, notifications=False): + total_shows_added = 0 try: + log.info("Started") + for list_type, type_amount in cfg.automatic.shows.items(): if list_type.lower() == 'interval': continue @@ -280,14 +334,34 @@ def automatic_shows(add_delay=2.5, no_search=False): 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) + if added_shows is None: + log.error("Failed adding shows from Trakt's %s list", list_type) + time.sleep(15) + continue + total_shows_added += added_shows + + # send notification + log.info("Added %d shows from Trakt's %s list", added_shows, list_type) + + # sleep + time.sleep(15) + + log.info("Finished, added %d shows in total to Sonarr", total_shows_added) except Exception: log.exception("Exception while automatically adding shows: ") return -def automatic_movies(add_delay=2.5, no_search=False): +def automatic_movies(add_delay=2.5, no_search=False, notifications=False): + total_movies_added = 0 try: + log.info("Started") + for list_type, type_amount in cfg.automatic.movies.items(): if list_type.lower() == 'interval': continue @@ -297,6 +371,23 @@ def automatic_movies(add_delay=2.5, no_search=False): 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) + if added_movies is None: + log.error("Failed adding movies from Trakt's %s list", list_type) + time.sleep(15) + continue + total_movies_added += added_movies + + # send notification + log.info("Added %d movies from Trakt's %s list", added_movies, list_type) + + # sleep + time.sleep(15) + + log.info("Finished, added %d movies in total to Radarr", total_movies_added) except Exception: log.exception("Exception while automatically adding movies: ") @@ -307,10 +398,12 @@ def automatic_movies(add_delay=2.5, no_search=False): @click.option('--add-delay', '-d', default=2.5, help='Seconds between each add request to Sonarr / Radarr.', show_default=True) @click.option('--no-search', is_flag=True, help='Disable search when adding to Sonarr / Radarr.') -def run(add_delay=2.5, no_search=False): +@click.option('--no-notifications', is_flag=True, help="Disable notifications.") +def run(add_delay=2.5, no_search=False, no_notifications=False): # add tasks to repeat - schedule.every(cfg.automatic.movies.interval).minutes.do(automatic_movies, add_delay, no_search) - schedule.every(cfg.automatic.shows.interval).minutes.do(automatic_shows, add_delay, no_search) + schedule.every(cfg.automatic.movies.interval).minutes.do(automatic_movies, add_delay, no_search, + not no_notifications) + schedule.every(cfg.automatic.shows.interval).minutes.do(automatic_shows, add_delay, no_search, not no_notifications) # run schedule log.info("Automatic mode is now running...") From 68c960e579b39001e3074194e959ee99d6d164b8 Mon Sep 17 00:00:00 2001 From: l3uddz Date: Fri, 9 Mar 2018 16:56:59 +0000 Subject: [PATCH 10/20] reduced sleep between runs for each trakt list. --- traktarr.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/traktarr.py b/traktarr.py index 1fad9f6..5b8b1ad 100644 --- a/traktarr.py +++ b/traktarr.py @@ -340,7 +340,7 @@ def automatic_shows(add_delay=2.5, no_search=False, notifications=False): notifications=notifications) if added_shows is None: log.error("Failed adding shows from Trakt's %s list", list_type) - time.sleep(15) + time.sleep(10) continue total_shows_added += added_shows @@ -348,7 +348,7 @@ def automatic_shows(add_delay=2.5, no_search=False, notifications=False): log.info("Added %d shows from Trakt's %s list", added_shows, list_type) # sleep - time.sleep(15) + time.sleep(10) log.info("Finished, added %d shows in total to Sonarr", total_shows_added) @@ -377,7 +377,7 @@ def automatic_movies(add_delay=2.5, no_search=False, notifications=False): notifications=notifications) if added_movies is None: log.error("Failed adding movies from Trakt's %s list", list_type) - time.sleep(15) + time.sleep(10) continue total_movies_added += added_movies @@ -385,7 +385,7 @@ def automatic_movies(add_delay=2.5, no_search=False, notifications=False): log.info("Added %d movies from Trakt's %s list", added_movies, list_type) # sleep - time.sleep(15) + time.sleep(10) log.info("Finished, added %d movies in total to Radarr", total_movies_added) From 934c2f23749da50ec9a693d340e127ebb47feaeb Mon Sep 17 00:00:00 2001 From: l3uddz Date: Fri, 9 Mar 2018 18:23:26 +0000 Subject: [PATCH 11/20] added notifications class and more work on notifications. --- misc/config.py | 12 ++--- misc/log.py | 1 + notifications/__init__.py | 54 +++++++++++++++++++++++ notifications/pushover.py | 33 ++++++++++++++ notifications/slack.py | 38 ++++++++++++++++ traktarr.py | 92 +++++++++++++++++++++++++++++---------- 6 files changed, 201 insertions(+), 29 deletions(-) create mode 100644 notifications/__init__.py create mode 100644 notifications/pushover.py create mode 100644 notifications/slack.py diff --git a/misc/config.py b/misc/config.py index fe0e659..7f60f5c 100644 --- a/misc/config.py +++ b/misc/config.py @@ -64,13 +64,15 @@ base_config = { } }, 'notifications': { - 'plex slack': { - 'type': 'slack', - 'webhook': 'http://' + 'verbose': False, + 'my slack': { + 'service': 'slack', + 'webhook_url': '' }, 'my pushover': { - 'client_id': '....', - 'client_secret': '....' + 'service': 'pushover', + 'app_token': '', + 'user_token': '' } } } diff --git a/misc/log.py b/misc/log.py index 0abfbcd..28e12f1 100644 --- a/misc/log.py +++ b/misc/log.py @@ -17,6 +17,7 @@ class Logger: self.root_logger.setLevel(log_level) # disable bloat loggers + logging.getLogger("requests").setLevel(logging.WARNING) logging.getLogger('urllib3').setLevel(logging.ERROR) logging.getLogger('schedule').setLevel(logging.ERROR) diff --git a/notifications/__init__.py b/notifications/__init__.py new file mode 100644 index 0000000..c1acab2 --- /dev/null +++ b/notifications/__init__.py @@ -0,0 +1,54 @@ +from misc.log import logger + +from .pushover import Pushover +from .slack import Slack + +log = logger.get_logger(__name__) + +SERVICES = { + 'pushover': Pushover, + 'slack': Slack +} + + +class Notifications: + def __init__(self): + self.services = [] + + def load(self, **kwargs): + if 'service' not in kwargs: + log.error("You must specify a service to load with the service parameter") + return False + elif kwargs['service'] not in SERVICES: + log.error("You specified an invalid service to load: %s", kwargs['service']) + return False + + try: + chosen_service = SERVICES[kwargs['service']] + del kwargs['service'] + + # load service + service = chosen_service(**kwargs) + self.services.append(service) + + except Exception: + log.exception("Exception while loading service, kwargs=%r: ", kwargs) + + def send(self, **kwargs): + try: + # remove service keyword if supplied + if 'service' in kwargs: + # send notification to specified service + chosen_service = kwargs['service'].lower() + del kwargs['service'] + else: + chosen_service = None + + # send notification(s) + for service in self.services: + if chosen_service and service.NAME.lower() != chosen_service: + continue + elif service.send(**kwargs): + log.info("Sent notification with %s", service.NAME) + except Exception: + log.exception("Exception sending notification, kwargs=%r: ", kwargs) diff --git a/notifications/pushover.py b/notifications/pushover.py new file mode 100644 index 0000000..9374dc1 --- /dev/null +++ b/notifications/pushover.py @@ -0,0 +1,33 @@ +import requests + +from misc.log import logger + +log = logger.get_logger(__name__) + + +class Pushover: + NAME = "Pushover" + + def __init__(self, app_token, user_token): + self.app_token = app_token + self.user_token = user_token + log.debug("Initialized Pushover notification agent") + + def send(self, **kwargs): + if not self.app_token or not self.user_token: + log.error("You must specify an app_token and user_token when initializing this class") + return False + + # send notification + try: + payload = { + 'token': self.app_token, + 'user': self.user_token, + 'message': kwargs['message'] + } + resp = requests.post('https://api.pushover.net/1/messages.json', data=payload, timeout=30) + return True if resp.status_code == 200 else False + + except Exception: + log.exception("Error sending notification to %r", self.user_token) + return False diff --git a/notifications/slack.py b/notifications/slack.py new file mode 100644 index 0000000..6c779fd --- /dev/null +++ b/notifications/slack.py @@ -0,0 +1,38 @@ +import requests + +from misc.log import logger + +log = logger.get_logger(__name__) + + +class Slack: + NAME = "Slack" + + def __init__(self, webhook_url, sender_name='traktarr', sender_icon=':movie_camera:', channel=None): + self.webhook_url = webhook_url + self.sender_name = sender_name + self.sender_icon = sender_icon + self.channel = channel + log.debug("Initialized Slack notification agent") + + def send(self, **kwargs): + if not self.webhook_url or not self.sender_name or not self.sender_icon: + log.error("You must specify an webhook_url, sender_name and sender_icon when initializing this class") + return False + + # send notification + try: + payload = { + 'text': kwargs['message'], + 'username': self.sender_name, + 'icon_emoji': self.sender_icon, + } + if self.channel: + payload['channel'] = self.channel + + resp = requests.post(self.webhook_url, json=payload, timeout=30) + return True if resp.status_code == 200 else False + + except Exception: + log.exception("Error sending notification to %r", self.webhook_url) + return False diff --git a/traktarr.py b/traktarr.py index 5b8b1ad..52d097f 100644 --- a/traktarr.py +++ b/traktarr.py @@ -10,6 +10,7 @@ from media.trakt import Trakt from misc import helpers from misc.config import cfg from misc.log import logger +from notifications import Notifications ############################################################ # INIT @@ -18,6 +19,9 @@ from misc.log import logger # Logging log = logger.get_logger('traktarr') +# Notifications +notify = Notifications() + # Click @click.group(help='Add new shows & movies to Sonarr/Radarr from Trakt lists.') @@ -45,7 +49,8 @@ def shows(list_type, add_limit=0, add_delay=2.5, no_search=False, notifications= if not trakt.validate_api_key(): log.error("Aborting due to failure to validate Trakt API Key") if notifications: - callback_notify({'event': 'abort', 'type': 'shows', 'reason': 'Failure to validate Trakt API Key'}) + callback_notify({'event': 'abort', 'type': 'shows', 'list_type': list_type, + 'reason': 'Failure to validate Trakt API Key'}) return None else: log.info("Validated Trakt API Key") @@ -55,7 +60,8 @@ def shows(list_type, add_limit=0, add_delay=2.5, no_search=False, notifications= if not sonarr.validate_api_key(): log.error("Aborting due to failure to validate Sonarr URL / API Key") if notifications: - callback_notify({'event': 'abort', 'type': 'shows', 'reason': 'Failure to validate Sonarr URL / API Key'}) + callback_notify({'event': 'abort', 'type': 'shows', 'list_type': list_type, + 'reason': 'Failure to validate Sonarr URL / API Key'}) return None else: log.info("Validated Sonarr URL & API Key") @@ -65,7 +71,7 @@ def shows(list_type, add_limit=0, add_delay=2.5, no_search=False, notifications= if not profile_id or not profile_id > 0: log.error("Aborting due to failure to retrieve Profile ID for: %s", cfg.sonarr.profile) if notifications: - callback_notify({'event': 'abort', 'type': 'shows', + callback_notify({'event': 'abort', 'type': 'shows', 'list_type': list_type, 'reason': 'Failure to retrieve Sonarr Profile ID of %s' % cfg.sonarr.profile}) return None else: @@ -76,7 +82,8 @@ def shows(list_type, add_limit=0, add_delay=2.5, no_search=False, notifications= if profile_tags is None: log.error("Aborting due to failure to retrieve Tag ID's") if notifications: - callback_notify({'event': 'abort', 'type': 'shows', 'reason': "Failure to retrieve Sonarr Tag ID's"}) + callback_notify({'event': 'abort', 'type': 'shows', 'list_type': list_type, + 'reason': "Failure to retrieve Sonarr Tag ID's"}) return None else: log.info("Retrieved %d Tag ID's", len(profile_tags)) @@ -86,7 +93,8 @@ def shows(list_type, add_limit=0, add_delay=2.5, no_search=False, notifications= if not sonarr_series_list: log.error("Aborting due to failure to retrieve Sonarr shows list") if notifications: - callback_notify({'event': 'abort', 'type': 'shows', 'reason': 'Failure to retrieve Sonarr shows list'}) + callback_notify({'event': 'abort', 'type': 'shows', 'list_type': list_type, + 'reason': 'Failure to retrieve Sonarr shows list'}) return None else: log.info("Retrieved Sonarr shows list, shows found: %d", len(sonarr_series_list)) @@ -102,13 +110,15 @@ def shows(list_type, add_limit=0, add_delay=2.5, no_search=False, notifications= else: log.error("Aborting due to unknown Trakt list type") if notifications: - callback_notify({'event': 'abort', 'type': 'shows', 'reason': 'Failure to determine Trakt list type'}) + 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) if notifications: callback_notify( - {'event': 'abort', 'type': 'shows', 'reason': 'Failure to retrieve Trakt %s shows list' % list_type}) + {'event': 'abort', 'type': 'shows', 'list_type': list_type, + 'reason': 'Failure to retrieve Trakt %s shows list' % list_type}) return None else: log.info("Retrieved Trakt %s shows list, shows found: %d", list_type, len(trakt_series_list)) @@ -118,7 +128,7 @@ def shows(list_type, add_limit=0, add_delay=2.5, no_search=False, notifications= if processed_series_list is None: log.error("Aborting due to failure to remove existing Sonarr shows from retrieved Trakt shows list") if notifications: - callback_notify({'event': 'abort', 'type': 'shows', + callback_notify({'event': 'abort', 'type': 'shows', 'list_type': list_type, 'reason': 'Failure to remove existing Sonarr shows from retrieved Trakt %s shows list' % list_type }) return None @@ -149,7 +159,7 @@ def shows(list_type, add_limit=0, add_delay=2.5, no_search=False, notifications= not no_search): log.info("ADDED %s (%d) with tags: %s", series['show']['title'], series['show']['year'], use_tags) if notifications: - callback_notify({'event': 'add_show', 'show': series['show']}) + callback_notify({'event': 'add_show', 'list_type': list_type, 'show': series['show']}) added_shows += 1 else: log.error("FAILED adding %s (%d) with tags: %s", series['show']['title'], series['show']['year'], @@ -188,7 +198,8 @@ def movies(list_type, add_limit=0, add_delay=2.5, no_search=False, notifications if not trakt.validate_api_key(): log.error("Aborting due to failure to validate Trakt API Key") if notifications: - callback_notify({'event': 'abort', 'type': 'movies', 'reason': 'Failure to validate Trakt API Key'}) + callback_notify({'event': 'abort', 'type': 'movies', 'list_type': list_type, + 'reason': 'Failure to validate Trakt API Key'}) return None else: log.info("Validated Trakt API Key") @@ -199,7 +210,8 @@ def movies(list_type, add_limit=0, add_delay=2.5, no_search=False, notifications log.error("Aborting due to failure to validate Radarr URL / API Key") if notifications: callback_notify( - {'event': 'abort', 'type': 'movies', 'reason': 'Failure to validate Radarr URL / API Key'}) + {'event': 'abort', 'type': 'movies', 'list_type': list_type, + 'reason': 'Failure to validate Radarr URL / API Key'}) return None else: log.info("Validated Radarr URL & API Key") @@ -209,7 +221,7 @@ def movies(list_type, add_limit=0, add_delay=2.5, no_search=False, notifications if not profile_id or not profile_id > 0: log.error("Aborting due to failure to retrieve Profile ID for: %s", cfg.radarr.profile) if notifications: - callback_notify({'event': 'abort', 'type': 'movies', + callback_notify({'event': 'abort', 'type': 'movies', 'list_type': list_type, 'reason': 'Failure to retrieve Radarr Profile ID of %s' % cfg.radarr.profile}) return None else: @@ -220,7 +232,8 @@ def movies(list_type, add_limit=0, add_delay=2.5, no_search=False, notifications if not radarr_movie_list: log.error("Aborting due to failure to retrieve Radarr movies list") if notifications: - callback_notify({'event': 'abort', 'type': 'movies', 'reason': 'Failure to retrieve Radarr movies list'}) + callback_notify({'event': 'abort', 'type': 'movies', 'list_type': list_type, + 'reason': 'Failure to retrieve Radarr movies list'}) return None else: log.info("Retrieved Radarr movies list, movies found: %d", len(radarr_movie_list)) @@ -238,13 +251,15 @@ def movies(list_type, add_limit=0, add_delay=2.5, no_search=False, notifications else: log.error("Aborting due to unknown Trakt list type") if notifications: - callback_notify({'event': 'abort', 'type': 'movies', 'reason': 'Failure to determine Trakt list type'}) + 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) if notifications: callback_notify( - {'event': 'abort', 'type': 'movies', 'reason': 'Failure to retrieve Trakt %s movies list' % list_type}) + {'event': 'abort', 'type': 'movies', 'list_type': list_type, + 'reason': 'Failure to retrieve Trakt %s movies list' % list_type}) return None else: log.info("Retrieved Trakt %s movies list, movies found: %d", list_type, len(trakt_movies_list)) @@ -254,7 +269,7 @@ def movies(list_type, add_limit=0, add_delay=2.5, no_search=False, notifications if processed_movies_list is None: log.error("Aborting due to failure to remove existing Radarr movies from retrieved Trakt movies list") if notifications: - callback_notify({'event': 'abort', 'type': 'movies', + callback_notify({'event': 'abort', 'type': 'movies', 'list_type': list_type, 'reason': 'Failure to remove existing Radarr movies from retrieved ' 'Trakt %s movies list' % list_type}) return None @@ -279,7 +294,7 @@ def movies(list_type, add_limit=0, add_delay=2.5, no_search=False, notifications movie['movie']['ids']['slug'], profile_id, cfg.radarr.root_folder, not no_search): log.info("ADDED %s (%d)", movie['movie']['title'], movie['movie']['year']) if notifications: - callback_notify({'event': 'add_movie', 'movie': movie['movie']}) + callback_notify({'event': 'add_movie', 'list_type': list_type, 'movie': movie['movie']}) added_movies += 1 else: log.error("FAILED adding %s (%d)", movie['movie']['title'], movie['movie']['year']) @@ -307,13 +322,17 @@ def callback_notify(data): # handle event if data['event'] == 'add_movie': - log.info("Added movie: %s (%d)", data['movie']['title'], data['movie']['year']) + if cfg.notifications.verbose: + notify.send( + message="Added %s movie: %s (%d)" % (data['list_type'], data['movie']['title'], data['movie']['year'])) return elif data['event'] == 'add_show': - log.info("Added show: %s (%d)", data['show']['title'], data['show']['year']) + if cfg.notifications.verbose: + notify.send( + message="Added %s show: %s (%d)" % (data['list_type'], data['show']['title'], data['show']['year'])) return elif data['event'] == 'abort': - log.error("Error while adding %s due to: %s", data['type'], data['reason']) + notify.send(message="Aborted adding Trakt %s %s due to: %s" % (data['list_type'], data['type'], data['reason'])) return else: log.error("Unexpected callback: %s", data) @@ -345,12 +364,16 @@ def automatic_shows(add_delay=2.5, no_search=False, notifications=False): total_shows_added += added_shows # send notification - log.info("Added %d shows from Trakt's %s list", added_shows, list_type) + if notifications and not cfg.notifications.verbose: + notify.send(message="Added %d shows from Trakt's %s list" % (added_shows, list_type)) # sleep time.sleep(10) - log.info("Finished, added %d shows in total to Sonarr", total_shows_added) + log.info("Added %d shows total to Sonarr", total_shows_added) + # send notification + if notifications: + notify.send(message="Added %d shows total to Sonarr" % total_shows_added) except Exception: log.exception("Exception while automatically adding shows: ") @@ -382,12 +405,16 @@ def automatic_movies(add_delay=2.5, no_search=False, notifications=False): total_movies_added += added_movies # send notification - log.info("Added %d movies from Trakt's %s list", added_movies, list_type) + if notifications and not cfg.notifications.verbose: + notify.send(message="Added %d movies from Trakt's %s list" % (added_movies, list_type)) # sleep time.sleep(10) - log.info("Finished, added %d movies in total to Radarr", total_movies_added) + log.info("Added %d movies total to Radarr", total_movies_added) + # send notification + if notifications: + notify.send(message="Added %d movies total to Radarr" % total_movies_added) except Exception: log.exception("Exception while automatically adding movies: ") @@ -416,9 +443,26 @@ def run(add_delay=2.5, no_search=False, no_notifications=False): time.sleep(1) +############################################################ +# MISC +############################################################ + +def init_notifications(): + try: + for notification_name, notification_config in cfg.notifications.items(): + if notification_name.lower() == 'verbose': + continue + + notify.load(**notification_config) + except Exception: + log.exception("Exception initializing notification agents: ") + return + + ############################################################ # MAIN ############################################################ if __name__ == "__main__": + init_notifications() app() From 4b94dd5cbfd7ce8c958e712eb8014d8b3e63aea6 Mon Sep 17 00:00:00 2001 From: l3uddz Date: Fri, 9 Mar 2018 18:28:01 +0000 Subject: [PATCH 12/20] changed schedule from minutes to hours for specified automatical interval. --- traktarr.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/traktarr.py b/traktarr.py index 52d097f..6363ffd 100644 --- a/traktarr.py +++ b/traktarr.py @@ -370,7 +370,7 @@ def automatic_shows(add_delay=2.5, no_search=False, notifications=False): # sleep time.sleep(10) - log.info("Added %d shows total to Sonarr", total_shows_added) + log.info("Finished, added %d shows total to Sonarr", total_shows_added) # send notification if notifications: notify.send(message="Added %d shows total to Sonarr" % total_shows_added) @@ -411,7 +411,7 @@ def automatic_movies(add_delay=2.5, no_search=False, notifications=False): # sleep time.sleep(10) - log.info("Added %d movies total to Radarr", total_movies_added) + log.info("Finished, added %d movies total to Radarr", total_movies_added) # send notification if notifications: notify.send(message="Added %d movies total to Radarr" % total_movies_added) @@ -428,9 +428,9 @@ def automatic_movies(add_delay=2.5, no_search=False, notifications=False): @click.option('--no-notifications', is_flag=True, help="Disable notifications.") def run(add_delay=2.5, no_search=False, no_notifications=False): # add tasks to repeat - schedule.every(cfg.automatic.movies.interval).minutes.do(automatic_movies, add_delay, no_search, - not no_notifications) - schedule.every(cfg.automatic.shows.interval).minutes.do(automatic_shows, add_delay, no_search, not no_notifications) + schedule.every(cfg.automatic.movies.interval).hours.do(automatic_movies, add_delay, no_search, + not no_notifications) + schedule.every(cfg.automatic.shows.interval).hours.do(automatic_shows, add_delay, no_search, not no_notifications) # run schedule log.info("Automatic mode is now running...") From f47831f22d3698f85a884941cb13edf2b0549949 Mon Sep 17 00:00:00 2001 From: l3uddz Date: Fri, 9 Mar 2018 18:29:36 +0000 Subject: [PATCH 13/20] added showtime to default config for amzn tag. --- misc/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misc/config.py b/misc/config.py index 7f60f5c..f0c0a12 100644 --- a/misc/config.py +++ b/misc/config.py @@ -19,7 +19,7 @@ base_config = { 'root_folder': '/tv/', 'tags': { 'amzn': ['hbo', 'amc', 'usa network', 'tnt', 'starz', 'the cw', 'fx', 'fox', 'abc', 'nbc', 'cbs', 'tbs', - 'amazon', 'syfy', 'cinemax', 'bravo'] + 'amazon', 'syfy', 'cinemax', 'bravo', 'showtime'] } }, 'radarr': { From 72fa87b2dde0208bb31da1f1e519861b54937f83 Mon Sep 17 00:00:00 2001 From: l3uddz Date: Fri, 9 Mar 2018 18:33:43 +0000 Subject: [PATCH 14/20] slight log/notification message change. --- traktarr.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/traktarr.py b/traktarr.py index 6363ffd..9418b16 100644 --- a/traktarr.py +++ b/traktarr.py @@ -370,10 +370,10 @@ def automatic_shows(add_delay=2.5, no_search=False, notifications=False): # sleep time.sleep(10) - log.info("Finished, added %d shows total to Sonarr", total_shows_added) + log.info("Finished, added %d shows total to Sonarr!", total_shows_added) # send notification if notifications: - notify.send(message="Added %d shows total to Sonarr" % total_shows_added) + notify.send(message="Added %d shows total to Sonarr!" % total_shows_added) except Exception: log.exception("Exception while automatically adding shows: ") @@ -411,10 +411,10 @@ def automatic_movies(add_delay=2.5, no_search=False, notifications=False): # sleep time.sleep(10) - log.info("Finished, added %d movies total to Radarr", total_movies_added) + log.info("Finished, added %d movies total to Radarr!", total_movies_added) # send notification if notifications: - notify.send(message="Added %d movies total to Radarr" % total_movies_added) + notify.send(message="Added %d movies total to Radarr!" % total_movies_added) except Exception: log.exception("Exception while automatically adding movies: ") From 318ab314dd42755517d88f10e6d0a779f6fafd4a Mon Sep 17 00:00:00 2001 From: l3uddz Date: Sat, 10 Mar 2018 08:42:24 +0000 Subject: [PATCH 15/20] 5 sec sleep between page requests --- media/trakt.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/media/trakt.py b/media/trakt.py index ef7e856..539aa95 100644 --- a/media/trakt.py +++ b/media/trakt.py @@ -1,3 +1,5 @@ +import time + import backoff import requests @@ -84,6 +86,7 @@ class Trakt: 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) else: log.error("Failed to retrieve anticipated shows, request response: %d", req.status_code) @@ -139,6 +142,7 @@ class Trakt: 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) else: log.error("Failed to retrieve trending shows, request response: %d", req.status_code) @@ -195,6 +199,7 @@ class Trakt: 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) else: log.error("Failed to retrieve popular shows, request response: %d", req.status_code) @@ -254,6 +259,7 @@ class Trakt: 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) else: log.error("Failed to retrieve anticipated movies, request response: %d", req.status_code) @@ -309,6 +315,7 @@ class Trakt: 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) else: log.error("Failed to retrieve trending movies, request response: %d", req.status_code) @@ -365,6 +372,7 @@ class Trakt: 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) else: log.error("Failed to retrieve popular movies, request response: %d", req.status_code) @@ -420,6 +428,7 @@ class Trakt: 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) else: log.error("Failed to retrieve boxoffice movies, request response: %d", req.status_code) From 2385b0a79ebeaa7316647e20a2e966388d586276 Mon Sep 17 00:00:00 2001 From: l3uddz Date: Sat, 10 Mar 2018 08:44:48 +0000 Subject: [PATCH 16/20] use debug log level for sent notification messages --- notifications/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/notifications/__init__.py b/notifications/__init__.py index c1acab2..903ca31 100644 --- a/notifications/__init__.py +++ b/notifications/__init__.py @@ -49,6 +49,6 @@ class Notifications: if chosen_service and service.NAME.lower() != chosen_service: continue elif service.send(**kwargs): - log.info("Sent notification with %s", service.NAME) + log.debug("Sent notification with %s", service.NAME) except Exception: log.exception("Exception sending notification, kwargs=%r: ", kwargs) From b2ae8f431592438a5e3b7096cf091ea8ba6d2857 Mon Sep 17 00:00:00 2001 From: l3uddz Date: Sat, 10 Mar 2018 08:52:29 +0000 Subject: [PATCH 17/20] added blacklisted_max_year to exclude movies/shows beyond this year --- misc/config.py | 4 +++- misc/helpers.py | 18 ++++++++++-------- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/misc/config.py b/misc/config.py index f0c0a12..785e701 100644 --- a/misc/config.py +++ b/misc/config.py @@ -38,12 +38,14 @@ base_config = { 'fox sports'], 'allowed_countries': ['us', 'gb', 'ca'], 'blacklisted_min_runtime': 15, - 'blacklisted_min_year': 2000 + 'blacklisted_min_year': 2000, + 'blacklisted_max_year': 2019 }, 'movies': { 'blacklisted_genres': ['documentary', 'music'], 'blacklisted_min_runtime': 60, 'blacklisted_min_year': 2000, + 'blacklisted_max_year': 2019, 'blacklist_title_keywords': ['untitled', 'barbie'], 'allowed_countries': ['us', 'gb', 'ca'] } diff --git a/misc/helpers.py b/misc/helpers.py index c02bf97..d235297 100644 --- a/misc/helpers.py +++ b/misc/helpers.py @@ -85,7 +85,7 @@ def trakt_blacklisted_show_genre(show, genres): return blacklisted -def trakt_blacklisted_show_year(show, earliest_year): +def trakt_blacklisted_show_year(show, earliest_year, latest_year): blacklisted = False try: year = misc_str.get_year_from_timestamp(show['show']['first_aired']) @@ -93,11 +93,11 @@ def trakt_blacklisted_show_year(show, earliest_year): log.debug("%s was blacklisted due to having an unknown first_aired date", show['show']['title']) blacklisted = True else: - if year < earliest_year: + if year < earliest_year or year > latest_year: log.debug("%s was blacklisted because it first aired in: %d", show['show']['title'], year) blacklisted = True except Exception: - log.exception("Exception determining if show is before earliest_year %s:", show) + log.exception("Exception determining if show is within min_year and max_year range %s:", show) return blacklisted @@ -156,7 +156,8 @@ def trakt_blacklisted_show_runtime(show, lowest_runtime): def trakt_is_show_blacklisted(show, blacklist_settings): blacklisted = False try: - if trakt_blacklisted_show_year(show, blacklist_settings.blacklisted_min_year): + if trakt_blacklisted_show_year(show, blacklist_settings.blacklisted_min_year, + blacklist_settings.blacklisted_max_year): blacklisted = True if trakt_blacklisted_show_country(show, blacklist_settings.allowed_countries): blacklisted = True @@ -240,7 +241,7 @@ def trakt_blacklisted_movie_genre(movie, genres): return blacklisted -def trakt_blacklisted_movie_year(movie, earliest_year): +def trakt_blacklisted_movie_year(movie, earliest_year, latest_year): blacklisted = False try: year = movie['movie']['year'] @@ -248,11 +249,11 @@ def trakt_blacklisted_movie_year(movie, earliest_year): log.debug("%s was blacklisted due to having an unknown year", movie['movie']['title']) blacklisted = True else: - if int(year) < earliest_year: + if int(year) < earliest_year or int(year) > latest_year: log.debug("%s was blacklisted because it's year is: %d", movie['movie']['title'], int(year)) blacklisted = True except Exception: - log.exception("Exception determining if movie is before earliest_year %s:", movie) + log.exception("Exception determining if movie is within min_year and max_year ranger %s:", movie) return blacklisted @@ -312,7 +313,8 @@ def trakt_is_movie_blacklisted(movie, blacklist_settings): try: if trakt_blacklisted_movie_title(movie, blacklist_settings.blacklist_title_keywords): blacklisted = True - if trakt_blacklisted_movie_year(movie, blacklist_settings.blacklisted_min_year): + if trakt_blacklisted_movie_year(movie, blacklist_settings.blacklisted_min_year, + blacklist_settings.blacklisted_max_year): blacklisted = True if trakt_blacklisted_movie_country(movie, blacklist_settings.allowed_countries): blacklisted = True From 5dcfffb6569499075a1f0a038f964c657e6e31eb Mon Sep 17 00:00:00 2001 From: l3uddz Date: Sat, 10 Mar 2018 09:18:57 +0000 Subject: [PATCH 18/20] make notifiations verbose by default. added systemd service file for automatic mode. --- misc/config.py | 2 +- systemd/traktarr.service | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 systemd/traktarr.service diff --git a/misc/config.py b/misc/config.py index 785e701..0525488 100644 --- a/misc/config.py +++ b/misc/config.py @@ -66,7 +66,7 @@ base_config = { } }, 'notifications': { - 'verbose': False, + 'verbose': True, 'my slack': { 'service': 'slack', 'webhook_url': '' diff --git a/systemd/traktarr.service b/systemd/traktarr.service new file mode 100644 index 0000000..43fb188 --- /dev/null +++ b/systemd/traktarr.service @@ -0,0 +1,17 @@ +# /etc/systemd/system/traktarr.service + +[Unit] +Description=traktarr +After=network-online.target unionfs.service + +[Service] +User=seed +Group=seed +Type=simple +WorkingDirectory=/opt/traktarr/ +ExecStart=/usr/bin/python3 /opt/traktarr/traktarr.py run +Restart=always +RestartSec=10 + +[Install] +WantedBy=default.target From 4a461530ad55793ff4664c8eb746fd447401f74a Mon Sep 17 00:00:00 2001 From: l3uddz Date: Sun, 11 Mar 2018 10:19:07 +0000 Subject: [PATCH 19/20] notification changes: verbose flag now only affects the sending of shows/movies added, others are not considered verbose. --- traktarr.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/traktarr.py b/traktarr.py index 9418b16..5cc9063 100644 --- a/traktarr.py +++ b/traktarr.py @@ -176,6 +176,11 @@ def shows(list_type, add_limit=0, add_delay=2.5, no_search=False, notifications= log.exception("Exception while processing show %s: ", series['show']['title']) log.info("Added %d new show(s) to Sonarr", added_shows) + + # send notification + if notifications: + notify.send(message="Added %d shows from Trakt's %s list" % (added_shows, list_type)) + return added_shows @@ -310,6 +315,11 @@ def movies(list_type, add_limit=0, add_delay=2.5, no_search=False, notifications log.exception("Exception while processing movie %s: ", movie['movie']['title']) log.info("Added %d new movie(s) to Radarr", added_movies) + + # send notification + if notifications: + notify.send(message="Added %d movies from Trakt's %s list" % (added_movies, list_type)) + return added_movies @@ -363,10 +373,6 @@ def automatic_shows(add_delay=2.5, no_search=False, notifications=False): continue total_shows_added += added_shows - # send notification - if notifications and not cfg.notifications.verbose: - notify.send(message="Added %d shows from Trakt's %s list" % (added_shows, list_type)) - # sleep time.sleep(10) @@ -404,10 +410,6 @@ def automatic_movies(add_delay=2.5, no_search=False, notifications=False): continue total_movies_added += added_movies - # send notification - if notifications and not cfg.notifications.verbose: - notify.send(message="Added %d movies from Trakt's %s list" % (added_movies, list_type)) - # sleep time.sleep(10) From cfd051c1846077b11b970da3f377fc62a69a9f32 Mon Sep 17 00:00:00 2001 From: l3uddz Date: Sun, 11 Mar 2018 10:22:04 +0000 Subject: [PATCH 20/20] version change --- traktarr.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/traktarr.py b/traktarr.py index 5cc9063..35cf2ef 100644 --- a/traktarr.py +++ b/traktarr.py @@ -25,7 +25,7 @@ notify = Notifications() # Click @click.group(help='Add new shows & movies to Sonarr/Radarr from Trakt lists.') -@click.version_option('1.0.0', prog_name='traktarr') +@click.version_option('1.1.0', prog_name='traktarr') def app(): pass