#!/bin/bash # ============================================================================================== # =============================== Emby → Lidarr Sync =========================================== # ============================================================================================== # # PURPOSE # ───────────────────────────────────────────────────────────────────────────── # One-shot tool. Finds artists played on Emby that are not tracked in Lidarr # and adds them. No scoring — if you played it, Lidarr should monitor it. # # Intended as a bootstrap / catch-up tool, not a scheduled script. Run it once # after Lidarr is set up, or any time you suspect gaps between what you listen # to and what Lidarr monitors. # # Cache-first Lidarr library fetch (2026-07-17) — comes from the shared tracked-data cache # via arr_get_tracked_data(), fresh (kept warm every 30min by arr_cache_prefill.sh), live # fetch as fallback. # # ============================================================================================== # FLOW # ============================================================================================== # # 1. Pull play completions from Emby activity log (all time, or --days N) # 2. Fetch current Lidarr artist library # 3. For each played artist not in Lidarr → add to Lidarr # # ============================================================================================== # OPERATIONAL SAFEGUARDS # ============================================================================================== # # Root Required # acquire_lock requires root. Script exits cleanly if not root. # # Dry-Run Mode # --dry-run shows all artists that would be added without making any Lidarr API calls. # Always run first when closing the gap after a fresh Lidarr install or database wipe. # # Add-Only # Only adds artists to Lidarr. Items already tracked (by name) are skipped without # modification. Safe to run multiple times — the second run finds nothing to add. # # ============================================================================================== # CONFIGURATION (host*.conf) # ============================================================================================== # # HOST*_LIDARR_URL / HOST*_LIDARR_API_KEY — Lidarr connection (aliased by detect_hosts) # HOST*_EMBY_URL / HOST*_EMBY_API_KEY — Emby connection (aliased by detect_hosts) # # ============================================================================================== # RUNTIME MODES # ============================================================================================== # # emby_to_lidarr_sync.sh — add all album artists not in Lidarr # emby_to_lidarr_sync.sh --dry-run — show what would be added, no changes # emby_to_lidarr_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 "${LIDARR_URL:-}" || -z "${LIDARR_API_KEY:-}" ]]; then error "LIDARR_URL / LIDARR_API_KEY not configured — check host*.conf" exit 1 fi log "$ICON_GEAR Config: emby=${EMBY_URL} lidarr=${LIDARR_URL}" [[ "$DRY_RUN" == true ]] && warn "DRY RUN — no artists will be added to Lidarr" # ============================================================================================== # ── API HELPERS ─────────────────────────────────────────────────────────────────────────────── # ============================================================================================== _lidarr_get() { curl -sf --max-time 30 \ -H "X-Api-Key: $LIDARR_API_KEY" \ "${LIDARR_URL}/api/v1/${1}" 2>/dev/null } _lidarr_lookup() { curl -sf --max-time 20 --get \ --data-urlencode "term=$1" \ -H "X-Api-Key: $LIDARR_API_KEY" \ "${LIDARR_URL}/api/v1/artist/lookup" 2>/dev/null } _lidarr_post() { curl -sf --max-time 20 -X POST \ -H "X-Api-Key: $LIDARR_API_KEY" \ -H "Content-Type: application/json" \ -d "$1" \ "${LIDARR_URL}/api/v1/artist" 2>/dev/null } _lidarr_command() { curl -sf --max-time 20 -X POST \ -H "X-Api-Key: $LIDARR_API_KEY" \ -H "Content-Type: application/json" \ -d "$1" \ "${LIDARR_URL}/api/v1/command" 2>/dev/null } _is_placeholder_artist() { local a="${1,,}" [[ "$a" =~ ^(va|various|various artists|unknown artist|unknown|soundtrack|original soundtrack|ost)$ ]] } # Dirty tag: comma-list, feat./ft., ampersand join, or "Artist - Album" in the AlbumArtist field _is_dirty_artist() { local lower="${1,,}" [[ "$1" == *", "* ]] && return 0 [[ "$lower" == *" feat."* ]] && return 0 [[ "$lower" == *" ft."* ]] && return 0 [[ "$1" == *" & "* ]] && return 0 [[ "$lower" == *" vs "* ]] && return 0 [[ "$1" == *" - "* ]] && return 0 [[ "$1" == *"?"* ]] && return 0 # ASCII ? — encoding corruption [[ "$1" == *$'\xef\xbf\xbd'* ]] && return 0 # U+FFFD replacement character — encoding corruption return 1 } # ============================================================================================== # ━━━ Fetch Emby Music Artist Library ━━━ # ============================================================================================== echo "" echo "━━━━━ $ICON_SUMMARY Emby → Lidarr Sync — $(date '+%Y-%m-%d %H:%M:%S') ━━━━━" echo "$ICON_HOST $MY_ID ($LOCAL_SERVER_NAME)" [[ "$DRY_RUN" == true ]] && warn "DRY RUN — no artists will be added" echo "" echo "━━━ $ICON_SYNC Emby Music Library ━━━" # Query MusicAlbum items and extract AlbumArtists — gives primary album artists only, # not the full tag credits (guest features, songwriters, etc.) that MusicArtist returns EMBY_ALBUMS_JSON=$(emby_api "Items?IncludeItemTypes=MusicAlbum&Recursive=true&Fields=AlbumArtists&Limit=10000") || { error "Could not fetch Emby music albums" exit 1 } declare -A PLAYED_ARTISTS while IFS= read -r artist; do [[ -z "$artist" || "$artist" == "null" ]] && continue _is_placeholder_artist "$artist" && continue _is_dirty_artist "$artist" && { log " $ICON_SKIP Skipping dirty tag: $artist"; continue; } PLAYED_ARTISTS["$artist"]=1 done < <(echo "$EMBY_ALBUMS_JSON" | jq -r '.Items[] | .AlbumArtists[]?.Name' 2>/dev/null) PLAYED_COUNT=${#PLAYED_ARTISTS[@]} log "$PLAYED_COUNT album artists in Emby library" if [[ "$PLAYED_COUNT" -eq 0 ]]; then warn "No album artists found in Emby library" exit 0 fi # ============================================================================================== # ━━━ Fetch Lidarr Library ━━━ # ============================================================================================== echo "" echo "━━━ $ICON_SYNC Lidarr 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. LIDARR_ARTISTS_JSON=$(arr_get_tracked_data "lidarr" "$LIDARR_URL" "$LIDARR_API_KEY" "v1") || { error "Could not fetch Lidarr artists"; exit 1; } LIDARR_NAMES=$(echo "$LIDARR_ARTISTS_JSON" | jq -r '.[].artistName' 2>/dev/null) LIDARR_COUNT=$(echo "$LIDARR_NAMES" | grep -c . 2>/dev/null || echo 0) log "$LIDARR_COUNT artists already in Lidarr" _in_lidarr() { echo "$LIDARR_NAMES" | grep -iq "^${1}$" } # ============================================================================================== # ━━━ Find Gaps ━━━ # ============================================================================================== echo "" echo "━━━ $ICON_GEAR Comparing Libraries ━━━" MISSING=() ALREADY=0 for artist in "${!PLAYED_ARTISTS[@]}"; do if _in_lidarr "$artist"; then (( ALREADY++ )) log " $ICON_SKIP Already tracked: $artist" else MISSING+=("$artist") log " $ICON_WARN Not in Lidarr: $artist" fi done IFS=$'\n' MISSING=($(printf '%s\n' "${MISSING[@]}" | sort)) echo " Already tracked: $ALREADY | Missing from Lidarr: ${#MISSING[@]}" if [[ "${#MISSING[@]}" -eq 0 ]]; then echo "Lidarr already tracks everything played on Emby" exit 0 fi echo "" echo "━━━ $ICON_SUMMARY Artists to add (${#MISSING[@]}) ━━━" for a in "${MISSING[@]}"; do echo " $a"; done if [[ "$DRY_RUN" == true ]]; then warn "DRY RUN complete — run without --dry-run to add these artists" exit 0 fi # ============================================================================================== # ━━━ Add to Lidarr ━━━ # ============================================================================================== echo "" echo "━━━ $ICON_SYNC Adding to Lidarr ━━━" LIDARR_ROOT=$(_lidarr_get "rootfolder" | jq -r 'first(.[] | select(.accessible == true)) | .path' 2>/dev/null) if [[ -z "$LIDARR_ROOT" ]]; then error "Could not determine Lidarr root folder"; exit 1; fi QUALITY_PROFILES=$(_lidarr_get "qualityprofile") || { error "Could not fetch quality profiles"; exit 1; } METADATA_PROFILES=$(_lidarr_get "metadataprofile") || { error "Could not fetch metadata profiles"; exit 1; } DEFAULT_QUALITY_ID=$(echo "$QUALITY_PROFILES" | jq -r '.[0].id' 2>/dev/null) DEFAULT_METADATA_ID=$(echo "$METADATA_PROFILES" | jq -r ' first(.[] | select(.name | test("Standard"; "i")) | .id) // .[0].id' 2>/dev/null) log "Quality profile: $DEFAULT_QUALITY_ID | Metadata profile: $DEFAULT_METADATA_ID" ADDED=0 FAILED=0 SKIPPED=0 for artist in "${MISSING[@]}"; do LOOKUP=$(_lidarr_lookup "$artist") if [[ -z "$LOOKUP" ]] || echo "$LOOKUP" | jq -e '. == [] or . == null' >/dev/null 2>&1; then warn " $ICON_WARN No match in Lidarr lookup: $artist" (( FAILED++ )) continue fi ARTIST_DATA=$(echo "$LOOKUP" | jq '.[0]' 2>/dev/null) MBID=$(echo "$ARTIST_DATA" | jq -r '.foreignArtistId // ""' 2>/dev/null) LIDARR_NAME=$(echo "$ARTIST_DATA" | jq -r '.artistName // ""' 2>/dev/null) if [[ -z "$MBID" ]]; then warn " $ICON_WARN No MusicBrainz ID for: $artist" (( FAILED++ )) continue fi PAYLOAD=$(echo "$ARTIST_DATA" | jq \ --arg root "$LIDARR_ROOT" \ --argjson qid "$DEFAULT_QUALITY_ID" \ --argjson mid "$DEFAULT_METADATA_ID" \ '. + { rootFolderPath: $root, qualityProfileId: $qid, metadataProfileId: $mid, monitored: true, addOptions: { monitor: "all", searchForMissingAlbums: false } }' 2>/dev/null) RESULT=$(_lidarr_post "$PAYLOAD") if echo "$RESULT" | jq -e '.id' >/dev/null 2>&1; then ARTIST_ID=$(echo "$RESULT" | jq -r '.id') _lidarr_command "{\"name\":\"ArtistSearch\",\"artistId\":${ARTIST_ID}}" >/dev/null log " $ICON_DONE Added: $LIDARR_NAME" (( ADDED++ )) else warn " $ICON_WARN Failed to add: $artist" 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 artist(s) added to Lidarr from Emby play history on $(hostname)" \ "Emby → Lidarr Sync" "normal" exit 0