#!/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 # platform_require_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 platform_require_cmd \ "$(command -v sqlite3 2>/dev/null || echo /usr/bin/sqlite3)" \ "--version" "." \ "sqlite3" || { error "sqlite3 not found — install sqlite package"; exit 1; } 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 echo "$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 echo "$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 echo "$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 echo "$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 echo "$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 echo " $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 " 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