import { Database } from 'bun:sqlite'; import { join } from 'node:path'; import { mkdirSync } from 'node:fs'; import { SCHEMA, DEFAULT_CONFIG } 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', subtitle_languages: 'SUBTITLE_LANGUAGES', }; /** 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 === 'subtitle_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); // Migrations for columns added after initial release try { _db.exec('ALTER TABLE stream_decisions ADD COLUMN custom_title TEXT'); } catch { /* already exists */ } try { _db.exec('ALTER TABLE review_plans ADD COLUMN subs_extracted INTEGER NOT NULL DEFAULT 0'); } catch { /* already exists */ } try { _db.exec("ALTER TABLE nodes ADD COLUMN movies_path TEXT NOT NULL DEFAULT ''"); } catch { /* already exists */ } try { _db.exec("ALTER TABLE nodes ADD COLUMN series_path TEXT NOT NULL DEFAULT ''"); } catch { /* already exists */ } seedDefaults(_db); return _db; } 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); } } 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; }