Files
shell/plex/plex.sh

1023 lines
38 KiB
Bash
Executable File
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/bin/bash
################################################################################
# Plex Media Server Management Script
################################################################################
#
# Author: Peter Wood <peter@peterwood.dev>
# Description: Modern, user-friendly Plex Media Server management script with
# styled output and comprehensive service control capabilities.
# Provides an interactive interface for common Plex operations.
#
# Features:
# - Service start/stop/restart/status operations
# - Styled console output with Unicode symbols
# - Service health monitoring
# - Process management and monitoring
# - Interactive menu system
#
# Related Scripts:
# - backup-plex.sh: Comprehensive backup solution
# - restore-plex.sh: Backup restoration utilities
# - scan-plex-libraries.sh: Library scanning and metadata management
# - monitor-plex-backup.sh: Backup system monitoring
# - validate-plex-backups.sh: Backup validation tools
# - test-plex-backup.sh: Testing framework
#
# Usage:
# ./plex.sh start # Start Plex service
# ./plex.sh stop # Stop Plex service
# ./plex.sh restart # Restart Plex service
# ./plex.sh status # Show service status
# ./plex.sh scan # Launch library scanner
# ./plex.sh repair # Repair Plex database issues
# ./plex.sh nuclear # Nuclear recovery mode
# ./plex.sh help # Show help menu
# ./plex.sh logs # Show recent logs
# ./plex.sh # Interactive menu
#
# Dependencies:
# - systemctl (systemd service management)
# - Plex Media Server package
# - Web browser (for web interface launching)
#
# Exit Codes:
# 0 - Success
# 1 - General error
# 2 - Service operation failure
# 3 - Invalid command or option
#
################################################################################
# 🎬 Plex Media Server Management Script
# A modern script for managing Plex Media Server with style
# Author: acedanger
# Version: 2.0
set -euo pipefail
# 🎨 Color definitions for sexy output
readonly RED=$'\033[0;31m'
readonly GREEN=$'\033[0;32m'
readonly YELLOW=$'\033[1;33m'
readonly BLUE=$'\033[0;34m'
readonly PURPLE=$'\033[0;35m'
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"
SCRIPT_NAME="$(basename "$0")"
readonly SCRIPT_NAME
SCRIPT_DIR="$(dirname "$(readlink -f "$0")")"
readonly SCRIPT_DIR
# DBRepair.sh location — searched in order: script dir, database dir, /usr/local/bin
readonly DBREPAIR_INSTALL_PATH="${SCRIPT_DIR}/DBRepair.sh"
readonly DBREPAIR_GITHUB_API="https://api.github.com/repos/ChuckPa/PlexDBRepair/releases"
readonly DBREPAIR_DOWNLOAD_BASE="https://github.com/ChuckPa/PlexDBRepair/releases/download"
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
}
# Get the latest non-beta release tag from GitHub
_dbrepair_latest_release_tag() {
# Fetch releases, filter out pre-releases & drafts, grab the first tag_name
local tag
tag=$(curl -fsSL "${DBREPAIR_GITHUB_API}" 2>/dev/null \
| grep -Eo '"tag_name"\s*:\s*"[^"]+"' \
| head -n 1 \
| sed 's/"tag_name"\s*:\s*"//;s/"//')
if [[ -z "$tag" ]]; then
return 1
fi
echo "$tag"
}
# Get currently installed DBRepair version (from its own version output)
_dbrepair_installed_version() {
local bin="$1"
# DBRepair prints "Version vX.YY.ZZ" near the top when run interactively;
# we can also grep the script file itself for the version string.
grep -oP 'Version\s+v\K[0-9.]+' "$bin" 2>/dev/null | head -n 1
}
# Install or update DBRepair.sh
install_or_update_dbrepair() {
print_status "${INFO}" "Checking DBRepair (ChuckPa/PlexDBRepair)..." "${BLUE}"
# Determine latest non-beta release
local latest_tag
if ! latest_tag=$(_dbrepair_latest_release_tag); then
print_status "${CROSS}" "Failed to query GitHub for the latest release" "${RED}"
print_status "${INFO}" "Check your internet connection or try: https://github.com/ChuckPa/PlexDBRepair/releases" "${YELLOW}"
return 1
fi
print_status "${INFO}" "Latest stable release: ${latest_tag}" "${BLUE}"
# Check if already installed
local dbrepair_bin
if dbrepair_bin=$(find_dbrepair); then
local installed_ver
installed_ver=$(_dbrepair_installed_version "$dbrepair_bin")
local remote_ver
remote_ver=$(echo "$latest_tag" | sed 's/^v//')
if [[ -n "$installed_ver" ]]; then
print_status "${INFO}" "Installed version: v${installed_ver} at ${dbrepair_bin}" "${BLUE}"
if [[ "$installed_ver" == "$remote_ver" ]]; then
print_status "${CHECKMARK}" "DBRepair is already up to date (v${installed_ver})" "${GREEN}"
return 0
else
print_status "${INFO}" "Update available: v${installed_ver} -> ${latest_tag}" "${YELLOW}"
fi
else
print_status "${INFO}" "Installed at ${dbrepair_bin} (version unknown), will update" "${YELLOW}"
fi
else
print_status "${INFO}" "DBRepair not found — installing to ${DBREPAIR_INSTALL_PATH}" "${BLUE}"
fi
# Download
local download_url="${DBREPAIR_DOWNLOAD_BASE}/${latest_tag}/DBRepair.sh"
print_status "${INFO}" "Downloading ${download_url}" "${BLUE}"
if curl -fsSL -o "${DBREPAIR_INSTALL_PATH}" "$download_url"; then
chmod +x "${DBREPAIR_INSTALL_PATH}"
print_status "${CHECKMARK}" "DBRepair ${latest_tag} installed to ${DBREPAIR_INSTALL_PATH}" "${GREEN}"
return 0
else
print_status "${CROSS}" "Download failed" "${RED}"
rm -f "${DBREPAIR_INSTALL_PATH}" 2>/dev/null
return 1
fi
}
# Suggest installing DBRepair when errors are found and it's not available
_hint_install_dbrepair() {
if ! find_dbrepair >/dev/null 2>&1; then
echo ""
print_status "${INFO}" "DBRepair is NOT installed. It can fix most database issues automatically." "${YELLOW}"
echo -e "${DIM}${CYAN} Install it now: ${SCRIPT_NAME} install-dbrepair${RESET}"
echo -e "${DIM}${CYAN} Then repair: ${SCRIPT_NAME} repair${RESET}"
fi
}
# 📦 List and manage database backup files
# Covers: DBRepair backups (-BACKUP-*, -BKUP-*), script backups (*.backup.*),
# corrupted dirs (corrupted-*), recovery files (*recovery*)
list_db_backups() {
local db_dir="$PLEX_DB_DIR"
local -a backup_files=()
local -a backup_paths=()
# Collect all backup-like files and dirs
while IFS= read -r -d '' entry; do
backup_paths+=("$entry")
done < <(sudo find "$db_dir" -maxdepth 1 \( \
-name '*-BACKUP-*' -o \
-name '*-BKUP-*' -o \
-name '*.backup.*' -o \
-name '*recovery*' -o \
-name 'corrupted-*' -o \
-name '*-BLOATED-*' \
\) -print0 2>/dev/null | sort -z)
if [[ ${#backup_paths[@]} -eq 0 ]]; then
print_status "${INFO}" "No database backup files found in the Plex database directory" "${YELLOW}"
return 0
fi
echo -e "\n${BOLD}${WHITE} # Type Size Created Name${RESET}"
echo -e "${DIM}${CYAN} --- ------------- ----------- ------------------------ ------------------------------------${RESET}"
local idx=0
for entry in "${backup_paths[@]}"; do
((idx++))
local name
name=$(basename "$entry")
# Determine type label
local type_label
if [[ "$name" == *-BACKUP-* || "$name" == *-BKUP-* ]]; then
type_label="DBRepair"
elif [[ "$name" == *-BLOATED-* ]]; then
type_label="Bloated"
elif [[ "$name" == *.backup.* ]]; then
type_label="Script"
elif [[ "$name" == corrupted-* ]]; then
type_label="Corrupted"
elif [[ "$name" == *recovery* ]]; then
type_label="Recovery"
else
type_label="Other"
fi
# Size (human-readable)
local size
if [[ -d "$entry" ]]; then
size=$(sudo du -sh "$entry" 2>/dev/null | cut -f1)
type_label="${type_label}/dir"
else
size=$(sudo stat --printf='%s' "$entry" 2>/dev/null)
if [[ -n "$size" ]]; then
size=$(numfmt --to=iec-i --suffix=B "$size" 2>/dev/null || echo "${size}B")
else
size="?"
fi
fi
# Created date
local created
created=$(sudo stat --printf='%y' "$entry" 2>/dev/null | cut -d. -f1)
[[ -z "$created" ]] && created="unknown"
printf " ${WHITE}%-3s${RESET} ${CYAN}%-13s${RESET} ${YELLOW}%-11s${RESET} ${DIM}%-24s${RESET} %s\n" \
"$idx" "$type_label" "$size" "$created" "$name"
backup_files+=("$entry")
done
echo -e "${DIM}${CYAN} --- ------------- ----------- ------------------------ ------------------------------------${RESET}"
echo -e " ${DIM}Total: ${idx} backup file(s)${RESET}\n"
# Store for use by delete function
_BACKUP_LIST=("${backup_files[@]}")
_BACKUP_COUNT=$idx
}
# Interactive backup deletion
delete_db_backups_interactive() {
list_db_backups
if [[ ${_BACKUP_COUNT:-0} -eq 0 ]]; then
return 0
fi
echo -e "${CYAN}Enter backup number(s) to delete (comma-separated), or 'q' to cancel:${RESET} "
read -r selection
if [[ "$selection" == "q" || -z "$selection" ]]; then
print_status "${INFO}" "Cancelled" "${YELLOW}"
return 0
fi
# Parse comma-separated numbers
IFS=',' read -ra nums <<< "$selection"
local deleted=0
for num in "${nums[@]}"; do
num=$(echo "$num" | tr -d ' ')
if ! [[ "$num" =~ ^[0-9]+$ ]] || (( num < 1 || num > _BACKUP_COUNT )); then
print_status "${CROSS}" "Invalid selection: $num (skipping)" "${RED}"
continue
fi
local target="${_BACKUP_LIST[$((num-1))]}"
local target_name
target_name=$(basename "$target")
echo -e "${YELLOW}Delete ${target_name}? [y/N]:${RESET} "
read -r confirm
if [[ "${confirm,,}" == "y" ]]; then
if [[ -d "$target" ]]; then
sudo rm -rf "$target"
else
sudo rm -f "$target"
fi
print_status "${CHECKMARK}" "Deleted: $target_name" "${GREEN}"
((deleted++))
else
print_status "${INFO}" "Skipped: $target_name" "${YELLOW}"
fi
done
echo ""
print_status "${INFO}" "Deleted $deleted backup(s)" "${BLUE}"
}
# Delete backup by name/pattern (for scripted use)
delete_db_backup_by_name() {
local pattern="$1"
local db_dir="$PLEX_DB_DIR"
local found=0
while IFS= read -r -d '' entry; do
local name
name=$(basename "$entry")
echo -e "${YELLOW}Delete ${name}? [y/N]:${RESET} "
read -r confirm
if [[ "${confirm,,}" == "y" ]]; then
if [[ -d "$entry" ]]; then
sudo rm -rf "$entry"
else
sudo rm -f "$entry"
fi
print_status "${CHECKMARK}" "Deleted: $name" "${GREEN}"
((found++))
fi
done < <(sudo find "$db_dir" -maxdepth 1 -name "*${pattern}*" -print0 2>/dev/null)
if [[ $found -eq 0 ]]; then
print_status "${CROSS}" "No backups matching '${pattern}' found" "${RED}"
return 1
fi
print_status "${INFO}" "Deleted $found file(s)" "${BLUE}"
}
# 🎭 ASCII symbols for compatible output
readonly CHECKMARK="[✓]"
readonly CROSS="[✗]"
readonly ROCKET="[>]"
readonly STOP_SIGN="[■]"
readonly RECYCLE="[~]"
readonly INFO="[i]"
readonly HOURGLASS="[*]"
readonly SPARKLES="[*]"
# 🎉 Function to print completion footer
print_footer() {
echo -e "\n${DIM}${CYAN}--- Operation completed [*] ---${RESET}\n"
}
# 🎯 Function to print status with style
print_status() {
local status="$1"
local message="$2"
local color="$3"
echo -e "${color}${BOLD}[${status}]${RESET} ${message}"
}
# ⏱️ Function to show loading animation
show_loading() {
local message="$1"
local pid="$2"
local spin='-\|/'
local i=0
echo -ne "${CYAN}${HOURGLASS} ${message}${RESET}"
while kill -0 "$pid" 2>/dev/null; do
i=$(( (i+1) %4 ))
printf "\r%s%s %s %s%s" "${CYAN}" "${HOURGLASS}" "${message}" "${spin:$i:1}" "${RESET}"
sleep 0.1
done
printf "\r%s%s %s %s%s\n" "${CYAN}" "${HOURGLASS}" "${message}" "${CHECKMARK}" "${RESET}"
}
# 🔧 Repair database using ChuckPa/DBRepair when available, else manual steps
# DBRepair: https://github.com/ChuckPa/PlexDBRepair
repair_database() {
print_status "${INFO}" "Attempting to repair Plex database..." "${BLUE}"
local main_db="$PLEX_DB_DIR/com.plexapp.plugins.library.db"
if [[ ! -f "$main_db" ]]; then
print_status "${CROSS}" "Main database not found at: $main_db" "${RED}"
return 1
fi
# ---------- Try DBRepair.sh (preferred) ----------
local dbrepair_bin
if dbrepair_bin=$(find_dbrepair); then
print_status "${CHECKMARK}" "Found DBRepair.sh: $dbrepair_bin" "${GREEN}"
print_status "${INFO}" "Running: stop → auto (check + repair + reindex + FTS rebuild) → start → exit" "${BLUE}"
# DBRepair supports scripted mode — pass commands directly
if sudo "$dbrepair_bin" stop auto start exit; then
print_status "${CHECKMARK}" "DBRepair automatic repair completed successfully!" "${GREEN}"
return 0
else
print_status "${CROSS}" "DBRepair automatic repair failed (exit code $?)" "${RED}"
print_status "${INFO}" "Falling back to manual repair steps..." "${YELLOW}"
fi
else
print_status "${INFO}" "DBRepair.sh not found — using manual repair" "${YELLOW}"
echo -e "${DIM}${CYAN} For better repairs, install DBRepair:${RESET}"
echo -e "${DIM}${CYAN} wget -O ${SCRIPT_DIR}/DBRepair.sh https://github.com/ChuckPa/PlexDBRepair/releases/latest/download/DBRepair.sh${RESET}"
echo -e "${DIM}${CYAN} chmod +x ${SCRIPT_DIR}/DBRepair.sh${RESET}"
fi
# ---------- Manual fallback (same approach DBRepair uses internally) ----------
# Prefer Plex's bundled SQLite for ICU compatibility
local sqlite_bin="sqlite3"
if [[ -x "$PLEX_SQLITE" ]]; then
sqlite_bin="$PLEX_SQLITE"
fi
# Stop Plex service first
print_status "${INFO}" "Stopping Plex service..." "${BLUE}"
sudo systemctl stop "$PLEX_SERVICE" 2>/dev/null || true
sleep 3
# Step 1: WAL checkpoint
if [[ -f "${main_db}-wal" ]]; then
print_status "${INFO}" "Checkpointing WAL journal..." "${BLUE}"
sudo -u plex "$sqlite_bin" "$main_db" "PRAGMA wal_checkpoint(TRUNCATE);" 2>/dev/null || true
fi
# Step 2: Export → import (the core of DBRepair's repair/optimize)
local ts
ts=$(date +%Y%m%d_%H%M%S)
local sql_dump="/tmp/plex_dump_${ts}.sql"
local new_db="/tmp/plex_repaired_${ts}.db"
local backup_db="$PLEX_DB_DIR/com.plexapp.plugins.library.db-BACKUP-${ts}"
print_status "${INFO}" "Exporting database to SQL..." "${BLUE}"
if sudo -u plex "$sqlite_bin" "$main_db" ".dump" | sudo -u plex tee "$sql_dump" >/dev/null 2>&1; then
print_status "${CHECKMARK}" "SQL export completed ($(du -sh "$sql_dump" | cut -f1))" "${GREEN}"
else
print_status "${CROSS}" "SQL export failed — database may be too damaged for this method" "${RED}"
rm -f "$sql_dump" 2>/dev/null
print_status "${INFO}" "Install DBRepair.sh for more robust repair capabilities" "${YELLOW}"
return 1
fi
print_status "${INFO}" "Importing into fresh database..." "${BLUE}"
if sudo cat "$sql_dump" | sudo -u plex "$sqlite_bin" "$new_db" 2>/dev/null; then
print_status "${CHECKMARK}" "Import into fresh database succeeded" "${GREEN}"
else
print_status "${CROSS}" "Import failed" "${RED}"
rm -f "$sql_dump" "$new_db" 2>/dev/null
return 1
fi
rm -f "$sql_dump" 2>/dev/null
# Step 3: Verify repaired database (structural + FTS)
print_status "${INFO}" "Verifying repaired database..." "${BLUE}"
local verify_out
verify_out=$(sudo -u plex "$sqlite_bin" "$new_db" "PRAGMA integrity_check;" 2>&1)
if [[ "$verify_out" != "ok" ]]; then
print_status "${CROSS}" "Repaired database failed integrity check" "${RED}"
rm -f "$new_db" 2>/dev/null
return 1
fi
# Step 4: Reindex (rebuilds all indexes INCLUDING FTS)
print_status "${INFO}" "Rebuilding indexes (including FTS)..." "${BLUE}"
sudo -u plex "$sqlite_bin" "$new_db" "REINDEX;" 2>/dev/null || true
# Rebuild FTS index content explicitly
local fts_tables
fts_tables=$(sudo -u plex "$sqlite_bin" "$new_db" \
"SELECT name FROM sqlite_master WHERE type='table' AND sql LIKE '%fts%';" 2>/dev/null) || true
if [[ -n "$fts_tables" ]]; then
while IFS= read -r table; do
[[ -z "$table" ]] && continue
print_status "${INFO}" "Rebuilding FTS index: $table" "${BLUE}"
sudo -u plex "$sqlite_bin" "$new_db" \
"INSERT INTO ${table}(${table}) VALUES('rebuild');" 2>/dev/null || true
done <<< "$fts_tables"
fi
# Step 5: Swap databases
print_status "${INFO}" "Backing up old database and activating repaired copy..." "${BLUE}"
sudo cp "$main_db" "$backup_db"
sudo mv "$new_db" "$main_db"
sudo chown plex:plex "$main_db"
sudo chmod 644 "$main_db"
# Remove stale journals for the old database
sudo rm -f "${main_db}-shm" "${main_db}-wal" 2>/dev/null || true
print_status "${CHECKMARK}" "Database repaired and activated. Backup at: $(basename "$backup_db")" "${GREEN}"
return 0
}
# 🔍 Function to check FTS (Full-Text Search) index integrity
# Standard PRAGMA integrity_check does NOT detect FTS corruption.
# This is the exact class of damage shown in the user's screenshot.
check_fts_integrity() {
local db_file="$1"
local sqlite_bin="${2:-sqlite3}" # Use Plex SQLite if available
local issues=0
# Discover FTS tables dynamically from sqlite_master
local fts_tables
fts_tables=$(sudo -u plex "$sqlite_bin" "$db_file" \
"SELECT name FROM sqlite_master WHERE type='table' AND sql LIKE '%fts%';" 2>/dev/null) || return 0
if [[ -z "$fts_tables" ]]; then
return 0 # No FTS tables — nothing to check
fi
print_status "${INFO}" "Checking FTS (Full-Text Search) indexes..." "${BLUE}"
while IFS= read -r table; do
[[ -z "$table" ]] && continue
local result
result=$(sudo -u plex "$sqlite_bin" "$db_file" \
"INSERT INTO ${table}(${table}) VALUES('integrity-check');" 2>&1) || true
if [[ -n "$result" ]]; then
print_status "${CROSS}" "FTS index '${table}' — DAMAGED" "${RED}"
echo -e "${DIM}${YELLOW} $result${RESET}"
((issues++))
else
print_status "${CHECKMARK}" "FTS index '${table}' — OK" "${GREEN}"
fi
done <<< "$fts_tables"
if (( issues > 0 )); then
print_status "${CROSS}" "FTS integrity check complete. $issues index(es) damaged." "${RED}"
print_status "${INFO}" "Run: ${SCRIPT_NAME} repair (uses DBRepair reindex to rebuild FTS)" "${YELLOW}"
return 1
fi
return 0
}
# 🔍 Function to check database integrity
check_database_integrity() {
print_status "${INFO}" "Checking database integrity..." "${BLUE}"
local main_db="$PLEX_DB_DIR/com.plexapp.plugins.library.db"
if [[ ! -f "$main_db" ]]; then
print_status "${CROSS}" "Main database not found at: $main_db" "${RED}"
return 1
fi
# Prefer Plex's bundled SQLite for ICU compatibility
local sqlite_bin="sqlite3"
if [[ -x "$PLEX_SQLITE" ]]; then
sqlite_bin="$PLEX_SQLITE"
fi
# Clean up stale WAL/SHM journals left by non-clean shutdowns.
if [[ -f "${main_db}-wal" ]]; then
print_status "${INFO}" "WAL journal found — attempting checkpoint before integrity check..." "${BLUE}"
if ! sudo -u plex "$sqlite_bin" "$main_db" "PRAGMA wal_checkpoint(TRUNCATE);" 2>/dev/null; then
print_status "${INFO}" "WAL checkpoint failed (non-critical, continuing check)" "${YELLOW}"
fi
fi
# --- Standard structural integrity check ---
local integrity_output
local sqlite_exit_code=0
integrity_output=$(sudo -u plex "$sqlite_bin" "$main_db" "PRAGMA integrity_check;" 2>&1) || sqlite_exit_code=$?
local struct_ok=true
if [[ "$integrity_output" == "ok" ]]; then
print_status "${CHECKMARK}" "Database structural integrity check passed" "${GREEN}"
elif [[ $sqlite_exit_code -ne 0 && -z "$integrity_output" ]]; then
print_status "${CROSS}" "sqlite3 failed to open the database (exit code $sqlite_exit_code)" "${RED}"
print_status "${INFO}" "Check file permissions and ensure Plex is fully stopped" "${YELLOW}"
return 1
else
struct_ok=false
print_status "${CROSS}" "Database structural integrity check reported issues:" "${RED}"
echo "$integrity_output" | head -n 5 | while IFS= read -r line; do
echo -e "${DIM}${YELLOW} $line${RESET}"
done
local total_lines
total_lines=$(echo "$integrity_output" | wc -l)
if (( total_lines > 5 )); then
echo -e "${DIM}${YELLOW} ... and $((total_lines - 5)) more issue(s)${RESET}"
fi
fi
# --- FTS index integrity (the most common unreported corruption) ---
local fts_ok=true
if ! check_fts_integrity "$main_db" "$sqlite_bin"; then
fts_ok=false
fi
if [[ "$struct_ok" == true && "$fts_ok" == true ]]; then
return 0
fi
print_status "${INFO}" "Consider running database repair: ${SCRIPT_NAME} repair" "${YELLOW}"
_hint_install_dbrepair
return 1
}
# <20>🚀 Enhanced start function
start_plex() {
print_status "${ROCKET}" "Starting Plex Media Server..." "${GREEN}"
if systemctl is-active --quiet "$PLEX_SERVICE"; then
print_status "${INFO}" "Plex is already running!" "${YELLOW}"
show_detailed_status
return 0
fi
# Reset any failed state first
sudo systemctl reset-failed "$PLEX_SERVICE" 2>/dev/null || true
# Check database integrity before starting (warn only — don't block startup).
# Many "failures" are benign WAL journal leftovers that Plex resolves on its own.
if ! check_database_integrity; then
print_status "${INFO}" "Database integrity issues detected — starting Plex anyway (it may self-repair)." "${YELLOW}"
echo -e "${DIM}${YELLOW} If Plex fails to start, run: ${SCRIPT_NAME} repair${RESET}"
_hint_install_dbrepair
fi
print_status "${INFO}" "Attempting to start service..." "${BLUE}"
if ! sudo systemctl start "$PLEX_SERVICE"; then
print_status "${CROSS}" "Failed to start Plex Media Server!" "${RED}"
print_status "${INFO}" "Checking service logs..." "${BLUE}"
# Show recent error logs
echo -e "\n${DIM}${RED}Recent error logs:${RESET}"
sudo journalctl -u "$PLEX_SERVICE" --no-pager -n 5 --since "1 minute ago" | tail -5
return 1
fi
# Wait and verify startup
sleep 3
local timeout=30
local elapsed=0
print_status "${HOURGLASS}" "Waiting for service to initialize..." "${CYAN}"
while [[ $elapsed -lt $timeout ]]; do
if systemctl is-active --quiet "$PLEX_SERVICE"; then
print_status "${CHECKMARK}" "Plex Media Server started successfully!" "${GREEN}"
print_footer
return 0
fi
sleep 2
elapsed=$((elapsed + 2))
echo -ne "${DIM}${CYAN} Waiting... ${elapsed}s/${timeout}s${RESET}\r"
done
echo ""
print_status "${CROSS}" "Service startup timeout or failed!" "${RED}"
# Show current status
local status
status=$(systemctl is-active "$PLEX_SERVICE" 2>/dev/null || echo "unknown")
print_status "${INFO}" "Current status: $status" "${YELLOW}"
if [[ "$status" == "failed" ]]; then
echo -e "\n${DIM}${RED}Recent error logs:${RESET}"
sudo journalctl -u "$PLEX_SERVICE" --no-pager -n 10 --since "2 minutes ago"
fi
return 1
}
# 🛑 Enhanced stop function
stop_plex() {
print_status "${STOP_SIGN}" "Stopping Plex Media Server..." "${YELLOW}"
if ! systemctl is-active --quiet "$PLEX_SERVICE"; then
print_status "${INFO}" "Plex is already stopped!" "${YELLOW}"
return 0
fi
sudo systemctl stop "$PLEX_SERVICE" &
local pid=$!
show_loading "Gracefully shutting down Plex" $pid
wait $pid
if ! systemctl is-active --quiet "$PLEX_SERVICE"; then
print_status "${CHECKMARK}" "Plex Media Server stopped successfully!" "${GREEN}"
print_footer
else
print_status "${CROSS}" "Failed to stop Plex Media Server!" "${RED}"
return 1
fi
}
# ♻️ Enhanced restart function
restart_plex() {
print_status "${RECYCLE}" "Restarting Plex Media Server..." "${BLUE}"
if systemctl is-active --quiet "$PLEX_SERVICE"; then
stop_plex
echo ""
fi
start_plex
}
# 📊 Enhanced status function with detailed info
show_detailed_status() {
local service_status
service_status=$(systemctl is-active "$PLEX_SERVICE" 2>/dev/null || echo "inactive")
echo -e "\n${BOLD}${BLUE}+==============================================================+${RESET}"
echo -e "${BOLD}${BLUE} SERVICE STATUS${RESET}"
echo -e "${BOLD}${BLUE}+==============================================================+${RESET}"
case "$service_status" in
"active")
print_status "${CHECKMARK}" "Service Status: ${GREEN}${BOLD}ACTIVE${RESET}" "${GREEN}"
# Get additional info
local uptime
uptime=$(systemctl show "$PLEX_SERVICE" --property=ActiveEnterTimestamp --value | xargs -I {} date -d {} "+%Y-%m-%d %H:%M:%S" 2>/dev/null || echo "Unknown")
local memory_usage
memory_usage=$(systemctl show "$PLEX_SERVICE" --property=MemoryCurrent --value 2>/dev/null || echo "0")
if [[ "$memory_usage" != "0" ]] && [[ "$memory_usage" =~ ^[0-9]+$ ]]; then
memory_usage="$(( memory_usage / 1024 / 1024 )) MB"
else
memory_usage="Unknown"
fi
echo -e "${DIM}${CYAN} Started: ${WHITE}${uptime}${RESET}"
echo -e "${DIM}${CYAN} Memory Usage: ${WHITE}${memory_usage}${RESET}"
echo -e "${DIM}${CYAN} Service Name: ${WHITE}${PLEX_SERVICE}${RESET}"
;;
"inactive")
print_status "${CROSS}" "Service Status: ${RED}${BOLD}INACTIVE${RESET}" "${RED}"
echo -e "${DIM}${YELLOW} Use '${SCRIPT_NAME} start' to start the service${RESET}"
;;
"failed")
print_status "${CROSS}" "Service Status: ${RED}${BOLD}FAILED${RESET}" "${RED}"
echo -e "${DIM}${RED} Check logs with: ${WHITE}journalctl -u ${PLEX_SERVICE}${RESET}"
;;
*)
print_status "${INFO}" "Service Status: ${YELLOW}${BOLD}${service_status^^}${RESET}" "${YELLOW}"
;;
esac
# Show recent logs
echo -e "\n${DIM}${CYAN}+--- Recent Service Logs (24h) ---+${RESET}"
# Try to get logs with sudo, fall back to user permissions
local logs
if logs=$(sudo journalctl -u "$PLEX_SERVICE" --no-pager -n 5 --since "24 hours ago" --output=short 2>/dev/null); then
if [[ -n "$logs" && "$logs" != "-- No entries --" ]]; then
echo -e "${DIM}${logs}${RESET}"
else
echo -e "${DIM}${YELLOW}No recent log entries found${RESET}"
fi
else
# Fallback: try without sudo
logs=$(journalctl -u "$PLEX_SERVICE" --no-pager -n 5 --since "24 hours ago" 2>/dev/null || echo "Unable to access logs")
if [[ "$logs" == "Unable to access logs" || "$logs" == "-- No entries --" ]]; then
echo -e "${DIM}${YELLOW}Unable to access recent logs (try: sudo journalctl -u ${PLEX_SERVICE})${RESET}"
else
echo -e "${DIM}${logs}${RESET}"
fi
fi
echo -e "${DIM}${CYAN}+----------------------------------+${RESET}"
}
# 🔧 Show available commands
show_help() {
echo -e "${BOLD}${WHITE}Usage:${RESET} ${CYAN}${SCRIPT_NAME}${RESET} ${YELLOW}<command>${RESET}"
echo ""
echo -e "${BOLD}${WHITE}Available Commands:${RESET}"
printf " ${GREEN}${BOLD}%-18s${RESET} %s %s\n" "start" "${ROCKET}" "Start Plex Media Server"
printf " ${YELLOW}${BOLD}%-18s${RESET} %s %s\n" "stop" "${STOP_SIGN}" "Stop Plex Media Server"
printf " ${BLUE}${BOLD}%-18s${RESET} %s %s\n" "restart" "${RECYCLE}" "Restart Plex Media Server"
printf " ${CYAN}${BOLD}%-18s${RESET} %s %s\n" "status" "${INFO}" "Show detailed service status"
printf " ${PURPLE}${BOLD}%-18s${RESET} %s %s\n" "scan" "${SPARKLES}" "Library scanner operations"
printf " ${RED}${BOLD}%-18s${RESET} %s %s\n" "repair" "[!]" "Repair database corruption issues"
printf " ${RED}${BOLD}%-18s${RESET} %s %s\n" "nuclear" "[!!]" "Nuclear database recovery (last resort)"
printf " ${CYAN}${BOLD}%-18s${RESET} %s %s\n" "backups" "[#]" "List and manage database backup files"
printf " ${GREEN}${BOLD}%-18s${RESET} %s %s\n" "install-dbrepair" "[+]" "Install or update DBRepair tool"
printf " ${WHITE}${BOLD}%-18s${RESET} %s %s\n" "tui" "[>]" "Launch interactive TUI dashboard"
printf " ${PURPLE}${BOLD}%-18s${RESET} %s %s\n" "help" "${HOURGLASS}" "Show this help message"
echo ""
echo -e "${DIM}${WHITE}Examples:${RESET}"
printf " ${DIM}%-40s # %s${RESET}\n" "${SCRIPT_NAME} start" "Start the Plex service"
printf " ${DIM}%-40s # %s${RESET}\n" "${SCRIPT_NAME} status" "Show current status"
printf " ${DIM}%-40s # %s${RESET}\n" "${SCRIPT_NAME} scan" "Launch library scanner interface"
printf " ${DIM}%-40s # %s${RESET}\n" "${SCRIPT_NAME} repair" "Fix database issues"
printf " ${DIM}%-40s # %s${RESET}\n" "${SCRIPT_NAME} nuclear" "Complete database replacement"
printf " ${DIM}%-40s # %s${RESET}\n" "${SCRIPT_NAME} backups" "List and manage DB backups"
printf " ${DIM}%-40s # %s${RESET}\n" "${SCRIPT_NAME} backups delete" "Interactive backup deletion"
printf " ${DIM}%-40s # %s${RESET}\n" "${SCRIPT_NAME} backups delete --name foo" "Delete by name pattern"
printf " ${DIM}%-40s # %s${RESET}\n" "${SCRIPT_NAME} install-dbrepair" "Install/update DBRepair"
printf " ${DIM}%-40s # %s${RESET}\n" "${SCRIPT_NAME} tui" "Launch full TUI dashboard"
echo ""
}
# <20> Function to launch the Python TUI dashboard
launch_tui() {
local venv_dir="${SCRIPT_DIR}/.venv"
local tui_app="${SCRIPT_DIR}/tui/app.py"
if [[ ! -f "$tui_app" ]]; then
print_status "${CROSS}" "TUI application not found: $tui_app" "${RED}"
return 1
fi
# Create venv and install dependencies if needed
if [[ ! -d "$venv_dir" ]]; then
print_status "${INFO}" "Setting up Python environment (first run)..." "${BLUE}"
if ! python3 -m venv "$venv_dir" 2>/dev/null; then
print_status "${CROSS}" "Failed to create Python venv. Install python3-venv." "${RED}"
return 1
fi
"${venv_dir}/bin/pip" install --quiet textual
print_status "${CHECKMARK}" "Python environment ready" "${GREEN}"
fi
exec "${venv_dir}/bin/python3" "$tui_app"
}
# <20>📚 Function to launch library scanner
launch_scanner() {
print_status "${SPARKLES}" "Launching Plex Library Scanner..." "${PURPLE}"
local scanner_script="${SCRIPT_DIR}/scan-plex-libraries.sh"
if [[ ! -f "$scanner_script" ]]; then
print_status "${CROSS}" "Library scanner script not found: $scanner_script" "${RED}"
print_status "${INFO}" "Expected location: ${SCRIPT_DIR}/scan-plex-libraries.sh" "${YELLOW}"
return 1
fi
if [[ ! -x "$scanner_script" ]]; then
print_status "${INFO}" "Making scanner script executable..." "${BLUE}"
chmod +x "$scanner_script"
fi
# Check if additional arguments were passed
if [[ $# -gt 1 ]]; then
# Pass remaining arguments to the scanner script
shift # Remove 'scan' argument
print_status "${INFO}" "Executing scanner with arguments: $*" "${BLUE}"
exec "$scanner_script" "$@"
else
# No additional arguments, launch interactive mode
print_status "${INFO}" "Launching scanner in interactive mode..." "${BLUE}"
exec "$scanner_script" interactive
fi
}
# Nuclear database recovery function
nuclear_recovery() {
print_status "${INFO}" "Starting nuclear database recovery..." "${RED}"
local nuclear_script="${SCRIPT_DIR}/nuclear-plex-recovery.sh"
if [[ ! -f "$nuclear_script" ]]; then
print_status "${CROSS}" "Nuclear recovery script not found: $nuclear_script" "${RED}"
print_status "${INFO}" "This script should be in the same directory as plex.sh" "${YELLOW}"
return 2
fi
# Warning message
echo -e "\n${RED}${BOLD}⚠️ WARNING: NUCLEAR RECOVERY ⚠️${RESET}"
echo -e "${RED}This will completely replace your Plex database with a backup!${RESET}"
echo -e "${RED}All changes since the backup was created will be lost!${RESET}"
echo -e "${YELLOW}This should only be used when standard repair methods have failed.${RESET}\n"
# Get user confirmation
echo -e "${CYAN}Do you want to proceed with nuclear recovery? ${RESET}"
echo -e "${DIM}Type 'YES' (uppercase) to confirm: ${RESET}"
read -r confirmation
if [[ "$confirmation" != "YES" ]]; then
print_status "${INFO}" "Nuclear recovery cancelled by user" "${YELLOW}"
return 0
fi
print_status "${INFO}" "Executing nuclear recovery script..." "${BLUE}"
# Execute the nuclear recovery script
if sudo "$nuclear_script" --auto; then
print_status "${CHECKMARK}" "Nuclear recovery completed successfully!" "${GREEN}"
print_status "${INFO}" "Your Plex server should now be operational" "${GREEN}"
print_footer
return 0
else
local exit_code=$?
print_status "${CROSS}" "Nuclear recovery failed!" "${RED}"
case $exit_code in
2) print_status "${INFO}" "Backup file issues - check backup integrity" "${YELLOW}" ;;
3) print_status "${INFO}" "Database replacement failure - check permissions" "${YELLOW}" ;;
4) print_status "${INFO}" "Service management failure - check systemctl" "${YELLOW}" ;;
5) print_status "${INFO}" "Rollback performed due to failure" "${YELLOW}" ;;
*) print_status "${INFO}" "Unknown error occurred during recovery" "${YELLOW}" ;;
esac
print_footer
return $exit_code
fi
}
# Database repair function
repair_plex() {
print_status "${INFO}" "Starting Plex database repair..." "${YELLOW}"
# Run the enhanced repair function
if repair_database; then
print_status "${CHECKMARK}" "Database repair completed successfully!" "${GREEN}"
# Try to start the service
print_status "${INFO}" "Starting Plex service..." "${BLUE}"
if sudo systemctl start "$PLEX_SERVICE"; then
# Wait for service to fully start
sleep 5
if systemctl is-active --quiet "$PLEX_SERVICE"; then
print_status "${CHECKMARK}" "Plex service started successfully!" "${GREEN}"
print_footer
return 0
else
print_status "${CROSS}" "Service started but may not be fully operational!" "${YELLOW}"
show_detailed_status
return 1
fi
else
print_status "${CROSS}" "Failed to start Plex service after repair!" "${RED}"
return 1
fi
else
local repair_exit_code=$?
print_status "${CROSS}" "Database repair failed!" "${RED}"
# Try to start the service anyway in case partial repair helped
print_status "${INFO}" "Attempting to start Plex service anyway..." "${BLUE}"
sudo systemctl start "$PLEX_SERVICE" 2>/dev/null || true
if [[ $repair_exit_code -eq 2 ]]; then
print_status "${INFO}" "Critical error - manual intervention required" "${YELLOW}"
else
print_status "${INFO}" "Repair failed but service may still work with corrupted database" "${YELLOW}"
fi
print_footer
return $repair_exit_code
fi
}
# 🎯 Main script logic
main() {
# Check if running as root
if [[ $EUID -eq 0 ]]; then
print_status "${CROSS}" "Don't run this script as root! Use your regular user account." "${RED}"
exit 1
fi
# Check if no arguments provided
if [[ $# -eq 0 ]]; then
show_help
exit 1
fi
case "${1,,}" in # Convert to lowercase
"start")
start_plex
;;
"stop")
stop_plex
;;
"restart"|"reload")
restart_plex
;;
"status"|"info")
show_detailed_status
;;
"scan"|"scanner"|"library")
launch_scanner "$@"
;;
"repair"|"fix")
repair_plex
;;
"nuclear"|"nuke")
nuclear_recovery
;;
"install-dbrepair"|"update-dbrepair"|"dbrepair")
install_or_update_dbrepair
;;
"tui"|"dashboard"|"ui")
launch_tui
;;
"backups"|"backup-list")
if [[ $# -ge 2 && "${2,,}" == "delete" ]]; then
if [[ $# -ge 4 && "${3}" == "--name" ]]; then
delete_db_backup_by_name "$4"
else
delete_db_backups_interactive
fi
else
list_db_backups
fi
;;
"help"|"--help"|"-h")
show_help
;;
*)
print_status "${CROSS}" "Unknown command: ${RED}${BOLD}$1${RESET}" "${RED}"
echo ""
show_help
exit 1
;;
esac
}
# 🚀 Execute main function with all arguments
main "$@"