/** * Persistent settings store. * * Settings are read from data/settings.json when present, with process.env * values used as fallbacks. This means existing .env.local setups keep working * with no changes; the UI just provides an alternative way to configure them. */ import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs"; import { join } from "path"; const DATA_DIR = join(process.cwd(), "data"); const SETTINGS_PATH = join(DATA_DIR, "settings.json"); export interface ServiceConfig { url: string; apiKey: string; } export interface DiscordConfig { webhookUrl: string; } export interface AppSettings { radarr: ServiceConfig; sonarr: ServiceConfig; seerr: ServiceConfig; tautulli: ServiceConfig; discord: DiscordConfig; } interface StoredSettings { radarr?: Partial; sonarr?: Partial; seerr?: Partial; tautulli?: Partial; discord?: Partial; } function readFile(): StoredSettings { try { return JSON.parse(readFileSync(SETTINGS_PATH, "utf-8")) as StoredSettings; } catch { return {}; } } /** Returns the merged settings (file values override env vars). */ export function getSettings(): AppSettings { const f = readFile(); return { radarr: { url: f.radarr?.url ?? process.env.RADARR_URL ?? "", apiKey: f.radarr?.apiKey ?? process.env.RADARR_API ?? "", }, sonarr: { url: f.sonarr?.url ?? process.env.SONARR_URL ?? "", apiKey: f.sonarr?.apiKey ?? process.env.SONARR_API ?? "", }, seerr: { url: f.seerr?.url ?? process.env.SEERR_URL ?? "", apiKey: f.seerr?.apiKey ?? process.env.SEERR_API ?? "", }, tautulli: { url: f.tautulli?.url ?? process.env.TAUTULLI_URL ?? "", apiKey: f.tautulli?.apiKey ?? process.env.TAUTULLI_API ?? "", }, discord: { webhookUrl: f.discord?.webhookUrl ?? process.env.DISCORD_WEBHOOK ?? "", }, }; } /** Returns true iff the three services required for the dashboard to function (Radarr, Sonarr, Overseerr/Jellyseerr) have both a URL and API key. */ export function isConfigured(s: AppSettings = getSettings()): boolean { const filled = (c: ServiceConfig) => c.url.trim() !== "" && c.apiKey.trim() !== ""; return filled(s.radarr) && filled(s.sonarr) && filled(s.seerr); } /** Saves the provided settings to disk and returns the merged result. */ export function saveSettings(settings: AppSettings): AppSettings { if (!existsSync(DATA_DIR)) mkdirSync(DATA_DIR, { recursive: true }); // Strip trailing slashes from URLs for consistency const clean: StoredSettings = { radarr: { url: settings.radarr.url.replace(/\/+$/, ""), apiKey: settings.radarr.apiKey }, sonarr: { url: settings.sonarr.url.replace(/\/+$/, ""), apiKey: settings.sonarr.apiKey }, seerr: { url: settings.seerr.url.replace(/\/+$/, ""), apiKey: settings.seerr.apiKey }, tautulli: { url: settings.tautulli.url.replace(/\/+$/, ""), apiKey: settings.tautulli.apiKey }, discord: { webhookUrl: settings.discord.webhookUrl.trim() }, }; writeFileSync(SETTINGS_PATH, JSON.stringify(clean, null, 2), "utf-8"); return getSettings(); }