feat: webhook

This commit is contained in:
auricom
2025-04-16 09:36:06 +02:00
parent 7372e1cb94
commit e14d7c3bb4
134 changed files with 951 additions and 264 deletions

View File

@@ -0,0 +1,27 @@
---
# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/external-secrets.io/externalsecret_v1.json
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
name: webhook
spec:
secretStoreRef:
kind: ClusterSecretStore
name: onepassword-connect
target:
name: webhook-secret
template:
data:
JELLYSEERR_PUSHOVER_URL: pover://{{ .PUSHOVER_USER_KEY }}@{{ .JELLYSEERR_PUSHOVER_TOKEN }}
RADARR_PUSHOVER_URL: pover://{{ .PUSHOVER_USER_KEY }}@{{ .RADARR_PUSHOVER_TOKEN }}
SONARR_PUSHOVER_URL: pover://{{ .PUSHOVER_USER_KEY }}@{{ .SONARR_PUSHOVER_TOKEN }}
SONARR_API_KEY: "{{ .SONARR__API_KEY }}"
dataFrom:
- extract:
key: jellyseerr
- extract:
key: radarr
- extract:
key: sonarr
- extract:
key: pushover

View File

@@ -0,0 +1,79 @@
---
# yaml-language-server: $schema=https://raw.githubusercontent.com/bjw-s/helm-charts/main/charts/other/app-template/schemas/helmrelease-helm-v2.schema.json
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: &app webhook
spec:
interval: 1h
chartRef:
kind: OCIRepository
name: app-template
install:
remediation:
retries: -1
upgrade:
cleanupOnFail: true
remediation:
retries: 3
values:
controllers:
webhook:
replicas: 2
strategy: RollingUpdate
annotations:
reloader.stakater.com/auto: "true"
containers:
app:
image:
repository: ghcr.io/home-operations/webhook
tag: 2.8.2@sha256:052e3d82aa13091459faa75550943faf06309c0ca71253ea1cece7880b1d5faa
env:
WEBHOOK__PORT: &port 8080
TZ: ${TIMEZONE}
envFrom:
- secretRef:
name: webhook-secret
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities: { drop: ["ALL"] }
resources:
requests:
cpu: 100m
limits:
memory: 256Mi
defaultPodOptions:
securityContext:
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
fsGroup: 1000
fsGroupChangePolicy: OnRootMismatch
service:
app:
controller: webhook
ports:
http:
port: *port
ingress:
app:
enabled: true
className: internal
hosts:
- host: &host "{{ .Release.Name }}.${SECRET_EXTERNAL_DOMAIN}"
paths:
- path: /
service:
identifier: app
port: *port
tls:
- hosts:
- *host
persistence:
config:
type: configMap
name: webhook-configmap
defaultMode: 0775
globalMounts:
- readOnly: true

View File

@@ -0,0 +1,20 @@
---
# yaml-language-server: $schema=https://json.schemastore.org/kustomization
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ./externalsecret.yaml
- ./helmrelease.yaml
configMapGenerator:
- name: webhook-configmap
files:
- hooks.yaml=./resources/hooks.yaml
- jellyseerr-pushover.sh=./resources/jellyseerr-pushover.sh
- radarr-pushover.sh=./resources/radarr-pushover.sh
- sonarr-pushover.sh=./resources/sonarr-pushover.sh
- sonarr-refresh-series.sh=./resources/sonarr-refresh-series.sh
- sonarr-tag-codecs.sh=./resources/sonarr-tag-codecs.sh
generatorOptions:
disableNameSuffixHash: true
annotations:
kustomize.toolkit.fluxcd.io/substitute: disabled

View File

