2639 lines
87 KiB
Bash
Executable File
2639 lines
87 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
|
|
set -euo pipefail
|
|
IFS=$'\n\t'
|
|
|
|
# Ensure we have system utilities in PATH
|
|
export PATH="/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/bin:$PATH"
|
|
|
|
trap 'echo -e "\n[ERROR] Line $LINENO failed. Exiting." >&2; exit 1' ERR
|
|
|
|
##############################################################################
|
|
# Build Configuration
|
|
##############################################################################
|
|
|
|
# Detect number of CPU cores for parallel builds
|
|
NCPU=$(sysctl -n hw.ncpu)
|
|
# Use all cores for maximum speed
|
|
MAKE_JOBS=$NCPU
|
|
|
|
##############################################################################
|
|
# Progress Tracking
|
|
##############################################################################
|
|
|
|
TOTAL_STEPS=19
|
|
CURRENT_STEP=0
|
|
CHECKPOINT_FILE=""
|
|
|
|
# Mark a step as complete
|
|
mark_step_complete() {
|
|
local step_name="$1"
|
|
if [ -n "$CHECKPOINT_FILE" ]; then
|
|
# Ensure the directory exists before writing
|
|
mkdir -p "$(dirname "$CHECKPOINT_FILE")"
|
|
echo "$step_name" >> "$CHECKPOINT_FILE"
|
|
fi
|
|
}
|
|
|
|
# Check if a step has been completed
|
|
is_step_complete() {
|
|
local step_name="$1"
|
|
if [ -n "$CHECKPOINT_FILE" ] && [ -f "$CHECKPOINT_FILE" ]; then
|
|
grep -q "^${step_name}$" "$CHECKPOINT_FILE" 2>/dev/null
|
|
return $?
|
|
fi
|
|
return 1
|
|
}
|
|
|
|
print_progress() {
|
|
local step_name="$1"
|
|
local log_file="${2:-}"
|
|
CURRENT_STEP=$((CURRENT_STEP + 1))
|
|
|
|
local percent=$((CURRENT_STEP * 100 / TOTAL_STEPS))
|
|
local filled=$((CURRENT_STEP * 30 / TOTAL_STEPS))
|
|
local empty=$((30 - filled))
|
|
|
|
# Clear screen and print progress
|
|
clear
|
|
echo "╔════════════════════════════════════════════════════════════════════════════╗"
|
|
echo "║ DISPATCHARR PORTABLE INSTALLATION ║"
|
|
echo "╚════════════════════════════════════════════════════════════════════════════╝"
|
|
echo ""
|
|
printf "Progress: [%${filled}s%${empty}s] %3d%% Step %2d/%2d\n" \
|
|
"$(printf '#%.0s' $(seq 1 $filled))" \
|
|
"$(printf ' %.0s' $(seq 1 $empty))" \
|
|
"$percent" "$CURRENT_STEP" "$TOTAL_STEPS"
|
|
echo ""
|
|
echo "────────────────────────────────────────────────────────────────────────────"
|
|
printf "Current Step: %s\n" "$step_name"
|
|
if [ -n "$log_file" ]; then
|
|
printf "Log File: %s\n" "$log_file"
|
|
fi
|
|
echo "────────────────────────────────────────────────────────────────────────────"
|
|
echo ""
|
|
}
|
|
|
|
print_step_complete() {
|
|
local message="${1:-Complete}"
|
|
echo "✓ $message"
|
|
echo ""
|
|
sleep 1
|
|
}
|
|
|
|
##############################################################################
|
|
# Portable Dispatcharr for macOS (Apple Silicon)
|
|
#
|
|
# This script creates a completely self-contained installation that:
|
|
# - Does NOT install system packages
|
|
# - Does NOT create system users
|
|
# - Does NOT modify system files
|
|
# - Bundles ALL dependencies in one directory
|
|
# - Can be moved/deleted without leaving traces
|
|
# - Works like a Docker container but native
|
|
##############################################################################
|
|
|
|
show_disclaimer() {
|
|
echo "**************************************************************"
|
|
echo "Dispatcharr Portable Installation for macOS (Apple Silicon)"
|
|
echo ""
|
|
echo "This creates a COMPLETELY SELF-CONTAINED installation:"
|
|
echo " ✓ All dependencies bundled in one directory"
|
|
echo " ✓ Can be deleted to remove everything"
|
|
echo " ✓ VideoToolbox hardware acceleration enabled"
|
|
echo ""
|
|
echo "Requirements:"
|
|
echo " - macOS 12.0+ (Monterey or later)"
|
|
echo " - Apple Silicon (M1/M2/M3/M4)"
|
|
echo " - 8GB+ RAM, 15GB+ free disk space"
|
|
echo " - Xcode Command Line Tools (for compilation)"
|
|
echo ""
|
|
echo "⚠️ This installation is UNOFFICIAL and UNSUPPORTED."
|
|
echo " Docker on Linux is the recommended installation."
|
|
echo ""
|
|
echo "NOTE: This process may take a while as dependencies are built"
|
|
echo " from source to enable VideoToolbox hardware acceleration"
|
|
echo " and ensure complete portability."
|
|
echo "**************************************************************"
|
|
echo ""
|
|
echo "Press Enter to continue..."
|
|
read
|
|
}
|
|
|
|
##############################################################################
|
|
# Select Branch
|
|
##############################################################################
|
|
|
|
select_branch() {
|
|
echo ""
|
|
echo "Select Dispatcharr branch to install:"
|
|
echo " 1) main - Stable releases (recommended)"
|
|
echo " 2) dev - Development branch (latest features, may be unstable)"
|
|
echo " 3) custom - Enter a specific branch name"
|
|
echo ""
|
|
echo -n "Choice [1]: "
|
|
read branch_choice
|
|
|
|
case "${branch_choice:-1}" in
|
|
1)
|
|
DISPATCH_BRANCH="main"
|
|
echo "Selected: main (stable)"
|
|
;;
|
|
2)
|
|
DISPATCH_BRANCH="dev"
|
|
echo "Selected: dev (development)"
|
|
;;
|
|
3)
|
|
echo -n "Enter branch name: "
|
|
read custom_branch
|
|
if [ -n "$custom_branch" ]; then
|
|
DISPATCH_BRANCH="$custom_branch"
|
|
echo "Selected: $custom_branch"
|
|
else
|
|
echo "No branch specified, using main"
|
|
DISPATCH_BRANCH="main"
|
|
fi
|
|
;;
|
|
*)
|
|
echo "Invalid choice, using main (stable)"
|
|
DISPATCH_BRANCH="main"
|
|
;;
|
|
esac
|
|
}
|
|
|
|
##############################################################################
|
|
# Detect Versions from Project Files
|
|
##############################################################################
|
|
|
|
detect_versions() {
|
|
local repo_dir="${1:-}"
|
|
|
|
# Default versions (fallback)
|
|
PYTHON_VERSION="3.12.7"
|
|
POSTGRES_VERSION="17.2"
|
|
REDIS_VERSION="7.4.1"
|
|
NODE_VERSION="24.0.0"
|
|
|
|
if [ -n "$repo_dir" ] && [ -d "$repo_dir" ]; then
|
|
echo ">>> Detecting versions from project files..."
|
|
|
|
# Detect Python version from DispatcharrBase Dockerfile
|
|
if [ -f "$repo_dir/docker/DispatcharrBase" ]; then
|
|
local py_ver=$(grep -oE "python3\.[0-9]+" "$repo_dir/docker/DispatcharrBase" | head -1 | grep -oE "[0-9]+\.[0-9]+")
|
|
if [ -n "$py_ver" ]; then
|
|
# Get latest patch version available
|
|
echo " Detected Python ${py_ver}.x from Dockerfile"
|
|
# We'll use the minor version and find latest patch
|
|
PYTHON_MINOR="$py_ver"
|
|
fi
|
|
fi
|
|
|
|
# Detect PostgreSQL version from DispatcharrBase Dockerfile
|
|
if [ -f "$repo_dir/docker/DispatcharrBase" ]; then
|
|
local pg_ver=$(grep -oE "postgresql-[0-9]+" "$repo_dir/docker/DispatcharrBase" | head -1 | grep -oE "[0-9]+")
|
|
if [ -n "$pg_ver" ]; then
|
|
echo " Detected PostgreSQL ${pg_ver}.x from Dockerfile"
|
|
POSTGRES_MAJOR="$pg_ver"
|
|
fi
|
|
fi
|
|
|
|
# Detect Node version from Dockerfile
|
|
if [ -f "$repo_dir/docker/Dockerfile" ]; then
|
|
local node_ver=$(grep -oE "FROM node:[0-9]+" "$repo_dir/docker/Dockerfile" | head -1 | grep -oE "[0-9]+")
|
|
if [ -n "$node_ver" ]; then
|
|
echo " Detected Node ${node_ver}.x from Dockerfile"
|
|
NODE_MAJOR="$node_ver"
|
|
fi
|
|
fi
|
|
|
|
# Detect Redis version from DispatcharrBase (7.x mentioned in comment)
|
|
if [ -f "$repo_dir/docker/DispatcharrBase" ]; then
|
|
if grep -q "Redis 7" "$repo_dir/docker/DispatcharrBase"; then
|
|
echo " Detected Redis 7.x from Dockerfile"
|
|
REDIS_MAJOR="7"
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
# Fetch latest patch versions
|
|
echo ">>> Fetching latest stable versions..."
|
|
|
|
# Python - get latest patch for detected minor version
|
|
if [ -n "${PYTHON_MINOR:-}" ]; then
|
|
local latest_py=$(curl -s https://www.python.org/ftp/python/ | grep -oE "${PYTHON_MINOR}\.[0-9]+" | sort -V | tail -1)
|
|
if [ -n "$latest_py" ]; then
|
|
PYTHON_VERSION="$latest_py"
|
|
echo " Python: ${PYTHON_VERSION}"
|
|
fi
|
|
else
|
|
echo " Python: ${PYTHON_VERSION} (default)"
|
|
fi
|
|
|
|
# PostgreSQL - get latest patch for detected major version
|
|
if [ -n "${POSTGRES_MAJOR:-}" ]; then
|
|
local latest_pg=$(curl -s https://www.postgresql.org/ftp/source/ | grep -oE "v${POSTGRES_MAJOR}\.[0-9]+" | grep -oE "[0-9]+\.[0-9]+" | sort -V | tail -1)
|
|
if [ -n "$latest_pg" ]; then
|
|
POSTGRES_VERSION="$latest_pg"
|
|
echo " PostgreSQL: ${POSTGRES_VERSION}"
|
|
fi
|
|
else
|
|
echo " PostgreSQL: ${POSTGRES_VERSION} (default)"
|
|
fi
|
|
|
|
# Node - get latest for detected major version
|
|
if [ -n "${NODE_MAJOR:-}" ]; then
|
|
local latest_node=$(curl -s https://nodejs.org/dist/latest-v${NODE_MAJOR}.x/ | grep -oE "node-v${NODE_MAJOR}\.[0-9]+\.[0-9]+" | head -1 | grep -oE "[0-9]+\.[0-9]+\.[0-9]+")
|
|
if [ -n "$latest_node" ]; then
|
|
NODE_VERSION="$latest_node"
|
|
echo " Node.js: ${NODE_VERSION}"
|
|
fi
|
|
else
|
|
echo " Node.js: ${NODE_VERSION} (default)"
|
|
fi
|
|
|
|
# Redis - get latest for major version
|
|
if [ -n "${REDIS_MAJOR:-}" ]; then
|
|
local latest_redis=$(curl -s https://download.redis.io/releases/ | grep -oE "redis-${REDIS_MAJOR}\.[0-9]+\.[0-9]+" | grep -oE "${REDIS_MAJOR}\.[0-9]+\.[0-9]+" | sort -V | tail -1)
|
|
if [ -n "$latest_redis" ]; then
|
|
REDIS_VERSION="$latest_redis"
|
|
echo " Redis: ${REDIS_VERSION}"
|
|
fi
|
|
else
|
|
echo " Redis: ${REDIS_VERSION} (default)"
|
|
fi
|
|
|
|
echo ""
|
|
}
|
|
|
|
##############################################################################
|
|
# Configuration
|
|
##############################################################################
|
|
|
|
configure_variables() {
|
|
# Get installation directory from user or use default
|
|
if [ -z "${INSTALL_DIR:-}" ]; then
|
|
echo ""
|
|
echo "Enter installation directory (default: $HOME/Dispatcharr):"
|
|
read user_dir
|
|
INSTALL_DIR="${user_dir:-$HOME/Dispatcharr}"
|
|
fi
|
|
|
|
# Expand tilde
|
|
INSTALL_DIR="${INSTALL_DIR/#\~/$HOME}"
|
|
|
|
# Set checkpoint file location
|
|
CHECKPOINT_FILE="${INSTALL_DIR}/.install_progress"
|
|
|
|
# Check if resuming from previous install
|
|
if [ -f "$CHECKPOINT_FILE" ]; then
|
|
echo ""
|
|
echo "──────────────────────────────────────────────────────────"
|
|
echo "⚠️ Found incomplete installation at: ${INSTALL_DIR}"
|
|
echo ""
|
|
echo "Completed steps:"
|
|
cat "$CHECKPOINT_FILE" | sed 's/^/ ✓ /'
|
|
echo ""
|
|
echo "Options:"
|
|
echo " 1) Resume installation (skip completed steps)"
|
|
echo " 2) Start fresh (delete checkpoint and reinstall everything)"
|
|
echo " 3) Cancel"
|
|
echo ""
|
|
echo -n "Choice [1]: "
|
|
read resume_choice
|
|
|
|
case "${resume_choice:-1}" in
|
|
1)
|
|
echo "Resuming installation..."
|
|
;;
|
|
2)
|
|
echo "Starting fresh..."
|
|
rm -f "$CHECKPOINT_FILE"
|
|
;;
|
|
3)
|
|
echo "Cancelled"
|
|
exit 0
|
|
;;
|
|
*)
|
|
echo "Invalid choice, resuming..."
|
|
;;
|
|
esac
|
|
fi
|
|
|
|
# Core directories
|
|
APP_DIR="${INSTALL_DIR}/app"
|
|
DATA_DIR="${INSTALL_DIR}/data"
|
|
DEPS_DIR="${INSTALL_DIR}/deps"
|
|
RUNTIME_DIR="${INSTALL_DIR}/runtime"
|
|
LOGS_DIR="${INSTALL_DIR}/logs"
|
|
|
|
# Dependency paths
|
|
PYTHON_PREFIX="${DEPS_DIR}/python"
|
|
POSTGRES_PREFIX="${DEPS_DIR}/postgresql"
|
|
REDIS_PREFIX="${DEPS_DIR}/redis"
|
|
FFMPEG_PREFIX="${DEPS_DIR}/ffmpeg"
|
|
NODE_PREFIX="${DEPS_DIR}/node"
|
|
|
|
# Configuration
|
|
POSTGRES_DB="dispatcharr"
|
|
POSTGRES_USER="dispatch"
|
|
POSTGRES_PASSWORD="$(openssl rand -base64 32)"
|
|
HTTP_PORT="9191"
|
|
|
|
echo ""
|
|
echo "Installation directory: ${INSTALL_DIR}"
|
|
echo "Branch: ${DISPATCH_BRANCH}"
|
|
echo "This will use approximately 10-15GB of disk space."
|
|
echo ""
|
|
echo "Press Enter to continue or Ctrl+C to cancel..."
|
|
read
|
|
}
|
|
|
|
##############################################################################
|
|
# Check Prerequisites
|
|
##############################################################################
|
|
|
|
check_prerequisites() {
|
|
if is_step_complete "prerequisites"; then
|
|
CURRENT_STEP=$((CURRENT_STEP + 1))
|
|
echo "✓ Prerequisites already verified (skipping)"
|
|
return
|
|
fi
|
|
|
|
print_progress "Checking system prerequisites..."
|
|
|
|
# Check for Command Line Tools
|
|
if ! xcode-select -p &>/dev/null; then
|
|
echo "Installing Xcode Command Line Tools..."
|
|
xcode-select --install
|
|
echo "Please complete the installation and run this script again."
|
|
exit 1
|
|
fi
|
|
|
|
# Check architecture
|
|
if [[ $(uname -m) != "arm64" ]]; then
|
|
echo "[ERROR] This script requires Apple Silicon (arm64 architecture)"
|
|
exit 1
|
|
fi
|
|
|
|
# Check disk space (require 20GB free)
|
|
local free_space=$(df -g . | awk 'NR==2 {print $4}')
|
|
if [ "$free_space" -lt 20 ]; then
|
|
echo "[WARNING] Less than 20GB free disk space. Installation may fail."
|
|
fi
|
|
|
|
mark_step_complete "prerequisites"
|
|
print_step_complete "Prerequisites verified"
|
|
}
|
|
|
|
##############################################################################
|
|
# Create Directory Structure
|
|
##############################################################################
|
|
|
|
create_directories() {
|
|
if is_step_complete "create_directories"; then
|
|
CURRENT_STEP=$((CURRENT_STEP + 1))
|
|
echo "✓ Directory structure already created (skipping)"
|
|
return
|
|
fi
|
|
|
|
print_progress "Creating directory structure..."
|
|
|
|
mkdir -p "$INSTALL_DIR"
|
|
mkdir -p "$APP_DIR"
|
|
mkdir -p "$DATA_DIR"/{logos,recordings,uploads/{m3us,epgs},m3us,epgs,plugins,media,logo_cache,db}
|
|
mkdir -p "$DEPS_DIR"
|
|
mkdir -p "$RUNTIME_DIR"/{pids,sockets}
|
|
mkdir -p "$LOGS_DIR"
|
|
|
|
# Set proper ownership if running with sudo
|
|
if [ -n "$SUDO_USER" ]; then
|
|
echo "Setting directory ownership to $SUDO_USER..."
|
|
chown -R "$SUDO_USER:staff" "$INSTALL_DIR"
|
|
fi
|
|
|
|
mark_step_complete "create_directories"
|
|
print_step_complete "Directory structure created"
|
|
}
|
|
|
|
##############################################################################
|
|
# Download and Build Python
|
|
##############################################################################
|
|
|
|
install_python() {
|
|
if is_step_complete "install_python"; then
|
|
CURRENT_STEP=$((CURRENT_STEP + 1))
|
|
echo "✓ Python ${PYTHON_VERSION} already installed (skipping)"
|
|
return
|
|
fi
|
|
|
|
print_progress "Installing Python ${PYTHON_VERSION}..." "${DEPS_DIR}/python-build.log"
|
|
|
|
if [ -f "${PYTHON_PREFIX}/bin/python3" ]; then
|
|
mark_step_complete "install_python"
|
|
print_step_complete "Python already installed"
|
|
return
|
|
fi
|
|
|
|
cd "${DEPS_DIR}"
|
|
|
|
# Clean up any partial builds from previous failed attempts
|
|
echo "Cleaning up any previous build attempts..."
|
|
rm -rf "Python-${PYTHON_VERSION}" "python-${PYTHON_VERSION}-macos11.pkg" 2>/dev/null || true
|
|
|
|
# Check for official prebuilt macOS installer
|
|
local py_major_minor=$(echo $PYTHON_VERSION | cut -d. -f1-2)
|
|
local py_installer="python-${PYTHON_VERSION}-macos11.pkg"
|
|
|
|
# Try prebuilt if running with sudo (can install .pkg files)
|
|
if [ "$EUID" -eq 0 ] || [ -n "$SUDO_USER" ]; then
|
|
echo "Checking for prebuilt Python binary (running with elevated privileges)..."
|
|
if curl -sfL "https://www.python.org/ftp/python/${PYTHON_VERSION}/${py_installer}" -o "${py_installer}" 2>/dev/null; then
|
|
echo " Installing prebuilt Python package..."
|
|
|
|
# Install the package to a temporary location and copy to our prefix
|
|
if installer -pkg "${py_installer}" -target CurrentUserHomeDirectory > "${DEPS_DIR}/python-install.log" 2>&1; then
|
|
# The .pkg installs to /Library/Frameworks/Python.framework
|
|
local framework_path="/Library/Frameworks/Python.framework/Versions/${py_major_minor}"
|
|
if [ -d "$framework_path" ]; then
|
|
echo " Copying to installation directory..."
|
|
cp -R "$framework_path"/* "${PYTHON_PREFIX}/"
|
|
rm -f "${py_installer}"
|
|
|
|
# Verify SSL support
|
|
if "${PYTHON_PREFIX}/bin/python3" -c "import ssl; print('SSL OK')" > /dev/null 2>&1; then
|
|
print_step_complete "Python ${PYTHON_VERSION} installed (prebuilt)"
|
|
return
|
|
else
|
|
echo " ⚠️ Prebuilt Python has SSL issues, will build from source instead..."
|
|
fi
|
|
fi
|
|
fi
|
|
rm -f "${py_installer}"
|
|
fi
|
|
fi
|
|
|
|
# Build from source (default path or fallback)
|
|
echo "Building Python from source with optimizations..."
|
|
|
|
# Ensure system utilities are in PATH
|
|
export PATH="/usr/bin:/bin:/usr/sbin:/sbin:$PATH"
|
|
|
|
# Download Python source
|
|
if [ ! -f "Python-${PYTHON_VERSION}.tar.xz" ]; then
|
|
echo " Downloading Python source..."
|
|
curl -LO "https://www.python.org/ftp/python/${PYTHON_VERSION}/Python-${PYTHON_VERSION}.tar.xz"
|
|
fi
|
|
|
|
echo " Extracting..."
|
|
tar -xf "Python-${PYTHON_VERSION}.tar.xz"
|
|
cd "Python-${PYTHON_VERSION}"
|
|
|
|
# Detect OpenSSL location (try common paths)
|
|
local openssl_path=""
|
|
for path in /opt/homebrew/opt/openssl@3 /opt/homebrew/opt/openssl@1.1 /usr/local/opt/openssl@3 /usr/local/opt/openssl; do
|
|
if [ -d "$path" ]; then
|
|
openssl_path="$path"
|
|
echo " Found OpenSSL at: $openssl_path"
|
|
break
|
|
fi
|
|
done
|
|
|
|
if [ -z "$openssl_path" ]; then
|
|
echo "❌ OpenSSL not found!"
|
|
echo ""
|
|
echo "Please install OpenSSL first:"
|
|
echo " brew install openssl@3"
|
|
echo ""
|
|
echo "Or if you don't have Homebrew, install it first:"
|
|
echo " /bin/bash -c \"\$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)\""
|
|
exit 1
|
|
fi
|
|
|
|
# Configure with optimizations for Apple Silicon
|
|
echo " Configuring for fast build..."
|
|
CPPFLAGS="-I${openssl_path}/include" \
|
|
LDFLAGS="-L${openssl_path}/lib" \
|
|
./configure \
|
|
--prefix="${PYTHON_PREFIX}" \
|
|
--enable-shared \
|
|
MACOSX_DEPLOYMENT_TARGET=12.0 \
|
|
--with-openssl="${openssl_path}" \
|
|
> "${DEPS_DIR}/python-configure.log" 2>&1
|
|
|
|
if [ $? -ne 0 ]; then
|
|
echo "❌ Configuration failed! Check ${DEPS_DIR}/python-configure.log"
|
|
exit 1
|
|
fi
|
|
|
|
# Build with all cores (faster without PGO/LTO optimizations)
|
|
echo " Compiling with ${MAKE_JOBS} cores (errors will be shown)..."
|
|
if ! make -j${MAKE_JOBS} > "${DEPS_DIR}/python-build.log" 2>&1; then
|
|
echo "❌ Build failed! Last 50 lines of log:"
|
|
tail -50 "${DEPS_DIR}/python-build.log"
|
|
exit 1
|
|
fi
|
|
|
|
echo " Installing..."
|
|
if ! make install > "${DEPS_DIR}/python-install.log" 2>&1; then
|
|
echo "❌ Installation failed! Check ${DEPS_DIR}/python-install.log"
|
|
exit 1
|
|
fi
|
|
|
|
# Verify SSL support
|
|
echo " Verifying SSL support..."
|
|
if ! "${PYTHON_PREFIX}/bin/python3" -c "import ssl; print('SSL OK')" > /dev/null 2>&1; then
|
|
echo "⚠️ WARNING: Python SSL module not available!"
|
|
echo " This may cause issues with pip and HTTPS connections."
|
|
echo " Check ${DEPS_DIR}/python-configure.log for details."
|
|
else
|
|
echo " ✅ SSL support verified"
|
|
fi
|
|
|
|
# Cleanup
|
|
cd "${DEPS_DIR}"
|
|
rm -rf "Python-${PYTHON_VERSION}"
|
|
|
|
mark_step_complete "install_python"
|
|
print_step_complete "Python ${PYTHON_VERSION} installed (optimized)"
|
|
}
|
|
|
|
##############################################################################
|
|
# Download and Build PostgreSQL
|
|
##############################################################################
|
|
|
|
install_postgresql() {
|
|
if is_step_complete "install_postgresql"; then
|
|
CURRENT_STEP=$((CURRENT_STEP + 1))
|
|
echo "✓ PostgreSQL ${POSTGRES_VERSION} already installed (skipping)"
|
|
return
|
|
fi
|
|
|
|
print_progress "Installing PostgreSQL ${POSTGRES_VERSION}..." "${DEPS_DIR}/postgresql-build.log"
|
|
|
|
if [ -f "${POSTGRES_PREFIX}/bin/postgres" ]; then
|
|
mark_step_complete "install_postgresql"
|
|
print_step_complete "PostgreSQL already installed"
|
|
return
|
|
fi
|
|
|
|
cd "${DEPS_DIR}"
|
|
|
|
# Clean up any partial builds from previous failed attempts
|
|
echo "Cleaning up any previous build attempts..."
|
|
rm -rf "postgresql-${POSTGRES_VERSION}" "postgresql-${POSTGRES_VERSION}-darwin-arm64.zip" pgsql 2>/dev/null || true
|
|
|
|
# Try to download prebuilt binary first
|
|
local pg_major=$(echo $POSTGRES_VERSION | cut -d. -f1)
|
|
local pg_archive="postgresql-${POSTGRES_VERSION}-darwin-arm64"
|
|
|
|
echo "Checking for prebuilt PostgreSQL binary..."
|
|
if curl -sfL "https://get.enterprisedb.com/postgresql/${pg_archive}.zip" -o "${pg_archive}.zip" 2>/dev/null; then
|
|
echo " Using prebuilt binary..."
|
|
unzip -q "${pg_archive}.zip"
|
|
mv pgsql "${POSTGRES_PREFIX}"
|
|
rm "${pg_archive}.zip"
|
|
mark_step_complete "install_postgresql"
|
|
print_step_complete "PostgreSQL ${POSTGRES_VERSION} installed (prebuilt)"
|
|
else
|
|
# Fallback to building from source
|
|
echo " Prebuilt not available, building from source..."
|
|
|
|
# Ensure system utilities are in PATH
|
|
export PATH="/usr/bin:/bin:/usr/sbin:/sbin:$PATH"
|
|
|
|
# Download PostgreSQL source
|
|
if [ ! -f "postgresql-${POSTGRES_VERSION}.tar.gz" ]; then
|
|
echo " Downloading PostgreSQL source..."
|
|
curl -LO "https://ftp.postgresql.org/pub/source/v${POSTGRES_VERSION}/postgresql-${POSTGRES_VERSION}.tar.gz"
|
|
fi
|
|
|
|
echo " Extracting and building PostgreSQL..."
|
|
tar -xf "postgresql-${POSTGRES_VERSION}.tar.gz"
|
|
cd "postgresql-${POSTGRES_VERSION}"
|
|
|
|
echo " Configuring..."
|
|
./configure \
|
|
--prefix="${POSTGRES_PREFIX}" \
|
|
--with-pgport=5432 \
|
|
> "${DEPS_DIR}/postgresql-configure.log" 2>&1
|
|
|
|
if [ $? -ne 0 ]; then
|
|
echo "❌ Configuration failed! Check ${DEPS_DIR}/postgresql-configure.log"
|
|
exit 1
|
|
fi
|
|
|
|
echo " Compiling (errors will be shown)..."
|
|
if ! make -j$(sysctl -n hw.ncpu) > "${DEPS_DIR}/postgresql-build.log" 2>&1; then
|
|
echo "❌ Build failed! Last 50 lines of log:"
|
|
tail -50 "${DEPS_DIR}/postgresql-build.log"
|
|
exit 1
|
|
fi
|
|
|
|
echo " Installing..."
|
|
if ! make install > "${DEPS_DIR}/postgresql-install.log" 2>&1; then
|
|
echo "❌ Installation failed! Check ${DEPS_DIR}/postgresql-install.log"
|
|
exit 1
|
|
fi
|
|
|
|
# Cleanup
|
|
cd "${DEPS_DIR}"
|
|
rm -rf "postgresql-${POSTGRES_VERSION}"
|
|
|
|
mark_step_complete "install_postgresql"
|
|
print_step_complete "PostgreSQL ${POSTGRES_VERSION} installed (from source)"
|
|
fi
|
|
}
|
|
|
|
##############################################################################
|
|
# Download and Build Redis
|
|
##############################################################################
|
|
|
|
install_redis() {
|
|
if is_step_complete "install_redis"; then
|
|
CURRENT_STEP=$((CURRENT_STEP + 1))
|
|
echo "✓ Redis ${REDIS_VERSION} already installed (skipping)"
|
|
return
|
|
fi
|
|
|
|
print_progress "Installing Redis ${REDIS_VERSION}..." "${DEPS_DIR}/redis-build.log"
|
|
|
|
if [ -f "${REDIS_PREFIX}/bin/redis-server" ]; then
|
|
mark_step_complete "install_redis"
|
|
print_step_complete "Redis already installed"
|
|
return
|
|
fi
|
|
|
|
cd "${DEPS_DIR}"
|
|
|
|
# Clean up any partial builds from previous failed attempts
|
|
echo "Cleaning up any previous build attempts..."
|
|
rm -rf "redis-${REDIS_VERSION}" 2>/dev/null || true
|
|
|
|
# Redis doesn't provide official prebuilt binaries, but it compiles quickly
|
|
# Build from source
|
|
|
|
# Ensure system utilities are in PATH
|
|
export PATH="/usr/bin:/bin:/usr/sbin:/sbin:$PATH"
|
|
|
|
# Download Redis source
|
|
if [ ! -f "redis-${REDIS_VERSION}.tar.gz" ]; then
|
|
echo "Downloading Redis source..."
|
|
curl -LO "https://download.redis.io/releases/redis-${REDIS_VERSION}.tar.gz"
|
|
fi
|
|
|
|
echo "Building Redis..."
|
|
tar -xf "redis-${REDIS_VERSION}.tar.gz"
|
|
cd "redis-${REDIS_VERSION}"
|
|
|
|
echo " Compiling (errors will be shown)..."
|
|
if ! make -j$(sysctl -n hw.ncpu) > "${DEPS_DIR}/redis-build.log" 2>&1; then
|
|
echo "❌ Build failed! Last 50 lines of log:"
|
|
tail -50 "${DEPS_DIR}/redis-build.log"
|
|
exit 1
|
|
fi
|
|
|
|
echo " Installing..."
|
|
if ! make PREFIX="${REDIS_PREFIX}" install > "${DEPS_DIR}/redis-install.log" 2>&1; then
|
|
echo "❌ Installation failed! Check ${DEPS_DIR}/redis-install.log"
|
|
exit 1
|
|
fi
|
|
|
|
# Create default config
|
|
mkdir -p "${REDIS_PREFIX}/etc"
|
|
cat > "${REDIS_PREFIX}/etc/redis.conf" <<EOF
|
|
bind 127.0.0.1
|
|
port 6379
|
|
daemonize no
|
|
dir ${RUNTIME_DIR}
|
|
logfile ${LOGS_DIR}/redis.log
|
|
pidfile ${RUNTIME_DIR}/pids/redis.pid
|
|
EOF
|
|
|
|
# Cleanup
|
|
cd "${DEPS_DIR}"
|
|
rm -rf "redis-${REDIS_VERSION}"
|
|
|
|
mark_step_complete "install_redis"
|
|
print_step_complete "Redis ${REDIS_VERSION} installed"
|
|
}
|
|
|
|
##############################################################################
|
|
# Download and Install nginx
|
|
##############################################################################
|
|
|
|
install_nginx() {
|
|
if is_step_complete "install_nginx"; then
|
|
CURRENT_STEP=$((CURRENT_STEP + 1))
|
|
echo "✓ nginx already installed (skipping)"
|
|
return
|
|
fi
|
|
|
|
print_progress "Installing nginx..." "${DEPS_DIR}/nginx-build.log"
|
|
|
|
NGINX_VERSION="1.26.2"
|
|
NGINX_PREFIX="${DEPS_DIR}/nginx"
|
|
|
|
if [ -f "${NGINX_PREFIX}/sbin/nginx" ]; then
|
|
mark_step_complete "install_nginx"
|
|
print_step_complete "nginx already installed"
|
|
return
|
|
fi
|
|
|
|
cd "${DEPS_DIR}"
|
|
|
|
# Clean up any partial builds
|
|
echo "Cleaning up any previous build attempts..."
|
|
rm -rf "nginx-${NGINX_VERSION}" 2>/dev/null || true
|
|
|
|
# Download nginx source
|
|
if [ ! -f "nginx-${NGINX_VERSION}.tar.gz" ]; then
|
|
echo "Downloading nginx source..."
|
|
curl -LO "http://nginx.org/download/nginx-${NGINX_VERSION}.tar.gz"
|
|
fi
|
|
|
|
echo "Extracting nginx..."
|
|
tar -xf "nginx-${NGINX_VERSION}.tar.gz"
|
|
cd "nginx-${NGINX_VERSION}"
|
|
|
|
echo " Configuring nginx..."
|
|
./configure \
|
|
--prefix="${NGINX_PREFIX}" \
|
|
--with-http_ssl_module \
|
|
--with-http_v2_module \
|
|
--with-http_realip_module \
|
|
--with-http_stub_status_module \
|
|
--without-http_gzip_module \
|
|
--without-http_rewrite_module \
|
|
> "${DEPS_DIR}/nginx-configure.log" 2>&1
|
|
|
|
echo " Compiling nginx..."
|
|
if ! make -j$(sysctl -n hw.ncpu) > "${DEPS_DIR}/nginx-build.log" 2>&1; then
|
|
echo "❌ Build failed! Last 50 lines of log:"
|
|
tail -50 "${DEPS_DIR}/nginx-build.log"
|
|
exit 1
|
|
fi
|
|
|
|
echo " Installing nginx..."
|
|
if ! make install > "${DEPS_DIR}/nginx-install.log" 2>&1; then
|
|
echo "❌ Installation failed! Check ${DEPS_DIR}/nginx-install.log"
|
|
exit 1
|
|
fi
|
|
|
|
# Cleanup
|
|
cd "${DEPS_DIR}"
|
|
rm -rf "nginx-${NGINX_VERSION}"
|
|
|
|
mark_step_complete "install_nginx"
|
|
print_step_complete "nginx installed"
|
|
}
|
|
|
|
##############################################################################
|
|
# Download and Install Node.js
|
|
##############################################################################
|
|
|
|
install_node() {
|
|
if is_step_complete "install_node"; then
|
|
CURRENT_STEP=$((CURRENT_STEP + 1))
|
|
echo "✓ Node.js ${NODE_VERSION} already installed (skipping)"
|
|
return
|
|
fi
|
|
|
|
print_progress "Installing Node.js ${NODE_VERSION}..."
|
|
|
|
if [ -f "${NODE_PREFIX}/bin/node" ]; then
|
|
mark_step_complete "install_node"
|
|
print_step_complete "Node.js already installed"
|
|
return
|
|
fi
|
|
|
|
cd "${DEPS_DIR}"
|
|
|
|
# Download Node.js binary (pre-built for arm64)
|
|
local node_archive="node-v${NODE_VERSION}-darwin-arm64"
|
|
if [ ! -f "${node_archive}.tar.gz" ]; then
|
|
echo "Downloading Node.js..."
|
|
curl -LO "https://nodejs.org/dist/v${NODE_VERSION}/${node_archive}.tar.gz"
|
|
fi
|
|
|
|
echo "Extracting Node.js..."
|
|
tar -xf "${node_archive}.tar.gz"
|
|
mv "${node_archive}" "${NODE_PREFIX}"
|
|
|
|
mark_step_complete "install_node"
|
|
print_step_complete "Node.js ${NODE_VERSION} installed"
|
|
}
|
|
|
|
##############################################################################
|
|
# Download and Build FFmpeg with VideoToolbox
|
|
##############################################################################
|
|
|
|
install_ffmpeg() {
|
|
if is_step_complete "install_ffmpeg"; then
|
|
CURRENT_STEP=$((CURRENT_STEP + 1))
|
|
echo "✓ FFmpeg with VideoToolbox already installed (skipping)"
|
|
return
|
|
fi
|
|
|
|
print_progress "Building FFmpeg with VideoToolbox..." "${DEPS_DIR}/ffmpeg-build.log"
|
|
|
|
if [ -f "${FFMPEG_PREFIX}/bin/ffmpeg" ]; then
|
|
mark_step_complete "install_ffmpeg"
|
|
print_step_complete "FFmpeg already installed"
|
|
return
|
|
fi
|
|
|
|
# Ensure system utilities are in PATH
|
|
export PATH="/usr/bin:/bin:/usr/sbin:/sbin:$PATH"
|
|
|
|
cd "${DEPS_DIR}"
|
|
|
|
# Clean up any partial builds (but keep the source repo if it exists)
|
|
if [ -d "ffmpeg-source" ]; then
|
|
echo "Cleaning previous build artifacts..."
|
|
cd ffmpeg-source
|
|
make distclean > /dev/null 2>&1 || true
|
|
cd "${DEPS_DIR}"
|
|
fi
|
|
|
|
# Clone FFmpeg
|
|
if [ ! -d "ffmpeg-source" ]; then
|
|
echo "Downloading FFmpeg source..."
|
|
git clone --depth 1 https://git.ffmpeg.org/ffmpeg.git ffmpeg-source
|
|
fi
|
|
|
|
cd ffmpeg-source
|
|
|
|
echo "Building FFmpeg with VideoToolbox..."
|
|
echo " Configuring..."
|
|
|
|
./configure \
|
|
--prefix="${FFMPEG_PREFIX}" \
|
|
--enable-videotoolbox \
|
|
--enable-audiotoolbox \
|
|
--enable-gpl \
|
|
--enable-version3 \
|
|
--enable-nonfree \
|
|
--enable-shared \
|
|
--disable-static \
|
|
--disable-debug \
|
|
--arch=arm64 \
|
|
--cpu=native \
|
|
> "${DEPS_DIR}/ffmpeg-configure.log" 2>&1
|
|
|
|
if [ $? -ne 0 ]; then
|
|
echo "❌ Configuration failed! Check ${DEPS_DIR}/ffmpeg-configure.log"
|
|
exit 1
|
|
fi
|
|
|
|
echo " Compiling (this takes the longest - errors will be shown)..."
|
|
if ! make -j$(sysctl -n hw.ncpu) > "${DEPS_DIR}/ffmpeg-build.log" 2>&1; then
|
|
echo "❌ Build failed! Last 50 lines of log:"
|
|
tail -50 "${DEPS_DIR}/ffmpeg-build.log"
|
|
exit 1
|
|
fi
|
|
|
|
echo " Installing..."
|
|
if ! make install > "${DEPS_DIR}/ffmpeg-install.log" 2>&1; then
|
|
echo "❌ Installation failed! Check ${DEPS_DIR}/ffmpeg-install.log"
|
|
exit 1
|
|
fi
|
|
|
|
mark_step_complete "install_ffmpeg"
|
|
print_step_complete "FFmpeg with VideoToolbox installed"
|
|
}
|
|
|
|
##############################################################################
|
|
# Install Streamlink (Python package, but we'll make it self-contained)
|
|
##############################################################################
|
|
|
|
install_streamlink() {
|
|
if is_step_complete "install_streamlink"; then
|
|
CURRENT_STEP=$((CURRENT_STEP + 1))
|
|
echo "✓ Streamlink already installed (skipping)"
|
|
return
|
|
fi
|
|
|
|
print_progress "Installing Streamlink..."
|
|
|
|
export PATH="${PYTHON_PREFIX}/bin:$PATH"
|
|
export DYLD_LIBRARY_PATH="${PYTHON_PREFIX}/lib:${DYLD_LIBRARY_PATH:-}"
|
|
|
|
# Set up pip cache in our installation directory to avoid permission issues
|
|
local pip_cache="${INSTALL_DIR}/.pip_cache"
|
|
mkdir -p "$pip_cache"
|
|
|
|
# If running with sudo, ensure cache is owned by the actual user
|
|
if [ -n "$SUDO_USER" ]; then
|
|
chown -R "$SUDO_USER:staff" "$pip_cache"
|
|
# Run pip as the actual user to avoid permission issues
|
|
sudo -u "$SUDO_USER" "${PYTHON_PREFIX}/bin/pip3" install --cache-dir="$pip_cache" streamlink --quiet
|
|
else
|
|
"${PYTHON_PREFIX}/bin/pip3" install --cache-dir="$pip_cache" streamlink --quiet
|
|
fi
|
|
|
|
mark_step_complete "install_streamlink"
|
|
print_step_complete "Streamlink installed"
|
|
}
|
|
|
|
##############################################################################
|
|
# Clone Dispatcharr Repository
|
|
##############################################################################
|
|
|
|
clone_dispatcharr() {
|
|
if is_step_complete "clone_dispatcharr"; then
|
|
CURRENT_STEP=$((CURRENT_STEP + 1))
|
|
echo "✓ Dispatcharr repository already cloned (skipping)"
|
|
return
|
|
fi
|
|
|
|
print_progress "Cloning Dispatcharr repository (branch: ${DISPATCH_BRANCH})..."
|
|
|
|
if [ -d "${APP_DIR}/.git" ]; then
|
|
cd "${APP_DIR}"
|
|
git fetch origin --quiet
|
|
git checkout $DISPATCH_BRANCH --quiet
|
|
git pull origin $DISPATCH_BRANCH --quiet
|
|
echo "Repository updated"
|
|
else
|
|
git clone -b $DISPATCH_BRANCH https://github.com/Dispatcharr/Dispatcharr.git "${APP_DIR}" --quiet
|
|
echo "Repository cloned"
|
|
fi
|
|
|
|
# Fix ownership if running with sudo (repository was cloned as root)
|
|
if [ -n "$SUDO_USER" ]; then
|
|
echo "Fixing repository ownership..."
|
|
chown -R "$SUDO_USER:staff" "${APP_DIR}"
|
|
fi
|
|
|
|
# Now re-detect versions from the cloned repository
|
|
echo ""
|
|
detect_versions "${APP_DIR}"
|
|
|
|
mark_step_complete "clone_dispatcharr"
|
|
print_step_complete "Dispatcharr repository ready"
|
|
}
|
|
|
|
##############################################################################
|
|
# Setup Python Virtual Environment
|
|
##############################################################################
|
|
|
|
setup_python_env() {
|
|
if is_step_complete "setup_python_env"; then
|
|
CURRENT_STEP=$((CURRENT_STEP + 1))
|
|
echo "✓ Python environment already configured (skipping)"
|
|
return
|
|
fi
|
|
|
|
print_progress "Setting up Python virtual environment..."
|
|
|
|
export PATH="${PYTHON_PREFIX}/bin:$PATH"
|
|
export DYLD_LIBRARY_PATH="${PYTHON_PREFIX}/lib:${DYLD_LIBRARY_PATH:-}"
|
|
|
|
# Clean up any partial venv from previous failed attempts
|
|
if [ -d "${APP_DIR}/env" ]; then
|
|
echo "Cleaning up previous virtual environment..."
|
|
rm -rf "${APP_DIR}/env"
|
|
fi
|
|
|
|
# Set up pip cache in our installation directory
|
|
local pip_cache="${INSTALL_DIR}/.pip_cache"
|
|
mkdir -p "$pip_cache"
|
|
|
|
# If running with sudo, ensure cache is owned by the actual user
|
|
if [ -n "$SUDO_USER" ]; then
|
|
chown -R "$SUDO_USER:staff" "$pip_cache"
|
|
fi
|
|
|
|
# Set pip cache directory explicitly
|
|
export PIP_CACHE_DIR="$pip_cache"
|
|
export XDG_CACHE_HOME="${INSTALL_DIR}"
|
|
|
|
cd "${APP_DIR}"
|
|
|
|
# Create venv (as actual user if using sudo)
|
|
echo "Creating virtual environment..."
|
|
if [ -n "$SUDO_USER" ]; then
|
|
sudo -u "$SUDO_USER" "${PYTHON_PREFIX}/bin/python3" -m venv env
|
|
else
|
|
"${PYTHON_PREFIX}/bin/python3" -m venv env
|
|
fi
|
|
source env/bin/activate
|
|
|
|
# Install packages as actual user if using sudo
|
|
if [ -n "$SUDO_USER" ]; then
|
|
# Upgrade pip (with explicit cache)
|
|
echo "Upgrading pip..."
|
|
sudo -u "$SUDO_USER" env/bin/pip install --cache-dir="$pip_cache" --upgrade pip wheel setuptools --quiet
|
|
|
|
# Install requirements (excluding torch to handle separately)
|
|
echo "Installing Python packages..."
|
|
grep -v "^torch" requirements.txt > requirements_no_torch.txt
|
|
sudo -u "$SUDO_USER" env/bin/pip install --cache-dir="$pip_cache" -r requirements_no_torch.txt --quiet
|
|
sudo -u "$SUDO_USER" env/bin/pip install --cache-dir="$pip_cache" whitenoise --quiet
|
|
rm requirements_no_torch.txt
|
|
|
|
# Install PyTorch for Apple Silicon (MPS support)
|
|
echo "Installing PyTorch with Apple Silicon support..."
|
|
sudo -u "$SUDO_USER" env/bin/pip install --cache-dir="$pip_cache" torch torchvision --quiet
|
|
else
|
|
# Upgrade pip (with explicit cache)
|
|
echo "Upgrading pip..."
|
|
pip install --cache-dir="$pip_cache" --upgrade pip wheel setuptools --quiet
|
|
|
|
# Install requirements (excluding torch to handle separately)
|
|
echo "Installing Python packages..."
|
|
grep -v "^torch" requirements.txt > requirements_no_torch.txt
|
|
pip install --cache-dir="$pip_cache" -r requirements_no_torch.txt --quiet
|
|
pip install --cache-dir="$pip_cache" whitenoise --quiet
|
|
rm requirements_no_torch.txt
|
|
|
|
# Install PyTorch for Apple Silicon (MPS support)
|
|
echo "Installing PyTorch with Apple Silicon support..."
|
|
# For macOS, use the default index which has proper Apple Silicon support
|
|
# The +cpu suffix in requirements.txt is for Linux Docker containers
|
|
pip install --cache-dir="$pip_cache" torch torchvision --quiet
|
|
fi
|
|
|
|
# Link FFmpeg
|
|
ln -sf "${FFMPEG_PREFIX}/bin/ffmpeg" env/bin/ffmpeg
|
|
ln -sf "${FFMPEG_PREFIX}/bin/ffprobe" env/bin/ffprobe
|
|
|
|
deactivate
|
|
|
|
mark_step_complete "setup_python_env"
|
|
print_step_complete "Python environment configured"
|
|
}
|
|
|
|
##############################################################################
|
|
# Build Frontend
|
|
##############################################################################
|
|
|
|
build_frontend() {
|
|
if is_step_complete "build_frontend"; then
|
|
CURRENT_STEP=$((CURRENT_STEP + 1))
|
|
echo "✓ Frontend already built (skipping)"
|
|
return
|
|
fi
|
|
|
|
print_progress "Building frontend..."
|
|
|
|
export PATH="${NODE_PREFIX}/bin:$PATH"
|
|
|
|
# Set up npm cache in our installation directory to avoid permission issues
|
|
local npm_cache="${INSTALL_DIR}/.npm_cache"
|
|
mkdir -p "$npm_cache"
|
|
|
|
# If running with sudo, ensure cache is owned by the actual user
|
|
if [ -n "$SUDO_USER" ]; then
|
|
chown -R "$SUDO_USER:staff" "$npm_cache"
|
|
fi
|
|
|
|
cd "${APP_DIR}/frontend"
|
|
|
|
# Clean up any partial builds from previous failed attempts
|
|
echo "Cleaning up previous build artifacts..."
|
|
rm -rf node_modules dist .vite 2>/dev/null || true
|
|
|
|
# Configure Vite to use /static/ base path to match Django's STATIC_URL
|
|
echo "Configuring Vite for Django static file serving..."
|
|
if grep -q "base:" vite.config.js; then
|
|
# Update existing base configuration
|
|
sed -i.bak "s|base:.*|base: '/static/',|" vite.config.js
|
|
else
|
|
# Add base configuration to defineConfig
|
|
sed -i.bak "/export default defineConfig({/a\\
|
|
base: '/static/',
|
|
" vite.config.js
|
|
fi
|
|
|
|
# Run npm as actual user if using sudo
|
|
if [ -n "$SUDO_USER" ]; then
|
|
echo "Installing npm packages..."
|
|
sudo -u "$SUDO_USER" npm install --legacy-peer-deps --quiet --cache="$npm_cache"
|
|
|
|
echo "Building frontend assets..."
|
|
sudo -u "$SUDO_USER" npm run build --quiet
|
|
else
|
|
echo "Installing npm packages..."
|
|
npm install --legacy-peer-deps --quiet --cache="$npm_cache"
|
|
|
|
echo "Building frontend assets..."
|
|
npm run build --quiet
|
|
fi
|
|
|
|
# Restore original vite.config.js to avoid git changes
|
|
if [ -f vite.config.js.bak ]; then
|
|
mv vite.config.js.bak vite.config.js
|
|
fi
|
|
|
|
mark_step_complete "build_frontend"
|
|
print_step_complete "Frontend built successfully"
|
|
}
|
|
|
|
##############################################################################
|
|
# Initialize PostgreSQL Database
|
|
##############################################################################
|
|
|
|
init_postgresql() {
|
|
if is_step_complete "init_postgresql"; then
|
|
CURRENT_STEP=$((CURRENT_STEP + 1))
|
|
echo "✓ PostgreSQL already initialized (skipping)"
|
|
return
|
|
fi
|
|
|
|
print_progress "Initializing PostgreSQL database..."
|
|
|
|
export PATH="${POSTGRES_PREFIX}/bin:$PATH"
|
|
export PGDATA="${DATA_DIR}/db"
|
|
|
|
# Check if database cluster is properly initialized
|
|
local need_init=false
|
|
|
|
if [ ! -d "${PGDATA}/base" ] || [ ! -f "${PGDATA}/PG_VERSION" ]; then
|
|
need_init=true
|
|
else
|
|
# Validate the database cluster is not corrupted
|
|
echo "Validating existing database cluster..."
|
|
if [ -f "${PGDATA}/postmaster.pid" ]; then
|
|
# Clean up stale PID file
|
|
local pg_pid=$(head -1 "${PGDATA}/postmaster.pid" 2>/dev/null)
|
|
if [ -n "$pg_pid" ] && ! kill -0 "$pg_pid" 2>/dev/null; then
|
|
echo " Cleaning up stale PID file..."
|
|
rm -f "${PGDATA}/postmaster.pid"
|
|
fi
|
|
fi
|
|
|
|
# Try a quick validation - if pg_controldata fails, the cluster is corrupted
|
|
if ! "${POSTGRES_PREFIX}/bin/pg_controldata" "${PGDATA}" > /dev/null 2>&1; then
|
|
echo " ⚠️ Database cluster appears corrupted, will reinitialize..."
|
|
need_init=true
|
|
# Clean up the corrupted database
|
|
rm -rf "${PGDATA}"
|
|
fi
|
|
fi
|
|
|
|
if [ "$need_init" = true ]; then
|
|
echo "Creating database cluster..."
|
|
echo " Data directory: ${PGDATA}"
|
|
|
|
# Ensure the postgres data directory exists and has proper permissions
|
|
mkdir -p "${PGDATA}"
|
|
if [ -n "$SUDO_USER" ]; then
|
|
chown -R "$SUDO_USER:staff" "${PGDATA}"
|
|
fi
|
|
|
|
# PostgreSQL initdb cannot run as root, so run as actual user if using sudo
|
|
if [ -n "$SUDO_USER" ]; then
|
|
echo " Running initdb as user $SUDO_USER..."
|
|
if ! sudo -u "$SUDO_USER" "${POSTGRES_PREFIX}/bin/initdb" -D "${PGDATA}" --auth=trust > "${LOGS_DIR}/initdb.log" 2>&1; then
|
|
echo "❌ Failed to initialize PostgreSQL!"
|
|
echo ""
|
|
echo "Last 30 lines of ${LOGS_DIR}/initdb.log:"
|
|
tail -30 "${LOGS_DIR}/initdb.log" || echo "Could not read log file"
|
|
exit 1
|
|
fi
|
|
else
|
|
echo " Running initdb..."
|
|
if ! "${POSTGRES_PREFIX}/bin/initdb" -D "${PGDATA}" --auth=trust > "${LOGS_DIR}/initdb.log" 2>&1; then
|
|
echo "❌ Failed to initialize PostgreSQL!"
|
|
echo ""
|
|
echo "Last 30 lines of ${LOGS_DIR}/initdb.log:"
|
|
tail -30 "${LOGS_DIR}/initdb.log" || echo "Could not read log file"
|
|
exit 1
|
|
fi
|
|
fi
|
|
|
|
echo " ✓ Database cluster initialized"
|
|
|
|
# Configure PostgreSQL (use simpler logging to avoid permission issues)
|
|
cat >> "${PGDATA}/postgresql.conf" <<EOF
|
|
|
|
# Dispatcharr custom settings
|
|
listen_addresses = '127.0.0.1'
|
|
port = 5432
|
|
unix_socket_directories = '${RUNTIME_DIR}/sockets'
|
|
logging_collector = off
|
|
EOF
|
|
else
|
|
echo "Database cluster already exists and is valid, skipping initdb"
|
|
fi
|
|
|
|
mark_step_complete "init_postgresql"
|
|
print_step_complete "PostgreSQL initialized"
|
|
}
|
|
|
|
##############################################################################
|
|
# Start Services (Background)
|
|
##############################################################################
|
|
|
|
start_services_background() {
|
|
print_progress "Starting database services..."
|
|
|
|
export PATH="${POSTGRES_PREFIX}/bin:${REDIS_PREFIX}/bin:${PYTHON_PREFIX}/bin:$PATH"
|
|
export DYLD_LIBRARY_PATH="${PYTHON_PREFIX}/lib:${POSTGRES_PREFIX}/lib:${FFMPEG_PREFIX}/lib:${DYLD_LIBRARY_PATH:-}"
|
|
export PGDATA="${DATA_DIR}/db"
|
|
|
|
# Ensure logs directory has proper permissions
|
|
if [ -n "$SUDO_USER" ]; then
|
|
echo "Ensuring proper permissions for logs and runtime directories..."
|
|
chown -R "$SUDO_USER:staff" "${LOGS_DIR}" "${RUNTIME_DIR}"
|
|
fi
|
|
|
|
# Start PostgreSQL (as actual user if using sudo)
|
|
echo "Starting PostgreSQL..."
|
|
|
|
# Check if PostgreSQL is already running using pg_isready (more reliable)
|
|
if "${POSTGRES_PREFIX}/bin/pg_isready" -h 127.0.0.1 -q 2>/dev/null; then
|
|
echo " PostgreSQL is already running"
|
|
else
|
|
# Clean up stale PID file if process is not running
|
|
if [ -f "${PGDATA}/postmaster.pid" ]; then
|
|
local pg_pid=$(head -1 "${PGDATA}/postmaster.pid" 2>/dev/null)
|
|
if [ -n "$pg_pid" ] && ! kill -0 "$pg_pid" 2>/dev/null; then
|
|
echo " Cleaning up stale PID file..."
|
|
rm -f "${PGDATA}/postmaster.pid"
|
|
fi
|
|
fi
|
|
|
|
# Start PostgreSQL
|
|
echo " Starting PostgreSQL server..."
|
|
if [ -n "$SUDO_USER" ]; then
|
|
if ! sudo -u "$SUDO_USER" "${POSTGRES_PREFIX}/bin/pg_ctl" \
|
|
-D "${PGDATA}" \
|
|
-l "${LOGS_DIR}/postgresql.log" \
|
|
-w \
|
|
start; then
|
|
echo "❌ Failed to start PostgreSQL!"
|
|
echo ""
|
|
echo "Checking for database corruption..."
|
|
if "${POSTGRES_PREFIX}/bin/pg_controldata" "${PGDATA}" 2>&1 | grep -q "could not open\|invalid\|corrupted"; then
|
|
echo " Database appears corrupted. Logs:"
|
|
fi
|
|
echo ""
|
|
echo "Last 30 lines of ${LOGS_DIR}/postgresql.log:"
|
|
tail -30 "${LOGS_DIR}/postgresql.log" 2>/dev/null || echo "Could not read log file"
|
|
echo ""
|
|
echo "To fix this, you can:"
|
|
echo " 1. Delete the corrupted database: rm -rf ${PGDATA}"
|
|
echo " 2. Remove the checkpoint: rm ${INSTALL_DIR}/.install_progress"
|
|
echo " 3. Re-run the installation script"
|
|
exit 1
|
|
fi
|
|
else
|
|
if ! "${POSTGRES_PREFIX}/bin/pg_ctl" \
|
|
-D "${PGDATA}" \
|
|
-l "${LOGS_DIR}/postgresql.log" \
|
|
-w \
|
|
start; then
|
|
echo "❌ Failed to start PostgreSQL!"
|
|
echo ""
|
|
echo "Checking for database corruption..."
|
|
if "${POSTGRES_PREFIX}/bin/pg_controldata" "${PGDATA}" 2>&1 | grep -q "could not open\|invalid\|corrupted"; then
|
|
echo " Database appears corrupted. Logs:"
|
|
fi
|
|
echo ""
|
|
echo "Last 30 lines of ${LOGS_DIR}/postgresql.log:"
|
|
tail -30 "${LOGS_DIR}/postgresql.log" 2>/dev/null || echo "Could not read log file"
|
|
echo ""
|
|
echo "To fix this, you can:"
|
|
echo " 1. Delete the corrupted database: rm -rf ${PGDATA}"
|
|
echo " 2. Remove the checkpoint: rm ${INSTALL_DIR}/.install_progress"
|
|
echo " 3. Re-run the installation script"
|
|
exit 1
|
|
fi
|
|
fi
|
|
echo " ✓ PostgreSQL started successfully"
|
|
fi
|
|
|
|
# Verify PostgreSQL is accepting connections
|
|
echo " Verifying PostgreSQL is accepting connections..."
|
|
local ready_count=0
|
|
for i in {1..15}; do
|
|
if "${POSTGRES_PREFIX}/bin/pg_isready" -h 127.0.0.1 -q 2>/dev/null; then
|
|
ready_count=$((ready_count + 1))
|
|
if [ $ready_count -ge 2 ]; then
|
|
echo " ✓ PostgreSQL is ready and accepting connections"
|
|
break
|
|
fi
|
|
fi
|
|
sleep 1
|
|
done
|
|
|
|
if [ $ready_count -lt 2 ]; then
|
|
echo "⚠️ Warning: PostgreSQL may not be fully ready"
|
|
fi
|
|
|
|
# Start Redis (as actual user if using sudo)
|
|
echo "Starting Redis..."
|
|
if "${REDIS_PREFIX}/bin/redis-cli" ping > /dev/null 2>&1; then
|
|
echo " Redis is already running"
|
|
else
|
|
if [ -n "$SUDO_USER" ]; then
|
|
sudo -u "$SUDO_USER" "${REDIS_PREFIX}/bin/redis-server" "${REDIS_PREFIX}/etc/redis.conf" &
|
|
echo $! > "${RUNTIME_DIR}/pids/redis.pid"
|
|
else
|
|
"${REDIS_PREFIX}/bin/redis-server" "${REDIS_PREFIX}/etc/redis.conf" &
|
|
echo $! > "${RUNTIME_DIR}/pids/redis.pid"
|
|
fi
|
|
sleep 2
|
|
echo " ✓ Redis started"
|
|
fi
|
|
|
|
print_step_complete "Database services started"
|
|
}
|
|
|
|
##############################################################################
|
|
# Setup Database and User
|
|
##############################################################################
|
|
|
|
setup_database() {
|
|
if is_step_complete "setup_database"; then
|
|
CURRENT_STEP=$((CURRENT_STEP + 1))
|
|
echo "✓ Database already configured (skipping)"
|
|
return
|
|
fi
|
|
|
|
print_progress "Configuring database..."
|
|
|
|
export PATH="${POSTGRES_PREFIX}/bin:$PATH"
|
|
export PGDATA="${DATA_DIR}/db"
|
|
|
|
# Determine the actual username (not "root" when using sudo)
|
|
local ACTUAL_USER="${SUDO_USER:-$USER}"
|
|
|
|
# Create user and database
|
|
echo "Creating database and user..."
|
|
echo " Connecting as PostgreSQL user: $ACTUAL_USER"
|
|
|
|
# Check if database exists, create if not
|
|
local db_exists=$("${POSTGRES_PREFIX}/bin/psql" -h 127.0.0.1 -U "$ACTUAL_USER" -d postgres -tAc "SELECT 1 FROM pg_database WHERE datname='$POSTGRES_DB'" 2>/dev/null)
|
|
if [ "$db_exists" != "1" ]; then
|
|
echo " Creating database '$POSTGRES_DB'..."
|
|
"${POSTGRES_PREFIX}/bin/psql" -h 127.0.0.1 -U "$ACTUAL_USER" -d postgres -c "CREATE DATABASE $POSTGRES_DB;"
|
|
else
|
|
echo " Database '$POSTGRES_DB' already exists"
|
|
fi
|
|
|
|
# Check if user exists, create if not
|
|
local user_exists=$("${POSTGRES_PREFIX}/bin/psql" -h 127.0.0.1 -U "$ACTUAL_USER" -d postgres -tAc "SELECT 1 FROM pg_roles WHERE rolname='$POSTGRES_USER'" 2>/dev/null)
|
|
if [ "$user_exists" != "1" ]; then
|
|
echo " Creating user '$POSTGRES_USER'..."
|
|
"${POSTGRES_PREFIX}/bin/psql" -h 127.0.0.1 -U "$ACTUAL_USER" -d postgres -c "CREATE USER $POSTGRES_USER WITH PASSWORD '$POSTGRES_PASSWORD';"
|
|
else
|
|
echo " User '$POSTGRES_USER' already exists"
|
|
fi
|
|
|
|
# Grant privileges
|
|
echo " Granting privileges..."
|
|
"${POSTGRES_PREFIX}/bin/psql" -h 127.0.0.1 -U "$ACTUAL_USER" -d postgres <<EOF
|
|
GRANT ALL PRIVILEGES ON DATABASE $POSTGRES_DB TO $POSTGRES_USER;
|
|
ALTER DATABASE $POSTGRES_DB OWNER TO $POSTGRES_USER;
|
|
EOF
|
|
|
|
"${POSTGRES_PREFIX}/bin/psql" -h 127.0.0.1 -U "$ACTUAL_USER" -d "$POSTGRES_DB" <<EOF
|
|
GRANT ALL ON SCHEMA public TO $POSTGRES_USER;
|
|
EOF
|
|
|
|
mark_step_complete "setup_database"
|
|
print_step_complete "Database configured"
|
|
}
|
|
|
|
##############################################################################
|
|
# Configure WhiteNoise for Static Files
|
|
##############################################################################
|
|
|
|
configure_whitenoise() {
|
|
print_progress "Configuring WhiteNoise for static file serving..."
|
|
|
|
cd "${APP_DIR}"
|
|
|
|
# Add WhiteNoise to middleware after SecurityMiddleware
|
|
if ! grep -q "whitenoise.middleware.WhiteNoiseMiddleware" dispatcharr/settings.py; then
|
|
echo " Adding WhiteNoise middleware..."
|
|
sed -i '' '/django.middleware.security.SecurityMiddleware/a\
|
|
"whitenoise.middleware.WhiteNoiseMiddleware",
|
|
' dispatcharr/settings.py
|
|
fi
|
|
|
|
# Add WhiteNoise storage configuration if not already present
|
|
if ! grep -q "STATICFILES_STORAGE.*whitenoise" dispatcharr/settings.py; then
|
|
echo " Adding WhiteNoise storage configuration..."
|
|
echo '' >> dispatcharr/settings.py
|
|
echo '# WhiteNoise configuration for serving static files with ASGI' >> dispatcharr/settings.py
|
|
echo 'STATICFILES_STORAGE = "whitenoise.storage.CompressedStaticFilesStorage"' >> dispatcharr/settings.py
|
|
fi
|
|
|
|
print_step_complete "WhiteNoise configured"
|
|
}
|
|
|
|
##############################################################################
|
|
# Run Django Migrations
|
|
##############################################################################
|
|
|
|
run_migrations() {
|
|
if is_step_complete "run_migrations"; then
|
|
CURRENT_STEP=$((CURRENT_STEP + 1))
|
|
echo "✓ Django migrations already run (skipping)"
|
|
return
|
|
fi
|
|
|
|
print_progress "Running Django migrations..."
|
|
|
|
cd "${APP_DIR}"
|
|
|
|
export PATH="${PYTHON_PREFIX}/bin:$PATH"
|
|
export DYLD_LIBRARY_PATH="${PYTHON_PREFIX}/lib:${POSTGRES_PREFIX}/lib:${FFMPEG_PREFIX}/lib:${DYLD_LIBRARY_PATH:-}"
|
|
|
|
# Run migrations as actual user if using sudo
|
|
if [ -n "$SUDO_USER" ]; then
|
|
sudo -u "$SUDO_USER" bash <<MIGRATIONS_EOF
|
|
source env/bin/activate
|
|
|
|
export POSTGRES_DB="$POSTGRES_DB"
|
|
export POSTGRES_USER="$POSTGRES_USER"
|
|
export POSTGRES_PASSWORD="$POSTGRES_PASSWORD"
|
|
export POSTGRES_HOST="127.0.0.1"
|
|
export REDIS_HOST="127.0.0.1"
|
|
export DISPATCHARR_PLUGINS_DIR="${DATA_DIR}/plugins"
|
|
|
|
echo "Applying database migrations..."
|
|
python manage.py migrate --noinput
|
|
|
|
echo "Collecting static files..."
|
|
python manage.py collectstatic --noinput --clear
|
|
|
|
deactivate
|
|
MIGRATIONS_EOF
|
|
else
|
|
source env/bin/activate
|
|
|
|
export POSTGRES_DB="$POSTGRES_DB"
|
|
export POSTGRES_USER="$POSTGRES_USER"
|
|
export POSTGRES_PASSWORD="$POSTGRES_PASSWORD"
|
|
export POSTGRES_HOST="127.0.0.1"
|
|
export REDIS_HOST="127.0.0.1"
|
|
export DISPATCHARR_PLUGINS_DIR="${DATA_DIR}/plugins"
|
|
|
|
echo "Applying database migrations..."
|
|
python manage.py migrate --noinput
|
|
|
|
echo "Collecting static files..."
|
|
python manage.py collectstatic --noinput --clear
|
|
|
|
deactivate
|
|
fi
|
|
|
|
mark_step_complete "run_migrations"
|
|
print_step_complete "Django setup complete"
|
|
}
|
|
|
|
##############################################################################
|
|
# Create uWSGI Configuration
|
|
##############################################################################
|
|
|
|
create_uwsgi_config() {
|
|
print_progress "Creating uWSGI configuration..."
|
|
|
|
cat > "${APP_DIR}/uwsgi.ini" <<UWSGI_EOF
|
|
[uwsgi]
|
|
# Core settings
|
|
chdir = ${APP_DIR}
|
|
module = dispatcharr.wsgi:application
|
|
home = ${APP_DIR}/env
|
|
master = true
|
|
env = DJANGO_SETTINGS_MODULE=dispatcharr.settings
|
|
socket = ${APP_DIR}/uwsgi.sock
|
|
chmod-socket = 777
|
|
vacuum = true
|
|
die-on-term = true
|
|
|
|
# Worker management
|
|
workers = 4
|
|
|
|
# Optimize for streaming - HTTP server on port 5656 for direct access
|
|
http = 127.0.0.1:5656
|
|
http-keepalive = 1
|
|
buffer-size = 65536
|
|
post-buffering = 4096
|
|
http-timeout = 600
|
|
lazy-apps = true
|
|
|
|
# Async mode (use gevent for high concurrency)
|
|
gevent = 400
|
|
|
|
# Performance tuning
|
|
thunder-lock = true
|
|
log-4xx = true
|
|
log-5xx = true
|
|
disable-logging = false
|
|
|
|
# Logging configuration
|
|
log-master = true
|
|
logformat-strftime = true
|
|
log-date = %%Y-%%m-%%d %%H:%%M:%%S
|
|
log-format = %%(ftime) [uwsgi] Worker ID: %%(wid) %%(method) %%(status) %%(uri) %%(msecs)ms
|
|
log-buffering = 1024
|
|
UWSGI_EOF
|
|
|
|
mark_step_complete "create_uwsgi_config"
|
|
print_step_complete "uWSGI configuration created"
|
|
}
|
|
|
|
##############################################################################
|
|
# Create nginx Configuration
|
|
##############################################################################
|
|
|
|
create_nginx_config() {
|
|
print_progress "Creating nginx configuration..."
|
|
|
|
NGINX_PREFIX="${DEPS_DIR}/nginx"
|
|
|
|
cat > "${NGINX_PREFIX}/conf/nginx.conf" <<NGINX_EOF
|
|
worker_processes auto;
|
|
daemon off;
|
|
pid ${RUNTIME_DIR}/pids/nginx.pid;
|
|
error_log ${LOGS_DIR}/nginx-error.log;
|
|
|
|
events {
|
|
worker_connections 1024;
|
|
}
|
|
|
|
http {
|
|
include mime.types;
|
|
default_type application/octet-stream;
|
|
|
|
access_log ${LOGS_DIR}/nginx-access.log;
|
|
|
|
sendfile on;
|
|
keepalive_timeout 65;
|
|
|
|
client_max_body_size 0;
|
|
|
|
upstream uwsgi_backend {
|
|
server unix:${APP_DIR}/uwsgi.sock;
|
|
}
|
|
|
|
upstream daphne_backend {
|
|
server 127.0.0.1:8001;
|
|
}
|
|
|
|
server {
|
|
listen ${HTTP_PORT:-9191};
|
|
server_name localhost;
|
|
|
|
proxy_connect_timeout 75;
|
|
proxy_send_timeout 300;
|
|
proxy_read_timeout 300;
|
|
|
|
proxy_set_header X-Real-IP \$remote_addr;
|
|
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
|
|
proxy_set_header X-Forwarded-Host \$host:\$server_port;
|
|
proxy_set_header X-Forwarded-Proto \$scheme;
|
|
proxy_set_header Host \$host;
|
|
proxy_set_header X-Forwarded-Port \$server_port;
|
|
|
|
# Serve Django via uWSGI (includes static files via WhiteNoise)
|
|
location / {
|
|
include ${NGINX_PREFIX}/conf/uwsgi_params;
|
|
uwsgi_pass uwsgi_backend;
|
|
}
|
|
|
|
location /logos/ {
|
|
alias ${DATA_DIR}/logos/;
|
|
}
|
|
|
|
# Logo cache endpoints - use HTTP for better performance
|
|
location /api/logos/ {
|
|
proxy_pass http://127.0.0.1:5656;
|
|
}
|
|
|
|
location /api/channels/logos/ {
|
|
proxy_pass http://127.0.0.1:5656;
|
|
}
|
|
|
|
# Route HDHR request to Django
|
|
location /hdhr {
|
|
include ${NGINX_PREFIX}/conf/uwsgi_params;
|
|
uwsgi_pass uwsgi_backend;
|
|
}
|
|
|
|
# Serve FFmpeg streams efficiently via HTTP
|
|
location /output/stream/ {
|
|
proxy_pass http://127.0.0.1:5656;
|
|
proxy_buffering off;
|
|
proxy_set_header Connection keep-alive;
|
|
proxy_set_header X-Real-IP \$remote_addr;
|
|
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
|
|
proxy_set_header Host \$host;
|
|
}
|
|
|
|
# WebSockets for real-time communication
|
|
location /ws/ {
|
|
proxy_pass http://daphne_backend;
|
|
proxy_http_version 1.1;
|
|
proxy_set_header Upgrade \$http_upgrade;
|
|
proxy_set_header Connection "Upgrade";
|
|
}
|
|
|
|
# Route TS proxy requests to uWSGI socket
|
|
location /proxy/ {
|
|
include ${NGINX_PREFIX}/conf/uwsgi_params;
|
|
uwsgi_pass uwsgi_backend;
|
|
}
|
|
}
|
|
}
|
|
NGINX_EOF
|
|
|
|
mark_step_complete "create_nginx_config"
|
|
print_step_complete "nginx configuration created"
|
|
}
|
|
|
|
##############################################################################
|
|
# Create Launcher Scripts
|
|
##############################################################################
|
|
|
|
create_launcher_scripts() {
|
|
# Main launcher
|
|
cat > "${INSTALL_DIR}/dispatcharr.sh" <<'LAUNCHER_EOF'
|
|
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
APP_DIR="${SCRIPT_DIR}/app"
|
|
DEPS_DIR="${SCRIPT_DIR}/deps"
|
|
RUNTIME_DIR="${SCRIPT_DIR}/runtime"
|
|
LOGS_DIR="${SCRIPT_DIR}/logs"
|
|
|
|
# Load environment
|
|
export PATH="${DEPS_DIR}/python/bin:${DEPS_DIR}/postgresql/bin:${DEPS_DIR}/redis/bin:${DEPS_DIR}/nginx/sbin:${DEPS_DIR}/ffmpeg/bin:$PATH"
|
|
export DYLD_LIBRARY_PATH="${DEPS_DIR}/python/lib:${DEPS_DIR}/postgresql/lib:${DEPS_DIR}/ffmpeg/lib:${DYLD_LIBRARY_PATH:-}"
|
|
export PGDATA="${SCRIPT_DIR}/data/db"
|
|
|
|
# Source credentials if they exist
|
|
if [ -f "${SCRIPT_DIR}/.env" ]; then
|
|
source "${SCRIPT_DIR}/.env"
|
|
fi
|
|
|
|
# Export required environment variables for Dispatcharr
|
|
export POSTGRES_DB="${POSTGRES_DB:-dispatcharr}"
|
|
export POSTGRES_USER="${POSTGRES_USER:-dispatch}"
|
|
export POSTGRES_PASSWORD="${POSTGRES_PASSWORD}"
|
|
export POSTGRES_HOST="${POSTGRES_HOST:-127.0.0.1}"
|
|
export POSTGRES_PORT="${POSTGRES_PORT:-5432}"
|
|
export REDIS_HOST="${REDIS_HOST:-127.0.0.1}"
|
|
export REDIS_DB="${REDIS_DB:-0}"
|
|
export CELERY_BROKER_URL="${CELERY_BROKER_URL:-redis://127.0.0.1:6379/0}"
|
|
export DISPATCHARR_PORT="${HTTP_PORT:-9191}"
|
|
export DISPATCHARR_DEBUG="${DISPATCHARR_DEBUG:-true}"
|
|
export DISPATCHARR_PLUGINS_DIR="${SCRIPT_DIR}/data/plugins"
|
|
|
|
case "${1:-}" in
|
|
start)
|
|
echo "Starting Dispatcharr services..."
|
|
|
|
# Start PostgreSQL
|
|
if ! pg_isready -h 127.0.0.1 >/dev/null 2>&1; then
|
|
pg_ctl -D "${PGDATA}" -l "${LOGS_DIR}/postgresql.log" start
|
|
echo "✓ PostgreSQL started"
|
|
else
|
|
echo "✓ PostgreSQL already running"
|
|
fi
|
|
|
|
# Start Redis
|
|
if ! redis-cli ping >/dev/null 2>&1; then
|
|
redis-server "${DEPS_DIR}/redis/etc/redis.conf" &
|
|
echo $! > "${RUNTIME_DIR}/pids/redis.pid"
|
|
echo "✓ Redis started"
|
|
else
|
|
echo "✓ Redis already running"
|
|
fi
|
|
|
|
sleep 2
|
|
|
|
# Start uWSGI (communicates with nginx via Unix socket)
|
|
if [ ! -f "${RUNTIME_DIR}/pids/uwsgi.pid" ] || ! kill -0 $(cat "${RUNTIME_DIR}/pids/uwsgi.pid") 2>/dev/null; then
|
|
cd "${APP_DIR}"
|
|
source env/bin/activate
|
|
uwsgi \
|
|
--ini uwsgi.ini \
|
|
--pidfile "${RUNTIME_DIR}/pids/uwsgi.pid" \
|
|
--daemonize "${LOGS_DIR}/uwsgi.log"
|
|
echo "✓ uWSGI started"
|
|
else
|
|
echo "✓ uWSGI already running"
|
|
fi
|
|
|
|
# Start Daphne (handles WebSockets on port 8001)
|
|
if [ ! -f "${RUNTIME_DIR}/pids/daphne.pid" ] || ! kill -0 $(cat "${RUNTIME_DIR}/pids/daphne.pid") 2>/dev/null; then
|
|
cd "${APP_DIR}"
|
|
source env/bin/activate
|
|
daphne \
|
|
-b 127.0.0.1 \
|
|
-p 8001 \
|
|
--access-log "${LOGS_DIR}/daphne-access.log" \
|
|
--proxy-headers \
|
|
--websocket_timeout 86400 \
|
|
--application-close-timeout 600 \
|
|
dispatcharr.asgi:application \
|
|
> "${LOGS_DIR}/daphne.log" 2>&1 &
|
|
echo $! > "${RUNTIME_DIR}/pids/daphne.pid"
|
|
echo "✓ Daphne started"
|
|
else
|
|
echo "✓ Daphne already running"
|
|
fi
|
|
|
|
# Start nginx (proxies to uWSGI and Daphne)
|
|
if [ ! -f "${RUNTIME_DIR}/pids/nginx.pid" ] || ! kill -0 $(cat "${RUNTIME_DIR}/pids/nginx.pid") 2>/dev/null; then
|
|
HTTP_PORT="${HTTP_PORT:-9191}" nginx -c "${DEPS_DIR}/nginx/conf/nginx.conf" > "${LOGS_DIR}/nginx-startup.log" 2>&1 &
|
|
echo $! > "${RUNTIME_DIR}/pids/nginx.pid"
|
|
echo "✓ nginx started (HTTP on port ${HTTP_PORT:-9191})"
|
|
else
|
|
echo "✓ nginx already running"
|
|
fi
|
|
|
|
# Start Celery worker
|
|
if [ ! -f "${RUNTIME_DIR}/pids/celery.pid" ] || ! kill -0 $(cat "${RUNTIME_DIR}/pids/celery.pid") 2>/dev/null; then
|
|
cd "${APP_DIR}"
|
|
source env/bin/activate
|
|
celery -A dispatcharr worker -l info \
|
|
--pidfile="${RUNTIME_DIR}/pids/celery.pid" \
|
|
--logfile="${LOGS_DIR}/celery.log" \
|
|
--detach
|
|
echo "✓ Celery worker started"
|
|
else
|
|
echo "✓ Celery worker already running"
|
|
fi
|
|
|
|
# Start Celery beat
|
|
if [ ! -f "${RUNTIME_DIR}/pids/celerybeat.pid" ] || ! kill -0 $(cat "${RUNTIME_DIR}/pids/celerybeat.pid") 2>/dev/null; then
|
|
cd "${APP_DIR}"
|
|
source env/bin/activate
|
|
celery -A dispatcharr beat -l info \
|
|
--pidfile="${RUNTIME_DIR}/pids/celerybeat.pid" \
|
|
--logfile="${LOGS_DIR}/celerybeat.log" \
|
|
--detach
|
|
echo "✓ Celery beat started"
|
|
else
|
|
echo "✓ Celery beat already running"
|
|
fi
|
|
|
|
echo ""
|
|
echo "Dispatcharr is running!"
|
|
echo "Access at: http://localhost:${HTTP_PORT:-9191}"
|
|
;;
|
|
|
|
stop)
|
|
echo "Stopping Dispatcharr services..."
|
|
|
|
# Stop nginx
|
|
if [ -f "${RUNTIME_DIR}/pids/nginx.pid" ]; then
|
|
kill $(cat "${RUNTIME_DIR}/pids/nginx.pid") 2>/dev/null || true
|
|
rm -f "${RUNTIME_DIR}/pids/nginx.pid"
|
|
echo "✓ nginx stopped"
|
|
fi
|
|
|
|
# Stop uWSGI
|
|
if [ -f "${RUNTIME_DIR}/pids/uwsgi.pid" ]; then
|
|
kill $(cat "${RUNTIME_DIR}/pids/uwsgi.pid") 2>/dev/null || true
|
|
rm -f "${RUNTIME_DIR}/pids/uwsgi.pid"
|
|
echo "✓ uWSGI stopped"
|
|
fi
|
|
|
|
# Stop Daphne
|
|
if [ -f "${RUNTIME_DIR}/pids/daphne.pid" ]; then
|
|
kill $(cat "${RUNTIME_DIR}/pids/daphne.pid") 2>/dev/null || true
|
|
rm -f "${RUNTIME_DIR}/pids/daphne.pid"
|
|
echo "✓ Daphne stopped"
|
|
fi
|
|
|
|
# Stop Celery beat
|
|
if [ -f "${RUNTIME_DIR}/pids/celerybeat.pid" ]; then
|
|
kill $(cat "${RUNTIME_DIR}/pids/celerybeat.pid") 2>/dev/null || true
|
|
rm -f "${RUNTIME_DIR}/pids/celerybeat.pid"
|
|
echo "✓ Celery beat stopped"
|
|
fi
|
|
|
|
# Stop Celery worker
|
|
if [ -f "${RUNTIME_DIR}/pids/celery.pid" ]; then
|
|
kill $(cat "${RUNTIME_DIR}/pids/celery.pid") 2>/dev/null || true
|
|
rm -f "${RUNTIME_DIR}/pids/celery.pid"
|
|
echo "✓ Celery worker stopped"
|
|
fi
|
|
|
|
# Stop Redis
|
|
if [ -f "${RUNTIME_DIR}/pids/redis.pid" ]; then
|
|
redis-cli shutdown 2>/dev/null || true
|
|
rm -f "${RUNTIME_DIR}/pids/redis.pid"
|
|
echo "✓ Redis stopped"
|
|
fi
|
|
|
|
# Stop PostgreSQL
|
|
if pg_isready -h 127.0.0.1 >/dev/null 2>&1; then
|
|
pg_ctl -D "${PGDATA}" stop
|
|
echo "✓ PostgreSQL stopped"
|
|
fi
|
|
|
|
echo "All services stopped"
|
|
;;
|
|
|
|
restart)
|
|
$0 stop
|
|
sleep 2
|
|
$0 start
|
|
;;
|
|
|
|
status)
|
|
echo "Dispatcharr Service Status:"
|
|
echo ""
|
|
|
|
if pg_isready -h 127.0.0.1 >/dev/null 2>&1; then
|
|
echo "✓ PostgreSQL: running"
|
|
else
|
|
echo "✗ PostgreSQL: stopped"
|
|
fi
|
|
|
|
if redis-cli ping >/dev/null 2>&1; then
|
|
echo "✓ Redis: running"
|
|
else
|
|
echo "✗ Redis: stopped"
|
|
fi
|
|
|
|
if [ -f "${RUNTIME_DIR}/pids/nginx.pid" ] && kill -0 $(cat "${RUNTIME_DIR}/pids/nginx.pid") 2>/dev/null; then
|
|
echo "✓ nginx: running on port ${HTTP_PORT:-9191} (PID $(cat "${RUNTIME_DIR}/pids/nginx.pid"))"
|
|
else
|
|
echo "✗ nginx: stopped"
|
|
fi
|
|
|
|
if [ -f "${RUNTIME_DIR}/pids/uwsgi.pid" ] && kill -0 $(cat "${RUNTIME_DIR}/pids/uwsgi.pid") 2>/dev/null; then
|
|
echo "✓ uWSGI: running (PID $(cat "${RUNTIME_DIR}/pids/uwsgi.pid"))"
|
|
else
|
|
echo "✗ uWSGI: stopped"
|
|
fi
|
|
|
|
if [ -f "${RUNTIME_DIR}/pids/daphne.pid" ] && kill -0 $(cat "${RUNTIME_DIR}/pids/daphne.pid") 2>/dev/null; then
|
|
echo "✓ Daphne (WebSockets): running (PID $(cat "${RUNTIME_DIR}/pids/daphne.pid"))"
|
|
else
|
|
echo "✗ Daphne: stopped"
|
|
fi
|
|
|
|
if [ -f "${RUNTIME_DIR}/pids/celery.pid" ] && kill -0 $(cat "${RUNTIME_DIR}/pids/celery.pid") 2>/dev/null; then
|
|
echo "✓ Celery worker: running (PID $(cat "${RUNTIME_DIR}/pids/celery.pid"))"
|
|
else
|
|
echo "✗ Celery worker: stopped"
|
|
fi
|
|
|
|
if [ -f "${RUNTIME_DIR}/pids/celerybeat.pid" ] && kill -0 $(cat "${RUNTIME_DIR}/pids/celerybeat.pid") 2>/dev/null; then
|
|
echo "✓ Celery beat: running (PID $(cat "${RUNTIME_DIR}/pids/celerybeat.pid"))"
|
|
else
|
|
echo "✗ Celery beat: stopped"
|
|
fi
|
|
;;
|
|
|
|
logs)
|
|
tail -f "${LOGS_DIR}"/*.log
|
|
;;
|
|
|
|
update)
|
|
echo "Updating Dispatcharr..."
|
|
|
|
# Save current branch
|
|
CURRENT_BRANCH=$(cd "${APP_DIR}" && git rev-parse --abbrev-ref HEAD)
|
|
echo "Current branch: ${CURRENT_BRANCH}"
|
|
|
|
# Check if there are local changes
|
|
if [ -n "$(cd "${APP_DIR}" && git status --porcelain)" ]; then
|
|
echo ""
|
|
echo "⚠️ WARNING: You have local changes in the app directory!"
|
|
echo "These will be overwritten by the update."
|
|
echo ""
|
|
echo -n "Continue with update? [y/N]: "
|
|
read confirm
|
|
if [ "$confirm" != "y" ] && [ "$confirm" != "Y" ]; then
|
|
echo "Update cancelled"
|
|
exit 0
|
|
fi
|
|
fi
|
|
|
|
$0 stop
|
|
|
|
echo "Pulling latest code from ${CURRENT_BRANCH}..."
|
|
cd "${APP_DIR}"
|
|
git fetch origin
|
|
git reset --hard "origin/${CURRENT_BRANCH}"
|
|
|
|
# Clear Python bytecode cache to ensure version updates
|
|
echo "Clearing Python cache..."
|
|
find . -type d -name "__pycache__" -exec rm -rf {} + 2>/dev/null || true
|
|
find . -type f -name "*.pyc" -delete 2>/dev/null || true
|
|
|
|
echo "Updating Python dependencies..."
|
|
source env/bin/activate
|
|
|
|
# Install requirements (excluding torch to handle separately for Apple Silicon)
|
|
grep -v "^torch" requirements.txt > requirements_no_torch.txt
|
|
pip install -r requirements_no_torch.txt --quiet --upgrade
|
|
rm requirements_no_torch.txt
|
|
|
|
# Install PyTorch for Apple Silicon (MPS support)
|
|
# The +cpu suffix in requirements.txt is for Linux Docker containers
|
|
pip install torch torchvision --quiet --upgrade
|
|
|
|
echo "Rebuilding frontend..."
|
|
cd frontend
|
|
|
|
# Configure Vite to use /static/ base path
|
|
if grep -q "base:" vite.config.js; then
|
|
sed -i.bak "s|base:.*|base: '/static/',|" vite.config.js
|
|
else
|
|
sed -i.bak "/export default defineConfig({/a\\
|
|
base: '/static/',
|
|
" vite.config.js
|
|
fi
|
|
|
|
npm install --legacy-peer-deps --quiet
|
|
npm run build --quiet
|
|
|
|
# Restore original vite.config.js
|
|
if [ -f vite.config.js.bak ]; then
|
|
mv vite.config.js.bak vite.config.js
|
|
fi
|
|
|
|
cd "${APP_DIR}"
|
|
|
|
# Ensure WhiteNoise is configured for static file serving
|
|
echo "Configuring WhiteNoise..."
|
|
if ! grep -q "whitenoise.middleware.WhiteNoiseMiddleware" dispatcharr/settings.py; then
|
|
sed -i.bak '/django.middleware.security.SecurityMiddleware/a\
|
|
"whitenoise.middleware.WhiteNoiseMiddleware",
|
|
' dispatcharr/settings.py
|
|
rm -f dispatcharr/settings.py.bak
|
|
fi
|
|
|
|
if ! grep -q "STATICFILES_STORAGE.*whitenoise" dispatcharr/settings.py; then
|
|
echo '' >> dispatcharr/settings.py
|
|
echo '# WhiteNoise configuration for serving static files with ASGI' >> dispatcharr/settings.py
|
|
echo 'STATICFILES_STORAGE = "whitenoise.storage.CompressedStaticFilesStorage"' >> dispatcharr/settings.py
|
|
fi
|
|
|
|
deactivate
|
|
|
|
# Start PostgreSQL for migrations
|
|
echo "Starting PostgreSQL..."
|
|
if ! pg_isready -h 127.0.0.1 >/dev/null 2>&1; then
|
|
pg_ctl -D "${PGDATA}" -l "${LOGS_DIR}/postgresql.log" start
|
|
sleep 2
|
|
fi
|
|
|
|
# Run migrations with database running
|
|
echo "Running database migrations..."
|
|
source env/bin/activate
|
|
python manage.py migrate --noinput
|
|
python manage.py collectstatic --noinput --clear
|
|
deactivate
|
|
|
|
"${SCRIPT_DIR}/dispatcharr.sh" start
|
|
echo "✓ Update complete"
|
|
;;
|
|
|
|
switch-branch)
|
|
if [ -z "${2:-}" ]; then
|
|
echo "Usage: ${BASH_SOURCE[0]} switch-branch <branch-name>"
|
|
echo ""
|
|
echo "Available branches:"
|
|
cd "${APP_DIR}" && git branch -r | grep -v HEAD | sed 's/origin\// /' || true
|
|
exit 1
|
|
fi
|
|
|
|
NEW_BRANCH="$2"
|
|
CURRENT_BRANCH=$(cd "${APP_DIR}" && git rev-parse --abbrev-ref HEAD)
|
|
|
|
echo "Switching from ${CURRENT_BRANCH} to ${NEW_BRANCH}..."
|
|
|
|
# Check if branch exists
|
|
if ! cd "${APP_DIR}" && git ls-remote --heads origin "${NEW_BRANCH}" | grep -q "${NEW_BRANCH}"; then
|
|
echo "Error: Branch '${NEW_BRANCH}' does not exist on remote"
|
|
exit 1
|
|
fi
|
|
|
|
"${SCRIPT_DIR}/dispatcharr.sh" stop
|
|
|
|
cd "${APP_DIR}"
|
|
git fetch origin
|
|
git checkout "${NEW_BRANCH}"
|
|
git pull origin "${NEW_BRANCH}"
|
|
|
|
# Clear Python bytecode cache to ensure version updates
|
|
echo "Clearing Python cache..."
|
|
find . -type d -name "__pycache__" -exec rm -rf {} + 2>/dev/null || true
|
|
find . -type f -name "*.pyc" -delete 2>/dev/null || true
|
|
|
|
echo "Updating dependencies for new branch..."
|
|
source env/bin/activate
|
|
|
|
# Install requirements (excluding torch to handle separately for Apple Silicon)
|
|
grep -v "^torch" requirements.txt > requirements_no_torch.txt
|
|
pip install -r requirements_no_torch.txt --quiet --upgrade
|
|
rm requirements_no_torch.txt
|
|
|
|
# Install PyTorch for Apple Silicon (MPS support)
|
|
# The +cpu suffix in requirements.txt is for Linux Docker containers
|
|
pip install torch torchvision --quiet --upgrade
|
|
|
|
cd frontend
|
|
|
|
# Configure Vite to use /static/ base path
|
|
if grep -q "base:" vite.config.js; then
|
|
sed -i.bak "s|base:.*|base: '/static/',|" vite.config.js
|
|
else
|
|
sed -i.bak "/export default defineConfig({/a\\
|
|
base: '/static/',
|
|
" vite.config.js
|
|
fi
|
|
|
|
npm install --legacy-peer-deps --quiet
|
|
npm run build --quiet
|
|
|
|
# Restore original vite.config.js
|
|
if [ -f vite.config.js.bak ]; then
|
|
mv vite.config.js.bak vite.config.js
|
|
fi
|
|
|
|
cd "${APP_DIR}"
|
|
|
|
# Ensure WhiteNoise is configured for static file serving
|
|
echo "Configuring WhiteNoise..."
|
|
if ! grep -q "whitenoise.middleware.WhiteNoiseMiddleware" dispatcharr/settings.py; then
|
|
sed -i.bak '/django.middleware.security.SecurityMiddleware/a\
|
|
"whitenoise.middleware.WhiteNoiseMiddleware",
|
|
' dispatcharr/settings.py
|
|
rm -f dispatcharr/settings.py.bak
|
|
fi
|
|
|
|
if ! grep -q "STATICFILES_STORAGE.*whitenoise" dispatcharr/settings.py; then
|
|
echo '' >> dispatcharr/settings.py
|
|
echo '# WhiteNoise configuration for serving static files with ASGI' >> dispatcharr/settings.py
|
|
echo 'STATICFILES_STORAGE = "whitenoise.storage.CompressedStaticFilesStorage"' >> dispatcharr/settings.py
|
|
fi
|
|
|
|
deactivate
|
|
|
|
# Update .env with new branch info
|
|
if grep -q "DISPATCH_BRANCH=" "${SCRIPT_DIR}/.env" 2>/dev/null; then
|
|
sed -i.bak "s/DISPATCH_BRANCH=.*/DISPATCH_BRANCH=${NEW_BRANCH}/" "${SCRIPT_DIR}/.env"
|
|
rm -f "${SCRIPT_DIR}/.env.bak"
|
|
else
|
|
echo "DISPATCH_BRANCH=${NEW_BRANCH}" >> "${SCRIPT_DIR}/.env"
|
|
fi
|
|
|
|
# Start PostgreSQL for migrations
|
|
echo "Starting PostgreSQL..."
|
|
if ! pg_isready -h 127.0.0.1 >/dev/null 2>&1; then
|
|
pg_ctl -D "${PGDATA}" -l "${LOGS_DIR}/postgresql.log" start
|
|
sleep 2
|
|
fi
|
|
|
|
# Run migrations with database running
|
|
echo "Running migrations..."
|
|
source env/bin/activate
|
|
python manage.py migrate --noinput
|
|
python manage.py collectstatic --noinput --clear
|
|
deactivate
|
|
|
|
"${SCRIPT_DIR}/dispatcharr.sh" start
|
|
echo "✓ Switched to branch: ${NEW_BRANCH}"
|
|
;;
|
|
|
|
version)
|
|
echo "Dispatcharr Installation Info:"
|
|
echo ""
|
|
if [ -f "${SCRIPT_DIR}/VERSIONS.txt" ]; then
|
|
cat "${SCRIPT_DIR}/VERSIONS.txt"
|
|
else
|
|
echo "Version info not found"
|
|
fi
|
|
echo ""
|
|
echo "Current branch: $(cd "${APP_DIR}" && git rev-parse --abbrev-ref HEAD)"
|
|
echo "Current commit: $(cd "${APP_DIR}" && git rev-parse --short HEAD)"
|
|
echo "Last update: $(cd "${APP_DIR}" && git log -1 --format=%cd --date=short)"
|
|
;;
|
|
|
|
*)
|
|
echo "Dispatcharr Portable Launcher"
|
|
echo ""
|
|
echo "Usage: ${BASH_SOURCE[0]} {start|stop|restart|status|logs|update|switch-branch|version}"
|
|
echo ""
|
|
echo " start - Start all services"
|
|
echo " stop - Stop all services"
|
|
echo " restart - Restart all services"
|
|
echo " status - Show service status"
|
|
echo " logs - Tail all logs"
|
|
echo " update - Update to latest on current branch"
|
|
echo " switch-branch - Switch to a different branch"
|
|
echo " version - Show version and branch info"
|
|
exit 1
|
|
;;
|
|
esac
|
|
LAUNCHER_EOF
|
|
|
|
chmod +x "${INSTALL_DIR}/dispatcharr.sh"
|
|
|
|
# Create environment file
|
|
cat > "${INSTALL_DIR}/.env" <<ENV_EOF
|
|
# Dispatcharr Environment Configuration
|
|
# Generated: $(date)
|
|
|
|
POSTGRES_DB=$POSTGRES_DB
|
|
POSTGRES_USER=$POSTGRES_USER
|
|
POSTGRES_PASSWORD=$POSTGRES_PASSWORD
|
|
POSTGRES_HOST=127.0.0.1
|
|
POSTGRES_PORT=5432
|
|
REDIS_HOST=127.0.0.1
|
|
REDIS_DB=0
|
|
CELERY_BROKER_URL=redis://127.0.0.1:6379/0
|
|
HTTP_PORT=$HTTP_PORT
|
|
DISPATCH_BRANCH=$DISPATCH_BRANCH
|
|
ENV_EOF
|
|
|
|
chmod 600 "${INSTALL_DIR}/.env"
|
|
}
|
|
|
|
##############################################################################
|
|
# Create Uninstaller
|
|
##############################################################################
|
|
|
|
create_uninstaller() {
|
|
cat > "${INSTALL_DIR}/UNINSTALL.sh" <<'UNINSTALL_EOF'
|
|
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
|
|
echo "========================================"
|
|
echo "Dispatcharr Portable Uninstaller"
|
|
echo "========================================"
|
|
echo ""
|
|
echo "This will:"
|
|
echo " 1. Stop all running services"
|
|
echo " 2. Delete everything in: ${SCRIPT_DIR}"
|
|
echo ""
|
|
echo "⚠️ WARNING: This will delete all your data!"
|
|
echo ""
|
|
echo "Type 'DELETE EVERYTHING' to confirm:"
|
|
read confirmation
|
|
|
|
if [ "$confirmation" != "DELETE EVERYTHING" ]; then
|
|
echo "Uninstall cancelled"
|
|
exit 1
|
|
fi
|
|
|
|
# Stop services
|
|
echo "Stopping services..."
|
|
"${SCRIPT_DIR}/dispatcharr.sh" stop 2>/dev/null || true
|
|
|
|
# Delete everything
|
|
echo "Deleting ${SCRIPT_DIR}..."
|
|
cd /
|
|
rm -rf "${SCRIPT_DIR}"
|
|
|
|
echo "✓ Dispatcharr completely removed (no trace left)"
|
|
UNINSTALL_EOF
|
|
|
|
chmod +x "${INSTALL_DIR}/UNINSTALL.sh"
|
|
}
|
|
|
|
##############################################################################
|
|
# Create README
|
|
##############################################################################
|
|
|
|
create_readme() {
|
|
cat > "${INSTALL_DIR}/README.txt" <<'README_EOF'
|
|
DISPATCHARR PORTABLE INSTALLATION
|
|
==================================
|
|
|
|
This is a completely self-contained installation of Dispatcharr.
|
|
Everything is in this directory - no system files modified.
|
|
|
|
USAGE
|
|
-----
|
|
|
|
Start: ./dispatcharr.sh start
|
|
Stop: ./dispatcharr.sh stop
|
|
Restart: ./dispatcharr.sh restart
|
|
Status: ./dispatcharr.sh status
|
|
Logs: ./dispatcharr.sh logs
|
|
Update: ./dispatcharr.sh update
|
|
Switch Branch: ./dispatcharr.sh switch-branch <branch-name>
|
|
Version Info: ./dispatcharr.sh version
|
|
|
|
ACCESSING DISPATCHARR
|
|
---------------------
|
|
|
|
Once started, access at: http://localhost:9191
|
|
|
|
The server binds to all network interfaces (0.0.0.0), so you can also
|
|
access it from other devices on your network using your Mac's IP address:
|
|
http://<your-mac-ip>:9191
|
|
|
|
To find your Mac's IP address, run: ipconfig getifaddr en0
|
|
|
|
UPDATES & BRANCHES
|
|
------------------
|
|
|
|
To update to latest on your current branch:
|
|
./dispatcharr.sh update
|
|
|
|
To switch branches (e.g., stable <-> dev):
|
|
./dispatcharr.sh switch-branch dev
|
|
./dispatcharr.sh switch-branch main
|
|
|
|
To check your current branch and versions:
|
|
./dispatcharr.sh version
|
|
|
|
Available branches:
|
|
main - Stable releases (recommended)
|
|
dev - Development branch (latest features)
|
|
|
|
DIRECTORY STRUCTURE
|
|
-------------------
|
|
|
|
app/ - Dispatcharr application code (git repository)
|
|
data/ - Your recordings, logos, uploads, etc.
|
|
deps/ - All dependencies (Python, PostgreSQL, Redis, FFmpeg, Node)
|
|
runtime/ - Runtime files (PIDs, sockets, PostgreSQL data)
|
|
logs/ - All log files
|
|
.env - Configuration (database credentials, current branch)
|
|
|
|
VIDEOTOOLBOX (Hardware Acceleration)
|
|
------------------------------------
|
|
|
|
FFmpeg is built with VideoToolbox support for Apple Silicon.
|
|
Hardware acceleration is automatically enabled.
|
|
|
|
MOVING THIS INSTALLATION
|
|
-------------------------
|
|
|
|
You can move this entire directory anywhere:
|
|
1. Stop services: ./dispatcharr.sh stop
|
|
2. Move the directory
|
|
3. Start services: ./dispatcharr.sh start
|
|
|
|
BACKUP
|
|
------
|
|
|
|
To backup your data, simply copy the entire directory.
|
|
Or just copy the 'data/' subdirectory for recordings/config.
|
|
|
|
UNINSTALL
|
|
---------
|
|
|
|
Run: ./UNINSTALL.sh
|
|
|
|
This will stop all services and delete this entire directory.
|
|
No traces will be left on your system.
|
|
|
|
CREDENTIALS
|
|
-----------
|
|
|
|
Database credentials are in: .env
|
|
Keep this file secure!
|
|
|
|
SUPPORT
|
|
-------
|
|
|
|
This is an experimental installation method.
|
|
For support, use Docker (official method).
|
|
|
|
README_EOF
|
|
|
|
# Create version info file
|
|
cat > "${INSTALL_DIR}/VERSIONS.txt" <<VERSION_EOF
|
|
INSTALLED VERSIONS
|
|
==================
|
|
|
|
Installation Date: $(date)
|
|
Branch: ${DISPATCH_BRANCH}
|
|
Commit: $(cd "${APP_DIR}" && git rev-parse --short HEAD)
|
|
|
|
Dependencies:
|
|
Python: ${PYTHON_VERSION}
|
|
PostgreSQL: ${POSTGRES_VERSION}
|
|
Redis: ${REDIS_VERSION}
|
|
Node.js: ${NODE_VERSION}
|
|
FFmpeg: Built from source with VideoToolbox support
|
|
|
|
These versions were auto-detected from the Dispatcharr
|
|
Dockerfile and requirements to ensure compatibility.
|
|
|
|
To verify versions:
|
|
deps/python/bin/python3 --version
|
|
deps/postgresql/bin/postgres --version
|
|
deps/redis/bin/redis-server --version
|
|
deps/node/bin/node --version
|
|
deps/ffmpeg/bin/ffmpeg -version
|
|
|
|
To check current branch:
|
|
cd app && git branch
|
|
|
|
To update:
|
|
./dispatcharr.sh update
|
|
|
|
To switch branches:
|
|
./dispatcharr.sh switch-branch <branch-name>
|
|
|
|
VERSION_EOF
|
|
|
|
echo "✓ README created"
|
|
}
|
|
|
|
##############################################################################
|
|
# Summary
|
|
##############################################################################
|
|
|
|
show_summary() {
|
|
clear
|
|
|
|
cat <<EOF
|
|
╔════════════════════════════════════════════════════════════════════════════╗
|
|
║ DISPATCHARR PORTABLE INSTALLATION ║
|
|
╚════════════════════════════════════════════════════════════════════════════╝
|
|
|
|
✓ COMPLETE! ✓
|
|
|
|
────────────────────────────────────────────────────────────────────────────
|
|
INSTALLATION DETAILS
|
|
────────────────────────────────────────────────────────────────────────────
|
|
|
|
Directory: ${INSTALL_DIR}
|
|
Disk Usage: $(du -sh "${INSTALL_DIR}" 2>/dev/null | cut -f1 || echo "~10-15GB")
|
|
|
|
Branch: ${DISPATCH_BRANCH}
|
|
Commit: $(cd "${APP_DIR}" && git rev-parse --short HEAD)
|
|
|
|
────────────────────────────────────────────────────────────────────────────
|
|
INSTALLED VERSIONS
|
|
────────────────────────────────────────────────────────────────────────────
|
|
|
|
Python: ${PYTHON_VERSION}
|
|
PostgreSQL: ${POSTGRES_VERSION}
|
|
Redis: ${REDIS_VERSION}
|
|
Node.js: ${NODE_VERSION}
|
|
FFmpeg: latest (with VideoToolbox)
|
|
|
|
────────────────────────────────────────────────────────────────────────────
|
|
QUICK START
|
|
────────────────────────────────────────────────────────────────────────────
|
|
|
|
1. Start services:
|
|
cd "${INSTALL_DIR}"
|
|
./dispatcharr.sh start
|
|
|
|
2. Open your browser:
|
|
http://localhost:${HTTP_PORT}
|
|
|
|
Architecture: nginx → uWSGI (HTTP/streaming) + Daphne (WebSockets)
|
|
(Matches Docker setup exactly)
|
|
|
|
────────────────────────────────────────────────────────────────────────────
|
|
COMMANDS
|
|
────────────────────────────────────────────────────────────────────────────
|
|
|
|
Start: ./dispatcharr.sh start
|
|
Stop: ./dispatcharr.sh stop
|
|
Status: ./dispatcharr.sh status
|
|
Logs: ./dispatcharr.sh logs
|
|
Update: ./dispatcharr.sh update
|
|
Switch Branch: ./dispatcharr.sh switch-branch <name>
|
|
Version Info: ./dispatcharr.sh version
|
|
|
|
────────────────────────────────────────────────────────────────────────────
|
|
FILES
|
|
────────────────────────────────────────────────────────────────────────────
|
|
|
|
README.txt - Quick reference guide
|
|
VERSIONS.txt - Installed versions
|
|
.env - Configuration & credentials
|
|
UNINSTALL.sh - Complete removal script
|
|
|
|
────────────────────────────────────────────────────────────────────────────
|
|
FEATURES
|
|
────────────────────────────────────────────────────────────────────────────
|
|
|
|
✓ Completely self-contained (no system modifications)
|
|
✓ VideoToolbox hardware acceleration enabled
|
|
✓ Can be moved anywhere on your system
|
|
✓ Delete directory to remove completely
|
|
✓ Zero traces left on system when uninstalled
|
|
|
|
────────────────────────────────────────────────────────────────────────────
|
|
|
|
Ready to start? Run: cd "${INSTALL_DIR}" && ./dispatcharr.sh start
|
|
|
|
╚════════════════════════════════════════════════════════════════════════════╝
|
|
|
|
EOF
|
|
}
|
|
|
|
##############################################################################
|
|
# Update Scripts Only
|
|
##############################################################################
|
|
|
|
update_scripts() {
|
|
local target_dir="${1:-}"
|
|
|
|
if [ -z "$target_dir" ]; then
|
|
echo "Usage: $0 --update-scripts <installation-directory>"
|
|
echo ""
|
|
echo "Example: $0 --update-scripts ~/Dispatcharr"
|
|
exit 1
|
|
fi
|
|
|
|
if [ ! -d "$target_dir" ]; then
|
|
echo "Error: Directory '$target_dir' does not exist"
|
|
exit 1
|
|
fi
|
|
|
|
if [ ! -f "$target_dir/.env" ]; then
|
|
echo "Error: '$target_dir' does not appear to be a Dispatcharr installation"
|
|
echo " (missing .env file)"
|
|
exit 1
|
|
fi
|
|
|
|
echo "╔════════════════════════════════════════════════════════════════════════════╗"
|
|
echo "║ UPDATING DISPATCHARR LAUNCHER SCRIPTS ║"
|
|
echo "╚════════════════════════════════════════════════════════════════════════════╝"
|
|
echo ""
|
|
echo "Target: $target_dir"
|
|
echo ""
|
|
|
|
# Set up variables for script generation
|
|
INSTALL_DIR="$target_dir"
|
|
APP_DIR="${INSTALL_DIR}/app"
|
|
DATA_DIR="${INSTALL_DIR}/data"
|
|
DEPS_DIR="${INSTALL_DIR}/deps"
|
|
RUNTIME_DIR="${INSTALL_DIR}/runtime"
|
|
LOGS_DIR="${INSTALL_DIR}/logs"
|
|
|
|
# Verify critical directories exist
|
|
if [ ! -d "$APP_DIR" ] || [ ! -d "$DEPS_DIR" ]; then
|
|
echo "Error: Missing required directories in $INSTALL_DIR"
|
|
exit 1
|
|
fi
|
|
|
|
echo "→ Regenerating dispatcharr.sh..."
|
|
if ! create_launcher_scripts; then
|
|
echo "✗ Failed to create launcher scripts"
|
|
exit 1
|
|
fi
|
|
echo "✓ dispatcharr.sh updated"
|
|
|
|
echo ""
|
|
echo "→ Regenerating UNINSTALL.sh..."
|
|
if ! create_uninstaller; then
|
|
echo "✗ Failed to create uninstaller"
|
|
exit 1
|
|
fi
|
|
echo "✓ UNINSTALL.sh updated"
|
|
|
|
echo ""
|
|
echo "→ Regenerating README.txt..."
|
|
if ! create_readme; then
|
|
echo "✗ Failed to create README"
|
|
exit 1
|
|
fi
|
|
echo "✓ README.txt updated"
|
|
|
|
echo ""
|
|
echo "╔════════════════════════════════════════════════════════════════════════════╗"
|
|
echo "║ SCRIPTS UPDATED SUCCESSFULLY ║"
|
|
echo "╚════════════════════════════════════════════════════════════════════════════╝"
|
|
echo ""
|
|
echo "The following scripts have been updated:"
|
|
echo " - dispatcharr.sh"
|
|
echo " - UNINSTALL.sh"
|
|
echo " - README.txt"
|
|
echo ""
|
|
}
|
|
|
|
##############################################################################
|
|
# Main Installation Flow
|
|
##############################################################################
|
|
|
|
main() {
|
|
show_disclaimer
|
|
select_branch
|
|
configure_variables
|
|
check_prerequisites
|
|
create_directories
|
|
|
|
# Clone the repository first to detect versions
|
|
clone_dispatcharr
|
|
|
|
# Always detect versions (even on resume, in case skipped clone)
|
|
if [ -d "${APP_DIR}/.git" ]; then
|
|
detect_versions "${APP_DIR}"
|
|
fi
|
|
|
|
install_python
|
|
install_postgresql
|
|
install_redis
|
|
install_nginx
|
|
install_node
|
|
install_ffmpeg
|
|
install_streamlink
|
|
|
|
setup_python_env
|
|
build_frontend
|
|
|
|
init_postgresql
|
|
start_services_background
|
|
setup_database
|
|
configure_whitenoise
|
|
run_migrations
|
|
create_uwsgi_config
|
|
create_nginx_config
|
|
|
|
# Stop services before creating launchers
|
|
echo ""
|
|
echo "═══════════════════════════════════════════════════════════"
|
|
echo " Finalizing installation..."
|
|
echo "═══════════════════════════════════════════════════════════"
|
|
|
|
export PATH="${POSTGRES_PREFIX}/bin:${REDIS_PREFIX}/bin:$PATH"
|
|
|
|
echo " Stopping PostgreSQL..."
|
|
if "${POSTGRES_PREFIX}/bin/pg_ctl" -D "${RUNTIME_DIR}/postgres" status > /dev/null 2>&1; then
|
|
"${POSTGRES_PREFIX}/bin/pg_ctl" -D "${RUNTIME_DIR}/postgres" stop -m fast > /dev/null 2>&1 || true
|
|
fi
|
|
|
|
echo " Stopping Redis..."
|
|
if [ -f "${RUNTIME_DIR}/pids/redis.pid" ]; then
|
|
kill $(cat "${RUNTIME_DIR}/pids/redis.pid") 2>/dev/null || true
|
|
rm -f "${RUNTIME_DIR}/pids/redis.pid"
|
|
fi
|
|
|
|
echo " Creating launcher scripts..."
|
|
if ! create_launcher_scripts; then
|
|
echo "❌ Failed to create launcher scripts!"
|
|
exit 1
|
|
fi
|
|
|
|
echo " Creating uninstaller..."
|
|
if ! create_uninstaller; then
|
|
echo "❌ Failed to create uninstaller!"
|
|
exit 1
|
|
fi
|
|
|
|
echo " Creating documentation..."
|
|
if ! create_readme; then
|
|
echo "❌ Failed to create documentation!"
|
|
exit 1
|
|
fi
|
|
|
|
# Fix ownership if running with sudo
|
|
if [ -n "$SUDO_USER" ]; then
|
|
echo "Fixing file ownership (running with sudo)..."
|
|
chown -R "$SUDO_USER:staff" "${INSTALL_DIR}"
|
|
fi
|
|
|
|
# Remove checkpoint file on successful completion
|
|
rm -f "$CHECKPOINT_FILE"
|
|
|
|
# Final step
|
|
CURRENT_STEP=$TOTAL_STEPS
|
|
show_summary
|
|
}
|
|
|
|
##############################################################################
|
|
# Script Entry Point
|
|
##############################################################################
|
|
|
|
# Parse command line arguments
|
|
if [ $# -gt 0 ]; then
|
|
case "$1" in
|
|
--update-scripts)
|
|
update_scripts "${2:-}"
|
|
exit 0
|
|
;;
|
|
--help|-h)
|
|
echo "Dispatcharr Portable Installation for macOS (Apple Silicon)"
|
|
echo ""
|
|
echo "Usage:"
|
|
echo " $0 Run full installation"
|
|
echo " $0 --update-scripts <dir> Update launcher scripts in existing installation"
|
|
echo " $0 --help Show this help message"
|
|
echo ""
|
|
echo "Examples:"
|
|
echo " $0 # Fresh install"
|
|
echo " $0 --update-scripts ~/Dispatcharr # Update scripts only"
|
|
exit 0
|
|
;;
|
|
*)
|
|
echo "Unknown option: $1"
|
|
echo "Run '$0 --help' for usage information"
|
|
exit 1
|
|
;;
|
|
esac
|
|
fi
|
|
|
|
main "$@"
|