mqtt test: use playback start as reliable trigger, drop auto-prefix
All checks were successful
Build and Push Docker Image / build (push) Successful in 48s

two fixes based on actual behavior of the jellyfin webhook plugin:

- 'Webhook Url' setup value no longer re-serialized with mqtt://. show
  the user's broker url verbatim so whatever protocol they use (ws://,
  http://, etc.) survives the round trip
- dropped the server-side 'trigger a jellyfin rescan during the test'
  machinery. a refresh that doesn't mutate metadata won't fire Item
  Added, so relying on it produced false negatives. now we just wait
  for any message on the topic; ui instructs the user to hit play on a
  movie in jellyfin while the test runs — playback start is a
  deterministic trigger, unlike library events
- setup panel now lists Notification Types as 'Item Added, Playback
  Start'. playback start is for the test only; the production handler
  still filters events down to item added / updated
This commit is contained in:
2026-04-14 09:55:32 +02:00
parent 7b138f4346
commit 425ee751ce
4 changed files with 27 additions and 62 deletions

View File

@@ -1,6 +1,6 @@
import { Hono } from "hono";
import { getAllConfig, getConfig, getDb, getEnvLockedKeys, reseedDefaults, setConfig } from "../db/index";
import { getUsers, refreshItem, testConnection as testJellyfin } from "../services/jellyfin";
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";
@@ -145,29 +145,13 @@ app.post("/mqtt/test", async (c) => {
const topic = (body.topic ?? "jellyfin/events").trim() || "jellyfin/events";
const password = body.password || getConfig("mqtt_password") || "";
const jellyfinUrl = getConfig("jellyfin_url") ?? "";
const jellyfinApiKey = getConfig("jellyfin_api_key") ?? "";
const jellyfinUserId = getConfig("jellyfin_user_id") ?? "";
const jfCfg = { url: jellyfinUrl, apiKey: jellyfinApiKey, userId: jellyfinUserId };
const triggerRefresh = async (): Promise<{ itemId: string; itemName: string } | null> => {
if (!jellyfinUrl || !jellyfinApiKey) return null;
// Grab any scanned movie/episode as the trigger target. Random ordering
// so repeated tests don't keep hammering the same item.
const row = getDb()
.prepare("SELECT jellyfin_id, name FROM media_items WHERE type IN ('Movie', 'Episode') ORDER BY RANDOM() LIMIT 1")
.get() as { jellyfin_id: string; name: string } | undefined;
if (!row) return null;
// Fire-and-poll: refreshItem blocks until Jellyfin finishes, which we
// don't want here — the webhook fires during the refresh itself.
// Kick it off in the background; the mqtt subscriber catches the event.
refreshItem(jfCfg, row.jellyfin_id, 2_000).catch(() => {});
return { itemId: row.jellyfin_id, itemName: row.name };
};
// 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 },
triggerRefresh,
async () => null,
30_000,
);
return c.json(result);

View File

@@ -192,17 +192,11 @@ export async function testMqttConnection(
});
c.on("message", (_topic, payload) => {
const raw = payload.toString("utf8");
// If we're waiting for a specific itemId, only accept matches.
if (expectedItemId) {
try {
const parsed = JSON.parse(raw) as { itemId?: string };
if (parsed.itemId !== expectedItemId) return;
} catch {
return;
}
}
done({ receivedMessage: true, samplePayload: raw.slice(0, 400) });
// Any message on the configured topic is enough — a rescan of an
// unchanged item won't fire Item Added, so the "itemId matches"
// filter would cause false failures. The user triggers real
// activity in Jellyfin if the auto-rescan doesn't wake anything.
done({ receivedMessage: true, samplePayload: payload.toString("utf8").slice(0, 400) });
});
c.on("error", (err) => {