386 lines
15 KiB
Bash
Executable File
386 lines
15 KiB
Bash
Executable File
#!/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 |