Files
arr-init/module.nix
Simon Gardling c946150c81 move to python scripts from shell scripts
Allows usage of Servarr python libraries, reduces
implementation-specific code.
2026-03-27 23:41:58 -07:00

1004 lines
32 KiB
Nix

{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.arrInit;
bazarrCfg = config.services.bazarrInit;
downloadClientModule = lib.types.submodule {
options = {
name = lib.mkOption {
type = lib.types.str;
description = "Display name of the download client (e.g. \"qBittorrent\").";
example = "qBittorrent";
};
implementation = lib.mkOption {
type = lib.types.str;
description = "Implementation identifier for the Servarr API.";
example = "QBittorrent";
};
configContract = lib.mkOption {
type = lib.types.str;
description = "Config contract identifier for the Servarr API.";
example = "QBittorrentSettings";
};
protocol = lib.mkOption {
type = lib.types.enum [
"torrent"
"usenet"
];
default = "torrent";
description = "Download protocol type.";
};
fields = lib.mkOption {
type = lib.types.attrsOf lib.types.anything;
default = { };
description = ''
Flat key/value pairs for the download client configuration.
These are converted to the API's [{name, value}] array format.
'';
example = {
host = "192.168.15.1";
port = 6011;
useSsl = false;
tvCategory = "tvshows";
};
};
serviceName = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
description = ''
Name of the systemd service for this download client.
When set, the init service will depend on (After + Requires) this service,
ensuring the download client is running before health checks execute.
'';
example = "qbittorrent";
};
};
};
syncedAppModule = lib.types.submodule {
options = {
name = lib.mkOption {
type = lib.types.str;
description = "Display name of the application to sync (e.g. \"Sonarr\").";
example = "Sonarr";
};
implementation = lib.mkOption {
type = lib.types.str;
description = "Implementation identifier for the Prowlarr application API.";
example = "Sonarr";
};
configContract = lib.mkOption {
type = lib.types.str;
description = "Config contract identifier for the Prowlarr application API.";
example = "SonarrSettings";
};
syncLevel = lib.mkOption {
type = lib.types.str;
default = "fullSync";
description = "Sync level for the application.";
};
prowlarrUrl = lib.mkOption {
type = lib.types.str;
description = "URL of the Prowlarr instance.";
example = "http://localhost:9696";
};
baseUrl = lib.mkOption {
type = lib.types.str;
description = "URL of the target application.";
example = "http://localhost:8989";
};
apiKeyFrom = lib.mkOption {
type = lib.types.str;
description = "Path to the config.xml file to read the API key from at runtime.";
example = "/services/sonarr/config.xml";
};
syncCategories = lib.mkOption {
type = lib.types.listOf lib.types.int;
default = [ ];
description = ''
List of Newznab category IDs to sync for this application.
When empty (default), categories are auto-detected at runtime
by querying Prowlarr's indexer/categories API endpoint and
collecting all IDs under the parent category matching the
implementation type (e.g. Sonarr -> TV, Radarr -> Movies).
'';
example = [
5000
5010
5020
];
};
serviceName = lib.mkOption {
type = lib.types.str;
description = "Name of the systemd service to depend on for reading the API key.";
example = "sonarr";
};
};
};
instanceModule = lib.types.submodule {
options = {
enable = lib.mkEnableOption "Servarr application API initialization";
serviceName = lib.mkOption {
type = lib.types.str;
description = "Name of the systemd service this init depends on.";
example = "sonarr";
};
dataDir = lib.mkOption {
type = lib.types.str;
description = "Path to the application data directory containing config.xml.";
example = "/var/lib/sonarr";
};
port = lib.mkOption {
type = lib.types.port;
description = "API port of the Servarr application.";
example = 8989;
};
apiVersion = lib.mkOption {
type = lib.types.str;
default = "v3";
description = "API version string used in the base URL.";
};
networkNamespacePath = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
description = "If set, run this init service inside the given network namespace path (e.g. /run/netns/wg).";
};
downloadClients = lib.mkOption {
type = lib.types.listOf downloadClientModule;
default = [ ];
description = "List of download clients to configure via the API.";
};
rootFolders = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
description = "List of root folder paths to configure via the API.";
example = [
"/media/tv"
"/media/movies"
];
};
syncedApps = lib.mkOption {
type = lib.types.listOf syncedAppModule;
default = [ ];
description = "Applications to register for indexer sync (Prowlarr only).";
};
healthChecks = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
When enabled, the init service will verify connectivity after provisioning:
- Tests all download clients are reachable via the application's testall API
- For Prowlarr instances: tests all synced applications are reachable
The init service will fail if any health check fails after all retries.
'';
};
healthCheckRetries = lib.mkOption {
type = lib.types.ints.unsigned;
default = 5;
description = ''
Number of times to retry health checks before failing.
Each retry waits healthCheckInterval seconds. This prevents transient
failures (e.g. download clients still starting) from triggering alerts.
'';
};
healthCheckInterval = lib.mkOption {
type = lib.types.ints.positive;
default = 10;
description = ''
Seconds to wait between health check retries.
'';
};
apiTimeout = lib.mkOption {
type = lib.types.ints.positive;
default = 90;
description = ''
Seconds to wait for the application API to become available before
considering the init attempt failed. When the API is not reachable
within this window, the service exits non-zero and systemd's
Restart=on-failure will schedule another attempt after RestartSec.
The systemd start limit is computed from this value to allow 5 full
retry cycles before the unit enters permanent failure (which would
trigger any configured OnFailure= target).
'';
};
naming = lib.mkOption {
type = lib.types.attrsOf lib.types.anything;
default = { };
description = ''
Naming configuration to set via the API's config/naming endpoint.
Keys/values map directly to the API fields (e.g. renameEpisodes,
standardEpisodeFormat for Sonarr; renameMovies, standardMovieFormat
for Radarr). Only specified fields are updated; unspecified fields
retain their current values.
'';
example = {
renameEpisodes = true;
standardEpisodeFormat = "{Series Title} - S{season:00}E{episode:00} - {Episode Title} {Quality Full}";
seasonFolderFormat = "Season {season}";
seriesFolderFormat = "{Series Title}";
};
};
};
};
bazarrProviderModule = lib.types.submodule {
options = {
enable = lib.mkEnableOption "provider connection";
dataDir = lib.mkOption {
type = lib.types.str;
description = "Path to the provider's data directory containing config.xml.";
example = "/services/sonarr";
};
port = lib.mkOption {
type = lib.types.port;
description = "API port of the provider.";
example = 8989;
};
serviceName = lib.mkOption {
type = lib.types.str;
description = "Name of the systemd service to depend on.";
example = "sonarr";
};
};
};
bazarrInitModule = lib.types.submodule {
options = {
enable = lib.mkEnableOption "Bazarr API initialization";
dataDir = lib.mkOption {
type = lib.types.str;
description = "Path to Bazarr's data directory containing config/config.ini.";
example = "/services/bazarr";
};
port = lib.mkOption {
type = lib.types.port;
default = 6767;
description = "API port of Bazarr.";
};
apiTimeout = lib.mkOption {
type = lib.types.ints.positive;
default = 90;
description = ''
Seconds to wait for the Bazarr API to become available before
considering the init attempt failed. When the API is not reachable
within this window, the service exits non-zero and systemd's
Restart=on-failure will schedule another attempt after RestartSec.
The systemd start limit is computed from this value to allow 5 full
retry cycles before the unit enters permanent failure (which would
trigger any configured OnFailure= target).
'';
};
sonarr = lib.mkOption {
type = bazarrProviderModule;
default = {
enable = false;
};
description = "Sonarr provider configuration.";
};
radarr = lib.mkOption {
type = bazarrProviderModule;
default = {
enable = false;
};
description = "Radarr provider configuration.";
};
};
};
# Python environment with pyarr for Servarr API operations and requests for
# Prowlarr, Bazarr, and health-check endpoints that pyarr doesn't cover.
pythonEnv = pkgs.python3.withPackages (
ps: with ps; [
pyarr
requests
]
);
# Build a JSON configuration blob for a Servarr instance, baked into the
# Python init script at Nix evaluation time.
mkInitConfig =
name: inst:
builtins.toJSON {
inherit name;
inherit (inst)
dataDir
port
apiVersion
apiTimeout
healthChecks
healthCheckRetries
healthCheckInterval
rootFolders
naming
;
downloadClients = map (dc: {
inherit (dc)
name
implementation
configContract
protocol
fields
;
}) inst.downloadClients;
syncedApps = map (app: {
inherit (app)
name
implementation
configContract
syncLevel
prowlarrUrl
baseUrl
apiKeyFrom
syncCategories
;
}) inst.syncedApps;
};
mkInitScript =
name: inst:
pkgs.writeScript "${name}-init" ''
#!${pythonEnv}/bin/python3
"""Declarative API initialization for ${name}.
Uses pyarr (SonarrAPI) for standard Servarr CRUD operations and the
requests library for Prowlarr-specific endpoints, health checks, and
download-client creation (which needs the forceSave query parameter
that pyarr does not expose).
"""
import json
import os
import re
import sys
import time
import requests as http
from pyarr import SonarrAPI
CONFIG = json.loads(${builtins.toJSON (mkInitConfig name inst)})
IMPLEMENTATION_CATEGORY_MAP = {
"Sonarr": "TV",
"Radarr": "Movies",
"Lidarr": "Audio",
"Readarr": "Books",
"Whisparr": "XXX",
}
def read_api_key(config_xml_path):
"""Extract <ApiKey> from a Servarr config.xml file."""
with open(config_xml_path) as fh:
content = fh.read()
match = re.search(r"<ApiKey>([^<]+)</ApiKey>", content)
if not match:
raise ValueError(f"Could not find ApiKey in {config_xml_path}")
return match.group(1)
def wait_for_api(base_url, api_key, timeout, name):
"""Poll the system/status endpoint until the API responds or timeout."""
print(f"Waiting for {name} API (timeout: {timeout}s)...")
for i in range(1, timeout + 1):
try:
resp = http.get(
f"{base_url}/system/status",
headers={"X-Api-Key": api_key},
timeout=5,
)
if resp.ok:
print(f"{name} API is ready")
return
except (http.ConnectionError, http.Timeout):
pass
if i == timeout:
print(
f"{name} API not available after {timeout} seconds",
file=sys.stderr,
)
sys.exit(1)
time.sleep(1)
def ensure_download_clients(client, base_url, api_key, download_clients):
"""Idempotently provision download clients."""
existing = client.get_download_client()
existing_names = {dc["name"] for dc in existing}
for dc in download_clients:
dc_name = dc["name"]
print(f"Checking download client '{dc_name}'...")
if dc_name in existing_names:
print(f"Download client '{dc_name}' already exists, skipping")
continue
print(f"Adding download client '{dc_name}'...")
payload = {
"enable": True,
"protocol": dc["protocol"],
"priority": 1,
"name": dc_name,
"implementation": dc["implementation"],
"configContract": dc["configContract"],
"fields": [
{"name": k, "value": v} for k, v in dc["fields"].items()
],
"tags": [],
}
resp = http.post(
f"{base_url}/downloadclient",
headers={
"X-Api-Key": api_key,
"Content-Type": "application/json",
},
params={"forceSave": "true"},
json=payload,
timeout=30,
)
resp.raise_for_status()
print(f"Download client '{dc_name}' added")
def ensure_root_folders(client, root_folders):
"""Idempotently provision root folders via pyarr."""
existing = client.get_root_folder()
existing_paths = {rf["path"] for rf in existing}
for path in root_folders:
print(f"Checking root folder '{path}'...")
if path in existing_paths:
print(f"Root folder '{path}' already exists, skipping")
continue
print(f"Adding root folder '{path}'...")
client.add_root_folder(path)
print(f"Root folder '{path}' added")
def resolve_sync_categories(base_url, api_key, implementation, explicit):
"""Resolve Newznab sync categories, auto-detecting from Prowlarr if needed."""
if explicit:
return explicit
category_name = IMPLEMENTATION_CATEGORY_MAP.get(implementation)
if not category_name:
return []
print(f"Auto-detecting sync categories for {implementation}...")
resp = http.get(
f"{base_url}/indexer/categories",
headers={"X-Api-Key": api_key},
timeout=30,
)
resp.raise_for_status()
sync_cats = []
for cat in resp.json():
if cat["name"] == category_name:
sync_cats.append(cat["id"])
for sub in cat.get("subCategories", []):
sync_cats.append(sub["id"])
if not sync_cats:
print(
f"Warning: could not auto-detect categories for "
f"'{category_name}', using empty list",
file=sys.stderr,
)
return []
print(f"Resolved sync categories: {sync_cats}")
return sync_cats
def ensure_synced_apps(base_url, api_key, synced_apps):
"""Idempotently provision synced applications (Prowlarr).
Uses requests directly because pyarr 5.x has no Prowlarr support.
"""
resp = http.get(
f"{base_url}/applications",
headers={"X-Api-Key": api_key},
timeout=30,
)
resp.raise_for_status()
existing_names = {app["name"] for app in resp.json()}
for app in synced_apps:
app_name = app["name"]
print(f"Checking synced app '{app_name}'...")
if app_name in existing_names:
print(f"Synced app '{app_name}' already exists, skipping")
continue
print(f"Adding synced app '{app_name}'...")
target_api_key = read_api_key(app["apiKeyFrom"])
sync_categories = resolve_sync_categories(
base_url,
api_key,
app["implementation"],
app.get("syncCategories", []),
)
payload = {
"name": app_name,
"implementation": app["implementation"],
"configContract": app["configContract"],
"syncLevel": app["syncLevel"],
"fields": [
{"name": "prowlarrUrl", "value": app["prowlarrUrl"]},
{"name": "baseUrl", "value": app["baseUrl"]},
{"name": "apiKey", "value": target_api_key},
{"name": "syncCategories", "value": sync_categories},
],
"tags": [],
}
resp = http.post(
f"{base_url}/applications",
headers={
"X-Api-Key": api_key,
"Content-Type": "application/json",
},
params={"forceSave": "true"},
json=payload,
timeout=30,
)
resp.raise_for_status()
print(f"Synced app '{app_name}' added")
def update_naming(client, naming_config):
"""Merge desired naming fields into the current config via pyarr."""
if not naming_config:
return
print("Checking naming configuration...")
current = client.get_config_naming()
needs_update = any(
naming_config.get(k) != current.get(k) for k in naming_config
)
if not needs_update:
print("Naming configuration already correct, skipping")
return
print("Updating naming configuration...")
merged = {**current, **naming_config}
client.upd_config_naming(merged)
print("Naming configuration updated")
def health_check_loop(url, api_key, entity_name, svc_name, max_retries, interval):
"""POST to a testall endpoint with retry logic."""
attempt = 0
while True:
healthy = True
last_error = ""
try:
resp = http.post(
url,
headers={
"X-Api-Key": api_key,
"Content-Type": "application/json",
},
timeout=30,
)
result = resp.json()
failures = [
item for item in result if not item.get("isValid", True)
]
if failures:
healthy = False
last_error = "\n".join(
f" - ID {f['id']}: "
+ ", ".join(
v["errorMessage"]
for v in f.get("validationFailures", [])
)
for f in failures
)
except Exception:
healthy = False
last_error = (
f"could not reach {svc_name} API for {entity_name} test"
)
if healthy:
print(f"All {entity_name}s healthy")
return
attempt += 1
if attempt > max_retries:
print(
f"Health check FAILED after {attempt} attempts: "
f"{entity_name}(s) unreachable:",
file=sys.stderr,
)
print(last_error, file=sys.stderr)
sys.exit(1)
print(
f"{entity_name.capitalize()} health check failed "
f"(attempt {attempt}/{max_retries}), "
f"retrying in {interval}s..."
)
time.sleep(interval)
def run_health_checks(base_url, api_key, name, cfg):
"""Run connectivity health checks if enabled."""
if not cfg["healthChecks"]:
return
print(f"Running {name} health checks...")
max_retries = cfg["healthCheckRetries"]
interval = cfg["healthCheckInterval"]
if cfg.get("downloadClients"):
print("Testing download client connectivity...")
health_check_loop(
f"{base_url}/downloadclient/testall",
api_key,
"download client",
name,
max_retries,
interval,
)
if cfg.get("syncedApps"):
print("Testing synced application connectivity...")
health_check_loop(
f"{base_url}/applications/testall",
api_key,
"synced application",
name,
max_retries,
interval,
)
print(f"{name} health checks passed")
def main():
name = CONFIG["name"]
data_dir = CONFIG["dataDir"]
port = CONFIG["port"]
api_version = CONFIG["apiVersion"]
api_timeout = CONFIG["apiTimeout"]
config_xml = f"{data_dir}/config.xml"
if not os.path.isfile(config_xml):
print(f"Config file {config_xml} not found, skipping {name} init")
return
api_key = read_api_key(config_xml)
base_url = f"http://127.0.0.1:{port}/api/{api_version}"
wait_for_api(base_url, api_key, api_timeout, name)
# pyarr client for standard Servarr operations (download clients,
# root folders, naming). SonarrAPI is used generically here because
# the relevant endpoints are identical across all Servarr applications
# and it accepts a custom ver_uri for API version selection.
client = SonarrAPI(
f"http://127.0.0.1:{port}", api_key, ver_uri=f"/{api_version}"
)
if CONFIG.get("downloadClients"):
ensure_download_clients(
client, base_url, api_key, CONFIG["downloadClients"]
)
if CONFIG.get("rootFolders"):
ensure_root_folders(client, CONFIG["rootFolders"])
if CONFIG.get("syncedApps"):
ensure_synced_apps(base_url, api_key, CONFIG["syncedApps"])
if CONFIG.get("naming"):
update_naming(client, CONFIG["naming"])
run_health_checks(base_url, api_key, name, CONFIG)
print(f"{name} init complete")
if __name__ == "__main__":
main()
'';
# Get list of service names that syncedApps depend on
getSyncedAppDeps = inst: map (app: "${app.serviceName}.service") inst.syncedApps;
# Get list of service names that download clients depend on
getDownloadClientDeps =
inst:
lib.concatMap (
dc: lib.optional (dc.serviceName != null) "${dc.serviceName}.service"
) inst.downloadClients;
enabledInstances = lib.filterAttrs (_: inst: inst.enable) cfg;
mkBazarrInitConfig = builtins.toJSON {
dataDir = bazarrCfg.dataDir;
port = bazarrCfg.port;
apiTimeout = bazarrCfg.apiTimeout;
providers =
{ }
// lib.optionalAttrs bazarrCfg.sonarr.enable {
sonarr = {
enable = true;
dataDir = bazarrCfg.sonarr.dataDir;
port = bazarrCfg.sonarr.port;
};
}
// lib.optionalAttrs bazarrCfg.radarr.enable {
radarr = {
enable = true;
dataDir = bazarrCfg.radarr.dataDir;
port = bazarrCfg.radarr.port;
};
};
};
mkBazarrInitScript = pkgs.writeScript "bazarr-init" ''
#!${pythonEnv}/bin/python3
"""Declarative API initialization for Bazarr provider connections.
Uses the requests library directly since Bazarr has its own API that
is not compatible with the Servarr/pyarr ecosystem.
"""
import json
import os
import re
import sys
import time
import requests as http
CONFIG = json.loads(${builtins.toJSON mkBazarrInitConfig})
def read_api_key_yaml(config_yaml_path):
"""Extract the apikey from Bazarr's config.yaml (auth section)."""
with open(config_yaml_path) as fh:
in_auth = False
for line in fh:
if line.strip().startswith("auth:"):
in_auth = True
elif in_auth and "apikey:" in line:
return line.split("apikey:")[-1].strip()
raise ValueError(f"Could not find apikey in {config_yaml_path}")
def read_api_key_xml(config_xml_path):
"""Extract <ApiKey> from a Servarr config.xml file."""
with open(config_xml_path) as fh:
content = fh.read()
match = re.search(r"<ApiKey>([^<]+)</ApiKey>", content)
if not match:
raise ValueError(f"Could not find ApiKey in {config_xml_path}")
return match.group(1)
def wait_for_api(base_url, api_key, timeout):
"""Poll Bazarr's system/status endpoint until available or timeout."""
print(f"Waiting for Bazarr API (timeout: {timeout}s)...")
for i in range(1, timeout + 1):
try:
resp = http.get(
f"{base_url}/api/system/status",
headers={"X-API-KEY": api_key},
timeout=5,
)
if resp.ok:
print("Bazarr API is ready")
return
except (http.ConnectionError, http.Timeout):
pass
if i == timeout:
print(
f"Bazarr API not available after {timeout} seconds",
file=sys.stderr,
)
sys.exit(1)
time.sleep(1)
def configure_provider(base_url, api_key, provider_type, provider_config):
"""Idempotently configure a Sonarr/Radarr provider in Bazarr."""
ltype = provider_type.lower()
print(f"Checking {provider_type} provider...")
resp = http.get(
f"{base_url}/api/system/settings",
headers={"X-API-KEY": api_key},
timeout=30,
)
resp.raise_for_status()
settings = resp.json()
use_flag = settings.get("general", {}).get(f"use_{ltype}", False)
existing_key = settings.get(ltype, {}).get("apikey", "")
if use_flag and existing_key:
print(f"{provider_type} provider already configured, skipping")
return
print(f"Adding {provider_type} provider...")
provider_api_key = read_api_key_xml(
f"{provider_config['dataDir']}/config.xml"
)
resp = http.post(
f"{base_url}/api/system/settings",
headers={"X-API-KEY": api_key},
data={
f"settings-general-use_{ltype}": "true",
f"settings-{ltype}-ip": "localhost",
f"settings-{ltype}-port": str(provider_config["port"]),
f"settings-{ltype}-apikey": provider_api_key,
f"settings-{ltype}-ssl": "false",
f"settings-{ltype}-base_url": "/",
},
timeout=30,
)
resp.raise_for_status()
print(f"{provider_type} provider added")
def main():
data_dir = CONFIG["dataDir"]
port = CONFIG["port"]
api_timeout = CONFIG["apiTimeout"]
config_yaml = f"{data_dir}/config/config.yaml"
if not os.path.isfile(config_yaml):
print(f"Config file {config_yaml} not found, skipping bazarr init")
return
api_key = read_api_key_yaml(config_yaml)
base_url = f"http://127.0.0.1:{port}"
wait_for_api(base_url, api_key, api_timeout)
providers = CONFIG.get("providers", {})
if providers.get("sonarr", {}).get("enable"):
configure_provider(base_url, api_key, "Sonarr", providers["sonarr"])
if providers.get("radarr", {}).get("enable"):
configure_provider(base_url, api_key, "Radarr", providers["radarr"])
print("Bazarr init complete")
if __name__ == "__main__":
main()
'';
bazarrDeps = [
"bazarr.service"
]
++ (lib.optional bazarrCfg.sonarr.enable "${bazarrCfg.sonarr.serviceName}.service")
++ (lib.optional bazarrCfg.radarr.enable "${bazarrCfg.radarr.serviceName}.service");
in
{
options.services.arrInit = lib.mkOption {
type = lib.types.attrsOf instanceModule;
default = { };
description = ''
Attribute set of Servarr application instances to initialize via their APIs.
Each instance generates a systemd oneshot service that idempotently configures
download clients, root folders, and synced applications.
'';
};
options.services.bazarrInit = lib.mkOption {
type = bazarrInitModule;
default = {
enable = false;
};
description = ''
Bazarr API initialization for connecting Sonarr and Radarr providers.
Bazarr uses a different API than Servarr applications, so it has its own module.
'';
};
config = lib.mkMerge [
(lib.mkIf (enabledInstances != { }) {
systemd.services = lib.mapAttrs' (
name: inst:
lib.nameValuePair "${inst.serviceName}-init" {
description = "Initialize ${name} API connections";
after = [
"${inst.serviceName}.service"
]
++ (getSyncedAppDeps inst)
++ (getDownloadClientDeps inst)
++ (lib.optional (inst.networkNamespacePath != null) "wg.service");
requires = [ "${inst.serviceName}.service" ] ++ (getDownloadClientDeps inst);
wantedBy = [ "multi-user.target" ];
unitConfig = {
# Allow 5 full retry cycles (apiTimeout + RestartSec each) before
# entering permanent failure, which is what triggers OnFailure=.
StartLimitIntervalSec = 5 * (inst.apiTimeout + 30);
StartLimitBurst = 5;
};
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
Restart = "on-failure";
RestartSec = 30;
ExecStart = "${mkInitScript name inst}";
}
// lib.optionalAttrs (inst.networkNamespacePath != null) {
NetworkNamespacePath = inst.networkNamespacePath;
};
}
) enabledInstances;
})
(lib.mkIf bazarrCfg.enable {
systemd.services.bazarr-init = {
description = "Initialize Bazarr API connections";
after = bazarrDeps;
requires = bazarrDeps;
wantedBy = [ "multi-user.target" ];
unitConfig = {
StartLimitIntervalSec = 5 * (bazarrCfg.apiTimeout + 30);
StartLimitBurst = 5;
};
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
Restart = "on-failure";
RestartSec = 30;
ExecStart = "${mkBazarrInitScript}";
};
};
})
];
}