mirror of
https://github.com/soulis-1256/assemblrr.git
synced 2026-08-29 09:31:40 +00:00
Fire Sonarr refresh once on import complete, skip a second /Library/Refresh so it cannot cancel a running scan, and heal series that still show 0 episodes despite video on disk.
175 lines
6 KiB
Bash
Executable file
175 lines
6 KiB
Bash
Executable file
#!/bin/sh
|
|
# Triggered by Radarr/Sonarr after import (see lib/jellyfin.sh).
|
|
#
|
|
# POST /Library/Refresh discovers new files. A second POST cancels the
|
|
# in-progress scan. Jellyfin 10.11 can then save a new series under a TVDB
|
|
# PresentationUniqueKey while its episodes keep the series GUID as
|
|
# SeriesPresentationUniqueKey — the UI shows 0 episodes / 0 minutes even
|
|
# though the files are in the DB. Do not cancel a running scan; after the
|
|
# scan goes idle, recursively refresh any series that still look empty
|
|
# despite video files on disk.
|
|
|
|
JF_HOST="${JELLYFIN_HOST:-jellyfin}"
|
|
JF_PORT="${JELLYFIN_PORT:-8096}"
|
|
JF_REFRESH_HEAL_POLL_SEC="${JF_REFRESH_HEAL_POLL_SEC:-5}"
|
|
JF_REFRESH_HEAL_MAX_POLLS="${JF_REFRESH_HEAL_MAX_POLLS:-36}"
|
|
JF_HEAL_LOCK_DIR="${JF_HEAL_LOCK_DIR:-/tmp/assemblrr-jf-heal.lock}"
|
|
|
|
jf_refresh_is_test_event() {
|
|
[ "${radarr_eventtype:-}" = "Test" ] || [ "${sonarr_eventtype:-}" = "Test" ]
|
|
}
|
|
|
|
# Arg: GET /ScheduledTasks JSON. Echo Idle when missing or unparseable.
|
|
jf_refresh_scan_state() {
|
|
local state
|
|
[ -n "${1:-}" ] || { echo Idle; return 0; }
|
|
state=$(printf '%s' "$1" | jq -r \
|
|
'([.[]? | select(.Key == "RefreshLibrary") | .State] | first) // "Idle"' \
|
|
2>/dev/null || true)
|
|
[ -n "$state" ] && [ "$state" != "null" ] || state=Idle
|
|
echo "$state"
|
|
}
|
|
|
|
jf_refresh_scan_busy() {
|
|
case "${1:-}" in
|
|
Running|Cancelling) return 0 ;;
|
|
*) return 1 ;;
|
|
esac
|
|
}
|
|
|
|
# True when the Shows API reports no episodes but the library folder has video.
|
|
# That is the 10.11 identity-key miss (series page empty; item-by-id still works).
|
|
jf_refresh_dir_has_video() {
|
|
local dir="${1:-}"
|
|
[ -n "$dir" ] && [ -d "$dir" ] || return 1
|
|
find "$dir" \( -iname '*.mkv' -o -iname '*.mp4' -o -iname '*.m4v' \
|
|
-o -iname '*.avi' -o -iname '*.ts' -o -iname '*.m2ts' -o -iname '*.wmv' \) \
|
|
-type f -print 2>/dev/null | head -1 | grep -q .
|
|
}
|
|
|
|
jf_refresh_empty_series_ids() {
|
|
printf '%s' "${1:-}" | jq -r '.Items[]? | .Id // empty' 2>/dev/null
|
|
}
|
|
|
|
if [ "${JF_REFRESH_SOURCE_ONLY:-0}" = "1" ]; then
|
|
return 0 2>/dev/null || exit 0
|
|
fi
|
|
|
|
if [ -f "/run/secrets/jellyfin_api_key.txt" ]; then
|
|
JF_API_KEY=$(cat /run/secrets/jellyfin_api_key.txt)
|
|
else
|
|
JF_API_KEY="${JELLYFIN_API_KEY:-}"
|
|
fi
|
|
|
|
jf_refresh_curl() {
|
|
curl -sS --connect-timeout 5 --max-time 30 \
|
|
-H "X-Emby-Token: ${JF_API_KEY}" \
|
|
"$@" 2>/dev/null
|
|
}
|
|
|
|
jf_refresh_heal_after_scan() {
|
|
local i=0
|
|
local tasks state series_json sid path eps
|
|
|
|
while [ "$i" -lt "$JF_REFRESH_HEAL_MAX_POLLS" ]; do
|
|
tasks=$(jf_refresh_curl "http://${JF_HOST}:${JF_PORT}/ScheduledTasks" || echo "")
|
|
state=$(jf_refresh_scan_state "$tasks")
|
|
if ! jf_refresh_scan_busy "$state"; then
|
|
break
|
|
fi
|
|
sleep "$JF_REFRESH_HEAL_POLL_SEC"
|
|
i=$((i + 1))
|
|
done
|
|
|
|
series_json=$(jf_refresh_curl \
|
|
"http://${JF_HOST}:${JF_PORT}/Items?IncludeItemTypes=Series&Recursive=true&Fields=Path" \
|
|
|| echo "")
|
|
|
|
echo "$series_json" | jq -c '.Items[]? | {Id, Path}' 2>/dev/null | while read -r row; do
|
|
sid=$(echo "$row" | jq -r '.Id // empty')
|
|
path=$(echo "$row" | jq -r '.Path // empty')
|
|
[ -n "$sid" ] || continue
|
|
jf_refresh_dir_has_video "$path" || continue
|
|
|
|
eps=$(jf_refresh_curl \
|
|
"http://${JF_HOST}:${JF_PORT}/Shows/${sid}/Episodes" || echo "")
|
|
if [ "$(echo "$eps" | jq -r '.TotalRecordCount // 0' 2>/dev/null || echo 0)" != "0" ]; then
|
|
continue
|
|
fi
|
|
|
|
echo "Jellyfin refresh: healing empty series ${sid} (${path})"
|
|
jf_refresh_curl -o /dev/null -X POST \
|
|
"http://${JF_HOST}:${JF_PORT}/Items/${sid}/Refresh?Recursive=true&MetadataRefreshMode=FullRefresh&ImageRefreshMode=Default&ReplaceAllMetadata=false" \
|
|
|| true
|
|
done
|
|
}
|
|
|
|
if [ "${1:-}" = "--heal-only" ]; then
|
|
if [ -z "$JF_API_KEY" ]; then
|
|
echo "Jellyfin refresh: JELLYFIN_API_KEY not set" >&2
|
|
exit 1
|
|
fi
|
|
if mkdir "$JF_HEAL_LOCK_DIR" 2>/dev/null; then
|
|
echo $$ >"$JF_HEAL_LOCK_DIR/pid"
|
|
trap 'rm -rf "$JF_HEAL_LOCK_DIR"' EXIT
|
|
jf_refresh_heal_after_scan
|
|
else
|
|
oldpid=$(cat "$JF_HEAL_LOCK_DIR/pid" 2>/dev/null || echo "")
|
|
if [ -n "$oldpid" ] && kill -0 "$oldpid" 2>/dev/null; then
|
|
echo "Jellyfin refresh: heal already running (pid ${oldpid})"
|
|
exit 0
|
|
fi
|
|
rm -rf "$JF_HEAL_LOCK_DIR"
|
|
mkdir "$JF_HEAL_LOCK_DIR" 2>/dev/null || exit 0
|
|
echo $$ >"$JF_HEAL_LOCK_DIR/pid"
|
|
trap 'rm -rf "$JF_HEAL_LOCK_DIR"' EXIT
|
|
jf_refresh_heal_after_scan
|
|
fi
|
|
exit 0
|
|
fi
|
|
|
|
if jf_refresh_is_test_event; then
|
|
echo "Jellyfin refresh: test event received, skipping"
|
|
exit 0
|
|
fi
|
|
|
|
if [ -z "$JF_API_KEY" ]; then
|
|
echo "Jellyfin refresh: JELLYFIN_API_KEY not set" >&2
|
|
exit 1
|
|
fi
|
|
|
|
TASKS_JSON=$(jf_refresh_curl "http://${JF_HOST}:${JF_PORT}/ScheduledTasks" || echo "")
|
|
SCAN_STATE=$(jf_refresh_scan_state "$TASKS_JSON")
|
|
|
|
if jf_refresh_scan_busy "$SCAN_STATE"; then
|
|
echo "Jellyfin refresh: scan already ${SCAN_STATE}, not cancelling"
|
|
else
|
|
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" --connect-timeout 5 \
|
|
-X POST \
|
|
-H "X-Emby-Token: ${JF_API_KEY}" \
|
|
"http://${JF_HOST}:${JF_PORT}/Library/Refresh" 2>/dev/null || echo "000")
|
|
|
|
if [ "$HTTP_CODE" -ge 200 ] && [ "$HTTP_CODE" -lt 300 ]; then
|
|
echo "Jellyfin refresh: triggered successfully (HTTP ${HTTP_CODE})"
|
|
else
|
|
echo "Jellyfin refresh: failed (HTTP ${HTTP_CODE})" >&2
|
|
exit 1
|
|
fi
|
|
fi
|
|
|
|
# Sonarr/Radarr wait on this process; heal in the background so we do not
|
|
# block their notification pipeline. nohup survives the parent exiting.
|
|
SCRIPT_PATH="$0"
|
|
case "$SCRIPT_PATH" in
|
|
/*) ;;
|
|
*) SCRIPT_PATH="$(pwd)/$SCRIPT_PATH" ;;
|
|
esac
|
|
nohup env \
|
|
JELLYFIN_HOST="$JF_HOST" \
|
|
JELLYFIN_PORT="$JF_PORT" \
|
|
JELLYFIN_API_KEY="$JF_API_KEY" \
|
|
JF_REFRESH_HEAL_POLL_SEC="$JF_REFRESH_HEAL_POLL_SEC" \
|
|
JF_REFRESH_HEAL_MAX_POLLS="$JF_REFRESH_HEAL_MAX_POLLS" \
|
|
JF_HEAL_LOCK_DIR="$JF_HEAL_LOCK_DIR" \
|
|
"$SCRIPT_PATH" --heal-only >/tmp/assemblrr-jf-heal.log 2>&1 &
|
|
exit 0
|