Files
Varaverk/Tools/emby_to_sonarr_sync.sh
T
Gmer4Lfe 6ffe5bea70 Add Sonarr discovery; wire search commands to all arr adds
- New: Media/playback_aware_sonarr_discovery.sh — two-stage TV show
  discovery using Emby episode play history → TMDB TV recommendations.
  Multi-user aware: diversity score (0-50) is primary seed driver so one
  user binge-watching alone cannot dominate. Per-user episode cap (default 8)
  limits single-user volume contribution. TMDB→TVDB via external_ids for
  reliable Sonarr lookup. monitor=future by default.

- Add to WEEKLY_MAINTENANCE_SCRIPTS; Sonarr discovery config block in
  master.conf (threshold=52, lookback=14d, max_adds=3, user_ep_cap=8).

- Fix: all arr add scripts were not triggering searches after adding.
  Added _radarr_command/_lidarr_command/_sonarr_command helpers and fire
  MoviesSearch/ArtistSearch/SeriesSearch immediately after each successful
  add. Affects: playback_aware_radarr_discovery.sh, emby_to_lidarr_sync.sh,
  emby_to_radarr_sync.sh, emby_to_sonarr_sync.sh.
2026-05-20 17:27:07 -04:00

320 lines
12 KiB
Bash

#!/bin/bash
# ==============================================================================================
# =============================== Emby → Sonarr Sync ==========================================
# ==============================================================================================
#
# PURPOSE
# ─────────────────────────────────────────────────────────────────────────────
# One-shot tool. Finds TV series present in Emby that are not tracked in Sonarr
# and adds them. No scoring — if it's in your library, Sonarr should monitor it.
#
# Intended as a bootstrap / catch-up tool. Run after Sonarr setup, after a
# database wipe, or any time you suspect gaps between your library and Sonarr.
#
# ==============================================================================================
# FLOW
# ==============================================================================================
#
# 1. Fetch all Series items from Emby (with TVDB provider IDs)
# 2. Fetch current Sonarr library (indexed by TVDB ID and title)
# 3. For each Emby series not in Sonarr → add to Sonarr
#
# Matching prefers TVDB ID when available (immune to title differences),
# falling back to case-insensitive title comparison.
#
# ==============================================================================================
# USAGE
# ==============================================================================================
#
# emby_to_sonarr_sync.sh — add all untracked series to Sonarr
# emby_to_sonarr_sync.sh --dry-run — show what would be added, no changes
# emby_to_sonarr_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 master_host*.conf"
exit 1
fi
if [[ -z "${SONARR_URL:-}" || -z "${SONARR_API_KEY:-}" ]]; then
error "SONARR_URL / SONARR_API_KEY not configured — check master_host*.conf"
exit 1
fi
[[ "$DRY_RUN" == true ]] && warn "DRY RUN — no series will be added to Sonarr"
# ==============================================================================================
# ── API HELPERS ───────────────────────────────────────────────────────────────────────────────
# ==============================================================================================
_emby_api() {
local response http_code body
response=$(curl -sf --max-time 30 \
-H "X-Emby-Token: $EMBY_API_KEY" \
-w "\n%{http_code}" \
"${EMBY_URL}/${1}" 2>/dev/null)
http_code=$(echo "$response" | tail -1)
body=$(echo "$response" | head -n -1)
[[ "$http_code" != "200" ]] && { error "Emby API HTTP $http_code: $1"; return 1; }
echo "$body"
}
_sonarr_get() {
curl -sf --max-time 30 \
-H "X-Api-Key: $SONARR_API_KEY" \
"${SONARR_URL}/api/v3/${1}" 2>/dev/null
}
_sonarr_lookup() {
curl -sf --max-time 20 --get \
--data-urlencode "term=$1" \
-H "X-Api-Key: $SONARR_API_KEY" \
"${SONARR_URL}/api/v3/series/lookup" 2>/dev/null
}
_sonarr_post() {
curl -sf --max-time 20 -X POST \
-H "X-Api-Key: $SONARR_API_KEY" \
-H "Content-Type: application/json" \
-d "$1" \
"${SONARR_URL}/api/v3/series" 2>/dev/null
}
_sonarr_command() {
curl -sf --max-time 20 -X POST \
-H "X-Api-Key: $SONARR_API_KEY" \
-H "Content-Type: application/json" \
-d "$1" \
"${SONARR_URL}/api/v3/command" 2>/dev/null
}
# Garbage title filter — catches multi-season folder names and bare fansub tags.
_is_garbage_title() {
local t="$1"
[[ "$t" =~ S[0-9]{2}-S[0-9]{2} ]] && return 0 # Fruits Basket S01-S03
[[ "$t" =~ ^\[.*\]$ ]] && return 0 # [Prof] bare bracket tag
[[ "$t" == *$'\xef\xbf\xbd'* || "$t" == *"?"* ]] && return 0 # encoding corruption
return 1
}
# ==============================================================================================
# ━━━ Fetch Emby Series Library ━━━
# ==============================================================================================
echo ""
echo "━━━━━ $ICON_SUMMARY Emby → Sonarr Sync — $(date '+%Y-%m-%d %H:%M:%S') ━━━━━"
echo "$ICON_HOST $MY_ID ($LOCAL_SERVER_NAME)"
[[ "$DRY_RUN" == true ]] && warn "DRY RUN — no series will be added"
echo ""
echo "━━━ $ICON_SYNC Emby Series Library ━━━"
if [[ "${#SONARR_EMBY_LIBRARIES[@]}" -eq 0 ]]; then
error "SONARR_EMBY_LIBRARIES not configured — check master.conf"
exit 1
fi
# Fetch library list and resolve names → ItemIds
LIBRARIES_JSON=$(_emby_api "Library/VirtualFolders") || { error "Could not fetch Emby libraries"; exit 1; }
declare -A EMBY_SERIES # name → tvdb_id (empty string if none)
for lib_name in "${SONARR_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=Series&Recursive=true&Fields=ProviderIds&Limit=5000") || {
warn "Could not fetch series from library: $lib_name"
continue
}
while IFS='|' read -r name tvdb_id; do
[[ -z "$name" || "$name" == "null" ]] && continue
_is_garbage_title "$name" && { log " $ICON_SKIP Skipping garbage title: $name"; continue; }
EMBY_SERIES["$name"]="$tvdb_id"
done < <(echo "$LIB_JSON" | jq -r '.Items[] | [.Name, (.ProviderIds.Tvdb // "")] | join("|")' 2>/dev/null)
done
EMBY_COUNT=${#EMBY_SERIES[@]}
log "$EMBY_COUNT series in Emby library"
if [[ "$EMBY_COUNT" -eq 0 ]]; then
warn "No series found in Emby library"
exit 0
fi
# ==============================================================================================
# ━━━ Fetch Sonarr Library ━━━
# ==============================================================================================
echo ""
echo "━━━ $ICON_SYNC Sonarr Library ━━━"
SONARR_SERIES_JSON=$(_sonarr_get "series") || { error "Could not fetch Sonarr series"; exit 1; }
declare -A SONARR_TVDB # tvdb_id → 1
declare -A SONARR_TITLES # lower(title) → 1
while IFS='|' read -r title tvdb_id; do
[[ -n "$title" ]] && SONARR_TITLES["${title,,}"]=1
[[ -n "$tvdb_id" && "$tvdb_id" != "0" ]] && SONARR_TVDB["$tvdb_id"]=1
done < <(echo "$SONARR_SERIES_JSON" | jq -r '.[] | [.title, (.tvdbId // 0 | tostring)] | join("|")' 2>/dev/null)
SONARR_COUNT=${#SONARR_TITLES[@]}
log "$SONARR_COUNT series already in Sonarr"
# ==============================================================================================
# ━━━ Find Gaps ━━━
# ==============================================================================================
echo ""
echo "━━━ $ICON_GEAR Comparing Libraries ━━━"
declare -A MISSING # name → tvdb_id
ALREADY=0
for name in "${!EMBY_SERIES[@]}"; do
tvdb_id="${EMBY_SERIES[$name]}"
if [[ -n "$tvdb_id" && "${SONARR_TVDB[$tvdb_id]+x}" ]]; then
(( ALREADY++ ))
log " $ICON_SKIP Already tracked (TVDB $tvdb_id): $name"
continue
fi
if [[ "${SONARR_TITLES[${name,,}]+x}" ]]; then
(( ALREADY++ ))
log " $ICON_SKIP Already tracked (title): $name"
continue
fi
MISSING["$name"]="$tvdb_id"
log " $ICON_WARN Not in Sonarr: $name${tvdb_id:+ (TVDB: $tvdb_id)}"
done
IFS=$'\n' SORTED_MISSING=($(printf '%s\n' "${!MISSING[@]}" | sort))
echo " Already tracked: $ALREADY | Missing from Sonarr: ${#MISSING[@]}"
if [[ "${#MISSING[@]}" -eq 0 ]]; then
log "Sonarr already tracks everything in Emby"
exit 0
fi
echo ""
echo "━━━ $ICON_SUMMARY Series to add (${#MISSING[@]}) ━━━"
for name in "${SORTED_MISSING[@]}"; do
_tvdb="${MISSING[$name]}"
echo " $name${_tvdb:+ (TVDB: $_tvdb)}"
done
if [[ "$DRY_RUN" == true ]]; then
warn "DRY RUN complete — run without --dry-run to add these series"
exit 0
fi
# ==============================================================================================
# ━━━ Add to Sonarr ━━━
# ==============================================================================================
echo ""
echo "━━━ $ICON_SYNC Adding to Sonarr ━━━"
SONARR_ROOT=$(_sonarr_get "rootfolder" | jq -r 'first(.[] | select(.accessible == true)) | .path' 2>/dev/null)
QUALITY_ID=$(_sonarr_get "qualityprofile" | jq -r '.[0].id' 2>/dev/null)
if [[ -z "$SONARR_ROOT" ]]; then
error "Could not determine Sonarr root folder"
exit 1
fi
log "Root folder: $SONARR_ROOT | Quality profile: $QUALITY_ID"
ADDED=0
FAILED=0
for name in "${SORTED_MISSING[@]}"; do
tvdb_id="${MISSING[$name]}"
if [[ -n "$tvdb_id" ]]; then
LOOKUP=$(_sonarr_lookup "tvdb:$tvdb_id")
else
LOOKUP=$(_sonarr_lookup "$name")
fi
if [[ -z "$LOOKUP" ]] || echo "$LOOKUP" | jq -e '. == [] or . == null' >/dev/null 2>&1; then
warn " $ICON_WARN No match in Sonarr lookup: $name"
(( FAILED++ ))
continue
fi
SERIES_DATA=$(echo "$LOOKUP" | jq '.[0]' 2>/dev/null)
SONARR_TITLE=$(echo "$SERIES_DATA" | jq -r '.title // ""' 2>/dev/null)
if [[ -z "$SONARR_TITLE" ]]; then
warn " $ICON_WARN Empty result for: $name"
(( FAILED++ ))
continue
fi
PAYLOAD=$(echo "$SERIES_DATA" | jq \
--arg root "$SONARR_ROOT" \
--argjson qid "$QUALITY_ID" \
'. + {
rootFolderPath: $root,
qualityProfileId: $qid,
monitored: true,
seasonFolder: true,
addOptions: {
monitor: "all",
searchForMissingEpisodes: false
}
}' 2>/dev/null)
RESULT=$(_sonarr_post "$PAYLOAD")
if echo "$RESULT" | jq -e '.id' >/dev/null 2>&1; then
SERIES_ID=$(echo "$RESULT" | jq -r '.id')
_sonarr_command "{\"name\":\"SeriesSearch\",\"seriesId\":${SERIES_ID}}" >/dev/null
log " $ICON_DONE Added: $SONARR_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 series added to Sonarr from Emby library on $(hostname)" \
"Emby → Sonarr Sync" "normal"
exit 0