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
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:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "netfelix-audio-fix",
|
||||
"version": "2026.04.14.6",
|
||||
"version": "2026.04.14.7",
|
||||
"scripts": {
|
||||
"dev:server": "NODE_ENV=development bun --hot server/index.tsx",
|
||||
"dev:client": "vite",
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -298,28 +298,12 @@ export function MqttSection({ cfg, locked }: { cfg: Record<string, string>; lock
|
||||
<div className={testResult.brokerConnected ? "text-green-700" : "text-red-700"}>
|
||||
{testResult.brokerConnected ? "✓" : "✗"} Broker reachable & credentials accepted
|
||||
</div>
|
||||
{/* Step 2: jellyfin rescan kicked off */}
|
||||
{/* Step 2: webhook received */}
|
||||
{testResult.brokerConnected && (
|
||||
<div
|
||||
className={
|
||||
testResult.jellyfinTriggered
|
||||
? "text-green-700"
|
||||
: testResult.error?.startsWith("jellyfin")
|
||||
? "text-red-700"
|
||||
: "text-gray-500"
|
||||
}
|
||||
>
|
||||
{testResult.jellyfinTriggered
|
||||
? `✓ Triggered Jellyfin rescan on "${testResult.itemName}"`
|
||||
: "— No Jellyfin items to rescan yet (run a library scan first, then retry)"}
|
||||
</div>
|
||||
)}
|
||||
{/* Step 3: webhook landed */}
|
||||
{testResult.brokerConnected && testResult.jellyfinTriggered && (
|
||||
<div className={testResult.receivedMessage ? "text-green-700" : "text-amber-700"}>
|
||||
{testResult.receivedMessage
|
||||
? `✓ Received matching webhook on topic "${topic || "jellyfin/events"}"`
|
||||
: "⚠ No webhook message arrived within the timeout. Check the Jellyfin Webhook plugin destination: events include Item Added, Item Type filter covers Movies/Episodes, MQTT server/topic match the values shown below, and the Jellyfin host can reach the broker."}
|
||||
? `✓ Received webhook on topic "${topic || "jellyfin/events"}" — the loop is closed.`
|
||||
: "⚠ No webhook in 30s. Start playing any movie or episode in Jellyfin (or add/edit an item) while the test is running. If that still produces nothing: verify the plugin destination is Enabled, Notification Type includes Playback Start (and Item Added), Item Type covers Movies/Episodes, and the Jellyfin host can reach this broker."}
|
||||
</div>
|
||||
)}
|
||||
{testResult.samplePayload && (
|
||||
@@ -358,12 +342,14 @@ export function MqttSection({ cfg, locked }: { cfg: Record<string, string>; lock
|
||||
|
||||
<div className="text-[11px] font-semibold text-gray-500 uppercase mt-3 mb-1">Generic destination</div>
|
||||
<SetupValue label="Webhook Name" value="Audio Fix" mono={false} />
|
||||
<SetupValue
|
||||
label="Webhook Url"
|
||||
value={`${broker.useTls ? "mqtts" : "mqtt"}://${broker.host || "broker.lan"}:${broker.port}`}
|
||||
/>
|
||||
<SetupValue label="Webhook Url" value={url || "(your broker URL)"} />
|
||||
<SetupValue label="Status" value="Enabled" mono={false} copyable={false} />
|
||||
<SetupValue label="Notification Type" value="Item Added" mono={false} copyable={false} />
|
||||
<SetupValue
|
||||
label="Notification Type"
|
||||
value="Item Added, Playback Start"
|
||||
mono={false}
|
||||
copyable={false}
|
||||
/>
|
||||
<SetupValue label="Item Type" value="Movies, Episodes" mono={false} copyable={false} />
|
||||
|
||||
<div className="text-[11px] font-semibold text-gray-500 uppercase mt-3 mb-1">MQTT settings</div>
|
||||
@@ -389,10 +375,11 @@ export function MqttSection({ cfg, locked }: { cfg: Record<string, string>; lock
|
||||
<SetupValue label="Template" value={HANDLEBARS_TEMPLATE} />
|
||||
</div>
|
||||
<p className="text-[11px] text-gray-400 mt-3">
|
||||
Notes: "Server Url" is Jellyfin's own base URL (used for rendered links in notification templates). "Webhook Url"
|
||||
is required by the form even for MQTT destinations, which ignore it — we fill it with the broker URL so
|
||||
validation passes. Jellyfin doesn't expose an "Item Updated" event — any file change fires{" "}
|
||||
<span className="font-mono">Item Added</span>, which is what we listen for.
|
||||
Notes: "Server Url" is Jellyfin's own base URL (used for rendered links). "Webhook Url" is required by
|
||||
the form even for MQTT destinations; paste your broker URL so validation passes. We subscribe to{" "}
|
||||
<span className="font-mono">Item Added</span> for the real ping-pong (post-ffmpeg file mutations show up
|
||||
as new adds in Jellyfin) and also enable <span className="font-mono">Playback Start</span> — it's a
|
||||
reliable trigger for the Test button and is ignored by the production handler.
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
|
||||
Reference in New Issue
Block a user