diff --git a/server/routes/webhook.js b/server/routes/webhook.js index 483c27c..0dce65f 100644 --- a/server/routes/webhook.js +++ b/server/routes/webhook.js @@ -43,9 +43,11 @@ function pruneReplayCache() { } } +// Prune the replay cache once per minute +setInterval(pruneReplayCache, 60 * 1000).unref(); + function isReplay(eventType, instanceName, eventDate) { if (!eventDate) return false; - pruneReplayCache(); const key = `${eventType}:${instanceName || ''}:${eventDate}`; if (recentEvents.has(key)) return true; recentEvents.set(key, Date.now()); @@ -237,24 +239,23 @@ router.post('/sonarr', webhookLimiter, (req, res) => { const { eventType, instanceName, eventDate } = validation; - if (isReplay(eventType, instanceName, eventDate)) { + const sonarrInstances = getSonarrInstances(); + const inst = sonarrInstances.find(i => i.name === instanceName || i.id === instanceName) || sonarrInstances[0]; + const resolvedInstanceName = inst ? inst.name : instanceName; + + if (isReplay(eventType, resolvedInstanceName, eventDate)) { logToFile(`[Webhook] Sonarr duplicate event ignored: ${eventType} @ ${eventDate}`); return res.status(200).json({ received: true, duplicate: true }); } try { - logToFile(`[Webhook] Sonarr event received - Type: ${eventType}, Instance: ${instanceName || 'unknown'}`); + logToFile(`[Webhook] Sonarr event received - Type: ${eventType}, Instance: ${resolvedInstanceName || 'unknown'}`); logToFile(`[Webhook] Sonarr payload: ${JSON.stringify(req.body)}`); // Phase 5.1: update webhook metrics for polling optimization - // Note: instanceName from webhook is often generic (e.g., "Sonarr"), not the configured name - // Update metrics for all Sonarr instances since we can't reliably match - const sonarrInstances = getSonarrInstances(); - if (sonarrInstances.length > 0) { - for (const inst of sonarrInstances) { - cache.updateWebhookMetrics(inst.url); - } - logToFile(`[Webhook] Updated metrics for ${sonarrInstances.length} Sonarr instance(s)`); + if (inst) { + cache.updateWebhookMetrics(inst.url); + logToFile(`[Webhook] Updated metrics for Sonarr instance: ${inst.name} (${inst.url})`); } // Phase 2: background cache refresh + SSE broadcast (fire-and-forget) @@ -290,24 +291,23 @@ router.post('/radarr', webhookLimiter, (req, res) => { const { eventType, instanceName, eventDate } = validation; - if (isReplay(eventType, instanceName, eventDate)) { + const radarrInstances = getRadarrInstances(); + const inst = radarrInstances.find(i => i.name === instanceName || i.id === instanceName) || radarrInstances[0]; + const resolvedInstanceName = inst ? inst.name : instanceName; + + if (isReplay(eventType, resolvedInstanceName, eventDate)) { logToFile(`[Webhook] Radarr duplicate event ignored: ${eventType} @ ${eventDate}`); return res.status(200).json({ received: true, duplicate: true }); } try { - logToFile(`[Webhook] Radarr event received - Type: ${eventType}, Instance: ${instanceName || 'unknown'}`); + logToFile(`[Webhook] Radarr event received - Type: ${eventType}, Instance: ${resolvedInstanceName || 'unknown'}`); logToFile(`[Webhook] Radarr payload: ${JSON.stringify(req.body)}`); // Phase 5.1: update webhook metrics for polling optimization - // Note: instanceName from webhook is often generic (e.g., "Radarr"), not the configured name - // Update metrics for all Radarr instances since we can't reliably match - const radarrInstances = getRadarrInstances(); - if (radarrInstances.length > 0) { - for (const inst of radarrInstances) { - cache.updateWebhookMetrics(inst.url); - } - logToFile(`[Webhook] Updated metrics for ${radarrInstances.length} Radarr instance(s)`); + if (inst) { + cache.updateWebhookMetrics(inst.url); + logToFile(`[Webhook] Updated metrics for Radarr instance: ${inst.name} (${inst.url})`); } // Phase 2: background cache refresh + SSE broadcast (fire-and-forget)