diff --git a/README.md b/README.md index c7c0df9..f1293bc 100644 --- a/README.md +++ b/README.md @@ -76,7 +76,10 @@ services: - PERMITTED_ATTEMPTS=3 - NO_STALLED_REMOVAL_QBIT_TAG=Don't Kill - IGNORE_PRIVATE_TRACKERS=True - - FAILED_IMPORT_MESSAGE_PATTERNS = '["Not a Custom Format upgrade for existing", "Not an upgrade for existing"]' + - FAILED_IMPORT_MESSAGE_PATTERNS: '[ + "Not a Custom Format upgrade for existing", + "Not an upgrade for existing" + ]' ## Radarr - RADARR_URL=http://radarr:7878 - RADARR_KEY=$RADARR_API_KEY @@ -243,11 +246,11 @@ Steers which type of cleaning is applied to the downloads queue **FAILED_IMPORT_MESSAGE_PATTERNS** - Works in together with REMOVE_FAILED_IMPORTS (only relevant if this setting is true) -- Defines the patterns based on which the tool decides if a import with a warning should be considered failed -- Queue items are considered failed, if any of the specified patterns is contained in one of the messages of the queue item -- Note: If left empty, any import with a warning is considered failed +- Defines the patterns based on which the tool decides if a completed download that has warnings on import should be considered failed +- Queue items are considered failed if any of the specified patterns is contained in one of the messages of the queue item +- Note: If left empty (or not specified), any such pending import with warning is considered failed - Type: List -- Suggested values: ["Not a Custom Format upgrade for existing", "Not an upgrade for existing"] +- Recommended values: ["Not a Custom Format upgrade for existing", "Not an upgrade for existing"] - Is Mandatory: No (Defaults to [], which means all messages are failures) --- diff --git a/config/config.py b/config/definitions.py similarity index 57% rename from config/config.py rename to config/definitions.py index dae75f4..d2dcc3a 100644 --- a/config/config.py +++ b/config/definitions.py @@ -1,88 +1,7 @@ #!/usr/bin/env python -import sys -import os -import configparser -import json -######################################################################################################################## -# Check if in Docker -IS_IN_DOCKER = os.environ.get('IS_IN_DOCKER') -IMAGE_TAG = os.environ.get('IMAGE_TAG', 'Local') -SHORT_COMMIT_ID = os.environ.get('SHORT_COMMIT_ID', 'n/a') - -######################################################################################################################## -def config_section_map(section): - 'Load the config file into a dictionary' - dict1 = {} - options = config.options(section) - for option in options: - try: - dict1[option] = config.get(section, option) - except: - print("exception on %s!" % option) - dict1[option] = None - return dict1 - -def cast(value, type_): - return type_(value) - -def get_config_value(key, config_section, is_mandatory, datatype, default_value = None): - 'Return for each key the corresponding value from the Docker Environment or the Config File' - if IS_IN_DOCKER: - config_value = os.environ.get(key) - if config_value is not None: - # print(f'The value retrieved for [{config_section}]: {key} is "{config_value}"') - config_value = config_value - # return config_value - elif is_mandatory: - print(f'[ ERROR ]: Variable not specified in Docker environment: {key}' ) - sys.exit(0) - else: - # return default_value - # print(f'The default value used for [{config_section}]: {key} is "{default_value}" (data type: {type(default_value).__name__})') - config_value = default_value - - else: - try: - config_value = config_section_map(config_section).get(key) - except configparser.NoSectionError: - config_value = None - if config_value is not None: - # print(f'The value retrieved for [{config_section}]: {key} is "{config_value}"') - config_value = config_value - # return config_value - elif is_mandatory: - print(f'[ ERROR ]: Mandatory variable not specified in config file, section [{config_section}]: {key} (data type: {datatype.__name__})') - sys.exit(0) - else: - # return default_value - # print(f'The default value used for [{config_section}]: {key} is "{default_value}" (data type: {type(default_value).__name__})') - config_value = default_value - - # Apply data type - try: - if datatype == bool: - config_value = eval(str(config_value).capitalize()) - elif datatype == list: - config_value = json.loads(config_value) - elif config_value is not None: - config_value = cast(config_value, datatype) - except Exception as e: - print(f'[ ERROR ]: The value retrieved for [{config_section}]: {key} is "{config_value}" and cannot be converted to data type {datatype}') - print(e) - sys.exit(0) - return config_value - -######################################################################################################################## -# Load Config File -config_file_name = 'config.conf' -config_file_full_path = os.path.join(os.path.abspath(os.path.dirname(__file__)), config_file_name) -sys.tracebacklimit = 0 # dont show stack traces in prod mode -config = configparser.ConfigParser() -config.optionxform = str # maintain capitalization of config keys -config.read(config_file_full_path) - -######################################################################################################################## -# Load Config +from config.parser import get_config_value +from config.env_vars import * +# Define data types and default values for settingsDict variables # General LOG_LEVEL = get_config_value('LOG_LEVEL', 'general', False, str, 'INFO') TEST_RUN = get_config_value('TEST_RUN', 'general', False, bool, False) @@ -103,7 +22,7 @@ MIN_DOWNLOAD_SPEED = get_config_value('MIN_DOWNLOAD_SPEED', PERMITTED_ATTEMPTS = get_config_value('PERMITTED_ATTEMPTS', 'features', False, int, 3) NO_STALLED_REMOVAL_QBIT_TAG = get_config_value('NO_STALLED_REMOVAL_QBIT_TAG', 'features', False, str, 'Don\'t Kill') IGNORE_PRIVATE_TRACKERS = get_config_value('IGNORE_PRIVATE_TRACKERS', 'features', False, bool, True) -FAILED_IMPORT_MESSAGE_PATTERNS = get_config_value('FAILED_IMPORT_MESSAGE_PATTERNS','features', False, list, '[]') +FAILED_IMPORT_MESSAGE_PATTERNS = get_config_value('FAILED_IMPORT_MESSAGE_PATTERNS','features', False, list, []) # Radarr RADARR_URL = get_config_value('RADARR_URL', 'radarr', False, str) @@ -137,9 +56,10 @@ QBITTORRENT_PASSWORD = get_config_value('QBITTORRENT_PASSWORD', ######################################################################################################################## ########### Validate settings + if not (RADARR_URL or SONARR_URL or LIDARR_URL or READARR_URL or WHISPARR_URL): print(f'[ ERROR ]: No Radarr/Sonarr/Lidarr/Readarr/Whisparr URLs specified (nothing to monitor)') - sys.exit(0) + exit() ########### Enrich setting variables if RADARR_URL: RADARR_URL += '/api/v3' diff --git a/config/env_vars.py b/config/env_vars.py new file mode 100644 index 0000000..3ed7d65 --- /dev/null +++ b/config/env_vars.py @@ -0,0 +1,4 @@ +import os +IS_IN_DOCKER = os.environ.get('IS_IN_DOCKER') +IMAGE_TAG = os.environ.get('IMAGE_TAG', 'Local') +SHORT_COMMIT_ID = os.environ.get('SHORT_COMMIT_ID', 'n/a') diff --git a/config/parser.py b/config/parser.py new file mode 100644 index 0000000..a17626a --- /dev/null +++ b/config/parser.py @@ -0,0 +1,80 @@ +#!/usr/bin/env python +import sys +import os +import configparser +import json +from config.env_vars import * + +# Configures how to parse configuration file +config_file_name = 'config.conf' +config_file_full_path = os.path.join(os.path.abspath(os.path.dirname(__file__)), config_file_name) +sys.tracebacklimit = 0 # dont show stack traces in prod mode +config = configparser.ConfigParser() +config.optionxform = str # maintain capitalization of config keys +config.read(config_file_full_path) + + +def config_section_map(section): + 'Load the config file into a dictionary' + dict1 = {} + options = config.options(section) + for option in options: + try: + dict1[option] = config.get(section, option) + except: + print("exception on %s!" % option) + dict1[option] = None + return dict1 + +def cast(value, type_): + return type_(value) + +def get_config_value(key, config_section, is_mandatory, datatype, default_value = None): + 'Return for each key the corresponding value from the Docker Environment or the Config File' + if IS_IN_DOCKER: + config_value = os.environ.get(key) + if config_value is not None: + # print(f'The value retrieved for [{config_section}]: {key} is "{config_value}"') + config_value = config_value + # return config_value + elif is_mandatory: + print(f'[ ERROR ]: Variable not specified in Docker environment: {key}' ) + sys.exit(0) + else: + # return default_value + # print(f'The default value used for [{config_section}]: {key} is "{default_value}" (data type: {type(default_value).__name__})') + config_value = default_value + + else: + try: + config_value = config_section_map(config_section).get(key) + except configparser.NoSectionError: + config_value = None + if config_value is not None: + # print(f'The value retrieved for [{config_section}]: {key} is "{config_value}"') + config_value = config_value + # return config_value + elif is_mandatory: + print(f'[ ERROR ]: Mandatory variable not specified in config file, section [{config_section}]: {key} (data type: {datatype.__name__})') + sys.exit(0) + else: + # return default_value + # print(f'The default value used for [{config_section}]: {key} is "{default_value}" (data type: {type(default_value).__name__})') + config_value = default_value + + # Apply data type + try: + if datatype == bool: + config_value = eval(str(config_value).capitalize()) + elif datatype == list: + if type(config_value) != list: # Default value is already a list, doesn't need to be pushed through json.loads + config_value = json.loads(config_value) + elif config_value is not None: + config_value = cast(config_value, datatype) + except Exception as e: + print(f'[ ERROR ]: The value retrieved for [{config_section}]: {key} is "{config_value}" and cannot be converted to data type {datatype}') + print(e) + sys.exit(0) + return config_value + + diff --git a/main.py b/main.py index 1af0246..ab0e08d 100644 --- a/main.py +++ b/main.py @@ -4,7 +4,7 @@ import logging, verboselogs logger = verboselogs.VerboseLogger(__name__) import json # Import Functions -from config.config import settingsDict +from config.definitions import settingsDict from src.utils.loadScripts import * from src.decluttarr import queueCleaner from src.utils.rest import rest_get, rest_post diff --git a/src/utils/loadScripts.py b/src/utils/loadScripts.py index 280b29c..1795961 100644 --- a/src/utils/loadScripts.py +++ b/src/utils/loadScripts.py @@ -44,7 +44,7 @@ def showSettings(settingsDict): logger.info('%s | Removing failed downloads (%s)', str(settingsDict['REMOVE_FAILED']), 'REMOVE_FAILED') logger.info('%s | Removing failed imports (%s)', str(settingsDict['REMOVE_FAILED_IMPORTS']), 'REMOVE_FAILED_IMPORTS') if settingsDict['REMOVE_FAILED_IMPORTS'] and not settingsDict['FAILED_IMPORT_MESSAGE_PATTERNS']: - logger.verbose ('> Any imports with a warning flag are considered failed. No message patterns specified (%s).', 'FAILED_IMPORT_MESSAGE_PATTERNS') + logger.verbose ('> Any imports with a warning flag are considered failed, as no patterns specified (%s).', 'FAILED_IMPORT_MESSAGE_PATTERNS') elif settingsDict['REMOVE_FAILED_IMPORTS'] and settingsDict['FAILED_IMPORT_MESSAGE_PATTERNS']: logger.verbose ('> Imports with a warning flag are considered failed if the status message contains any of the following patterns:') for pattern in settingsDict['FAILED_IMPORT_MESSAGE_PATTERNS']: diff --git a/src/utils/rest.py b/src/utils/rest.py index e3cdca4..86651b6 100644 --- a/src/utils/rest.py +++ b/src/utils/rest.py @@ -4,7 +4,7 @@ import asyncio import requests from requests.exceptions import RequestException import json -from config.config import settingsDict +from config.definitions import settingsDict # GET async def rest_get(url, api_key=None, params=None, cookies=None):