From 48ac68c2973742155b824767ae5fc0d1fcb767ed Mon Sep 17 00:00:00 2001 From: Simon Gardling Date: Fri, 17 Apr 2026 19:47:26 -0400 Subject: [PATCH] jellyfin: add webhook plugin helper --- services/jellyfin/jellyfin-webhook-plugin.nix | 105 ++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 services/jellyfin/jellyfin-webhook-plugin.nix diff --git a/services/jellyfin/jellyfin-webhook-plugin.nix b/services/jellyfin/jellyfin-webhook-plugin.nix new file mode 100644 index 0000000..371f818 --- /dev/null +++ b/services/jellyfin/jellyfin-webhook-plugin.nix @@ -0,0 +1,105 @@ +{ pkgs, lib }: +let + pluginVersion = "18.0.0.0"; + # GUID from the plugin's meta.json; addresses it on /Plugins//Configuration. + pluginGuid = "71552a5a-5c5c-4350-a2ae-ebe451a30173"; + + package = pkgs.stdenvNoCC.mkDerivation { + pname = "jellyfin-plugin-webhook"; + version = pluginVersion; + src = pkgs.fetchurl { + url = "https://repo.jellyfin.org/files/plugin/webhook/webhook_${pluginVersion}.zip"; + hash = "sha256-LFFojiPnBGl9KJ0xVyPBnCmatcaeVbllRwRkz5Z3dqI="; + }; + nativeBuildInputs = [ pkgs.unzip ]; + unpackPhase = ''unzip "$src"''; + installPhase = '' + mkdir -p "$out" + cp *.dll meta.json "$out/" + ''; + dontFixup = true; # managed .NET assemblies must not be patched + }; + + # Minimal Handlebars template, base64 encoded. The monitor only needs the POST; + # NotificationType is parsed for the debug log line. + # Decoded: {"NotificationType":"{{NotificationType}}"} + templateB64 = "eyJOb3RpZmljYXRpb25UeXBlIjoie3tOb3RpZmljYXRpb25UeXBlfX0ifQ=="; + + # Build a PluginConfiguration payload accepted by Jellyfin's JSON deserializer. + # Each webhook is `{ name, uri, notificationTypes }`. + mkConfigJson = + webhooks: + builtins.toJSON { + ServerUrl = ""; + GenericOptions = map (w: { + NotificationTypes = w.notificationTypes; + WebhookName = w.name; + WebhookUri = w.uri; + EnableMovies = true; + EnableEpisodes = true; + EnableVideos = true; + EnableWebhook = true; + Template = templateB64; + Headers = [ + { + Key = "Content-Type"; + Value = "application/json"; + } + ]; + }) webhooks; + }; + + # Oneshot that POSTs the plugin configuration. Retries past the window + # between Jellyfin API health and plugin registration. + mkConfigureScript = + { jellyfinUrl, webhooks }: + pkgs.writeShellScript "jellyfin-webhook-configure" '' + set -euo pipefail + export PATH=${ + lib.makeBinPath [ + pkgs.coreutils + pkgs.curl + ] + } + + URL=${lib.escapeShellArg jellyfinUrl} + AUTH="Authorization: MediaBrowser Token=\"$(cat "$CREDENTIALS_DIRECTORY/jellyfin-api-key")\"" + CONFIG=${lib.escapeShellArg (mkConfigJson webhooks)} + + for _ in $(seq 1 120); do curl -sf -o /dev/null "$URL/health" && break; sleep 1; done + curl -sf -o /dev/null "$URL/health" + + for _ in $(seq 1 60); do + if printf '%s' "$CONFIG" | curl -sf -X POST \ + -H "$AUTH" -H "Content-Type: application/json" --data-binary @- \ + "$URL/Plugins/${pluginGuid}/Configuration"; then + echo "Jellyfin webhook plugin configured"; exit 0 + fi + sleep 1 + done + echo "Failed to configure webhook plugin" >&2; exit 1 + ''; + + # Materialise a writable copy of the plugin. Jellyfin rewrites meta.json at + # runtime, so a read-only nix-store symlink would EACCES. + mkInstallScript = + { pluginsDir }: + pkgs.writeShellScript "jellyfin-webhook-install" '' + set -euo pipefail + export PATH=${lib.makeBinPath [ pkgs.coreutils ]} + dst=${lib.escapeShellArg "${pluginsDir}/Webhook_${pluginVersion}"} + mkdir -p ${lib.escapeShellArg pluginsDir} + rm -rf "$dst" && mkdir -p "$dst" + cp ${package}/*.dll ${package}/meta.json "$dst/" + chmod u+rw "$dst"/* + ''; +in +{ + inherit + package + pluginVersion + pluginGuid + mkConfigureScript + mkInstallScript + ; +}