Files
netfelix-audio-fix/server/api/settings.ts
Felix Förtsch 94a460be9d
All checks were successful
Build and Push Docker Image / build (push) Successful in 36s
rename setup → settings throughout; persist arr creds even on test failure
Two cleanups:

1. Rename the page from 'Setup' to 'Settings' all the way down. The H1
   already said Settings; the file/component/api path were lying.
   - src/features/setup/ → src/features/settings/
   - SetupPage.tsx → SettingsPage.tsx, SetupPage → SettingsPage,
     SetupData → SettingsData, setupCache → settingsCache
   - server/api/setup.ts → server/api/settings.ts
   - /api/setup → /api/settings (only consumer is our frontend)
   - server/index.tsx import + route mount renamed
   - ScanPage's local setupChecked → configChecked

2. Sonarr (and Radarr) save flow: persist the values BEFORE running the
   connection test. The previous code returned early if the test failed,
   silently dropping what the user typed — explained the user's report
   that Sonarr 'forgets' the input. Now setConfig fires unconditionally
   on a valid (non-empty) URL+key; the test result is returned as
   { ok, saved, testError } so the UI can show 'Saved & connected' on
   success or '⚠ Saved, but connection test failed: …' on failure
   instead of erasing the input.

Note: setup_complete config key kept as-is — it represents 'has the user
configured Jellyfin' which is conceptually setup and not user-visible.
2026-04-13 12:26:30 +02:00

110 lines
3.6 KiB
TypeScript

import { Hono } from "hono";
import { getAllConfig, getDb, getEnvLockedKeys, setConfig } from "../db/index";
import { getUsers, testConnection as testJellyfin } from "../services/jellyfin";
import { testConnection as testRadarr } from "../services/radarr";
import { testConnection as testSonarr } from "../services/sonarr";
const app = new Hono();
app.get("/", (c) => {
const config = getAllConfig();
const envLocked = Array.from(getEnvLockedKeys());
return c.json({ config, envLocked });
});
app.post("/jellyfin", async (c) => {
const body = await c.req.json<{ url: string; api_key: string }>();
const url = body.url?.replace(/\/$/, "");
const apiKey = body.api_key;
if (!url || !apiKey) return c.json({ ok: false, error: "URL and API key are required" }, 400);
const result = await testJellyfin({ url, apiKey });
if (!result.ok) return c.json({ ok: false, error: result.error });
setConfig("jellyfin_url", url);
setConfig("jellyfin_api_key", apiKey);
setConfig("setup_complete", "1");
try {
const users = await getUsers({ url, apiKey });
const admin = users.find((u) => u.Name === "admin") ?? users[0];
if (admin?.Id) setConfig("jellyfin_user_id", admin.Id);
} catch {
/* ignore */
}
return c.json({ ok: true });
});
// Persist values BEFORE testing the connection. The previous behaviour
// silently dropped what the user typed when the test failed (e.g. Sonarr
// not yet reachable), making the field appear to "forget" the input on
// reload. Save first, surface the test result as a warning the UI can show.
app.post("/radarr", async (c) => {
const body = await c.req.json<{ url?: string; api_key?: string }>();
const url = body.url?.replace(/\/$/, "");
const apiKey = body.api_key;
if (!url || !apiKey) {
setConfig("radarr_enabled", "0");
return c.json({ ok: false, error: "URL and API key are required" }, 400);
}
setConfig("radarr_url", url);
setConfig("radarr_api_key", apiKey);
setConfig("radarr_enabled", "1");
const result = await testRadarr({ url, apiKey });
return c.json({ ok: result.ok, saved: true, testError: result.ok ? undefined : result.error });
});
app.post("/sonarr", async (c) => {
const body = await c.req.json<{ url?: string; api_key?: string }>();
const url = body.url?.replace(/\/$/, "");
const apiKey = body.api_key;
if (!url || !apiKey) {
setConfig("sonarr_enabled", "0");
return c.json({ ok: false, error: "URL and API key are required" }, 400);
}
setConfig("sonarr_url", url);
setConfig("sonarr_api_key", apiKey);
setConfig("sonarr_enabled", "1");
const result = await testSonarr({ url, apiKey });
return c.json({ ok: result.ok, saved: true, testError: result.ok ? undefined : result.error });
});
app.post("/subtitle-languages", async (c) => {
const body = await c.req.json<{ langs: string[] }>();
if (body.langs?.length > 0) {
setConfig("subtitle_languages", JSON.stringify(body.langs));
}
return c.json({ ok: true });
});
app.post("/audio-languages", async (c) => {
const body = await c.req.json<{ langs: string[] }>();
setConfig("audio_languages", JSON.stringify(body.langs ?? []));
return c.json({ ok: true });
});
app.post("/clear-scan", (c) => {
const db = getDb();
// Delete children first to avoid slow cascade deletes
db.transaction(() => {
db.prepare("DELETE FROM stream_decisions").run();
db.prepare("DELETE FROM jobs").run();
db.prepare("DELETE FROM subtitle_files").run();
db.prepare("DELETE FROM review_plans").run();
db.prepare("DELETE FROM media_streams").run();
db.prepare("DELETE FROM media_items").run();
db.prepare("UPDATE config SET value = '0' WHERE key = 'scan_running'").run();
})();
return c.json({ ok: true });
});
export default app;