import { Database } from "bun:sqlite"; import { mkdirSync } from "node:fs"; import { join } from "node:path"; import { DEFAULT_CONFIG, SCHEMA } from "./schema"; const dataDir = process.env.DATA_DIR ?? "./data"; mkdirSync(dataDir, { recursive: true }); const isDev = process.env.NODE_ENV === "development"; const dbPath = join(dataDir, isDev ? "netfelix-dev.db" : "netfelix.db"); // ─── Env-var → config key mapping ───────────────────────────────────────────── const ENV_MAP: Record = { jellyfin_url: "JELLYFIN_URL", jellyfin_api_key: "JELLYFIN_API_KEY", jellyfin_user_id: "JELLYFIN_USER_ID", radarr_url: "RADARR_URL", radarr_api_key: "RADARR_API_KEY", radarr_enabled: "RADARR_ENABLED", sonarr_url: "SONARR_URL", sonarr_api_key: "SONARR_API_KEY", sonarr_enabled: "SONARR_ENABLED", audio_languages: "AUDIO_LANGUAGES", mqtt_enabled: "MQTT_ENABLED", mqtt_url: "MQTT_URL", mqtt_topic: "MQTT_TOPIC", mqtt_username: "MQTT_USERNAME", mqtt_password: "MQTT_PASSWORD", }; /** Read a config key from environment variables (returns null if not set). */ function envValue(key: string): string | null { const envKey = ENV_MAP[key]; if (!envKey) return null; const val = process.env[envKey]; if (!val) return null; if (key.endsWith("_enabled")) return val === "1" || val.toLowerCase() === "true" ? "1" : "0"; if (key === "audio_languages") return JSON.stringify(val.split(",").map((s) => s.trim())); if (key.endsWith("_url")) return val.replace(/\/$/, ""); return val; } /** True when minimum required Jellyfin env vars are present — skips the setup wizard. */ function isEnvConfigured(): boolean { return !!(process.env.JELLYFIN_URL && process.env.JELLYFIN_API_KEY); } // ─── Database ────────────────────────────────────────────────────────────────── let _db: Database | null = null; export function getDb(): Database { if (_db) return _db; _db = new Database(dbPath, { create: true }); _db.exec(SCHEMA); migrate(_db); seedDefaults(_db); return _db; } /** * Idempotent ALTER TABLE migrations for columns added after the initial * CREATE TABLE ships. Each block swallows "duplicate column" errors so the * same code path is safe on fresh and existing databases. Do not remove old * migrations — databases in the wild may be several versions behind. */ function migrate(db: Database): void { const alter = (sql: string) => { try { db.exec(sql); } catch (_err) { // column already present — ignore } }; alter("ALTER TABLE review_plans ADD COLUMN webhook_verified INTEGER NOT NULL DEFAULT 0"); // 2026-04-14: renamed webhook_verified → verified once we realized the // signal would come from our own ffprobe, not from a Jellyfin webhook. // RENAME COLUMN preserves values; both alters are no-ops on fresh DBs. alter("ALTER TABLE review_plans RENAME COLUMN webhook_verified TO verified"); alter("ALTER TABLE review_plans DROP COLUMN verified"); alter("ALTER TABLE media_items ADD COLUMN ingest_source TEXT NOT NULL DEFAULT 'scan'"); } function seedDefaults(db: Database): void { const insert = db.prepare("INSERT OR IGNORE INTO config (key, value) VALUES (?, ?)"); for (const [key, value] of Object.entries(DEFAULT_CONFIG)) { insert.run(key, value); } } /** Re-seed config defaults after a truncating reset. Caller owns the delete. */ export function reseedDefaults(): void { seedDefaults(getDb()); } export function getConfig(key: string): string | null { // Env vars take precedence over DB const fromEnv = envValue(key); if (fromEnv !== null) return fromEnv; // Auto-complete setup when all required Jellyfin env vars are present if (key === "setup_complete" && isEnvConfigured()) return "1"; const row = getDb().prepare("SELECT value FROM config WHERE key = ?").get(key) as { value: string } | undefined; return row?.value ?? null; } export function setConfig(key: string, value: string): void { getDb().prepare("INSERT OR REPLACE INTO config (key, value) VALUES (?, ?)").run(key, value); } /** Returns the set of config keys currently overridden by environment variables. */ export function getEnvLockedKeys(): Set { const locked = new Set(); for (const key of Object.keys(ENV_MAP)) { if (envValue(key) !== null) locked.add(key); } return locked; } export function getAllConfig(): Record { const rows = getDb().prepare("SELECT key, value FROM config").all() as { key: string; value: string }[]; const result = Object.fromEntries(rows.map((r) => [r.key, r.value ?? ""])); // Apply env overrides on top of DB values for (const key of Object.keys(ENV_MAP)) { const fromEnv = envValue(key); if (fromEnv !== null) result[key] = fromEnv; } // Auto-complete setup when all required Jellyfin env vars are present if (isEnvConfigured()) result.setup_complete = "1"; return result; }