Files
Varaverk/Tools/emby_database_repair.sh
T
Gmer4Lfe 95151c2278 Watchdogs/ folder + host conf rename
Move all watchdog scripts to a dedicated Watchdogs/ folder:
  Docker_Essentials/docker_watchdog.sh   → Watchdogs/
  unRAID_Essentials/system_watchdog.sh   → Watchdogs/
  unRAID_Essentials/resource_watchdog.sh → Watchdogs/
  Orchestrators/watchdog_orchestrator.sh → Watchdogs/
  Tools/watchdog_skip_list_manager.sh    → Watchdogs/

Rename host config files:
  master_host1.conf → host1.conf
  master_host2.conf → host2.conf

Update all references across the ecosystem:
  master.conf: WATCHDOG_ORCHESTRATOR_SCRIPTS paths → Watchdogs/
  load_config.sh: host*.conf glob + all comments
  git_pull_execute.sh: sparse checkout glob + all comments
  Partnership/ssh_setup.sh: HOST_CONF path construction
  user_script_plug-in.sh: all script paths + per-host conf path
  common.sh, README.md, README-User_Script_Plug-in.md: comment refs
  All Partnership, Fallback, Monitors, Transcodes, Tools scripts: comment refs
2026-05-22 17:08:36 -04:00

386 lines
15 KiB
Bash

