Files
netfelix-audio-fix/server/db/index.ts
Felix Förtsch 874f04b7a5 wire scheduler into queue, add retry, dev-reset cleanup, biome 2.4 migrate
- execute: actually call isInScheduleWindow/waitForWindow/sleepBetweenJobs in runSequential (they were dead code); emit queue_status SSE events (running/paused/sleeping/idle) so the pipeline's existing QueueStatus listener lights up
- review: POST /:id/retry resets an errored plan to approved, wipes old done/error jobs, rebuilds command from current decisions, queues fresh job
- scan: dev-mode DELETE now also wipes jobs + subtitle_files (previously orphaned after every dev reset)
- biome: migrate config to 2.4 schema, autoformat 68 files (strings + indentation), relax opinionated a11y/hooks-deps/index-key rules that don't fit this codebase
- routeTree.gen.ts regenerated after /nodes removal
2026-04-13 07:41:19 +02:00

138 lines
4.7 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);
// 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 jobs ADD COLUMN job_type TEXT NOT NULL DEFAULT 'audio'");
} catch {
/* already exists */
}
// Apple compat pipeline columns
try {
_db.exec("ALTER TABLE review_plans ADD COLUMN confidence TEXT NOT NULL DEFAULT 'low'");
} catch {
/* already exists */
}
try {
_db.exec("ALTER TABLE review_plans ADD COLUMN apple_compat TEXT");
} catch {
/* already exists */
}
try {
_db.exec("ALTER TABLE review_plans ADD COLUMN job_type TEXT NOT NULL DEFAULT 'copy'");
} catch {
/* already exists */
}
try {
_db.exec("ALTER TABLE stream_decisions ADD COLUMN transcode_codec TEXT");
} 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<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;
}