remove jellyfin, mqtt, webhook services, fix tests, add schema migrations
- delete server/services/jellyfin.ts, webhook.ts, mqtt.ts and their tests - strip jellyfin/mqtt imports and startup calls from index.tsx and settings.ts - remove /jellyfin, /mqtt, /mqtt/status, /mqtt/test, /jellyfin/webhook-plugin endpoints from settings router - clean ENV_MAP and isEnvConfigured of jellyfin/mqtt keys - add db/index.ts migrations for series_key, duration_seconds, scan_status, scan_error, last_scanned_at (new columns absent on older dev DBs) - move idx_media_items_series_key out of SCHEMA into migrate() so it runs after the column is added - fix all test fixtures: drop jellyfin_id/series_jellyfin_id column refs, update MediaItem/MediaStream object literals to match current types Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+3
-112
@@ -1,7 +1,5 @@
|
||||
import { Hono } from "hono";
|
||||
import { getAllConfig, getConfig, getDb, getEnvLockedKeys, reseedDefaults, setConfig } from "../db/index";
|
||||
import { getUsers, testConnection as testJellyfin } from "../services/jellyfin";
|
||||
import { getMqttStatus, startMqttClient, testMqttConnection } from "../services/mqtt";
|
||||
import { testConnection as testRadarr } from "../services/radarr";
|
||||
import { getScheduleConfig, type ScheduleConfig, updateScheduleConfig } from "../services/scheduler";
|
||||
import { testConnection as testSonarr } from "../services/sonarr";
|
||||
@@ -11,7 +9,7 @@ const app = new Hono();
|
||||
// Config keys that hold credentials. `GET /` returns these as "***" when set,
|
||||
// "" when unset. Real values only reach the client via the explicit
|
||||
// GET /reveal?key=<key> endpoint (eye-icon toggle in the settings UI).
|
||||
const SECRET_KEYS = new Set(["jellyfin_api_key", "radarr_api_key", "sonarr_api_key", "mqtt_password"]);
|
||||
const SECRET_KEYS = new Set(["radarr_api_key", "sonarr_api_key"]);
|
||||
|
||||
app.get("/", (c) => {
|
||||
const config = getAllConfig();
|
||||
@@ -35,39 +33,6 @@ function resolveSecret(incoming: string | undefined, storedKey: string): string
|
||||
return incoming ?? "";
|
||||
}
|
||||
|
||||
app.post("/jellyfin", async (c) => {
|
||||
const body = await c.req.json<{ url: string; api_key: string }>();
|
||||
const url = body.url?.replace(/\/$/, "");
|
||||
const apiKey = resolveSecret(body.api_key, "jellyfin_api_key");
|
||||
|
||||
if (!url || !apiKey) return c.json({ ok: false, error: "URL and API key are required" }, 400);
|
||||
|
||||
// Save first so the user's input is never silently dropped on a test
|
||||
// failure (matches the Radarr/Sonarr pattern). The frontend reads the
|
||||
// { ok, saved, testError } shape to decide what message to show.
|
||||
setConfig("jellyfin_url", url);
|
||||
setConfig("jellyfin_api_key", apiKey);
|
||||
|
||||
const result = await testJellyfin({ url, apiKey });
|
||||
|
||||
// Only mark setup complete when the connection actually works. Setting
|
||||
// setup_complete=1 on a failing test would let the user click past the
|
||||
// wizard into an app that then dies on the first Jellyfin call.
|
||||
if (result.ok) {
|
||||
setConfig("setup_complete", "1");
|
||||
// Best-effort admin discovery only when the connection works; ignore failures.
|
||||
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: result.ok, saved: true, testError: result.ok ? undefined : result.error });
|
||||
});
|
||||
|
||||
// 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
|
||||
@@ -169,80 +134,6 @@ app.patch("/schedule", async (c) => {
|
||||
return c.json(getScheduleConfig());
|
||||
});
|
||||
|
||||
// ─── MQTT ────────────────────────────────────────────────────────────────────
|
||||
|
||||
app.post("/mqtt", async (c) => {
|
||||
const body = await c.req.json<{
|
||||
enabled?: boolean;
|
||||
url?: string;
|
||||
topic?: string;
|
||||
username?: string;
|
||||
password?: string;
|
||||
}>();
|
||||
const enabled = body.enabled === true;
|
||||
const url = (body.url ?? "").trim();
|
||||
const topic = (body.topic ?? "jellyfin/events").trim();
|
||||
const username = (body.username ?? "").trim();
|
||||
const password = body.password ?? "";
|
||||
|
||||
setConfig("mqtt_enabled", enabled ? "1" : "0");
|
||||
setConfig("mqtt_url", url);
|
||||
setConfig("mqtt_topic", topic || "jellyfin/events");
|
||||
setConfig("mqtt_username", username);
|
||||
// Only overwrite password when a real value is sent. The UI leaves the
|
||||
// field blank or sends "***" (masked placeholder) when the user didn't
|
||||
// touch it — both mean "keep the existing one".
|
||||
if (password && password !== "***") setConfig("mqtt_password", password);
|
||||
|
||||
// Reconnect with the new config. Best-effort; failures surface in status.
|
||||
startMqttClient().catch(() => {});
|
||||
|
||||
return c.json({ ok: true, saved: true });
|
||||
});
|
||||
|
||||
app.get("/mqtt/status", (c) => {
|
||||
return c.json(getMqttStatus());
|
||||
});
|
||||
|
||||
app.post("/mqtt/test", async (c) => {
|
||||
const body = await c.req.json<{ url?: string; topic?: string; username?: string; password?: string }>();
|
||||
const url = (body.url ?? "").trim();
|
||||
if (!url) return c.json({ ok: false, error: "Broker URL required" }, 400);
|
||||
const topic = (body.topic ?? "jellyfin/events").trim() || "jellyfin/events";
|
||||
const password = body.password || getConfig("mqtt_password") || "";
|
||||
|
||||
// The user triggers real activity in Jellyfin (start playback / add an
|
||||
// item) while the test runs — a blind metadata refresh from here often
|
||||
// doesn't fire any webhook (the plugin only emits Item Added on actual
|
||||
// additions, which a no-op refresh isn't).
|
||||
const result = await testMqttConnection(
|
||||
{ url, topic, username: (body.username ?? "").trim(), password },
|
||||
async () => null,
|
||||
30_000,
|
||||
);
|
||||
return c.json(result);
|
||||
});
|
||||
|
||||
/**
|
||||
* Returns whether Jellyfin has the Webhook plugin installed. The Settings
|
||||
* panel uses this to decide between "setup steps" vs "install this plugin".
|
||||
*/
|
||||
app.get("/jellyfin/webhook-plugin", async (c) => {
|
||||
const url = getConfig("jellyfin_url");
|
||||
const apiKey = getConfig("jellyfin_api_key");
|
||||
if (!url || !apiKey) return c.json({ ok: false, error: "Jellyfin not configured" }, 400);
|
||||
|
||||
try {
|
||||
const res = await fetch(`${url}/Plugins`, { headers: { "X-Emby-Token": apiKey } });
|
||||
if (!res.ok) return c.json({ ok: false, error: `HTTP ${res.status}` }, 502);
|
||||
const plugins = (await res.json()) as { Name?: string; Id?: string; Version?: string }[];
|
||||
const hit = plugins.find((p) => typeof p.Name === "string" && p.Name.toLowerCase().includes("webhook"));
|
||||
return c.json({ ok: true, installed: !!hit, plugin: hit ?? null });
|
||||
} catch (err) {
|
||||
return c.json({ ok: false, error: String(err) }, 502);
|
||||
}
|
||||
});
|
||||
|
||||
app.post("/clear-scan", (c) => {
|
||||
const db = getDb();
|
||||
// Delete children first to avoid slow cascade deletes
|
||||
@@ -260,8 +151,8 @@ app.post("/clear-scan", (c) => {
|
||||
/**
|
||||
* Full factory reset. Truncates every table including config, re-seeds the
|
||||
* defaults so the setup wizard reappears, and returns. Env-backed config
|
||||
* keys (JELLYFIN_URL, etc.) continue to resolve via getConfig's env fallback
|
||||
* — they don't live in the DB to begin with.
|
||||
* keys continue to resolve via getConfig's env fallback — they don't live
|
||||
* in the DB to begin with.
|
||||
*/
|
||||
app.post("/reset", (c) => {
|
||||
const db = getDb();
|
||||
|
||||
Reference in New Issue
Block a user