feat: two-stage Lidarr discovery + Emby→arr sync tools
Lidarr Discovery (playback_aware_lidarr_discovery.sh): - Complete rewrite to two-stage pipeline: Stage 1 scores weekly Emby plays → top seeds; Stage 2 runs Last.fm getSimilar on seeds → scores candidates → adds top 0-5 to Lidarr - Per-user influence cap (35%) prevents single listener dominating discovery - Requires 2+ seeds to accept a candidate (single-seed skipped) - Last.fm similarity limit 10 for tighter, higher-quality candidates - 30-day reject cooldown history; MAX_ADDS=5 cap enforced - Root folder fetched from Lidarr API at runtime (no config path) - Added to WEEKLY_MAINTENANCE_SCRIPTS (uncommented) Emby→arr sync tools (Tools/): - emby_to_lidarr_sync.sh: finds Emby music artists not in Lidarr, adds them - Dirty tag filter: comma-list, feat./ft., &, vs, " - " patterns skipped - Root folder fetched from Lidarr API at runtime - emby_to_sonarr_sync.sh: finds Emby series not in Sonarr, adds them - TVDB ID matching with title fallback - emby_to_radarr_sync.sh: finds Emby movies not in Radarr, adds them - TMDB ID matching with title fallback - All three: searchForMissing*: false (monitoring only, no searches triggered) - All three documented in Tools/Manual-Tools.md
This commit is contained in:
Executable
+291
@@ -0,0 +1,291 @@
|
||||
#!/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.
|
||||
#
|
||||
# ==============================================================================================
|
||||
# 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
|
||||
#
|
||||
# ==============================================================================================
|
||||
# USAGE
|
||||
# ==============================================================================================
|
||||
#
|
||||
# emby_to_lidarr_sync.sh — add all played artists not in Lidarr
|
||||
# emby_to_lidarr_sync.sh --dry-run — show what would be added, no changes
|
||||
# emby_to_lidarr_sync.sh --days N — limit to plays in the last N days
|
||||
# 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 master_host*.conf"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -z "${LIDARR_URL:-}" || -z "${LIDARR_API_KEY:-}" ]]; then
|
||||
error "LIDARR_URL / LIDARR_API_KEY not configured — check master_host*.conf"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
[[ "$DRY_RUN" == true ]] && warn "DRY RUN — no artists will be added to Lidarr"
|
||||
|
||||
# ==============================================================================================
|
||||
# ── API HELPERS ───────────────────────────────────────────────────────────────────────────────
|
||||
# ==============================================================================================
|
||||
|
||||
_emby_api() {
|
||||
local endpoint="$1"
|
||||
local response http_code body
|
||||
response=$(curl -sf --max-time 30 \
|
||||
-H "X-Emby-Token: $EMBY_API_KEY" \
|
||||
-w "\n%{http_code}" \
|
||||
"${EMBY_URL}/${endpoint}" 2>/dev/null)
|
||||
http_code=$(echo "$response" | tail -1)
|
||||
body=$(echo "$response" | head -n -1)
|
||||
[[ "$http_code" != "200" ]] && { error "Emby API HTTP $http_code: $endpoint"; return 1; }
|
||||
echo "$body"
|
||||
}
|
||||
|
||||
_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
|
||||
}
|
||||
|
||||
_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
|
||||
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 ━━━"
|
||||
|
||||
LIDARR_ARTISTS_JSON=$(_lidarr_get "artist") || { 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
|
||||
log "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
|
||||
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
|
||||
Reference in New Issue
Block a user