#!/bin/bash
# ==============================================================================================
# ============================= Emby Database Repair ===========================================
# ==============================================================================================
#
# PURPOSE
# ─────────────────────────────────────────────────────────────────────────────
# Stops Emby, runs SQLite PRAGMA integrity_check on all Emby databases, and
# restarts. Use when Emby reports corruption, unexpected crashes, or playback
# state issues.
#
# Reports which databases are corrupted. Does NOT automatically repair.
# Repair requires manual steps — guidance is printed in the summary output.
# Always take a backup before deleting any database file.
#
# ==============================================================================================
# OPERATIONAL MODEL
# ==============================================================================================
#
# Databases Checked
# library.db — media library metadata (largest, most critical)
# library.db-wal — write-ahead log (if present — uncommitted transactions)
# librarydb.db — legacy library database
# users.db — user accounts and settings
# authentication.db — API keys and sessions
# activity.db — activity log (least critical, safe to delete if corrupt)
#
# Missing databases are skipped gracefully — not all files exist on all setups.
# Emby's config path is detected from the Docker mount — no hardcoded paths.
#
# ==============================================================================================
# OPERATIONAL SAFEGUARDS
# ==============================================================================================
#
# Guaranteed Restart
# EXIT trap ensures Emby is always restarted even if the script crashes
# mid-check — Emby is never left stopped due to a script error.
#
# Docker Timeout
# DOCKER_TIMEOUT (30s) protects all docker calls against a hung daemon.
# Emby can take time to stop cleanly — 30s is intentionally generous.
#
# Tool Validation
# validate_unraid_cmd confirms sqlite3 and the notify script are present
# before use. jq is checked separately — required for config path detection.
#
# Post-Restart Verify
# Checks that Emby is still running after restart — detects cases where
# Emby crashes immediately after start (which would indicate deeper trouble).
#
# Silent When Healthy
# Only corruption produces visible output and a notification.
#
# ==============================================================================================
# CONFIGURATION
# ==============================================================================================
#
# host*.conf
#
# HOST*_EMBY_CONTAINER
# Name of the Emby Docker container on this host.
# Aliased by detect_hosts() → EMBY_CONTAINER.
#
# ==============================================================================================
# RUNTIME MODES
# ==============================================================================================
#
# emby_database_repair.sh
# Stop Emby, check all databases with PRAGMA integrity_check, restart.
#
# emby_database_repair.sh --dry-run
# Show which databases would be checked and Emby container name. No stop.
#
# emby_database_repair.sh --log
# Verbose output with per-database check result.
#
# emby_database_repair.sh --status
# Show Emby container name and config path detected from Docker. Then exit.
#
# ==============================================================================================
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/../load_config.sh"
parse_args "$@"
DOCKER_TIMEOUT=30 # Emby can take time to stop cleanly
# ==============================================================================================
# ━━━ Setup ━━━
# ==============================================================================================
if [[ "$EUID" -ne 0 ]]; then
error "Must be run as root"
exit 1
fi
# Validate required tools
validate_unraid_cmd \
"$(command -v sqlite3 2>/dev/null || echo /usr/bin/sqlite3)" \
"--version" "." \
"sqlite3" || { error "sqlite3 not found — install sqlite package"; exit 1; }
validate_unraid_cmd \
"/usr/local/emhttp/plugins/dynamix/scripts/notify" \
"" "" \
"unRAID notify script" || warn "unRAID notify script not found — native notifications disabled"
if ! command -v jq >/dev/null 2>&1; then
error "jq not found — required to detect Emby config path from Docker mounts"
exit 1
fi
acquire_lock
if ! command -v docker &>/dev/null; then
error "Docker command not found"
exit 1
fi
# detect_hosts() sets MY_ID and aliases HOST*_EMBY_CONTAINER → EMBY_CONTAINER
detect_hosts
if [[ -z "${EMBY_CONTAINER:-}" ]]; then
error "EMBY_CONTAINER not set for $MY_ID — check HOST*_EMBY_CONTAINER in host*.conf"
exit 1
fi
log "Identity: $MY_ID ($LOCAL_SERVER_NAME)"
log "Emby container: $EMBY_CONTAINER"
# Detect Emby config path from Docker container mounts
EMBY_CONFIG_HOST=$(timeout "$DOCKER_TIMEOUT" docker inspect "$EMBY_CONTAINER" 2>/dev/null | \
jq -r '.[] | .Mounts[] | select(.Destination == "/config") | .Source' 2>/dev/null)
if [[ -z "$EMBY_CONFIG_HOST" ]]; then
error "Could not detect Emby config path from Docker mounts"
error "Is $EMBY_CONTAINER the correct container name? Check HOST*_EMBY_CONTAINER in host*.conf"
exit 1
fi
log "Emby config: $EMBY_CONFIG_HOST"
[[ "$DRY_RUN" == true ]] && warn "DRY RUN — Emby will not be stopped, no checks run"
# ==============================================================================================
# ━━━ Status ━━━
# ==============================================================================================
if [[ "$SHOW_STATUS" == true ]]; then
echo ""
echo "━━━━━ $ICON_SUMMARY STATUS ━━━━━"
echo "$ICON_HOST Identity: $MY_ID ($LOCAL_SERVER_NAME)"
echo "$ICON_EMBY Container: $EMBY_CONTAINER"
echo "$ICON_EMBY Config: $EMBY_CONFIG_HOST"
echo "$ICON_GEAR Dry Run: $DRY_RUN"
echo ""
echo "━━━ Database Files ━━━"
for db_rel in "data/library.db" "data/library.db-wal" "data/librarydb.db" \
"data/users.db" "data/authentication.db" "data/activity.db"; do
db_path="${EMBY_CONFIG_HOST}/${db_rel}"
db_name=$(basename "$db_rel")
if [[ -f "$db_path" ]]; then
db_size=$(du -sh "$db_path" 2>/dev/null | cut -f1)
echo " $ICON_SUCCESS $db_name ($db_size)"
else
echo " $ICON_SKIP $db_name — not found"
fi
done
echo "━━━━━━━━━━━━━━━━━━━━━━━"
exit 0
fi
# ── EXIT trap — Emby always restarted if it was running ───────────────────────────────────────
EMBY_WAS_RUNNING=false
cleanup_on_exit() {
local exit_code=$?
if [[ "$EMBY_WAS_RUNNING" == true && "$DRY_RUN" == false ]]; then
local status
status=$(timeout "$DOCKER_TIMEOUT" docker inspect -f '{{.State.Running}}' \
"$EMBY_CONTAINER" 2>/dev/null)
if [[ "$status" != "true" ]]; then
warn "Restarting $EMBY_CONTAINER (cleanup)..."
timeout "$DOCKER_TIMEOUT" docker start "$EMBY_CONTAINER" >/dev/null 2>&1 || \
error "Failed to restart $EMBY_CONTAINER — start it manually"
fi
fi
}
trap cleanup_on_exit EXIT
# ==============================================================================================
# ━━━ Stop Emby ━━━
# ==============================================================================================
echo ""
echo "━━━ $ICON_STOP Stop Emby ━━━"
STATUS=$(timeout "$DOCKER_TIMEOUT" docker inspect -f '{{.State.Running}}' \
"$EMBY_CONTAINER" 2>/dev/null)
case "$STATUS" in
true)
EMBY_WAS_RUNNING=true
warn "Stopping $EMBY_CONTAINER — active sessions will be interrupted"
if [[ "$DRY_RUN" == false ]]; then
if timeout "$DOCKER_TIMEOUT" docker stop "$EMBY_CONTAINER" >/dev/null 2>&1; then
log "$EMBY_CONTAINER stopped ✅"
sleep 3 # let file handles release
else
error "Failed to stop $EMBY_CONTAINER — aborting"
exit 1
fi
else
warn "DRY RUN — would stop $EMBY_CONTAINER"
fi
;;
false)
log "$EMBY_CONTAINER is not running — proceeding with checks"
;;
"")
error "$EMBY_CONTAINER not found — check container name"
exit 1
;;
*)
warn "$EMBY_CONTAINER status: $STATUS — proceeding with caution"
;;
esac
# ==============================================================================================
# ━━━ Database Integrity Check ━━━
# ==============================================================================================
echo ""
echo "━━━ $ICON_HEALTH Database Integrity Check ━━━"
START=$(date +%s)
DB_FILES=(
"data/library.db"
"data/library.db-wal"
"data/librarydb.db"
"data/users.db"
"data/authentication.db"
"data/activity.db"
)
PASS_DBS=()
FAIL_DBS=()
MISSING_DBS=()
for db_rel in "${DB_FILES[@]}"; do
db_path="${EMBY_CONFIG_HOST}/${db_rel}"
db_name=$(basename "$db_rel")
if [[ ! -f "$db_path" ]]; then
log "$db_name — not found, skipping"
MISSING_DBS+=("$db_name")
continue
fi
DB_SIZE=$(du -sh "$db_path" 2>/dev/null | cut -f1)
log "Checking $db_name ($DB_SIZE)..."
# WAL file — different check (not a full SQLite database)
if [[ "$db_name" == *.wal ]]; then
if [[ -s "$db_path" ]]; then
warn "$db_name exists and is non-empty (${DB_SIZE})"
warn "Uncommitted WAL data — will be merged when Emby next starts cleanly"
PASS_DBS+=("$db_name (WAL — see warning)")
else
log "$db_name exists but is empty — no pending transactions ✅"
PASS_DBS+=("$db_name")
fi
continue
fi
if [[ "$DRY_RUN" == true ]]; then
warn "DRY RUN — would check: $db_path"
continue
fi
# Full integrity check
RESULT=$(sqlite3 "$db_path" "PRAGMA integrity_check;" 2>/dev/null)
EXIT_CODE=$?
if [[ "$EXIT_CODE" -ne 0 ]]; then
error "$db_name — sqlite3 could not open database (locked or corrupt)"
FAIL_DBS+=("$db_name")
elif [[ "$RESULT" == "ok" ]]; then
log "$db_name — integrity check passed ✅"
PASS_DBS+=("$db_name")
else
error "$db_name — CORRUPTION DETECTED"
echo "$RESULT" | head -10 | while IFS= read -r line; do
error " $line"
done
FAIL_DBS+=("$db_name")
fi
done
END=$(date +%s)
# ==============================================================================================
# ━━━ Restart Emby ━━━
# ==============================================================================================
echo ""
echo "━━━ $ICON_START Restart Emby ━━━"
RESTART_OK=false
if [[ "$EMBY_WAS_RUNNING" == true ]]; then
if [[ "$DRY_RUN" == false ]]; then
log "Restarting $EMBY_CONTAINER..."
if timeout "$DOCKER_TIMEOUT" docker start "$EMBY_CONTAINER" >/dev/null 2>&1; then
sleep 5 # Emby takes longer to initialise than most containers
POST_STATUS=$(timeout "$DOCKER_TIMEOUT" docker inspect -f \
'{{.State.Running}}' "$EMBY_CONTAINER" 2>/dev/null)
if [[ "$POST_STATUS" == "true" ]]; then
log "$EMBY_CONTAINER restarted and running ✅"
RESTART_OK=true
else
error "$EMBY_CONTAINER started but crashed — database may be corrupt"
error "Check Docker logs: docker logs $EMBY_CONTAINER"
notify "$EMBY_CONTAINER crashed on restart — possible database corruption on $(hostname)" \
"Emby DB Repair" "warning"
fi
else
error "Failed to restart $EMBY_CONTAINER — start it manually"
notify "$EMBY_CONTAINER failed to restart after integrity check on $(hostname)" \
"Emby DB Repair" "warning"
fi
else
warn "DRY RUN — would restart $EMBY_CONTAINER"
RESTART_OK=true
fi
else
log "$EMBY_CONTAINER was not running — leaving stopped (state respected) ✅"
RESTART_OK=true
fi
# Clear EXIT trap — clean exit
trap - EXIT
# ==============================================================================================
# ━━━ Summary ━━━
# ==============================================================================================
echo ""
echo "━━━━━ $ICON_SUMMARY EMBY DATABASE REPAIR SUMMARY ━━━━━"
echo "$ICON_HOST Identity: $MY_ID ($LOCAL_SERVER_NAME)"
echo "$ICON_EMBY Container: $EMBY_CONTAINER"
echo "$ICON_EMBY Config: $EMBY_CONFIG_HOST"
echo "$ICON_TIME Duration: $(format_duration $(( END - START )))"
echo ""
echo " $ICON_SUCCESS Passed: ${#PASS_DBS[@]}"
[[ ${#FAIL_DBS[@]} -gt 0 ]] && echo " $ICON_ERROR Failed: ${#FAIL_DBS[@]}"
[[ ${#MISSING_DBS[@]} -gt 0 ]] && echo " Skipped: ${#MISSING_DBS[@]} (not found)"
echo ""
[[ ${#PASS_DBS[@]} -gt 0 ]] && for db in "${PASS_DBS[@]}"; do log " $ICON_SUCCESS $db"; done
[[ ${#FAIL_DBS[@]} -gt 0 ]] && for db in "${FAIL_DBS[@]}"; do echo " $ICON_ERROR $db"; done
echo ""
if [[ "$DRY_RUN" == true ]]; then
warn "DRY RUN — no checks performed"
elif [[ ${#FAIL_DBS[@]} -gt 0 ]]; then
echo "$ICON_ERROR Status: CORRUPTION FOUND — manual intervention needed"
echo ""
echo "$ICON_INFO Next steps per corrupted database:"
echo " library.db — Delete file, restart Emby — rebuilds from media (slow first start)"
echo " library.db-wal — Delete WAL file, restart Emby — safe, no permanent data loss"
echo " librarydb.db — Delete file, restart Emby — legacy, Emby recreates"
echo " users.db — Restore from backup or delete — deleting resets all user accounts"
echo " authentication.db — Delete file, restart Emby — API keys regenerated automatically"
echo " activity.db — Delete file, restart Emby — activity log only, no media data"
echo ""
warn "⚠️ Always take a backup before deleting any database file"
warn " Run: container_data_export.sh $EMBY_CONTAINER <config_path> <backup_dir>"
notify "Emby database CORRUPTION on $(hostname) — failed: ${FAIL_DBS[*]} — manual intervention needed" \
"Emby DB Repair" "warning"
else
echo "$ICON_DONE Status: all ${#PASS_DBS[@]} databases healthy ✅"
fi
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
[[ ${#FAIL_DBS[@]} -gt 0 ]] && exit 1
exit 0