diff --git a/package.json b/package.json index bfd7b57..7c8a603 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/server/api/settings.ts b/server/api/settings.ts index c8ee4b2..1f69a91 100644 --- a/server/api/settings.ts +++ b/server/api/settings.ts @@ -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); diff --git a/server/services/mqtt.ts b/server/services/mqtt.ts index 5a16a7b..b6c2678 100644 --- a/server/services/mqtt.ts +++ b/server/services/mqtt.ts @@ -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) => { diff --git a/src/features/settings/MqttSection.tsx b/src/features/settings/MqttSection.tsx index 1e2cc33..a8ef654 100644 --- a/src/features/settings/MqttSection.tsx +++ b/src/features/settings/MqttSection.tsx @@ -298,28 +298,12 @@ export function MqttSection({ cfg, locked }: { cfg: Record; lock
{testResult.brokerConnected ? "✓" : "✗"} Broker reachable & credentials accepted
- {/* Step 2: jellyfin rescan kicked off */} + {/* Step 2: webhook received */} {testResult.brokerConnected && ( -
- {testResult.jellyfinTriggered - ? `✓ Triggered Jellyfin rescan on "${testResult.itemName}"` - : "— No Jellyfin items to rescan yet (run a library scan first, then retry)"} -
- )} - {/* Step 3: webhook landed */} - {testResult.brokerConnected && testResult.jellyfinTriggered && (
{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."}
)} {testResult.samplePayload && ( @@ -358,12 +342,14 @@ export function MqttSection({ cfg, locked }: { cfg: Record; lock
Generic destination
- + - +
MQTT settings
@@ -389,10 +375,11 @@ export function MqttSection({ cfg, locked }: { cfg: Record; lock

- 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{" "} - Item Added, 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{" "} + Item Added for the real ping-pong (post-ffmpeg file mutations show up + as new adds in Jellyfin) and also enable Playback Start — it's a + reliable trigger for the Test button and is ignored by the production handler.