From bec81cfd8d9aeb4f240a95c1ac266a2f36a8f4ab Mon Sep 17 00:00:00 2001 From: Broque Thomas <26755000+Nezreka@users.noreply.github.com> Date: Thu, 2 Apr 2026 12:22:07 -0700 Subject: [PATCH] Add webhook POST then-action for automation engine - New 'webhook' then-action: sends HTTP POST with JSON payload to any user-configured URL (Gotify, Home Assistant, Slack, n8n, etc.) - Config: URL, optional custom headers (Key: Value per line with variable substitution), optional custom message - Payload includes all event variables as JSON fields - 15s timeout, errors on 400+ status codes - Follows exact same pattern as Discord/Pushbullet/Telegram handlers - Frontend: config fields, config reader, icon, help docs - Updated changelogs with webhook, M3U fix, orchestrator hardening --- core/automation_engine.py | 35 ++++++++++++++++++++++++ web_server.py | 7 ++++- webui/static/helper.js | 1 + webui/static/script.js | 57 ++++++++++++++++++++++++++++++++++++++- 4 files changed, 98 insertions(+), 2 deletions(-) diff --git a/core/automation_engine.py b/core/automation_engine.py index 28fe623e..48a77715 100644 --- a/core/automation_engine.py +++ b/core/automation_engine.py @@ -836,6 +836,8 @@ class AutomationEngine: self._send_pushbullet_notification(c, variables) elif t == 'telegram': self._send_telegram_notification(c, variables) + elif t == 'webhook': + self._send_webhook(c, variables) elif t == 'fire_signal': sig = self._sanitize_signal_name(c.get('signal_name', '')) if sig: @@ -966,3 +968,36 @@ class AutomationEngine: data = resp.json() if resp.status_code == 200 else {} if not data.get('ok'): raise RuntimeError(f"Telegram returned {resp.status_code}: {resp.text[:200]}") + + def _send_webhook(self, config, variables): + """Send a POST request to a user-configured webhook URL with JSON payload.""" + url = config.get('url', '').strip() + if not url: + raise ValueError("No webhook URL configured") + + # Build headers — always include Content-Type, plus optional custom headers + headers = {'Content-Type': 'application/json'} + custom_headers = config.get('headers', '').strip() + if custom_headers: + for line in custom_headers.split('\n'): + line = line.strip() + if ':' in line: + key, value = line.split(':', 1) + # Substitute variables in header values + for vk, vv in variables.items(): + value = value.replace('{' + vk + '}', vv) + headers[key.strip()] = value.strip() + + # Build JSON payload with all variables + payload = dict(variables) + + # Add custom message if configured + message = config.get('message', '').strip() + if message: + for key, value in variables.items(): + message = message.replace('{' + key + '}', value) + payload['message'] = message + + resp = requests.post(url, json=payload, headers=headers, timeout=15) + if resp.status_code >= 400: + raise RuntimeError(f"Webhook returned {resp.status_code}: {resp.text[:200]}") diff --git a/web_server.py b/web_server.py index 2c7473ad..8084cbfa 100644 --- a/web_server.py +++ b/web_server.py @@ -5631,6 +5631,8 @@ def get_automation_blocks(): "variables": ["time", "name", "run_count", "status"]}, {"type": "telegram", "label": "Telegram", "icon": "message", "description": "Send a Telegram message", "available": True, "variables": ["time", "name", "run_count", "status"]}, + {"type": "webhook", "label": "Webhook (POST)", "icon": "globe", "description": "Send a POST request to any URL", "available": True, + "variables": ["time", "name", "run_count", "status"]}, # Signal fire action {"type": "fire_signal", "label": "Fire Signal", "icon": "zap", "description": "Fire a signal that other automations can listen for", "available": True, @@ -20300,7 +20302,10 @@ def get_version_info(): "• Track source-info and redownload work with Jellyfin string IDs (#237)", "• Clear Match button to undo wrong manual matches (#236)", "• Tidal auth no longer crashes when download orchestrator not initialized", - "• Copy Debug Info includes API call rates and Spotify rate limit state" + "• Download orchestrator hardened — one failing client no longer kills all download sources", + "• Webhook THEN action — send HTTP POST to any URL (Gotify, Home Assistant, Slack, n8n) from automations", + "• M3U auto-export now skips albums — only generates for playlists (#241)", + "• Copy Debug Info includes API call rates, Spotify rate limit state, and download client failures" ] }, { diff --git a/webui/static/helper.js b/webui/static/helper.js index 05cdcf2c..fe26cbac 100644 --- a/webui/static/helper.js +++ b/webui/static/helper.js @@ -3403,6 +3403,7 @@ function closeHelperSearch() { const WHATS_NEW = { '2.2': [ // Newest features first + { title: 'Webhook THEN Action', desc: 'Send HTTP POST to any URL when automations complete — integrate with Gotify, Home Assistant, Slack, n8n. Configurable headers and message template', page: 'automations' }, { title: 'API Rate Monitor', desc: 'Real-time speedometer gauges for all 9 enrichment services on the Dashboard. Click any gauge for 24h history chart. Spotify shows per-endpoint breakdown', page: 'dashboard' }, { title: 'Configurable Concurrent Downloads', desc: 'Set max simultaneous downloads per batch (1-10) in Settings. Soulseek albums stay at 1 for source reuse. Higher values speed up playlists and wishlists' }, { title: 'Streaming Search Sources', desc: 'Apple Music and other slow sources now stream results progressively — see artists, albums, tracks as each loads instead of waiting for all 3' }, diff --git a/webui/static/script.js b/webui/static/script.js index 92e65f71..e759cf1c 100644 --- a/webui/static/script.js +++ b/webui/static/script.js @@ -23157,6 +23157,34 @@ const TOOL_HELP_CONTENT = { ` }, + 'auto-webhook': { + title: 'Webhook (POST)', + content: ` +
Sends an HTTP POST request with a JSON payload to any URL when the automation's action completes. Use it to integrate with Gotify, Home Assistant, Slack, n8n, or any service that accepts webhooks.
+ +https://gotify.example.com/message?token=xxx)Key: Value format. Useful for auth tokens.The POST body always includes all event variables as JSON fields:
+{"time": "2026-04-02 ...", "name": "My Automation", "status": "success", ...}
+
+ Use these in your message or header values:
+{time} — When the automation ran{name} — Automation name{run_count} — How many times this automation has run{status} — Result status of the action