@@ -0,0 +1,109 @@
---
- id: jellyseerr-pushover
execute-command: /config/jellyseerr-pushover.sh
command-working-directory: /config
pass-arguments-to-command:
- source: string
name: '{{ getenv "JELLYSEERR_PUSHOVER_URL" }}'
- source: entire-payload
- id: radarr-pushover
execute-command: /config/radarr-pushover.sh
command-working-directory: /config
pass-environment-to-command:
- envname: RADARR_PUSHOVER_URL
source: string
name: '{{ getenv "RADARR_PUSHOVER_URL" }}'
- envname: RADARR_EVENT_TYPE
source: payload
name: eventType
- envname: RADARR_APPLICATION_URL
source: payload
name: applicationUrl
- envname: RADARR_MOVIE_TITLE
source: payload
name: movie.title
- envname: RADARR_MOVIE_YEAR
source: payload
name: movie.year
- envname: RADARR_MOVIE_OVERVIEW
source: payload
name: movie.overview
- envname: RADARR_MOVIE_TMDB_ID
source: payload
name: movie.tmdbId
- envname: RADARR_DOWNLOAD_CLIENT
source: payload
name: downloadClient
- id: sonarr-pushover
execute-command: /config/sonarr-pushover.sh
command-working-directory: /config
pass-environment-to-command:
- envname: SONARR_PUSHOVER_URL
source: string
name: '{{ getenv "SONARR_PUSHOVER_URL" }}'
- envname: SONARR_EVENT_TYPE
source: payload
name: eventType
- envname: SONARR_APPLICATION_URL
source: payload
name: applicationUrl
- envname: SONARR_SERIES_TITLE
source: payload
name: series.title
- envname: SONARR_SERIES_TITLE_SLUG
source: payload
name: series.titleSlug
- envname: SONARR_EPISODE_TITLE
source: payload
name: episodes.0.title
- envname: SONARR_EPISODE_SEASON_NUMBER
source: payload
name: episodes.0.seasonNumber
- envname: SONARR_EPISODE_NUMBER
source: payload
name: episodes.0.episodeNumber
- envname: SONARR_DOWNLOAD_CLIENT
source: payload
name: downloadClient
- id: sonarr-refresh-series
execute-command: /config/sonarr-refresh-series.sh
command-working-directory: /config
pass-environment-to-command:
- envname: SONARR_REMOTE_ADDR
source: request
name: remote-addr
- envname: SONARR_API_KEY
source: string
name: '{{ getenv "SONARR_API_KEY" }}'
- envname: SONARR_EVENT_TYPE
source: payload
name: eventType
- envname: SONARR_SERIES_ID
source: payload
name: series.id
- envname: SONARR_SERIES_TITLE
source: payload
name: series.title
- id: sonarr-tag-codecs
execute-command: /config/sonarr-tag-codecs.sh
command-working-directory: /config
pass-environment-to-command:
- envname: SONARR_REMOTE_ADDR
source: request
name: remote-addr
- envname: SONARR_API_KEY
source: string
name: '{{ getenv "SONARR_API_KEY" }}'
- envname: SONARR_EVENT_TYPE
source: payload
name: eventType
- envname: SONARR_SERIES_ID
source: payload
name: series.id
- envname: SONARR_SERIES_TITLE
source: payload
name: series.title

View File

@@ -0,0 +1,32 @@
#!/usr/bin/env bash
set -Eeuo pipefail
JELLYSEERR_PUSHOVER_URL=${1:?}
PAYLOAD=${2:?}
echo "[DEBUG] Payload: ${PAYLOAD}"
function _jq() {
jq --raw-output "${1:?}" <<<"${PAYLOAD}"
}
function notify() {
local type="$(_jq '.notification_type')"
if [[ "${type}" == "TEST_NOTIFICATION" ]]; then
printf -v PUSHOVER_TITLE "Test Notification"
printf -v PUSHOVER_MESSAGE "Howdy this is a test notification from <b>%s</b>" "Jellyseerr"
printf -v PUSHOVER_URL "%s" "https://requests.devbu.io"
printf -v PUSHOVER_URL_TITLE "Open %s" "Jellyseerr"
printf -v PUSHOVER_PRIORITY "%s" "low"
fi
apprise -vv --title "${PUSHOVER_TITLE}" --body "${PUSHOVER_MESSAGE}" --input-format html \
"${JELLYSEERR_PUSHOVER_URL}?url=${PUSHOVER_URL}&url_title=${PUSHOVER_URL_TITLE}&priority=${PUSHOVER_PRIORITY}&format=html"
}
function main() {
notify
}
main "$@"

View File

