Files
Varaverk/Tools/emby_to_radarr_sync.sh
T
Gmer4Lfe 5866097d6a Make all arr library-list fetches cache-first with live fallback
Every script that fetches the full Lidarr/Sonarr/Radarr tracked-library
list now goes through arr_get_tracked_data() instead of hitting the API
directly -- cache-first when fresh, live fetch as fallback when stale,
waits out an active rescan before either. Per-item file data (trackFile/
episodefile/moviefile) stays live-only everywhere, since that's the
actual disk-truth these scripts' decisions depend on and was never part
of what's cached.

Also adds arr_cache_prefill.sh to CRITICAL_MAINTENANCE_SCRIPTS (30min
tier) with a short 1min wait ceiling, so the cache stays consistently
fresh instead of only refreshing whenever some other script happens to
write through. A full cache refresh for all three arrs measured at ~12s
total live -- nothing like the multi-hour cost of an actual rescan.
2026-07-16 23:27:23 -04:00

346 lines
14 KiB
Bash
Executable File

#!/bin/bash
# ==============================================================================================
# =============================== Emby → Radarr Sync ==========================================
# ==============================================================================================
#
# PURPOSE
# ─────────────────────────────────────────────────────────────────────────────
# One-shot tool. Finds movies present in Emby that are not tracked in Radarr
# and adds them. No scoring — if it's in your library, Radarr should monitor it.
#
# Intended as a bootstrap / catch-up tool. Run after Radarr setup, after a
# database wipe, or any time you suspect gaps between your library and Radarr.
#
# ==============================================================================================
# FLOW
# ==============================================================================================
#
# 1. Fetch all Movie items from Emby (with TMDB provider IDs)
# 2. Fetch current Radarr library (indexed by TMDB ID and title)
# 3. For each Emby movie not in Radarr → add to Radarr
#
# Matching prefers TMDB ID when available (immune to title differences),
# falling back to case-insensitive title comparison.
#
# ==============================================================================================
# OPERATIONAL SAFEGUARDS
# ==============================================================================================
#
# Root Required
# acquire_lock requires root. Script exits cleanly if not root.
#
# Dry-Run Mode
# --dry-run shows all movies that would be added without making any Radarr API calls.
# Always run first when closing the gap after a fresh Radarr install or database wipe.
#
# Add-Only
# Only adds movies to Radarr. Items already tracked by TMDB ID or title are skipped
# without modification. Safe to run multiple times — the second run finds nothing to add.
#
# ==============================================================================================
# CONFIGURATION (master.conf / host*.conf)
# ==============================================================================================
#
# RADARR_EMBY_LIBRARIES — Emby library names to scan (master.conf); empty = all libraries
# HOST*_RADARR_URL / HOST*_RADARR_API_KEY — Radarr connection (aliased by detect_hosts)
# HOST*_EMBY_URL / HOST*_EMBY_API_KEY — Emby connection (aliased by detect_hosts)
#
# ==============================================================================================
# RUNTIME MODES
# ==============================================================================================
#
# emby_to_radarr_sync.sh — add all untracked movies to Radarr
# emby_to_radarr_sync.sh --dry-run — show what would be added, no changes
# emby_to_radarr_sync.sh --log — verbose output
#
# ==============================================================================================
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/../load_config.sh"
parse_args "$@"
if [[ "$EUID" -ne 0 ]]; then
error "Must be run as root"
exit 1
fi
acquire_lock
detect_hosts
if ! command -v jq >/dev/null 2>&1; then
error "jq not installed"
exit 1
fi
if [[ -z "${EMBY_URL:-}" || -z "${EMBY_API_KEY:-}" ]]; then
error "EMBY_URL / EMBY_API_KEY not configured — check host*.conf"
exit 1
fi
if [[ -z "${RADARR_URL:-}" || -z "${RADARR_API_KEY:-}" ]]; then
error "RADARR_URL / RADARR_API_KEY not configured — check host*.conf"
exit 1
fi
log "$ICON_GEAR Config: emby=${EMBY_URL} radarr=${RADARR_URL}"
[[ "$DRY_RUN" == true ]] && warn "DRY RUN — no movies will be added to Radarr"
# ==============================================================================================
# ── API HELPERS ───────────────────────────────────────────────────────────────────────────────
# ==============================================================================================
_radarr_get() {
curl -sf --max-time 30 \
-H "X-Api-Key: $RADARR_API_KEY" \
"${RADARR_URL}/api/v3/${1}" 2>/dev/null
}
_radarr_lookup() {
curl -sf --max-time 20 --get \
--data-urlencode "term=$1" \
-H "X-Api-Key: $RADARR_API_KEY" \
"${RADARR_URL}/api/v3/movie/lookup" 2>/dev/null
}
_radarr_post() {
curl -sf --max-time 20 -X POST \
-H "X-Api-Key: $RADARR_API_KEY" \
-H "Content-Type: application/json" \
-d "$1" \
"${RADARR_URL}/api/v3/movie" 2>/dev/null
}
_radarr_command() {
curl -sf --max-time 20 -X POST \
-H "X-Api-Key: $RADARR_API_KEY" \
-H "Content-Type: application/json" \
-d "$1" \
"${RADARR_URL}/api/v3/command" 2>/dev/null
}
# Episode/garbage title filter — catches anime episodes stored as individual files
# in the Movies library, fansub release names, and codec metadata strings.
_is_episode_title() {
local t="$1"
[[ "$t" == *"_-_"* ]] && return 0 # NANA_-_01_
[[ "$t" == *".-."* ]] && return 0 # Fractale.-.01
[[ "$t" =~ ^\[.*\]\[.*\] ]] && return 0 # [Title][NNN][...]
[[ "$t" =~ [[:space:]]'-'[[:space:]][0-9]{2,3}$ ]] && return 0 # Title - 001
[[ "$t" =~ [[:space:]]E[0-9]{2,3}([[:space:]]|\[) ]] && return 0 # Title E01 [group]
[[ "${t^^}" == *"HEVC"* ]] && return 0 # codec metadata
[[ "$t" == *"(TMDB:"* ]] && return 0 # codec metadata
[[ "$t" == *$'\xef\xbf\xbd'* || "$t" == *"?"* ]] && return 0 # encoding corruption
return 1
}
# ==============================================================================================
# ━━━ Fetch Emby Movie Library ━━━
# ==============================================================================================
echo ""
echo "━━━━━ $ICON_SUMMARY Emby → Radarr Sync — $(date '+%Y-%m-%d %H:%M:%S') ━━━━━"
echo "$ICON_HOST $MY_ID ($LOCAL_SERVER_NAME)"
[[ "$DRY_RUN" == true ]] && warn "DRY RUN — no movies will be added"
echo ""
echo "━━━ $ICON_SYNC Emby Movie Library ━━━"
declare -A EMBY_MOVIES # name → tmdb_id (empty string if none)
if [[ "${#RADARR_EMBY_LIBRARIES[@]}" -gt 0 ]]; then
LIBRARIES_JSON=$(emby_api "Library/VirtualFolders") || { error "Could not fetch Emby libraries"; exit 1; }
for lib_name in "${RADARR_EMBY_LIBRARIES[@]}"; do
lib_id=$(echo "$LIBRARIES_JSON" | jq -r --arg n "$lib_name" '.[] | select(.Name == $n) | .ItemId' 2>/dev/null)
if [[ -z "$lib_id" ]]; then
warn "Emby library not found: $lib_name"
continue
fi
log "Scanning library: $lib_name (ItemId: $lib_id)"
LIB_JSON=$(emby_api "Items?ParentId=${lib_id}&IncludeItemTypes=Movie&Recursive=true&Fields=ProviderIds&Limit=5000") || {
warn "Could not fetch movies from library: $lib_name"
continue
}
while IFS='|' read -r name tmdb_id; do
[[ -z "$name" || "$name" == "null" ]] && continue
_is_episode_title "$name" && { log " $ICON_SKIP Skipping episode/garbage: $name"; continue; }
EMBY_MOVIES["$name"]="$tmdb_id"
done < <(echo "$LIB_JSON" | jq -r '.Items[] | [.Name, (.ProviderIds.Tmdb // "")] | join("|")' 2>/dev/null)
done
else
log "RADARR_EMBY_LIBRARIES empty — scanning all Emby libraries"
EMBY_MOVIES_JSON=$(emby_api "Items?IncludeItemTypes=Movie&Recursive=true&Fields=ProviderIds&Limit=10000") || {
error "Could not fetch Emby movies"
exit 1
}
while IFS='|' read -r name tmdb_id; do
[[ -z "$name" || "$name" == "null" ]] && continue
_is_episode_title "$name" && { log " $ICON_SKIP Skipping episode/garbage: $name"; continue; }
EMBY_MOVIES["$name"]="$tmdb_id"
done < <(echo "$EMBY_MOVIES_JSON" | jq -r '.Items[] | [.Name, (.ProviderIds.Tmdb // "")] | join("|")' 2>/dev/null)
fi
EMBY_COUNT=${#EMBY_MOVIES[@]}
log "$EMBY_COUNT movies in Emby library"
if [[ "$EMBY_COUNT" -eq 0 ]]; then
warn "No movies found in Emby library"
exit 0
fi
# ==============================================================================================
# ━━━ Fetch Radarr Library ━━━
# ==============================================================================================
echo ""
echo "━━━ $ICON_SYNC Radarr Library ━━━"
# Cache-first — arr_get_tracked_data() serves the shared cache when it's fresh (kept current
# every 30min by arr_cache_prefill.sh in CRITICAL_MAINTENANCE_SCRIPTS), falls back to a live
# fetch when it's stale, and waits out an active rescan before either.
RADARR_MOVIES_JSON=$(arr_get_tracked_data "radarr" "$RADARR_URL" "$RADARR_API_KEY" "v3") || { error "Could not fetch Radarr movies"; exit 1; }
declare -A RADARR_TMDB # tmdb_id → 1
declare -A RADARR_TITLES # lower(title) → 1
while IFS='|' read -r title tmdb_id; do
[[ -n "$title" ]] && RADARR_TITLES["${title,,}"]=1
[[ -n "$tmdb_id" && "$tmdb_id" != "0" ]] && RADARR_TMDB["$tmdb_id"]=1
done < <(echo "$RADARR_MOVIES_JSON" | jq -r '.[] | [.title, (.tmdbId // 0 | tostring)] | join("|")' 2>/dev/null)
RADARR_COUNT=${#RADARR_TITLES[@]}
log "$RADARR_COUNT movies already in Radarr"
# ==============================================================================================
# ━━━ Find Gaps ━━━
# ==============================================================================================
echo ""
echo "━━━ $ICON_GEAR Comparing Libraries ━━━"
declare -A MISSING # name → tmdb_id
ALREADY=0
for name in "${!EMBY_MOVIES[@]}"; do
tmdb_id="${EMBY_MOVIES[$name]}"
if [[ -n "$tmdb_id" && "${RADARR_TMDB[$tmdb_id]+x}" ]]; then
(( ALREADY++ ))
log " $ICON_SKIP Already tracked (TMDB $tmdb_id): $name"
continue
fi
if [[ "${RADARR_TITLES[${name,,}]+x}" ]]; then
(( ALREADY++ ))
log " $ICON_SKIP Already tracked (title): $name"
continue
fi
MISSING["$name"]="$tmdb_id"
log " $ICON_WARN Not in Radarr: $name${tmdb_id:+ (TMDB: $tmdb_id)}"
done
IFS=$'\n' SORTED_MISSING=($(printf '%s\n' "${!MISSING[@]}" | sort))
echo " Already tracked: $ALREADY | Missing from Radarr: ${#MISSING[@]}"
if [[ "${#MISSING[@]}" -eq 0 ]]; then
echo "Radarr already tracks everything in Emby"
exit 0
fi
echo ""
echo "━━━ $ICON_SUMMARY Movies to add (${#MISSING[@]}) ━━━"
for name in "${SORTED_MISSING[@]}"; do
_tmdb="${MISSING[$name]}"
log " $name${_tmdb:+ (TMDB: $_tmdb)}"
done
if [[ "$DRY_RUN" == true ]]; then
warn "DRY RUN complete — run without --dry-run to add these movies"
exit 0
fi
# ==============================================================================================
# ━━━ Add to Radarr ━━━
# ==============================================================================================
echo ""
echo "━━━ $ICON_SYNC Adding to Radarr ━━━"
RADARR_ROOT=$(_radarr_get "rootfolder" | jq -r 'first(.[] | select(.accessible == true)) | .path' 2>/dev/null)
QUALITY_ID=$(_radarr_get "qualityprofile" | jq -r '.[0].id' 2>/dev/null)
if [[ -z "$RADARR_ROOT" ]]; then
error "Could not determine Radarr root folder"
exit 1
fi
log "Root folder: $RADARR_ROOT | Quality profile: $QUALITY_ID"
ADDED=0
FAILED=0
for name in "${SORTED_MISSING[@]}"; do
tmdb_id="${MISSING[$name]}"
if [[ -n "$tmdb_id" ]]; then
LOOKUP=$(_radarr_lookup "tmdb:$tmdb_id")
else
LOOKUP=$(_radarr_lookup "$name")
fi
if [[ -z "$LOOKUP" ]] || echo "$LOOKUP" | jq -e '. == [] or . == null' >/dev/null 2>&1; then
warn " $ICON_WARN No match in Radarr lookup: $name"
(( FAILED++ ))
continue
fi
MOVIE_DATA=$(echo "$LOOKUP" | jq '.[0]' 2>/dev/null)
RADARR_TITLE=$(echo "$MOVIE_DATA" | jq -r '.title // ""' 2>/dev/null)
if [[ -z "$RADARR_TITLE" ]]; then
warn " $ICON_WARN Empty result for: $name"
(( FAILED++ ))
continue
fi
PAYLOAD=$(echo "$MOVIE_DATA" | jq \
--arg root "$RADARR_ROOT" \
--argjson qid "$QUALITY_ID" \
'. + {
rootFolderPath: $root,
qualityProfileId: $qid,
monitored: true,
addOptions: {
searchForMovie: false
}
}' 2>/dev/null)
RESULT=$(_radarr_post "$PAYLOAD")
if echo "$RESULT" | jq -e '.id' >/dev/null 2>&1; then
MOVIE_ID=$(echo "$RESULT" | jq -r '.id')
_radarr_command "{\"name\":\"MoviesSearch\",\"movieIds\":[${MOVIE_ID}]}" >/dev/null
log " $ICON_DONE Added: $RADARR_TITLE"
(( ADDED++ ))
else
warn " $ICON_WARN Failed to add: $name"
log " $(echo "$RESULT" | head -c 200)"
(( FAILED++ ))
fi
done
# ==============================================================================================
# ━━━ Summary ━━━
# ==============================================================================================
echo ""
echo "━━━━━ $ICON_SUMMARY SYNC COMPLETE ━━━━━"
echo " $ICON_DONE Added: $ADDED"
[[ "$FAILED" -gt 0 ]] && echo " $ICON_WARN Failed: $FAILED"
echo " $ICON_SKIP Already tracked: $ALREADY"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
[[ "$ADDED" -gt 0 ]] && notify \
"$ADDED movie(s) added to Radarr from Emby library on $(hostname)" \
"Emby → Radarr Sync" "normal"
exit 0