From c82416bfa3b6bcc61561f2ad35afab1e3073945d Mon Sep 17 00:00:00 2001 From: l3uddz Date: Wed, 18 Apr 2018 13:14:03 +0100 Subject: [PATCH 01/11] check for json in response content-type and log a debug message for response text. --- media/radarr.py | 6 ++++-- media/sonarr.py | 8 +++++--- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/media/radarr.py b/media/radarr.py index 57e761c..3f517cc 100644 --- a/media/radarr.py +++ b/media/radarr.py @@ -93,9 +93,11 @@ class Radarr: req = requests.post(urljoin(self.server_url, 'api/movie'), json=payload, headers=self.headers, timeout=30) log.debug("Request URL: %s", req.url) log.debug("Request Payload: %s", payload) - log.debug("Request Response: %d", req.status_code) + log.debug("Request Response Code: %d", req.status_code) + log.debug("Request Response Text:\n%s", req.text) - if (req.status_code == 201 or req.status_code == 200) and req.json()['tmdbId'] == movie_tmdbid: + if (req.status_code == 201 or req.status_code == 200) and 'json' in req.headers['Content-Type'].lower() \ + and req.json()['tmdbId'] == movie_tmdbid: log.debug("Successfully added %s (%d)", movie_title, movie_tmdbid) return True elif 'json' in req.headers['Content-Type'].lower() and 'message' in req.text: diff --git a/media/sonarr.py b/media/sonarr.py index 11e86ad..1d10f16 100644 --- a/media/sonarr.py +++ b/media/sonarr.py @@ -139,9 +139,11 @@ class Sonarr: req = requests.post(urljoin(self.server_url, 'api/series'), json=payload, headers=self.headers, timeout=30) log.debug("Request URL: %s", req.url) log.debug("Request Payload: %s", payload) - log.debug("Request Response: %d", req.status_code) - - if (req.status_code == 201 or req.status_code == 200) and req.json()['tvdbId'] == series_tvdbid: + log.debug("Request Response Code: %d", req.status_code) + log.debug("Request Response Text:\n%s", req.text) + + if (req.status_code == 201 or req.status_code == 200) and 'json' in req.headers['Content-Type'].lower() \ + and req.json()['tvdbId'] == series_tvdbid: log.debug("Successfully added %s (%d)", series_title, series_tvdbid) return True elif 'json' in req.headers['Content-Type'].lower() and 'errorMessage' in req.text: From c0c3c9dfafd55a9167b4391cfc2b8820f24d4b26 Mon Sep 17 00:00:00 2001 From: l3uddz Date: Wed, 18 Apr 2018 14:29:14 +0100 Subject: [PATCH 02/11] should solve issue where a list of dicts is returned by sonarr/radarr instead of a singular dict --- media/radarr.py | 13 +++++++++---- media/sonarr.py | 15 ++++++++++----- misc/helpers.py | 17 ++++++++++++++++- 3 files changed, 35 insertions(+), 10 deletions(-) diff --git a/media/radarr.py b/media/radarr.py index 3f517cc..298b758 100644 --- a/media/radarr.py +++ b/media/radarr.py @@ -4,6 +4,7 @@ import backoff import requests from misc.log import logger +from misc import helpers log = logger.get_logger(__name__) @@ -96,13 +97,17 @@ class Radarr: log.debug("Request Response Code: %d", req.status_code) log.debug("Request Response Text:\n%s", req.text) - if (req.status_code == 201 or req.status_code == 200) and 'json' in req.headers['Content-Type'].lower() \ - and req.json()['tmdbId'] == movie_tmdbid: + response_json = None + if 'json' in req.headers['Content-Type'].lower(): + response_json = helpers.get_response_dict(req.json()) + + if (req.status_code == 201 or req.status_code == 200) and (response_json and 'tmdbId' in response_json) \ + and response_json['tmdbId'] == movie_tmdbid: log.debug("Successfully added %s (%d)", movie_title, movie_tmdbid) return True - elif 'json' in req.headers['Content-Type'].lower() and 'message' in req.text: + elif response_json and 'message' in response_json: log.error("Failed to add %s (%d) - status_code: %d, reason: %s", movie_title, movie_tmdbid, - req.status_code, req.json()['message']) + req.status_code, response_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 1d10f16..bd5ce7f 100644 --- a/media/sonarr.py +++ b/media/sonarr.py @@ -3,6 +3,7 @@ from urllib.parse import urljoin import backoff import requests +from misc import helpers from misc.log import logger log = logger.get_logger(__name__) @@ -141,14 +142,18 @@ class Sonarr: log.debug("Request Payload: %s", payload) log.debug("Request Response Code: %d", req.status_code) log.debug("Request Response Text:\n%s", req.text) - - if (req.status_code == 201 or req.status_code == 200) and 'json' in req.headers['Content-Type'].lower() \ - and req.json()['tvdbId'] == series_tvdbid: + + response_json = None + if 'json' in req.headers['Content-Type'].lower(): + response_json = helpers.get_response_dict(req.json()) + + if (req.status_code == 201 or req.status_code == 200) and (response_json and 'tvdbId' in response_json) \ + and response_json['tvdbId'] == series_tvdbid: log.debug("Successfully added %s (%d)", series_title, series_tvdbid) return True - elif 'json' in req.headers['Content-Type'].lower() and 'errorMessage' in req.text: + elif response_json and 'errorMessage' in response_json: log.error("Failed to add %s (%d) - status_code: %d, reason: %s", series_title, series_tvdbid, - req.status_code, req.json()['errorMessage']) + req.status_code, response_json['errorMessage']) return False else: log.error("Failed to add %s (%d), unexpected response:\n%s", series_title, series_tvdbid, req.text) diff --git a/misc/helpers.py b/misc/helpers.py index c5c9f6f..5cca113 100644 --- a/misc/helpers.py +++ b/misc/helpers.py @@ -27,7 +27,7 @@ def sonarr_readable_tag_from_ids(profile_tag_ids, chosen_tag_ids): try: if not chosen_tag_ids: return None - + tags = [] for tag_name, tag_id in profile_tag_ids.items(): if tag_id in chosen_tag_ids: @@ -380,3 +380,18 @@ def trakt_is_movie_blacklisted(movie, blacklist_settings): except Exception: log.exception("Exception determining if movie was blacklisted %s: ", movie) return blacklisted + + +def get_response_dict(response): + found_response = None + try: + if isinstance(response, list): + found_response = response[0] + elif isinstance(response, dict): + found_response = response + else: + log.error("Unexpected response instance type of %s for %s", type(response).__name__, response) + + except Exception: + log.exception("Exception determining response for %s: ", response) + return found_response From 1845ceaa102f38185c243d6347f0476039247a5b Mon Sep 17 00:00:00 2001 From: l3uddz Date: Wed, 18 Apr 2018 14:34:51 +0100 Subject: [PATCH 03/11] add a comment seperator to helpers.py --- misc/helpers.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/misc/helpers.py b/misc/helpers.py index 5cca113..20790d6 100644 --- a/misc/helpers.py +++ b/misc/helpers.py @@ -382,6 +382,11 @@ def trakt_is_movie_blacklisted(movie, blacklist_settings): return blacklisted +############################################################ +# MISC +############################################################ + + def get_response_dict(response): found_response = None try: From 5449a550e3acadda4d95a3d7a8b7805e1ffa02e3 Mon Sep 17 00:00:00 2001 From: l3uddz Date: Wed, 18 Apr 2018 15:17:16 +0100 Subject: [PATCH 04/11] iterate response list looking for matching result dict. --- media/radarr.py | 2 +- media/sonarr.py | 2 +- misc/helpers.py | 14 ++++++++++++-- 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/media/radarr.py b/media/radarr.py index 298b758..353b35e 100644 --- a/media/radarr.py +++ b/media/radarr.py @@ -99,7 +99,7 @@ class Radarr: response_json = None if 'json' in req.headers['Content-Type'].lower(): - response_json = helpers.get_response_dict(req.json()) + response_json = helpers.get_response_dict(req.json(), 'tmdbId', movie_tmdbid) if (req.status_code == 201 or req.status_code == 200) and (response_json and 'tmdbId' in response_json) \ and response_json['tmdbId'] == movie_tmdbid: diff --git a/media/sonarr.py b/media/sonarr.py index bd5ce7f..8447efb 100644 --- a/media/sonarr.py +++ b/media/sonarr.py @@ -145,7 +145,7 @@ class Sonarr: response_json = None if 'json' in req.headers['Content-Type'].lower(): - response_json = helpers.get_response_dict(req.json()) + response_json = helpers.get_response_dict(req.json(), 'tvdbId', series_tvdbid) if (req.status_code == 201 or req.status_code == 200) and (response_json and 'tvdbId' in response_json) \ and response_json['tvdbId'] == series_tvdbid: diff --git a/misc/helpers.py b/misc/helpers.py index 20790d6..60ad23e 100644 --- a/misc/helpers.py +++ b/misc/helpers.py @@ -387,11 +387,21 @@ def trakt_is_movie_blacklisted(movie, blacklist_settings): ############################################################ -def get_response_dict(response): +def get_response_dict(response, key_field=None, key_value=None): found_response = None try: if isinstance(response, list): - found_response = response[0] + if not key_field or not key_value: + found_response = response[0] + else: + for result in response: + if isinstance(result, dict) and key_field in result and result[key_field] == key_value: + found_response = result + break + + if not found_response: + log.error("Unable to find a result with key %s where the value is %s", key_field, key_value) + elif isinstance(response, dict): found_response = response else: From 601512ba09a27bf5cbce18d393158a97953f3840 Mon Sep 17 00:00:00 2001 From: Filipe Santos Date: Sat, 21 Apr 2018 02:13:48 +1200 Subject: [PATCH 05/11] Bump requirements --- requirements.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/requirements.txt b/requirements.txt index 27f2206..b865c62 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ -backoff==1.4.3 -schedule==0.4.3 +backoff==1.5.0 +schedule==0.5.0 attrdict==2.0.0 click==6.7 -requests==2.18.4 +requests~=2.18.4 From 35cd1b836f0e4e8b07e23c4593dc0aff6a5d0ad4 Mon Sep 17 00:00:00 2001 From: Filipe Santos Date: Sat, 21 Apr 2018 02:37:18 +1200 Subject: [PATCH 06/11] Add config and file location support --- README.md | 4 +- media/__init__.py | 1 - media/radarr.py | 33 ++++-- media/sonarr.py | 39 +++--- media/trakt.py | 64 +++++++--- misc/__init__.py | 2 - misc/config.py | 296 +++++++++++++++++++++++++--------------------- misc/log.py | 5 +- traktarr.py | 63 +++++++--- 9 files changed, 307 insertions(+), 200 deletions(-) diff --git a/README.md b/README.md index 1be76c8..1d28ba4 100644 --- a/README.md +++ b/README.md @@ -138,14 +138,14 @@ To have Traktarr get Movies and Shows for you automatically, on set interval. "api_key": "", "profile": "HD-1080p", "root_folder": "/movies/", - "url": "http://localhost:7878" + "url": "http://localhost:7878/" }, "sonarr": { "api_key": "", "profile": "HD-1080p", "root_folder": "/tv/", "tags": {}, - "url": "http://localhost:8989" + "url": "http://localhost:8989/" }, "trakt": { "api_key": "" diff --git a/media/__init__.py b/media/__init__.py index cac681c..e69de29 100644 --- a/media/__init__.py +++ b/media/__init__.py @@ -1 +0,0 @@ -from media import trakt, sonarr, radarr \ No newline at end of file diff --git a/media/radarr.py b/media/radarr.py index 353b35e..d4d38d6 100644 --- a/media/radarr.py +++ b/media/radarr.py @@ -27,7 +27,7 @@ class Radarr: def validate_api_key(self): try: # request system status to validate api_key - req = requests.get(urljoin(self.server_url, 'api/system/status'), headers=self.headers, timeout=30) + req = requests.get(urljoin(self.server_url, 'api/system/status'), headers=self.headers, timeout=60) log.debug("Request Response: %d", req.status_code) if req.status_code == 200 and 'version' in req.json(): @@ -41,7 +41,7 @@ class Radarr: def get_movies(self): try: # make request - req = requests.get(urljoin(self.server_url, 'api/movie'), headers=self.headers, timeout=30) + req = requests.get(urljoin(self.server_url, 'api/movie'), headers=self.headers, timeout=60) log.debug("Request URL: %s", req.url) log.debug("Request Response: %d", req.status_code) @@ -59,7 +59,7 @@ class Radarr: def get_profile_id(self, profile_name): try: # make request - req = requests.get(urljoin(self.server_url, 'api/profile'), headers=self.headers, timeout=30) + req = requests.get(urljoin(self.server_url, 'api/profile'), headers=self.headers, timeout=60) log.debug("Request URL: %s", req.url) log.debug("Request Response: %d", req.status_code) @@ -82,16 +82,29 @@ class Radarr: try: # generate payload payload = { - 'tmdbId': movie_tmdbid, 'title': movie_title, 'year': movie_year, - 'qualityProfileId': profile_id, 'images': [], - 'monitored': True, 'rootFolderPath': root_folder, - 'minimumAvailability': 'released', 'titleSlug': movie_title_slug, - 'addOptions': {'ignoreEpisodesWithFiles': False, 'ignoreEpisodesWithoutFiles': False, - 'searchForMovie': search_missing} + 'tmdbId': movie_tmdbid, + 'title': movie_title, + 'year': movie_year, + 'qualityProfileId': profile_id, + 'images': [], + 'monitored': True, + 'rootFolderPath': root_folder, + 'minimumAvailability': 'released', + 'titleSlug': movie_title_slug, + 'addOptions': { + 'ignoreEpisodesWithFiles': False, + 'ignoreEpisodesWithoutFiles': False, + 'searchForMovie': search_missing + } } # make request - req = requests.post(urljoin(self.server_url, 'api/movie'), json=payload, headers=self.headers, timeout=30) + req = requests.post( + urljoin(self.server_url, 'api/movie'), + headers=self.headers, + json=payload, + timeout=60 + ) log.debug("Request URL: %s", req.url) log.debug("Request Payload: %s", payload) log.debug("Request Response Code: %d", req.status_code) diff --git a/media/sonarr.py b/media/sonarr.py index 8447efb..e2167af 100644 --- a/media/sonarr.py +++ b/media/sonarr.py @@ -20,14 +20,13 @@ class Sonarr: self.server_url = server_url self.api_key = api_key self.headers = { - 'Content-Type': 'application/json', 'X-Api-Key': self.api_key, } def validate_api_key(self): try: # request system status to validate api_key - req = requests.get(urljoin(self.server_url, 'api/system/status'), headers=self.headers, timeout=30) + req = requests.get(urljoin(self.server_url, 'api/system/status'), headers=self.headers, timeout=60) log.debug("Request Response: %d", req.status_code) if req.status_code == 200 and 'version' in req.json(): @@ -41,7 +40,7 @@ class Sonarr: def get_series(self): try: # make request - req = requests.get(urljoin(self.server_url, 'api/series'), headers=self.headers, timeout=30) + req = requests.get(urljoin(self.server_url, 'api/series'), headers=self.headers, timeout=60) log.debug("Request URL: %s", req.url) log.debug("Request Response: %d", req.status_code) @@ -59,7 +58,7 @@ class Sonarr: def get_profile_id(self, profile_name): try: # make request - req = requests.get(urljoin(self.server_url, 'api/profile'), headers=self.headers, timeout=30) + req = requests.get(urljoin(self.server_url, 'api/profile'), headers=self.headers, timeout=60) log.debug("Request URL: %s", req.url) log.debug("Request Response: %d", req.status_code) @@ -81,7 +80,7 @@ class Sonarr: def get_tag_id(self, tag_name): try: # make request - req = requests.get(urljoin(self.server_url, 'api/tag'), headers=self.headers, timeout=30) + req = requests.get(urljoin(self.server_url, 'api/tag'), headers=self.headers, timeout=60) log.debug("Request URL: %s", req.url) log.debug("Request Response: %d", req.status_code) @@ -104,7 +103,7 @@ class Sonarr: tags = {} try: # make request - req = requests.get(urljoin(self.server_url, 'api/tag'), headers=self.headers, timeout=30) + req = requests.get(urljoin(self.server_url, 'api/tag'), headers=self.headers, timeout=60) log.debug("Request URL: %s", req.url) log.debug("Request Response: %d", req.status_code) @@ -126,18 +125,30 @@ class Sonarr: try: # generate payload payload = { - 'tvdbId': series_tvdbid, 'title': series_title, 'titleSlug': series_title_slug, - 'qualityProfileId': profile_id, 'tags': [] if not tag_ids or not isinstance(tag_ids, list) else tag_ids, + 'tvdbId': series_tvdbid, + 'title': series_title, + 'titleSlug': series_title_slug, + 'qualityProfileId': profile_id, + 'tags': [] if not tag_ids or not isinstance(tag_ids, list) else tag_ids, 'images': [], - 'seasons': [], 'seasonFolder': True, - 'monitored': True, 'rootFolderPath': root_folder, - 'addOptions': {'ignoreEpisodesWithFiles': False, - 'ignoreEpisodesWithoutFiles': False, - 'searchForMissingEpisodes': search_missing} + 'seasons': [], + 'seasonFolder': True, + 'monitored': True, + 'rootFolderPath': root_folder, + 'addOptions': { + 'ignoreEpisodesWithFiles': False, + 'ignoreEpisodesWithoutFiles': False, + 'searchForMissingEpisodes': search_missing + } } # make request - req = requests.post(urljoin(self.server_url, 'api/series'), json=payload, headers=self.headers, timeout=30) + req = requests.post( + urljoin(self.server_url, 'api/series'), + headers=self.headers, + json=payload, + timeout=60 + ) log.debug("Request URL: %s", req.url) log.debug("Request Payload: %s", payload) log.debug("Request Response Code: %d", req.status_code) diff --git a/media/trakt.py b/media/trakt.py index 539aa95..b2cf3c4 100644 --- a/media/trakt.py +++ b/media/trakt.py @@ -29,8 +29,12 @@ class Trakt: payload = {'extended': 'full', 'limit': 1000} # make request - req = requests.get('https://api.trakt.tv/shows/anticipated', params=payload, headers=self.headers, - timeout=30) + req = requests.get( + 'https://api.trakt.tv/shows/anticipated', + headers=self.headers, + params=payload, + timeout=30 + ) log.debug("Request Response: %d", req.status_code) if req.status_code == 200: @@ -59,8 +63,12 @@ class Trakt: # make request while True: - req = requests.get('https://api.trakt.tv/shows/anticipated', params=payload, headers=self.headers, - timeout=30) + req = requests.get( + 'https://api.trakt.tv/shows/anticipated', + headers=self.headers, + params=payload, + timeout=30 + ) log.debug("Request URL: %s", req.url) log.debug("Request Payload: %s", payload) log.debug("Response Code: %d", req.status_code) @@ -115,8 +123,12 @@ class Trakt: # make request while True: - req = requests.get('https://api.trakt.tv/shows/trending', params=payload, headers=self.headers, - timeout=30) + req = requests.get( + 'https://api.trakt.tv/shows/trending', + headers=self.headers, + params=payload, + timeout=30 + ) log.debug("Request URL: %s", req.url) log.debug("Request Payload: %s", payload) log.debug("Response Code: %d", req.status_code) @@ -171,8 +183,12 @@ class Trakt: # make request while True: - req = requests.get('https://api.trakt.tv/shows/popular', params=payload, headers=self.headers, - timeout=30) + req = requests.get( + 'https://api.trakt.tv/shows/popular', + headers=self.headers, + params=payload, + timeout=30 + ) log.debug("Request URL: %s", req.url) log.debug("Request Payload: %s", payload) log.debug("Response Code: %d", req.status_code) @@ -232,8 +248,12 @@ class Trakt: # make request while True: - req = requests.get('https://api.trakt.tv/movies/anticipated', params=payload, headers=self.headers, - timeout=30) + req = requests.get( + 'https://api.trakt.tv/movies/anticipated', + headers=self.headers, + params=payload, + timeout=30 + ) log.debug("Request URL: %s", req.url) log.debug("Request Payload: %s", payload) log.debug("Response Code: %d", req.status_code) @@ -288,8 +308,12 @@ class Trakt: # make request while True: - req = requests.get('https://api.trakt.tv/movies/trending', params=payload, headers=self.headers, - timeout=30) + req = requests.get( + 'https://api.trakt.tv/movies/trending', + headers=self.headers, + params=payload, + timeout=30 + ) log.debug("Request URL: %s", req.url) log.debug("Request Payload: %s", payload) log.debug("Response Code: %d", req.status_code) @@ -344,8 +368,12 @@ class Trakt: # make request while True: - req = requests.get('https://api.trakt.tv/movies/popular', params=payload, headers=self.headers, - timeout=30) + req = requests.get( + 'https://api.trakt.tv/movies/popular', + headers=self.headers, + params=payload, + timeout=30 + ) log.debug("Request URL: %s", req.url) log.debug("Request Payload: %s", payload) log.debug("Response Code: %d", req.status_code) @@ -401,8 +429,12 @@ class Trakt: # make request while True: - req = requests.get('https://api.trakt.tv/movies/boxoffice', params=payload, headers=self.headers, - timeout=30) + req = requests.get( + 'https://api.trakt.tv/movies/boxoffice', + headers=self.headers, + params=payload, + timeout=30 + ) log.debug("Request URL: %s", req.url) log.debug("Request Payload: %s", payload) log.debug("Response Code: %d", req.status_code) diff --git a/misc/__init__.py b/misc/__init__.py index 537ee21..e69de29 100644 --- a/misc/__init__.py +++ b/misc/__init__.py @@ -1,2 +0,0 @@ -from misc import config, str, helpers -from misc.log import logger diff --git a/misc/config.py b/misc/config.py index 536a98f..37d00ca 100644 --- a/misc/config.py +++ b/misc/config.py @@ -4,68 +4,15 @@ import sys from attrdict import AttrDict -config_path = os.path.join(os.path.dirname(os.path.realpath(sys.argv[0])), 'config.json') -base_config = { - 'core': { - 'debug': False - }, - 'trakt': { - 'api_key': '' - }, - 'sonarr': { - 'url': 'http://localhost:8989', - 'api_key': '', - 'profile': 'HD-1080p', - 'root_folder': '/tv/', - 'tags': { - } - }, - 'radarr': { - 'url': 'http://localhost:7878', - 'api_key': '', - 'profile': 'HD-1080p', - 'root_folder': '/movies/' - }, - 'filters': { - 'shows': { - 'blacklisted_genres': [], - 'blacklisted_networks': [], - 'allowed_countries': [], - 'blacklisted_min_runtime': 15, - 'blacklisted_min_year': 2000, - 'blacklisted_max_year': 2019, - 'blacklisted_tvdb_ids': [], - }, - 'movies': { - 'blacklisted_genres': [], - 'blacklisted_min_runtime': 60, - 'blacklisted_min_year': 2000, - 'blacklisted_max_year': 2019, - 'blacklist_title_keywords': [], - 'blacklisted_tmdb_ids': [], - 'allowed_countries': [] - } - }, - 'automatic': { - 'movies': { - 'interval': 20, - 'anticipated': 3, - 'trending': 3, - 'popular': 3, - 'boxoffice': 10 - }, - 'shows': { - 'interval': 48, - 'anticipated': 10, - 'trending': 1, - 'popular': 1 - } - }, - 'notifications': { - 'verbose': True - } -} -cfg = None + +class Singleton(type): + _instances = {} + + def __call__(cls, *args, **kwargs): + if cls not in cls._instances: + cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs) + + return cls._instances[cls] class AttrConfig(AttrDict): @@ -85,77 +32,154 @@ class AttrConfig(AttrDict): return None -def build_config(): - if not os.path.exists(config_path): - print("Dumping default config to: %s" % config_path) - with open(config_path, 'w') as fp: - json.dump(base_config, fp, sort_keys=True, indent=2) - return True - else: - return False - - -def dump_config(): - if os.path.exists(config_path): - with open(config_path, 'w') as fp: - json.dump(cfg, fp, sort_keys=True, indent=2) - return True - else: - return False - - -def load_config(): - with open(config_path, 'r') as fp: - return AttrConfig(json.load(fp)) - - -def upgrade_settings(defaults, 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): - did_upgrade, merged[k] = 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 sub_upgraded, merged - - upgraded, upgraded_settings = inner_upgrade(defaults, currents) - return upgraded, AttrConfig(upgraded_settings) - - -############################################################ -# LOAD CFG -############################################################ - -# dump/load config -if build_config(): - print("Please edit the default configuration before running again!") - sys.exit(0) -else: - tmp = load_config() - upgraded, cfg = upgrade_settings(base_config, tmp) - if upgraded: - dump_config() - print("New config options were added, adjust and restart!") - sys.exit(0) +class Config(object, metaclass=Singleton): + + base_config = { + 'core': { + 'debug': False + }, + 'trakt': { + 'api_key': '' + }, + 'sonarr': { + 'url': 'http://localhost:8989/', + 'api_key': '', + 'profile': 'HD-1080p', + 'root_folder': '/tv/', + 'tags': { + } + }, + 'radarr': { + 'url': 'http://localhost:7878/', + 'api_key': '', + 'profile': 'HD-1080p', + 'root_folder': '/movies/' + }, + 'filters': { + 'shows': { + 'blacklisted_genres': [], + 'blacklisted_networks': [], + 'allowed_countries': [], + 'blacklisted_min_runtime': 15, + 'blacklisted_min_year': 2000, + 'blacklisted_max_year': 2019, + 'blacklisted_tvdb_ids': [], + }, + 'movies': { + 'blacklisted_genres': [], + 'blacklisted_min_runtime': 60, + 'blacklisted_min_year': 2000, + 'blacklisted_max_year': 2019, + 'blacklist_title_keywords': [], + 'blacklisted_tmdb_ids': [], + 'allowed_countries': [] + } + }, + 'automatic': { + 'movies': { + 'interval': 20, + 'anticipated': 3, + 'trending': 3, + 'popular': 3, + 'boxoffice': 10 + }, + 'shows': { + 'interval': 48, + 'anticipated': 10, + 'trending': 1, + 'popular': 1 + } + }, + 'notifications': { + 'verbose': True + } + } + + def __init__(self, config_path, logfile): + """Initializes config""" + self.conf = None + + self.config_path = config_path + self.log_path = logfile + + @property + def cfg(self): + # Return existing loaded config + if self.conf: + return self.conf + + # Built initial config if it doesn't exist + if self.build_config(): + print("Please edit the default configuration before running again!") + sys.exit(0) + # Load config, upgrade if necessary + else: + tmp = self.load_config() + self.conf, upgraded = self.upgrade_settings(tmp) + + # Save config if upgraded + if upgraded: + self.dump_config() + print("New config options were added, adjust and restart!") + sys.exit(0) + + return self.conf + + @property + def logfile(self): + return self.log_path + + def build_config(self): + if not os.path.exists(self.config_path): + print("Dumping default config to: %s" % self.config_path) + with open(self.config_path, 'w') as fp: + json.dump(self.base_config, fp, sort_keys=True, indent=2) + return True + else: + return False + + def dump_config(self): + if os.path.exists(self.config_path): + with open(self.config_path, 'w') as fp: + json.dump(self.conf, fp, sort_keys=True, indent=2) + return True + else: + return False + + def load_config(self): + with open(self.config_path, 'r') as fp: + return AttrConfig(json.load(fp)) + + 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) + return AttrConfig(upgraded_settings), upgraded diff --git a/misc/log.py b/misc/log.py index 204c448..befde96 100644 --- a/misc/log.py +++ b/misc/log.py @@ -3,7 +3,7 @@ import os import sys from logging.handlers import RotatingFileHandler -from misc.config import cfg +from misc.config import Config class Logger: @@ -49,4 +49,5 @@ class Logger: return self.root_logger.getChild(name) -logger = Logger('activity.log', logging.DEBUG if cfg.core.debug else logging.INFO) +# Default logger +logger = Logger(Config().logfile, logging.DEBUG if Config().cfg.core.debug else logging.INFO) diff --git a/traktarr.py b/traktarr.py index 0da7ab1..9641e89 100755 --- a/traktarr.py +++ b/traktarr.py @@ -1,33 +1,55 @@ #!/usr/bin/env python3 +import os.path import time import click import schedule -from media.radarr import Radarr -from media.sonarr import Sonarr -from media.trakt import Trakt -from misc import helpers -from misc.config import cfg -from misc.log import logger -from notifications import Notifications - ############################################################ # INIT ############################################################ - -# Logging -log = logger.get_logger('traktarr') - -# Notifications -notify = Notifications() +cfg = None +log = None +notify = None # Click @click.group(help='Add new shows & movies to Sonarr/Radarr from Trakt lists.') @click.version_option('1.1.2', prog_name='traktarr') -def app(): - pass +@click.option( + '--config', + envvar='TRAKTARR_CONFIG', + type=click.Path(file_okay=True, dir_okay=False), + help='Configuration file', + show_default=True, + default=os.path.join(os.path.dirname(os.path.abspath(__file__)), "config.json") +) +@click.option( + '--logfile', + envvar='TRAKTARR_LOGFILE', + type=click.Path(file_okay=True, dir_okay=False), + help='Log file', + show_default=True, + default=os.path.join(os.path.dirname(os.path.abspath(__file__)), "activity.log") +) +def app(config, logfile): + # Setup global variables + global cfg, log, notify + + # Load config + from misc.config import Config + cfg = Config(config_path=config, logfile=logfile).cfg + + # Load logger + from misc.log import logger + log = logger.get_logger('traktarr') + + # Load notifications + from notifications import Notifications + notify = Notifications() + + # Notifications + init_notifications() ############################################################ @@ -44,6 +66,10 @@ def app(): @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): + from media.sonarr import Sonarr + from media.trakt import Trakt + from misc import helpers + added_shows = 0 # remove genre from shows blacklisted_genres if supplied @@ -214,6 +240,10 @@ def shows(list_type, add_limit=0, add_delay=2.5, genre=None, folder=None, no_sea @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): + from media.radarr import Radarr + from media.trakt import Trakt + from misc import helpers + added_movies = 0 # remove genre from movies blacklisted_genres if supplied @@ -500,5 +530,4 @@ def init_notifications(): ############################################################ if __name__ == "__main__": - init_notifications() app() From 7912e1bd0b48dfdb70d2d2ef50f73b7175a6e65c Mon Sep 17 00:00:00 2001 From: Filipe Santos Date: Sat, 21 Apr 2018 04:00:03 +1200 Subject: [PATCH 07/11] Dockerfile --- .dockerignore | 23 +++++++++++++++++++++++ Dockerfile | 31 +++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+) create mode 100644 .dockerignore create mode 100644 Dockerfile diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..e2c9e06 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,23 @@ +# Git +.git + +# Systemd files +systemd + +# Logs +*.log* + +# Configs +*.json + +# Byte-compiled / optimized / DLL files +**/__pycache__ +*.py[cod] +*$py.class +*.pyc + +# Pyenv +**/.python-version + +# User-specific stuff: +.idea diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..3034b41 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,31 @@ +FROM python:3.6-alpine3.7 + +ENV \ + # App directory + APP_DIR=traktarr \ + # Branch to clone + BRANCH=master \ + # Config file + TRAKTARR_CONFIG=/config/config.json \ + # Log file + TRAKTARR_LOGFILE=/config/traktarr.log + +RUN \ + echo "** Upgrade all packages **" && \ + apk --no-cache -U upgrade && \ + echo "** Install OS dependencies **" && \ + apk --no-cache -U add git && \ + echo "** Get Traktarr **" && \ + git clone --depth 1 --branch ${BRANCH} https://github.com/l3uddz/traktarr.git /${APP_DIR} && \ + echo "** Install PIP dependencies **" && \ + pip install --no-cache-dir --upgrade pip setuptools && \ + pip install --no-cache-dir --upgrade -r /${APP_DIR}/requirements.txt + +# Change directory +WORKDIR /${APP_DIR} + +# Config volume +VOLUME /config + +# Entrypoint +ENTRYPOINT ["python", "traktarr.py"] From e5d473600e58293fe9bd05281d731360e2ad61b3 Mon Sep 17 00:00:00 2001 From: Filipe Santos Date: Sat, 21 Apr 2018 04:00:23 +1200 Subject: [PATCH 08/11] Fix partial url paths join --- media/radarr.py | 23 +++++++++++++++++------ media/sonarr.py | 31 +++++++++++++++++++++++-------- 2 files changed, 40 insertions(+), 14 deletions(-) diff --git a/media/radarr.py b/media/radarr.py index d4d38d6..bc0c3af 100644 --- a/media/radarr.py +++ b/media/radarr.py @@ -1,7 +1,6 @@ -from urllib.parse import urljoin - import backoff import requests +import os.path from misc.log import logger from misc import helpers @@ -27,7 +26,11 @@ class Radarr: def validate_api_key(self): try: # request system status to validate api_key - req = requests.get(urljoin(self.server_url, 'api/system/status'), headers=self.headers, timeout=60) + req = requests.get( + os.path.join(self.server_url, 'api/system/status'), + headers=self.headers, + timeout=60 + ) log.debug("Request Response: %d", req.status_code) if req.status_code == 200 and 'version' in req.json(): @@ -41,7 +44,11 @@ class Radarr: def get_movies(self): try: # make request - req = requests.get(urljoin(self.server_url, 'api/movie'), headers=self.headers, timeout=60) + req = requests.get( + os.path.join(self.server_url, 'api/movie'), + headers=self.headers, + timeout=60 + ) log.debug("Request URL: %s", req.url) log.debug("Request Response: %d", req.status_code) @@ -59,7 +66,11 @@ class Radarr: def get_profile_id(self, profile_name): try: # make request - req = requests.get(urljoin(self.server_url, 'api/profile'), headers=self.headers, timeout=60) + req = requests.get( + os.path.join(self.server_url, 'api/profile'), + headers=self.headers, + timeout=60 + ) log.debug("Request URL: %s", req.url) log.debug("Request Response: %d", req.status_code) @@ -100,7 +111,7 @@ class Radarr: # make request req = requests.post( - urljoin(self.server_url, 'api/movie'), + os.path.join(self.server_url, 'api/movie'), headers=self.headers, json=payload, timeout=60 diff --git a/media/sonarr.py b/media/sonarr.py index e2167af..9315c62 100644 --- a/media/sonarr.py +++ b/media/sonarr.py @@ -1,7 +1,6 @@ -from urllib.parse import urljoin - import backoff import requests +import os.path from misc import helpers from misc.log import logger @@ -26,7 +25,7 @@ class Sonarr: def validate_api_key(self): try: # request system status to validate api_key - req = requests.get(urljoin(self.server_url, 'api/system/status'), headers=self.headers, timeout=60) + req = requests.get(os.path.join(self.server_url, 'api/system/status'), headers=self.headers, timeout=60) log.debug("Request Response: %d", req.status_code) if req.status_code == 200 and 'version' in req.json(): @@ -40,7 +39,11 @@ class Sonarr: def get_series(self): try: # make request - req = requests.get(urljoin(self.server_url, 'api/series'), headers=self.headers, timeout=60) + req = requests.get( + os.path.join(self.server_url, 'api/series'), + headers=self.headers, + timeout=60 + ) log.debug("Request URL: %s", req.url) log.debug("Request Response: %d", req.status_code) @@ -58,7 +61,11 @@ class Sonarr: def get_profile_id(self, profile_name): try: # make request - req = requests.get(urljoin(self.server_url, 'api/profile'), headers=self.headers, timeout=60) + req = requests.get( + os.path.join(self.server_url, 'api/profile'), + headers=self.headers, + timeout=60 + ) log.debug("Request URL: %s", req.url) log.debug("Request Response: %d", req.status_code) @@ -80,7 +87,11 @@ class Sonarr: def get_tag_id(self, tag_name): try: # make request - req = requests.get(urljoin(self.server_url, 'api/tag'), headers=self.headers, timeout=60) + req = requests.get( + os.path.join(self.server_url, 'api/tag'), + headers=self.headers, + timeout=60 + ) log.debug("Request URL: %s", req.url) log.debug("Request Response: %d", req.status_code) @@ -103,7 +114,11 @@ class Sonarr: tags = {} try: # make request - req = requests.get(urljoin(self.server_url, 'api/tag'), headers=self.headers, timeout=60) + req = requests.get( + os.path.join(self.server_url, 'api/tag'), + headers=self.headers, + timeout=60 + ) log.debug("Request URL: %s", req.url) log.debug("Request Response: %d", req.status_code) @@ -144,7 +159,7 @@ class Sonarr: # make request req = requests.post( - urljoin(self.server_url, 'api/series'), + os.path.join(self.server_url, 'api/series'), headers=self.headers, json=payload, timeout=60 From 482c11dbec3978a7b2a9d800190415276e65b8ef Mon Sep 17 00:00:00 2001 From: l3uddz Date: Fri, 20 Apr 2018 22:03:34 +0100 Subject: [PATCH 09/11] ensure server url ends with / --- media/radarr.py | 14 ++++++++------ media/sonarr.py | 17 ++++++++++------- misc/str.py | 7 +++++++ 3 files changed, 25 insertions(+), 13 deletions(-) diff --git a/media/radarr.py b/media/radarr.py index bc0c3af..981af7e 100644 --- a/media/radarr.py +++ b/media/radarr.py @@ -1,9 +1,11 @@ +import os.path + import backoff import requests -import os.path -from misc.log import logger from misc import helpers +from misc import str as misc_str +from misc.log import logger log = logger.get_logger(__name__) @@ -27,7 +29,7 @@ class Radarr: try: # request system status to validate api_key req = requests.get( - os.path.join(self.server_url, 'api/system/status'), + os.path.join(misc_str.ensure_endswith(self.server_url, "/"), 'api/system/status'), headers=self.headers, timeout=60 ) @@ -45,7 +47,7 @@ class Radarr: try: # make request req = requests.get( - os.path.join(self.server_url, 'api/movie'), + os.path.join(misc_str.ensure_endswith(self.server_url, "/"), 'api/movie'), headers=self.headers, timeout=60 ) @@ -67,7 +69,7 @@ class Radarr: try: # make request req = requests.get( - os.path.join(self.server_url, 'api/profile'), + os.path.join(misc_str.ensure_endswith(self.server_url, "/"), 'api/profile'), headers=self.headers, timeout=60 ) @@ -111,7 +113,7 @@ class Radarr: # make request req = requests.post( - os.path.join(self.server_url, 'api/movie'), + os.path.join(misc_str.ensure_endswith(self.server_url, "/"), 'api/movie'), headers=self.headers, json=payload, timeout=60 diff --git a/media/sonarr.py b/media/sonarr.py index 9315c62..6caa082 100644 --- a/media/sonarr.py +++ b/media/sonarr.py @@ -1,8 +1,10 @@ +import os.path + import backoff import requests -import os.path from misc import helpers +from misc import str as misc_str from misc.log import logger log = logger.get_logger(__name__) @@ -25,7 +27,8 @@ class Sonarr: def validate_api_key(self): try: # request system status to validate api_key - req = requests.get(os.path.join(self.server_url, 'api/system/status'), headers=self.headers, timeout=60) + req = requests.get(os.path.join(misc_str.ensure_endswith(self.server_url, "/"), 'api/system/status'), + headers=self.headers, timeout=60) log.debug("Request Response: %d", req.status_code) if req.status_code == 200 and 'version' in req.json(): @@ -40,7 +43,7 @@ class Sonarr: try: # make request req = requests.get( - os.path.join(self.server_url, 'api/series'), + os.path.join(misc_str.ensure_endswith(self.server_url, "/"), 'api/series'), headers=self.headers, timeout=60 ) @@ -62,7 +65,7 @@ class Sonarr: try: # make request req = requests.get( - os.path.join(self.server_url, 'api/profile'), + os.path.join(misc_str.ensure_endswith(self.server_url, "/"), 'api/profile'), headers=self.headers, timeout=60 ) @@ -88,7 +91,7 @@ class Sonarr: try: # make request req = requests.get( - os.path.join(self.server_url, 'api/tag'), + os.path.join(misc_str.ensure_endswith(self.server_url, "/"), 'api/tag'), headers=self.headers, timeout=60 ) @@ -115,7 +118,7 @@ class Sonarr: try: # make request req = requests.get( - os.path.join(self.server_url, 'api/tag'), + os.path.join(misc_str.ensure_endswith(self.server_url, "/"), 'api/tag'), headers=self.headers, timeout=60 ) @@ -159,7 +162,7 @@ class Sonarr: # make request req = requests.post( - os.path.join(self.server_url, 'api/series'), + os.path.join(misc_str.ensure_endswith(self.server_url, "/"), 'api/series'), headers=self.headers, json=payload, timeout=60 diff --git a/misc/str.py b/misc/str.py index 91c9cab..e278409 100644 --- a/misc/str.py +++ b/misc/str.py @@ -26,3 +26,10 @@ def is_ascii(string): log.exception(u"Exception checking if %r was ascii: ", string) return False return True + + +def ensure_endswith(data, endswith_key): + if not data.strip().endswith(endswith_key): + return "%s%s" % (data.strip(), endswith_key) + else: + return data From 39c0a2987c376024acb16640b6ed6ed0d237b6f6 Mon Sep 17 00:00:00 2001 From: l3uddz Date: Fri, 20 Apr 2018 22:13:05 +0100 Subject: [PATCH 10/11] use realpath from argv[0] (otherwise symlinked default config locations = /usr/bin/local/config.json) --- traktarr.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/traktarr.py b/traktarr.py index 9641e89..aaa5c74 100755 --- a/traktarr.py +++ b/traktarr.py @@ -1,5 +1,6 @@ #!/usr/bin/env python3 import os.path +import sys import time import click @@ -22,7 +23,7 @@ notify = None type=click.Path(file_okay=True, dir_okay=False), help='Configuration file', show_default=True, - default=os.path.join(os.path.dirname(os.path.abspath(__file__)), "config.json") + default=os.path.join(os.path.dirname(os.path.realpath(sys.argv[0])), "config.json") ) @click.option( '--logfile', @@ -30,7 +31,7 @@ notify = None type=click.Path(file_okay=True, dir_okay=False), help='Log file', show_default=True, - default=os.path.join(os.path.dirname(os.path.abspath(__file__)), "activity.log") + default=os.path.join(os.path.dirname(os.path.realpath(sys.argv[0])), "activity.log") ) def app(config, logfile): # Setup global variables From 74654c3fa95b84c18c12429407e6e2cb7151bf18 Mon Sep 17 00:00:00 2001 From: l3uddz Date: Fri, 20 Apr 2018 22:13:47 +0100 Subject: [PATCH 11/11] increase version --- traktarr.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/traktarr.py b/traktarr.py index aaa5c74..97c7f14 100755 --- a/traktarr.py +++ b/traktarr.py @@ -16,7 +16,7 @@ notify = None # Click @click.group(help='Add new shows & movies to Sonarr/Radarr from Trakt lists.') -@click.version_option('1.1.2', prog_name='traktarr') +@click.version_option('1.1.3', prog_name='traktarr') @click.option( '--config', envvar='TRAKTARR_CONFIG',