@@ -0,0 +1,42 @@
#!/usr/bin/env bash
set -Eeuo pipefail
function notify() {
if [[ "${RADARR_EVENT_TYPE}" == "Test" ]]; then
printf -v PUSHOVER_TITLE "Test Notification"
printf -v PUSHOVER_MESSAGE "Howdy this is a test notification"
printf -v PUSHOVER_URL "%s" "${RADARR_APPLICATION_URL}"
printf -v PUSHOVER_URL_TITLE "View Movies"
printf -v PUSHOVER_PRIORITY "low"
elif [[ "${RADARR_EVENT_TYPE}" == "ManualInteractionRequired" ]]; then
printf -v PUSHOVER_TITLE "Movie Requires Manual Interaction"
printf -v PUSHOVER_MESSAGE "<b>%s (%s)</b><small>\n<b>Client:</b> %s</small>" \
"${RADARR_MOVIE_TITLE}" \
"${RADARR_MOVIE_YEAR}" \
"${RADARR_DOWNLOAD_CLIENT}"
printf -v PUSHOVER_URL "%s/activity/queue" "${RADARR_APPLICATION_URL}"
printf -v PUSHOVER_URL_TITLE "View Queue"
printf -v PUSHOVER_PRIORITY "high"
elif [[ "${RADARR_EVENT_TYPE}" == "Download" ]]; then
printf -v PUSHOVER_TITLE "Movie Added"
printf -v PUSHOVER_MESSAGE "<b>%s (%s)</b><small>\n%s</small><small>\n\n<b>Client:</b> %s</small>" \
"${RADARR_MOVIE_TITLE}" \
"${RADARR_MOVIE_YEAR}" \
"${RADARR_MOVIE_OVERVIEW}" \
"${RADARR_DOWNLOAD_CLIENT}"
printf -v PUSHOVER_URL "%s/movie/%s" \
"${RADARR_APPLICATION_URL}" \
"${RADARR_MOVIE_TMDB_ID}"
printf -v PUSHOVER_URL_TITLE "View Movie"
printf -v PUSHOVER_PRIORITY "low"
fi
apprise -vv --title "${PUSHOVER_TITLE}" --body "${PUSHOVER_MESSAGE}" --input-format html \
"${RADARR_PUSHOVER_URL}?url=${PUSHOVER_URL}&url_title=${PUSHOVER_URL_TITLE}&priority=${PUSHOVER_PRIORITY}&format=html"
}
function main() {
notify
}
main "$@"

View File

@@ -0,0 +1,42 @@
#!/usr/bin/env bash
set -Eeuo pipefail
function notify() {
if [[ "${SONARR_EVENT_TYPE}" == "Test" ]]; then
printf -v PUSHOVER_TITLE "Test Notification"
printf -v PUSHOVER_MESSAGE "Howdy this is a test notification"
printf -v PUSHOVER_URL "%s" "${SONARR_APPLICATION_URL}"
printf -v PUSHOVER_URL_TITLE "View Series"
printf -v PUSHOVER_PRIORITY "low"
elif [[ "${SONARR_EVENT_TYPE}" == "ManualInteractionRequired" ]]; then
printf -v PUSHOVER_TITLE "Episode Requires Manual Interaction"
printf -v PUSHOVER_MESSAGE "<b>%s</b><small>\n<b>Client:</b> %s</small>" \
"${SONARR_SERIES_TITLE}" \
"${SONARR_DOWNLOAD_CLIENT}"
printf -v PUSHOVER_URL "%s/activity/queue" "${SONARR_APPLICATION_URL}"
printf -v PUSHOVER_URL_TITLE "View Queue"
printf -v PUSHOVER_PRIORITY "high"
elif [[ "${SONARR_EVENT_TYPE}" == "Download" ]]; then
printf -v PUSHOVER_TITLE "Episode Added"
printf -v PUSHOVER_MESSAGE "<b>%s (S%02dE%02d)</b><small>\n%s</small><small>\n\n<b>Client:</b> %s</small><small>" \
"${SONARR_SERIES_TITLE}" \
"${SONARR_EPISODE_SEASON_NUMBER}" \
"${SONARR_EPISODE_NUMBER}" \
"${SONARR_EPISODE_TITLE}" \
"${SONARR_DOWNLOAD_CLIENT}"
printf -v PUSHOVER_URL "%s/series/%s" \
"${SONARR_APPLICATION_URL}" \
"${SONARR_SERIES_TITLE_SLUG}"
printf -v PUSHOVER_URL_TITLE "View Series"
printf -v PUSHOVER_PRIORITY "low"
fi
apprise -vv --title "${PUSHOVER_TITLE}" --body "${PUSHOVER_MESSAGE}" --input-format html \
"${SONARR_PUSHOVER_URL}?url=${PUSHOVER_URL}&url_title=${PUSHOVER_URL_TITLE}&priority=${PUSHOVER_PRIORITY}&format=html"
}
function main() {
notify
}
main "$@"

