mirror of
https://github.com/ManiMatter/decluttarr.git
synced 2026-04-26 18:55:45 +02:00
Code Rewrite to support multi instances
This commit is contained in:
@@ -1,48 +0,0 @@
|
||||
[general]
|
||||
LOG_LEVEL = VERBOSE
|
||||
TEST_RUN = True
|
||||
|
||||
[features]
|
||||
REMOVE_TIMER = 10
|
||||
REMOVE_FAILED = True
|
||||
REMOVE_FAILED_IMPORTS = True
|
||||
REMOVE_METADATA_MISSING = True
|
||||
REMOVE_MISSING_FILES = True
|
||||
REMOVE_ORPHANS = True
|
||||
REMOVE_SLOW = True
|
||||
REMOVE_STALLED = True
|
||||
REMOVE_UNMONITORED = True
|
||||
RUN_PERIODIC_RESCANS = {"SONARR": {"MISSING": true, "CUTOFF_UNMET": true, "MAX_CONCURRENT_SCANS": 3, "MIN_DAYS_BEFORE_RESCAN": 7}, "RADARR": {"MISSING": true, "CUTOFF_UNMET": true, "MAX_CONCURRENT_SCANS": 3, "MIN_DAYS_BEFORE_RESCAN": 7}}
|
||||
|
||||
[feature_settings]
|
||||
MIN_DOWNLOAD_SPEED = 100
|
||||
PERMITTED_ATTEMPTS = 3
|
||||
NO_STALLED_REMOVAL_QBIT_TAG = Don't Kill
|
||||
IGNORE_PRIVATE_TRACKERS = FALSE
|
||||
FAILED_IMPORT_MESSAGE_PATTERNS = ["Not a Custom Format upgrade for existing", "Not an upgrade for existing"]
|
||||
IGNORED_DOWNLOAD_CLIENTS = ["emulerr"]
|
||||
|
||||
[radarr]
|
||||
RADARR_URL = http://radarr:7878
|
||||
RADARR_KEY = $RADARR_API_KEY
|
||||
|
||||
[sonarr]
|
||||
SONARR_URL = http://sonarr:8989
|
||||
SONARR_KEY = $SONARR_API_KEY
|
||||
|
||||
[lidarr]
|
||||
LIDARR_URL = http://lidarr:8686
|
||||
LIDARR_KEY = $LIDARR_API_KEY
|
||||
|
||||
[readarr]
|
||||
READARR_URL = http://lidarr:8787
|
||||
READARR_KEY = $READARR_API_KEY
|
||||
|
||||
[whisparr]
|
||||
WHISPARR_URL = http://whisparr:6969
|
||||
WHISPARR_KEY = $WHISPARR_API_KEY
|
||||
|
||||
[qbittorrent]
|
||||
QBITTORRENT_URL = http://qbittorrent:8080
|
||||
QBITTORRENT_USERNAME = Your name (or empty)
|
||||
QBITTORRENT_PASSWORD = Your password (or empty)
|
||||
63
config/config_example.yaml
Normal file
63
config/config_example.yaml
Normal file
@@ -0,0 +1,63 @@
|
||||
general:
|
||||
log_level: INFO
|
||||
test_run: true
|
||||
timer: 10
|
||||
# ignored_download_clients: ["emulerr"]
|
||||
# ssl_verification: false # Optional: Defaults to true
|
||||
# private_tracker_handling: "obsolete_tag" # remove, skip, obsolete_tag. Optional. Default: remove
|
||||
# public_tracker_handling: "remove" # remove, skip, obsolete_tag. Optional. Default: remove
|
||||
# obsolete_tag: "Obsolete" # optional. Default: "Obsolete"
|
||||
# protected_tag: "Keep" # optional. Default: "Keep"
|
||||
|
||||
job_defaults:
|
||||
max_strikes: 3
|
||||
min_days_between_searches: 7
|
||||
max_concurrent_searches: 3
|
||||
|
||||
jobs:
|
||||
remove_bad_files:
|
||||
remove_failed_downloads:
|
||||
remove_failed_imports:
|
||||
message_patterns:
|
||||
- Not a Custom Format upgrade for existing*
|
||||
- Not an upgrade for existing*
|
||||
remove_metadata_missing:
|
||||
# max_strikes: 3
|
||||
remove_missing_files:
|
||||
remove_orphans:
|
||||
remove_slow:
|
||||
# min_speed: 100
|
||||
# max_strikes: 3
|
||||
remove_stalled:
|
||||
# max_strikes: 3
|
||||
remove_unmonitored:
|
||||
search_unmet_cutoff_content:
|
||||
# min_days_between_searches: 7
|
||||
# max_concurrent_searches: 3
|
||||
search_missing_content:
|
||||
# min_days_between_searches: 7
|
||||
# max_concurrent_searches: 3
|
||||
|
||||
instances:
|
||||
sonarr:
|
||||
- base_url: "http://sonarr:8989"
|
||||
api_key: "xxxx"
|
||||
radarr:
|
||||
- base_url: "http://radarr:7878"
|
||||
api_key: "xxxx"
|
||||
readarr:
|
||||
- base_url: "http://readarr:8787"
|
||||
api_key: "xxxx"
|
||||
lidarr:
|
||||
- base_url: "http://lidarr:8686"
|
||||
api_key: "xxxx"
|
||||
whisparr:
|
||||
- base_url: "http://whisparr:6969"
|
||||
api_key: "xxxx"
|
||||
|
||||
download_clients:
|
||||
qbittorrent:
|
||||
- base_url: "http://qbittorrent:8080" # You can use decluttar without qbit (not all features available, see readme).
|
||||
# username: xxxx # (optional -> if not provided, assuming not needed)
|
||||
# password: xxxx # (optional -> if not provided, assuming not needed)
|
||||
# name: "qBittorrent" # (optional -> if not provided, assuming "qBittorrent". Must correspond with what is specified in your *arr as download client name)
|
||||
@@ -1,130 +0,0 @@
|
||||
#### Turning off black formatting
|
||||
# fmt: off
|
||||
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)
|
||||
SSL_VERIFICATION = get_config_value('SSL_VERIFICATION', 'general', False, bool, True)
|
||||
|
||||
# Features
|
||||
REMOVE_TIMER = get_config_value('REMOVE_TIMER', 'features', False, float, 10)
|
||||
REMOVE_FAILED = get_config_value('REMOVE_FAILED', 'features', False, bool, False)
|
||||
REMOVE_FAILED_IMPORTS = get_config_value('REMOVE_FAILED_IMPORTS' , 'features', False, bool, False)
|
||||
REMOVE_METADATA_MISSING = get_config_value('REMOVE_METADATA_MISSING', 'features', False, bool, False)
|
||||
REMOVE_MISSING_FILES = get_config_value('REMOVE_MISSING_FILES', 'features', False, bool, False)
|
||||
REMOVE_NO_FORMAT_UPGRADE = get_config_value('REMOVE_NO_FORMAT_UPGRADE', 'features', False, bool, False) # OUTDATED - WILL RETURN WARNING
|
||||
REMOVE_ORPHANS = get_config_value('REMOVE_ORPHANS', 'features', False, bool, False)
|
||||
REMOVE_SLOW = get_config_value('REMOVE_SLOW', 'features', False, bool, False)
|
||||
REMOVE_STALLED = get_config_value('REMOVE_STALLED', 'features', False, bool, False)
|
||||
REMOVE_UNMONITORED = get_config_value('REMOVE_UNMONITORED', 'features', False, bool, False)
|
||||
RUN_PERIODIC_RESCANS = get_config_value('RUN_PERIODIC_RESCANS', 'features', False, dict, {})
|
||||
|
||||
# Feature Settings
|
||||
MIN_DOWNLOAD_SPEED = get_config_value('MIN_DOWNLOAD_SPEED', 'feature_settings', False, int, 0)
|
||||
PERMITTED_ATTEMPTS = get_config_value('PERMITTED_ATTEMPTS', 'feature_settings', False, int, 3)
|
||||
NO_STALLED_REMOVAL_QBIT_TAG = get_config_value('NO_STALLED_REMOVAL_QBIT_TAG', 'feature_settings', False, str, 'Don\'t Kill')
|
||||
IGNORE_PRIVATE_TRACKERS = get_config_value('IGNORE_PRIVATE_TRACKERS', 'feature_settings', False, bool, True)
|
||||
FAILED_IMPORT_MESSAGE_PATTERNS = get_config_value('FAILED_IMPORT_MESSAGE_PATTERNS','feature_settings', False, list, [])
|
||||
IGNORED_DOWNLOAD_CLIENTS = get_config_value('IGNORED_DOWNLOAD_CLIENTS', 'feature_settings', False, list, [])
|
||||
|
||||
# Radarr
|
||||
RADARR_URL = get_config_value('RADARR_URL', 'radarr', False, str)
|
||||
RADARR_KEY = None if RADARR_URL == None else \
|
||||
get_config_value('RADARR_KEY', 'radarr', True, str)
|
||||
|
||||
# Sonarr
|
||||
SONARR_URL = get_config_value('SONARR_URL', 'sonarr', False, str)
|
||||
SONARR_KEY = None if SONARR_URL == None else \
|
||||
get_config_value('SONARR_KEY', 'sonarr', True, str)
|
||||
|
||||
# Lidarr
|
||||
LIDARR_URL = get_config_value('LIDARR_URL', 'lidarr', False, str)
|
||||
LIDARR_KEY = None if LIDARR_URL == None else \
|
||||
get_config_value('LIDARR_KEY', 'lidarr', True, str)
|
||||
|
||||
# Readarr
|
||||
READARR_URL = get_config_value('READARR_URL', 'readarr', False, str)
|
||||
READARR_KEY = None if READARR_URL == None else \
|
||||
get_config_value('READARR_KEY', 'readarr', True, str)
|
||||
|
||||
# Whisparr
|
||||
WHISPARR_URL = get_config_value('WHISPARR_URL', 'whisparr', False, str)
|
||||
WHISPARR_KEY = None if WHISPARR_URL == None else \
|
||||
get_config_value('WHISPARR_KEY', 'whisparr', True, str)
|
||||
|
||||
# qBittorrent
|
||||
QBITTORRENT_URL = get_config_value('QBITTORRENT_URL', 'qbittorrent', False, str, '')
|
||||
QBITTORRENT_USERNAME = get_config_value('QBITTORRENT_USERNAME', 'qbittorrent', False, str, '')
|
||||
QBITTORRENT_PASSWORD = get_config_value('QBITTORRENT_PASSWORD', 'qbittorrent', False, str, '')
|
||||
|
||||
########################################################################################################################
|
||||
########### Validate settings
|
||||
if not (IS_IN_PYTEST or 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)')
|
||||
exit()
|
||||
|
||||
|
||||
#### Validate rescan settings
|
||||
PERIODIC_RESCANS = get_config_value("PERIODIC_RESCANS", "features", False, dict, {})
|
||||
|
||||
rescan_supported_apps = ["SONARR", "RADARR"]
|
||||
rescan_default_values = {
|
||||
"MISSING": (True, bool),
|
||||
"CUTOFF_UNMET": (True, bool),
|
||||
"MAX_CONCURRENT_SCANS": (3, int),
|
||||
"MIN_DAYS_BEFORE_RESCAN": (7, int),
|
||||
}
|
||||
|
||||
|
||||
# Remove rescan apps that are not supported
|
||||
for key in list(RUN_PERIODIC_RESCANS.keys()):
|
||||
if key not in rescan_supported_apps:
|
||||
print(f"[ WARNING ]: Removed '{key}' from RUN_PERIODIC_RESCANS since only {rescan_supported_apps} are supported.")
|
||||
RUN_PERIODIC_RESCANS.pop(key)
|
||||
|
||||
# Ensure SONARR and RADARR have the required parameters with default values if they are present
|
||||
for app in rescan_supported_apps:
|
||||
if app in RUN_PERIODIC_RESCANS:
|
||||
for param, (default, expected_type) in rescan_default_values.items():
|
||||
if param not in RUN_PERIODIC_RESCANS[app]:
|
||||
print(f"[ INFO ]: Adding missing parameter '{param}' to '{app}' with default value '{default}'.")
|
||||
RUN_PERIODIC_RESCANS[app][param] = default
|
||||
else:
|
||||
# Check the type and correct if necessary
|
||||
current_value = RUN_PERIODIC_RESCANS[app][param]
|
||||
if not isinstance(current_value, expected_type):
|
||||
print(
|
||||
f"[ INFO ]: Parameter '{param}' for '{app}' must be of type {expected_type.__name__} and found value '{current_value}' (type '{type(current_value).__name__}'). Defaulting to '{default}'."
|
||||
)
|
||||
RUN_PERIODIC_RESCANS[app][param] = default
|
||||
|
||||
########### Enrich setting variables
|
||||
if RADARR_URL: RADARR_URL = RADARR_URL.rstrip('/') + '/api/v3'
|
||||
if SONARR_URL: SONARR_URL = SONARR_URL.rstrip('/') + '/api/v3'
|
||||
if LIDARR_URL: LIDARR_URL = LIDARR_URL.rstrip('/') + '/api/v1'
|
||||
if READARR_URL: READARR_URL = READARR_URL.rstrip('/') + '/api/v1'
|
||||
if WHISPARR_URL: WHISPARR_URL = WHISPARR_URL.rstrip('/') + '/api/v3'
|
||||
if QBITTORRENT_URL: QBITTORRENT_URL = QBITTORRENT_URL.rstrip('/') + '/api/v2'
|
||||
|
||||
|
||||
RADARR_MIN_VERSION = "5.3.6.8608"
|
||||
if "RADARR" in PERIODIC_RESCANS:
|
||||
RADARR_MIN_VERSION = "5.10.3.9171"
|
||||
|
||||
SONARR_MIN_VERSION = "4.0.1.1131"
|
||||
if "SONARR" in PERIODIC_RESCANS:
|
||||
SONARR_MIN_VERSION = "4.0.9.2332"
|
||||
LIDARR_MIN_VERSION = None
|
||||
READARR_MIN_VERSION = None
|
||||
WHISPARR_MIN_VERSION = '2.0.0.548'
|
||||
QBITTORRENT_MIN_VERSION = '4.3.0'
|
||||
|
||||
SUPPORTED_ARR_APPS = ['RADARR', 'SONARR', 'LIDARR', 'READARR', 'WHISPARR']
|
||||
|
||||
########### Add Variables to Dictionary
|
||||
settingsDict = {}
|
||||
for var_name in dir():
|
||||
if var_name.isupper():
|
||||
settingsDict[var_name] = locals()[var_name]
|
||||
@@ -1,6 +0,0 @@
|
||||
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")
|
||||
IS_IN_PYTEST = os.environ.get("IS_IN_PYTEST")
|
||||
@@ -1,82 +0,0 @@
|
||||
#!/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:
|
||||
value = config.get(section, option)
|
||||
# Attempt to parse JSON for dictionary-like values
|
||||
try:
|
||||
dict1[option] = json.loads(value)
|
||||
except json.JSONDecodeError:
|
||||
dict1[option] = value
|
||||
except Exception as e:
|
||||
print(f"Exception on {option}: {e}")
|
||||
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:
|
||||
config_value = config_value
|
||||
elif is_mandatory:
|
||||
print(f"[ ERROR ]: Variable not specified in Docker environment: {key}")
|
||||
sys.exit(0)
|
||||
else:
|
||||
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:
|
||||
config_value = 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:
|
||||
config_value = default_value
|
||||
|
||||
# Apply data type
|
||||
try:
|
||||
if datatype == bool:
|
||||
config_value = eval(str(config_value).capitalize())
|
||||
elif datatype == list or datatype == dict:
|
||||
if not isinstance(config_value, datatype):
|
||||
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
|
||||
Reference in New Issue
Block a user