All checks were successful
Build and Push Docker Image / build (push) Successful in 1m54s
- install ffmpeg in dockerfile (fixes exit code 127) - buildCommand() now audio-only remux, no subtitle extraction - add unapprove endpoint + ui button for approved items - add batch extract-all subtitles endpoint + ui button - audio detail page shows only video+audio streams - remove global movies_path/series_path config, add per-node path mapping - remove docker-in-docker command building (buildDockerCommand, buildDockerExtractOnlyCommand) - ssh execution translates /movies/ and /series/ to node-specific paths - remove media paths section from setup page - add unraid-template.xml Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
111 lines
4.2 KiB
TypeScript
111 lines
4.2 KiB
TypeScript
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<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',
|
|
};
|
|
|
|
/** 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<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;
|
|
}
|