View File

@@ -0,0 +1,27 @@
#!/usr/bin/env bash
set -Eeuo pipefail
function refresh() {
if [[ "${SONARR_EVENT_TYPE}" == "Test" ]]; then
echo "[DEBUG] test event received from ${SONARR_REMOTE_ADDR}, nothing to do ..."
elif [[ "${SONARR_EVENT_TYPE}" == "Grab" ]]; then
episodes=$(
curl -fsSL --header "X-Api-Key: ${SONARR_API_KEY}" "http://${SONARR_REMOTE_ADDR}/api/v3/episode?seriesId=${SERIES_ID}" |
jq --raw-output '[.[] | select((.title == "TBA") or (.title == "TBD"))] | length'
)
if ((episodes > 0)); then
echo "[INFO] episode titles found with TBA/TBD titles, refreshing series ${SONARR_SERIES_TITLE} ..."
curl -fsSL --request POST \
--header "X-Api-Key: ${SONARR_API_KEY}" \
--header "Content-Type: application/json" \
--data-binary "$(jo name=RefreshSeries seriesId="${SERIES_ID}")" \
"http://${SONARR_REMOTE_ADDR}/api/v3/command" &>/dev/null
fi
fi
}
function main() {
refresh
}
main "$@"

View File

@@ -0,0 +1,148 @@
#!/usr/bin/env bash
set -Eeuo pipefail
# Cache existing tags once at the start
declare -A TAG_CACHE
# Function to cache existing tags
function cache_existing_tags() {
existing_tags_cache=$(curl -fsSL --header "X-Api-Key: ${SONARR_API_KEY}" "http://${SONARR_REMOTE_ADDR}/api/v3/tag")
while IFS=":" read -r id label; do
TAG_CACHE["$id"]="$label"
done < <(echo "${existing_tags_cache}" | jq --raw-output '.[] | "\(.id):\(.label)"')
}
# Function to get codec tags for a series
function get_codec_tags() {
local series_id=$1
# Extract and map codecs in one pass
local codecs
codecs=$(
curl -fsSL --header "X-Api-Key: ${SONARR_API_KEY}" "http://${SONARR_REMOTE_ADDR}/api/v3/episodefile?seriesId=${series_id}" | jq --raw-output '
[
.[] |
(.mediaInfo.videoCodec // "other" |
gsub("x"; "h") | ascii_downcase |
if test("hevc") then "h265"
elif test("divx|mpeg2|xvid") then "h264"
elif test("av1") then "av1"
elif test("h264|h265") then . else "other" end
) | "codec:" + .
] | unique | .[]'
)
echo "${codecs[@]}"
}
# Function to check if a tag exists, if not create it and return the tag ID
function get_or_create_tag_id() {
local tag_label=$1
local tag_id
# Search cached tags
tag_id=$(echo "${existing_tags_cache}" | jq --raw-output ".[] | select(.label == \"${tag_label}\") | .id")
# If tag doesn't exist, create it
if [[ -z "${tag_id}" ]]; then
local new_tag
new_tag=$(curl -fsSL --request POST --header "X-Api-Key: ${SONARR_API_KEY}" --header "Content-Type: application/json" --data "$(jo label="${tag_label}")" "http://${SONARR_REMOTE_ADDR}/api/v3/tag")
tag_id=$(echo "${new_tag}" | jq --raw-output '.id')
# Update cache
existing_tags_cache=$(echo "${existing_tags_cache}" | jq ". += [{\"id\": ${tag_id}, \"label\": \"${tag_label}\"}]")
TAG_CACHE["$tag_id"]="${tag_label}"
fi
echo "${tag_id}"
}
# Function to update series tags in bulk
function update_series_tags() {
local series_data="$1"
local codecs="$2"
# Get the current series tags
local series_tags
series_tags=$(echo "$series_data" | jq --raw-output '.tags')
# Track tags to add/remove
local tags_to_add=()
local tags_to_remove=()
# Identify tags to add
for codec in $codecs; do
local tag_id
tag_id=$(get_or_create_tag_id "${codec}")
if ! echo "${series_tags}" | jq --exit-status ". | index(${tag_id})" &>/dev/null; then
tags_to_add+=("$tag_id")
fi
done
# Identify tags to remove
for tag_id in $(echo "${series_tags}" | jq --raw-output '.[]'); do
local tag_label="${TAG_CACHE[$tag_id]}"
if [[ -n "${tag_label}" && ! " ${codecs} " =~ ${tag_label} ]] && [[ "${tag_label}" =~ codec:.* ]]; then
tags_to_remove+=("$tag_id")
fi
done
if [[ ${#tags_to_add[@]} -gt 0 ]]; then
series_data=$(echo "${series_data}" | jq --argjson add_tags "$(printf '%s\n' "${tags_to_add[@]}" | jq --raw-input . | jq --slurp 'map(tonumber)')" '.tags = (.tags + $add_tags | unique)')
fi
if [[ ${#tags_to_remove[@]} -gt 0 ]]; then
series_data=$(echo "${series_data}" | jq --argjson remove_tags "$(printf '%s\n' "${tags_to_remove[@]}" | jq --raw-input . | jq --slurp 'map(tonumber)')" '.tags |= map(select(. as $tag | $remove_tags | index($tag) | not))')
fi
echo "${series_data}"
}
function tag() {
if [[ "${SONARR_EVENT_TYPE}" == "Test" ]]; then
echo "[DEBUG] test event received from ${SONARR_REMOTE_ADDR}, nothing to do ..."
elif [[ "${SONARR_EVENT_TYPE}" == "Download" ]]; then
cache_existing_tags
local orig_series_data
orig_series_data=$(curl -fsSL --header "X-Api-Key: ${SONARR_API_KEY}" "http://${SONARR_REMOTE_ADDR}/api/v3/series/${SONARR_SERIES_ID}")
local series_episode_file_count
series_episode_file_count=$(echo "${orig_series_data}" | jq --raw-output '.statistics.episodeFileCount')
if [[ "${series_episode_file_count}" == "null" || "${series_episode_file_count}" -eq 0 ]]; then
echo "Skipping ${SONARR_SERIES_TITLE} (ID: ${SONARR_SERIES_ID}) due to no episode files"
exit 0
fi
# Get unique codecs for the series
local codecs
codecs=$(get_codec_tags "${SONARR_SERIES_ID}")
# Update the series tags
local updated_series_data
updated_series_data=$(update_series_tags "${orig_series_data}" "${codecs}")
local orig_tags updated_tags
orig_tags=$(echo "${orig_series_data}" | jq --compact-output '.tags')
updated_tags=$(echo "${updated_series_data}" | jq --compact-output '.tags')
if [[ "${orig_tags}" == "${updated_tags}" ]]; then
echo "[INFO] skipping ${SONARR_SERIES_TITLE} (ID: ${SONARR_SERIES_ID}, Tags: [${codecs//$'\n'/,}]) due to no changes"
exit 0
fi
echo "[INFO] updating ${SONARR_SERIES_TITLE} (ID: ${SONARR_SERIES_ID}, Tags: [${codecs//$'\n'/,}])"
curl -fsSL --header "X-Api-Key: ${SONARR_API_KEY}" \
--request PUT \
--header "Content-Type: application/json" \
--data "${updated_series_data}" "http://${SONARR_REMOTE_ADDR}/api/v3/series" &>/dev/null
fi
}
function main() {
tag
}
main "$@"

View File

@@ -0,0 +1,27 @@
---
# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/kustomize.toolkit.fluxcd.io/kustomization_v1.json
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: &app webhook
namespace: &namespace default
spec:
commonMetadata:
labels:
app.kubernetes.io/name: *app
components:
- ../../../../components/gatus/guarded
interval: 1h
path: ./kubernetes/apps/default/webhook/app
postBuild:
substitute:
APP: *app
prune: true
retryInterval: 2m
sourceRef:
kind: GitRepository
name: home-ops-kubernetes
namespace: flux-system
targetNamespace: *namespace
timeout: 5m
wait: false