Some checks failed
Build and Push Docker Image / build (push) Has been cancelled
the existing clear-scan button only drops media_items + related; settings survived. useful when schema changes or corrupt state make you want a full do-over on a running container without ssh-ing in to rm data/netfelix.db. POST /api/settings/reset truncates everything (config included) then re-seeds DEFAULT_CONFIG via the exported reseedDefaults helper. env-var overrides keep working through getConfig's env fallback. ui lives next to clear-scan in the danger zone with a double confirm and reload to /, so the setup wizard shows. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
145 lines
5.6 KiB
TypeScript
145 lines
5.6 KiB
TypeScript
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<string, string> = {
|
|
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",
|
|
audio_languages: "AUDIO_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" || 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);
|
|
applyMigrations(_db);
|
|
seedDefaults(_db);
|
|
return _db;
|
|
}
|
|
|
|
/**
|
|
* Add columns that landed after the initial schema. `CREATE TABLE IF NOT
|
|
* EXISTS` above skips existing tables, so upgraded installs need per-column
|
|
* ALTERs to pick up new fields. Each call is wrapped in try/catch because
|
|
* SQLite has no native `ADD COLUMN IF NOT EXISTS` — the error is benign.
|
|
*
|
|
* Do NOT remove entries here just because the schema block also declares the
|
|
* column. Fresh installs get it from SCHEMA; existing deployments (e.g. the
|
|
* Unraid container with a persistent volume) only get it from this function.
|
|
*/
|
|
function applyMigrations(db: Database): void {
|
|
const addColumn = (table: string, column: string, type: string) => {
|
|
try {
|
|
db.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${type}`);
|
|
} catch {
|
|
/* already exists */
|
|
}
|
|
};
|
|
|
|
// 2026-04-13: canonical-language + full-jellyfin-capture rewrite
|
|
addColumn("media_items", "original_title", "TEXT");
|
|
addColumn("media_items", "runtime_ticks", "INTEGER");
|
|
addColumn("media_items", "date_last_refreshed", "TEXT");
|
|
addColumn("media_items", "jellyfin_raw", "TEXT");
|
|
addColumn("media_items", "external_raw", "TEXT");
|
|
addColumn("media_items", "last_executed_at", "TEXT");
|
|
addColumn("media_streams", "profile", "TEXT");
|
|
addColumn("media_streams", "bit_depth", "INTEGER");
|
|
|
|
// Earlier migrations kept here so installs that predate the big rewrite
|
|
// still converge to the current schema.
|
|
addColumn("stream_decisions", "custom_title", "TEXT");
|
|
addColumn("stream_decisions", "transcode_codec", "TEXT");
|
|
addColumn("review_plans", "subs_extracted", "INTEGER NOT NULL DEFAULT 0");
|
|
addColumn("review_plans", "confidence", "TEXT NOT NULL DEFAULT 'low'");
|
|
addColumn("review_plans", "apple_compat", "TEXT");
|
|
addColumn("review_plans", "job_type", "TEXT NOT NULL DEFAULT 'copy'");
|
|
}
|
|
|
|
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<string> {
|
|
const locked = new Set<string>();
|
|
for (const key of Object.keys(ENV_MAP)) {
|
|
if (envValue(key) !== null) locked.add(key);
|
|
}
|
|
return locked;
|
|
}
|
|
|
|
export function getAllConfig(): Record<string, string> {
|
|
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;
|
|
}
|