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
This commit is contained in:
parent
7c85c31e8b
commit
bec81cfd8d
4 changed files with 98 additions and 2 deletions
|
|
@ -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]}")
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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' },
|
||||
|
|
|
|||
|
|
@ -23157,6 +23157,34 @@ const TOOL_HELP_CONTENT = {
|
|||
`
|
||||
},
|
||||
|
||||
'auto-webhook': {
|
||||
title: 'Webhook (POST)',
|
||||
content: `
|
||||
<h4>What does this then-action do?</h4>
|
||||
<p>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.</p>
|
||||
|
||||
<h4>Configuration</h4>
|
||||
<ul>
|
||||
<li><strong>URL:</strong> The endpoint to POST to (e.g. <code>https://gotify.example.com/message?token=xxx</code>)</li>
|
||||
<li><strong>Headers:</strong> Optional custom headers, one per line in <code>Key: Value</code> format. Useful for auth tokens.</li>
|
||||
<li><strong>Custom Message:</strong> Optional message with variable placeholders. Added as a "message" field in the JSON payload.</li>
|
||||
</ul>
|
||||
|
||||
<h4>JSON payload</h4>
|
||||
<p>The POST body always includes all event variables as JSON fields:</p>
|
||||
<pre style="background:rgba(255,255,255,0.05);padding:8px;border-radius:6px;font-size:11px;">{"time": "2026-04-02 ...", "name": "My Automation", "status": "success", ...}</pre>
|
||||
|
||||
<h4>Available variables</h4>
|
||||
<p>Use these in your message or header values:</p>
|
||||
<ul>
|
||||
<li><code>{time}</code> — When the automation ran</li>
|
||||
<li><code>{name}</code> — Automation name</li>
|
||||
<li><code>{run_count}</code> — How many times this automation has run</li>
|
||||
<li><code>{status}</code> — Result status of the action</li>
|
||||
</ul>
|
||||
`
|
||||
},
|
||||
|
||||
// ==================== Signal System Help ====================
|
||||
|
||||
'auto-signal_received': {
|
||||
|
|
@ -62695,7 +62723,7 @@ const _autoIcons = {
|
|||
process_wishlist: '\uD83D\uDCCB', scan_watchlist: '\uD83D\uDC41\uFE0F',
|
||||
scan_library: '\uD83D\uDD04', refresh_mirrored: '\uD83D\uDCC2', sync_playlist: '\uD83D\uDD01',
|
||||
discover_playlist: '\uD83D\uDD0D', discovery_completed: '\uD83D\uDD0D',
|
||||
notify_only: '\uD83D\uDD14', discord_webhook: '\uD83D\uDCAC', pushbullet: '\uD83D\uDD14', telegram: '\u2709\uFE0F',
|
||||
notify_only: '\uD83D\uDD14', discord_webhook: '\uD83D\uDCAC', pushbullet: '\uD83D\uDD14', telegram: '\u2709\uFE0F', webhook: '\uD83C\uDF10',
|
||||
signal_received: '\u26A1', fire_signal: '\u26A1',
|
||||
// Phase 3
|
||||
wishlist_processing_completed: '\u2705', watchlist_scan_completed: '\u2705',
|
||||
|
|
@ -64758,6 +64786,26 @@ function _renderBlockConfigFields(slotKey, blockType, config) {
|
|||
</div>
|
||||
${_notifyVarHtml(slotKey)}`;
|
||||
}
|
||||
if (blockType === 'webhook') {
|
||||
const url = _escAttr(config.url || '');
|
||||
const hdrs = (config.headers || '').replace(/"/g, '"');
|
||||
return `<div class="config-row">
|
||||
<label>URL</label>
|
||||
<input type="text" id="cfg-${slotKey}-url" value="${url}" placeholder="https://your-server.com/hook">
|
||||
</div>
|
||||
<div class="config-row">
|
||||
<label>Headers <span style="opacity:0.4;font-weight:400">(one per line, Key: Value)</span></label>
|
||||
<textarea id="cfg-${slotKey}-headers" placeholder="Authorization: Bearer token123\nX-Custom: value" style="font-family:monospace;font-size:11px;">${hdrs}</textarea>
|
||||
</div>
|
||||
<div class="config-row">
|
||||
<label>Custom Message <span style="opacity:0.4;font-weight:400">(optional)</span></label>
|
||||
<textarea id="cfg-${slotKey}-message" placeholder="Message with {variables}...">${config.message || ''}</textarea>
|
||||
</div>
|
||||
<div class="config-row" style="color:rgba(255,255,255,0.35);font-size:11px;">
|
||||
Sends a JSON POST with all event variables. Custom message added as "message" field if set.
|
||||
</div>
|
||||
${_notifyVarHtml(slotKey)}`;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
|
|
@ -64999,6 +65047,13 @@ function _readPlacedConfig(slotKey) {
|
|||
message: document.getElementById('cfg-' + slotKey + '-message')?.value || '',
|
||||
};
|
||||
}
|
||||
if (type === 'webhook') {
|
||||
return {
|
||||
url: document.getElementById('cfg-' + slotKey + '-url')?.value?.trim() || '',
|
||||
headers: document.getElementById('cfg-' + slotKey + '-headers')?.value || '',
|
||||
message: document.getElementById('cfg-' + slotKey + '-message')?.value || '',
|
||||
};
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue