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"] 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 57e761c..981af7e 100644 --- a/media/radarr.py +++ b/media/radarr.py @@ -1,8 +1,10 @@ -from urllib.parse import urljoin +import os.path import backoff import requests +from misc import helpers +from misc import str as misc_str from misc.log import logger log = logger.get_logger(__name__) @@ -26,7 +28,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=30) + 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 +46,11 @@ 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( + os.path.join(misc_str.ensure_endswith(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) @@ -58,7 +68,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=30) + req = requests.get( + os.path.join(misc_str.ensure_endswith(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,26 +95,45 @@ 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( + os.path.join(misc_str.ensure_endswith(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: %d", req.status_code) + log.debug("Request Response Code: %d", req.status_code) + log.debug("Request Response Text:\n%s", req.text) + + response_json = None + if 'json' in req.headers['Content-Type'].lower(): + response_json = helpers.get_response_dict(req.json(), 'tmdbId', movie_tmdbid) - 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 (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 11e86ad..6caa082 100644 --- a/media/sonarr.py +++ b/media/sonarr.py @@ -1,8 +1,10 @@ -from urllib.parse import urljoin +import os.path import backoff import requests +from misc import helpers +from misc import str as misc_str from misc.log import logger log = logger.get_logger(__name__) @@ -19,14 +21,14 @@ 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(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 +42,11 @@ 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( + os.path.join(misc_str.ensure_endswith(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 +64,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=30) + req = requests.get( + os.path.join(misc_str.ensure_endswith(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 +90,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=30) + req = requests.get( + os.path.join(misc_str.ensure_endswith(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 +117,11 @@ class Sonarr: tags = {} try: # make request - req = requests.get(urljoin(self.server_url, 'api/tag'), headers=self.headers, timeout=30) + req = requests.get( + os.path.join(misc_str.ensure_endswith(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) @@ -125,28 +143,46 @@ 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( + os.path.join(misc_str.ensure_endswith(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: %d", req.status_code) + log.debug("Request Response Code: %d", req.status_code) + log.debug("Request Response Text:\n%s", req.text) + + response_json = None + if 'json' in req.headers['Content-Type'].lower(): + response_json = helpers.get_response_dict(req.json(), 'tvdbId', series_tvdbid) - if (req.status_code == 201 or req.status_code == 200) and 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: 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/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/helpers.py b/misc/helpers.py index c5c9f6f..60ad23e 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,33 @@ def trakt_is_movie_blacklisted(movie, blacklist_settings): except Exception: log.exception("Exception determining if movie was blacklisted %s: ", movie) return blacklisted + + +############################################################ +# MISC +############################################################ + + +def get_response_dict(response, key_field=None, key_value=None): + found_response = None + try: + if isinstance(response, list): + 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: + 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 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/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 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 diff --git a/traktarr.py b/traktarr.py index 0da7ab1..97c7f14 100755 --- a/traktarr.py +++ b/traktarr.py @@ -1,33 +1,56 @@ #!/usr/bin/env python3 +import os.path +import sys 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.version_option('1.1.3', prog_name='traktarr') +@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.realpath(sys.argv[0])), "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.realpath(sys.argv[0])), "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 +67,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 +241,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 +531,4 @@ def init_notifications(): ############################################################ if __name__ == "__main__": - init_notifications() app()