#!/bin/bash ################################################################################ # Consolidated Plex Database Management Script ################################################################################ # # Author: Peter Wood # Description: Consolidated database management functionality combining the best # features from plex-database-repair.sh, recover-plex-database.sh, # and nuclear-plex-recovery.sh into a single, safe interface. # # Features: # - Read-only integrity checking # - Progressive repair strategies (gentle to aggressive) # - Service management with proper synchronization # - Comprehensive logging and error handling # - Manual intervention prompts for safety # # Usage: # ./plex-db-manager.sh check # Read-only integrity check # ./plex-db-manager.sh repair # Interactive repair # ./plex-db-manager.sh repair --gentle # Gentle repair only # ./plex-db-manager.sh repair --force # Aggressive repair # ./plex-db-manager.sh nuclear # Nuclear recovery # # Exit Codes: # 0 - Success # 1 - Database issues detected (no repair attempted) # 2 - Repair failed # 3 - Service management failure # ################################################################################ set -euo pipefail # Color codes for output readonly RED='\033[0;31m' readonly GREEN='\033[0;32m' readonly YELLOW='\033[1;33m' readonly BLUE='\033[0;34m' readonly CYAN='\033[0;36m' readonly WHITE='\033[1;37m' readonly BOLD='\033[1m' readonly DIM='\033[2m' readonly RESET='\033[0m' # Configuration readonly PLEX_SERVICE="plexmediaserver" readonly PLEX_SQLITE="/usr/lib/plexmediaserver/Plex SQLite" readonly PLEX_DB_DIR="/var/lib/plexmediaserver/Library/Application Support/Plex Media Server/Plug-in Support/Databases" readonly MAIN_DB="$PLEX_DB_DIR/com.plexapp.plugins.library.db" readonly BLOBS_DB="$PLEX_DB_DIR/com.plexapp.plugins.library.blobs.db" readonly BACKUP_ROOT="/mnt/share/media/backups/plex" SCRIPT_DIR="$(dirname "$(readlink -f "$0")")" readonly SCRIPT_DIR readonly LOG_FILE="$SCRIPT_DIR/logs/db-manager-$(date +%Y%m%d_%H%M%S).log" # DBRepair.sh location — searched in order: script dir, database dir, /usr/local/bin find_dbrepair() { local candidates=( "${SCRIPT_DIR}/DBRepair.sh" "${PLEX_DB_DIR}/DBRepair.sh" "/usr/local/bin/DBRepair.sh" ) for path in "${candidates[@]}"; do if [[ -x "$path" ]]; then echo "$path" return 0 fi done return 1 } # Create log directory mkdir -p "$SCRIPT_DIR/logs" # Logging functions log_message() { local message="$1" local timestamp timestamp=$(date '+%Y-%m-%d %H:%M:%S') echo -e "${CYAN}[${timestamp}]${RESET} ${message}" echo "[${timestamp}] ${message}" >> "$LOG_FILE" 2>/dev/null || true } log_error() { local message="$1" local timestamp timestamp=$(date '+%Y-%m-%d %H:%M:%S') echo -e "${RED}[${timestamp}] ERROR:${RESET} ${message}" >&2 echo "[${timestamp}] ERROR: ${message}" >> "$LOG_FILE" 2>/dev/null || true } log_success() { local message="$1" local timestamp timestamp=$(date '+%Y-%m-%d %H:%M:%S') echo -e "${GREEN}[${timestamp}] SUCCESS:${RESET} ${message}" echo "[${timestamp}] SUCCESS: ${message}" >> "$LOG_FILE" 2>/dev/null || true } log_warning() { local message="$1" local timestamp timestamp=$(date '+%Y-%m-%d %H:%M:%S') echo -e "${YELLOW}[${timestamp}] WARNING:${RESET} ${message}" echo "[${timestamp}] WARNING: ${message}" >> "$LOG_FILE" 2>/dev/null || true } # Print header print_header() { echo -e "\n${BOLD}${CYAN}+================================================================+${RESET}" echo -e "${BOLD}${CYAN}| PLEX DATABASE MANAGEMENT TOOL |${RESET}" echo -e "${BOLD}${CYAN}+================================================================+${RESET}\n" } # Check prerequisites check_prerequisites() { log_message "Checking prerequisites..." if [[ $EUID -eq 0 ]]; then log_error "Don't run this script as root! Use your regular user account." exit 3 fi if [[ ! -f "$PLEX_SQLITE" ]]; then log_error "Plex SQLite binary not found at: $PLEX_SQLITE" exit 3 fi if ! sudo chmod +x "$PLEX_SQLITE" 2>/dev/null; then log_warning "Could not make Plex SQLite executable, but will try to use it" fi if [[ ! -f "$MAIN_DB" ]]; then log_error "Main database not found at: $MAIN_DB" exit 3 fi log_success "Prerequisites check passed" } # Safe service management with proper synchronization manage_service() { local action="$1" local timeout="${2:-30}" case "$action" in "stop") log_message "Stopping Plex service..." if ! systemctl is-active --quiet "$PLEX_SERVICE"; then log_message "Plex service is already stopped" return 0 fi if sudo systemctl stop "$PLEX_SERVICE"; then # Wait for complete shutdown local count=0 while systemctl is-active --quiet "$PLEX_SERVICE" && [[ $count -lt $timeout ]]; do sleep 1 ((count++)) done if systemctl is-active --quiet "$PLEX_SERVICE"; then log_error "Service failed to stop within ${timeout}s" return 1 else log_success "Plex service stopped successfully" # Additional wait for complete cleanup sleep 3 return 0 fi else log_error "Failed to stop Plex service" return 1 fi ;; "start") log_message "Starting Plex service..." if systemctl is-active --quiet "$PLEX_SERVICE"; then log_message "Plex service is already running" return 0 fi if sudo systemctl start "$PLEX_SERVICE"; then # Wait for startup local count=0 while ! systemctl is-active --quiet "$PLEX_SERVICE" && [[ $count -lt $timeout ]]; do sleep 1 ((count++)) done if systemctl is-active --quiet "$PLEX_SERVICE"; then log_success "Plex service started successfully" return 0 else log_error "Service failed to start within ${timeout}s" return 1 fi else log_error "Failed to start Plex service" return 1 fi ;; esac } # Safe database integrity check (read-only) check_database_integrity() { local db_file="$1" local db_name db_name=$(basename "$db_file") log_message "Checking database integrity: $db_name" # WAL checkpoint if needed (read-only operation) local wal_file="${db_file}-wal" if [[ -f "$wal_file" ]]; then log_message "WAL file detected, performing read-only checkpoint..." if sudo "$PLEX_SQLITE" "$db_file" "PRAGMA wal_checkpoint(PASSIVE);" >/dev/null 2>&1; then log_success "WAL checkpoint completed" else log_warning "WAL checkpoint failed, proceeding with integrity check" fi fi # Run structural integrity check local integrity_result integrity_result=$(sudo "$PLEX_SQLITE" "$db_file" "PRAGMA integrity_check;" 2>&1) local check_exit_code=$? if [[ $check_exit_code -ne 0 && -z "$integrity_result" ]]; then log_error "Failed to open database: $db_name (exit code $check_exit_code)" return 1 fi local struct_ok=true if [[ "$integrity_result" == "ok" ]]; then log_success "Structural integrity check passed: $db_name" else struct_ok=false log_warning "Structural integrity issues detected in $db_name:" echo "$integrity_result" | head -n 10 | while IFS= read -r line; do log_warning " $line" done fi # FTS (Full-Text Search) index integrity check # Standard PRAGMA integrity_check does NOT detect FTS corruption. local fts_ok=true local fts_tables fts_tables=$(sudo "$PLEX_SQLITE" "$db_file" \ "SELECT name FROM sqlite_master WHERE type='table' AND sql LIKE '%fts%';" 2>/dev/null) || true if [[ -n "$fts_tables" ]]; then log_message "Checking FTS (Full-Text Search) indexes in $db_name..." while IFS= read -r table; do [[ -z "$table" ]] && continue local fts_result fts_result=$(sudo "$PLEX_SQLITE" "$db_file" \ "INSERT INTO ${table}(${table}) VALUES('integrity-check');" 2>&1) || true if [[ -n "$fts_result" ]]; then fts_ok=false log_warning "FTS index '${table}' — DAMAGED: $fts_result" else log_success "FTS index '${table}' — OK" fi done <<< "$fts_tables" fi if [[ "$struct_ok" == true && "$fts_ok" == true ]]; then return 0 fi return 1 } # Check all databases check_all_databases() { local issues=0 log_message "Starting comprehensive database integrity check..." for db in "$MAIN_DB" "$BLOBS_DB"; do if [[ -f "$db" ]]; then if ! check_database_integrity "$db"; then ((issues++)) fi else log_warning "Database file not found: $(basename "$db")" ((issues++)) fi done if [[ $issues -eq 0 ]]; then log_success "All database integrity checks passed" return 0 else log_warning "Found integrity issues in $issues database(s)" return 1 fi } # Show help show_help() { echo -e "${BOLD}${WHITE}Usage:${RESET} ${CYAN}$(basename "$0")${RESET} ${YELLOW}${RESET} ${DIM}[options]${RESET}" echo "" echo -e "${BOLD}${WHITE}Commands:${RESET}" echo -e " ${GREEN}${BOLD}check${RESET} Read-only database integrity check" echo -e " ${YELLOW}${BOLD}repair${RESET} Interactive database repair" echo -e " ${YELLOW}${BOLD}repair --gentle${RESET} Gentle repair methods only" echo -e " ${RED}${BOLD}repair --force${RESET} Aggressive repair methods" echo -e " ${RED}${BOLD}nuclear${RESET} Nuclear recovery (replace from backup)" echo -e " ${CYAN}${BOLD}help${RESET} Show this help message" echo "" echo -e "${BOLD}${WHITE}Examples:${RESET}" echo -e " ${DIM}$(basename "$0") check # Safe integrity check${RESET}" echo -e " ${DIM}$(basename "$0") repair # Interactive repair${RESET}" echo -e " ${DIM}$(basename "$0") repair --gentle # Minimal repair only${RESET}" echo "" echo -e "${BOLD}${YELLOW}⚠️ WARNING:${RESET} Always run ${CYAN}check${RESET} first before attempting repairs!" echo "" } # Main function main() { if [[ $# -eq 0 ]]; then print_header show_help exit 0 fi case "${1,,}" in "check") print_header check_prerequisites # Stop service for clean check if ! manage_service stop; then log_error "Cannot perform integrity check while service is running" exit 3 fi # Check databases if check_all_databases; then exit_code=0 else exit_code=1 fi # Restart service if ! manage_service start; then log_error "Failed to restart service after check" exit 3 fi exit $exit_code ;; "repair") print_header check_prerequisites local dbrepair_bin if dbrepair_bin=$(find_dbrepair); then log_success "Found DBRepair.sh: $dbrepair_bin" log_message "Running: stop → auto (check + repair + reindex + FTS rebuild) → start → exit" if sudo "$dbrepair_bin" stop auto start exit; then log_success "DBRepair automatic repair completed successfully" exit 0 else log_error "DBRepair automatic repair failed" exit 2 fi else echo -e "${RED}${BOLD}⚠️ DBRepair.sh NOT FOUND${RESET}" echo -e "${YELLOW}Install DBRepair for repair/optimize/reindex/FTS-rebuild:${RESET}" echo -e " ${CYAN}wget -O ${SCRIPT_DIR}/DBRepair.sh https://github.com/ChuckPa/PlexDBRepair/releases/latest/download/DBRepair.sh${RESET}" echo -e " ${CYAN}chmod +x ${SCRIPT_DIR}/DBRepair.sh${RESET}" echo -e "${YELLOW}Then re-run: $(basename "$0") repair${RESET}" exit 2 fi ;; "nuclear") print_header check_prerequisites echo -e "\n${RED}${BOLD}⚠️ WARNING: NUCLEAR RECOVERY ⚠️${RESET}" echo -e "${RED}This replaces your Plex database with the best available PMS backup!${RESET}" echo -e "${YELLOW}All changes since the backup was created will be lost.${RESET}\n" echo -e "${CYAN}Type 'YES' to proceed: ${RESET}" read -r confirmation if [[ "$confirmation" != "YES" ]]; then log_message "Nuclear recovery cancelled by user" exit 0 fi local dbrepair_bin if dbrepair_bin=$(find_dbrepair); then log_success "Found DBRepair.sh: $dbrepair_bin" log_message "Running: stop → replace → reindex → start → exit" if sudo "$dbrepair_bin" stop replace reindex start exit; then log_success "Nuclear recovery (replace from backup) completed" exit 0 else log_error "Nuclear recovery failed" exit 2 fi else # Fallback to dedicated nuclear script local nuclear_script="${SCRIPT_DIR}/nuclear-plex-recovery.sh" if [[ -x "$nuclear_script" ]]; then log_message "DBRepair not found, falling back to nuclear-plex-recovery.sh" if sudo "$nuclear_script" --auto; then log_success "Nuclear recovery completed" exit 0 else log_error "Nuclear recovery failed" exit 2 fi else log_error "Neither DBRepair.sh nor nuclear-plex-recovery.sh found" exit 2 fi fi ;; "help"|"--help"|"-h") print_header show_help ;; *) print_header log_error "Unknown command: $1" echo "" show_help exit 1 ;; esac } # Execute main function main "$@"