assemblrr/bin/setup.sh

775 lines
28 KiB
Bash

#!/bin/bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
APP_ROOT=""
# Support both repository layout (`bin/` + `lib/`) and installed layout
# (entrypoints copied into the install root with `lib/` underneath).
if [ -f "$SCRIPT_DIR/../lib/core.sh" ]; then
APP_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
elif [ -f "$SCRIPT_DIR/lib/core.sh" ]; then
APP_ROOT="$SCRIPT_DIR"
else
echo -e "\033[0;31mError: lib/core.sh not found. Re-run setup from a complete assemblrr tree.\033[0m" >&2
exit 1
fi
resolve_project_file() {
local relative_path="$1"
local candidate="$APP_ROOT/$relative_path"
if [ -f "$candidate" ]; then
echo "$candidate"
return 0
fi
case "$relative_path" in
bin/*)
candidate="$APP_ROOT/${relative_path#bin/}"
;;
esac
if [ -f "$candidate" ]; then
echo "$candidate"
return 0
fi
return 1
}
# Source core library (logging, colors, safe_source, path utilities)
source "$APP_ROOT/lib/core.sh"
# Source branding — all names/paths come from here
source "$APP_ROOT/lib/branding.sh"
load_branding "$APP_ROOT"
# Source Docker Compose builder
source "$APP_ROOT/lib/compose.sh"
# Source VPN helpers
source "$APP_ROOT/lib/vpn.sh"
echo "===================================================="
echo " | | | | "
echo " __ _ ___ ___ ___ _ __ ___ | |__ | |_ __ _ __ "
echo " / _\` / __/ __|/ _ \ '_ \` _ \\| '_ \\| | '__| '__|"
echo "| (_| \\__ \\__ \\ __/ | | | | | |_) | | | | | "
echo " \\__,_|___/___/\\___|_| |_| |_|_.__/|_|_| |_| "
echo " "
echo "===================================================="
echo "Welcome to ${APP_DISPLAY_NAME} (${APP_DESCRIPTION})"
echo "===================================================="
echo ""
# --- Setup mode ---
SETUP_MODE=""
echo " 1) Express (recommended — minimal questions, smart defaults)"
echo " 2) Manual (full control over every setting)"
while true; do
read -p "Choose setup mode [1]: " setup_mode_choice
setup_mode_choice=${setup_mode_choice:-1}
case "$setup_mode_choice" in
1) SETUP_MODE="express"; break ;;
2) SETUP_MODE="manual"; break ;;
*) log_warning "Invalid choice. Please choose 1 or 2." ;;
esac
done
export SETUP_MODE
if [ "$SETUP_MODE" = "express" ]; then
echo ""
log_success "Express mode — we'll use smart defaults and only ask the essentials."
echo ""
else
echo ""
log_info "Manual mode — you'll configure every setting."
echo ""
fi
# Constants
readonly SUPPORTED_MEDIA_SERVICES=("jellyfin" "emby" "plex")
readonly DEFAULT_MEDIA_SERVICE="jellyfin"
readonly DEFAULT_VPN_SERVICE="protonvpn"
readonly MEDIA_SUBDIRS=("torrents/movies" "torrents/tv" "media/movies" "media/tv" "blackhole")
# Dependencies
readonly REQUIRED_COMMANDS=("curl" "jq" "sed" "awk" "envsubst" "fzf")
# LOG_FILE is set in main() — lib/core.sh log_info/log_debug handle file logging natively
# Path utilities (expand_path, read_masked, create_and_verify_directory, etc.) are in lib/core.sh
detect_package_manager() {
if command -v apt-get &>/dev/null; then
echo "apt"
elif command -v pacman &>/dev/null; then
echo "pacman"
elif command -v dnf &>/dev/null; then
echo "dnf"
elif command -v zypper &>/dev/null; then
echo "zypper"
else
echo "unknown"
fi
}
install_packages() {
local pkgs=("$@")
local pm
pm=$(detect_package_manager)
case "$pm" in
apt)
sudo apt-get update -qq && sudo apt-get install -y "${pkgs[@]}"
;;
pacman)
echo
log_warning "Arch Linux: about to run a full system upgrade (pacman -Syu)."
log_info "A partial install (-Sy without -u) can break jq (GLIBC mismatch)."
log_info "This updates the whole system, then installs: ${pkgs[*]}"
echo
sudo pacman -Syu --noconfirm --needed "${pkgs[@]}"
;;
dnf)
sudo dnf install -y "${pkgs[@]}"
;;
zypper)
sudo zypper -n in "${pkgs[@]}"
;;
*)
log_error "Unsupported package manager. Please install manually: ${pkgs[*]}"
;;
esac
}
check_dependencies() {
local missing_packages=()
for pkg in "${REQUIRED_COMMANDS[@]}"; do
local works=false
if command -v "$pkg" &>/dev/null; then
case "$pkg" in
curl) curl --version &>/dev/null && works=true ;;
jq) echo "{}" | jq . &>/dev/null && works=true ;;
awk) echo "" | awk '{print}' &>/dev/null && works=true ;;
sed) echo "" | sed '' &>/dev/null && works=true ;;
envsubst) echo "" | envsubst &>/dev/null && works=true ;;
fzf) fzf --version &>/dev/null && works=true ;;
*) works=true ;;
esac
fi
if ! $works; then
missing_packages+=("$pkg")
else
log_success "$pkg works"
fi
done
if [ ${#missing_packages[@]} -gt 0 ]; then
log_warning "Missing required packages: ${missing_packages[*]}"
read -p "Would you like to install the missing packages? (y/N) [Default = n]: " install_deps
install_deps=${install_deps:-"n"}
if [ "${install_deps,,}" = "y" ]; then
echo "Installing missing packages..."
if ! install_packages "${missing_packages[@]}"; then
log_error "Failed to install missing packages. Please install them manually: ${missing_packages[*]}"
fi
local still_broken=()
local pkg
for pkg in "${missing_packages[@]}"; do
case "$pkg" in
jq) echo "{}" | jq . &>/dev/null || still_broken+=("$pkg") ;;
fzf) fzf --version &>/dev/null || still_broken+=("$pkg") ;;
curl) curl --version &>/dev/null || still_broken+=("$pkg") ;;
*) command -v "$pkg" &>/dev/null || still_broken+=("$pkg") ;;
esac
done
if [ ${#still_broken[@]} -gt 0 ]; then
echo
log_warning "Installed, but still not runnable: ${still_broken[*]}"
if printf '%s\n' "${still_broken[@]}" | grep -qx jq; then
jq . </dev/null 2>&1 | head -5 || true
log_info "On Arch this is usually a partial upgrade. In WSL run: sudo pacman -Syu"
log_info "Then open a new WSL terminal and re-run setup."
fi
log_error "Please fix the packages above and re-run setup."
fi
log_success "Successfully installed missing packages"
else
log_error "Please install the required packages manually: ${missing_packages[*]}"
fi
fi
# Check Docker and Docker Compose
if ! verify_docker; then
if ! command -v docker &>/dev/null || ! docker compose version &>/dev/null; then
log_warning "Docker/Docker Compose not found!"
# If in WSL2, do not attempt to install Docker inside the distro.
# Recommend installing Docker Desktop on Windows.
if grep -qi microsoft /proc/version 2>/dev/null; then
log_error "WSL2 environment detected, but Docker Desktop is not running or not installed on Windows.\n\nPlease install Docker Desktop for Windows and enable WSL2 integration:\n https://docs.docker.com/desktop/wsl/"
fi
read -p "Install Docker and Docker Compose? (y/N) [Default = n]: " install_docker
install_docker=${install_docker:-"n"}
if [ "${install_docker,,}" = "y" ]; then
bash "$SCRIPT_DIR/docker-install.sh"
if ! verify_docker; then
log_error "Docker daemon is not running. Please start Docker and try again."
fi
else
log_error "Please install Docker and Docker Compose first"
fi
else
log_error "Docker daemon is not running. Please start Docker and try again."
fi
fi
# Perform snap check if docker is installed
if command -v docker &>/dev/null; then
if [ "$(command -v docker)" = "/snap/bin/docker" ]; then
log_error "Docker is installed via snap. ${APP_DISPLAY_NAME} requires the official Docker installation. Please remove snap Docker and install from https://docs.docker.com/engine/install/"
fi
log_success "Docker is installed and running"
fi
return 0
}
# --- Setup prompts (sourced from lib/prompts.sh) ---
# Requires: lib/core.sh already sourced, constants defined above
# fzf-tui helpers (fzf_single_select, fzf_multi_select) must be sourced before prompts
source "$APP_ROOT/lib/managed_files.sh"
source "$APP_ROOT/lib/fzf-tui.sh"
source "$APP_ROOT/lib/services.sh"
source "$APP_ROOT/lib/quality.sh"
source "$APP_ROOT/lib/prompts.sh"
_copy_stall_error() {
log_error "Timed out writing to ${install_directory} — the drive looks stuck (flaky USB / WSL). From PowerShell: wsl --shutdown — then check the drive in Explorer, or re-run setup and pick Home."
}
copy_configuration_files() {
local copied=0 src dest src_path dest_path dest_dir rc
if storage_needs_probe "$install_directory"; then
log_info "Copying install files to ${install_directory}..."
if ! probe_writable_path "$(storage_mount_root "$install_directory")"; then
log_error "Cannot copy install files to ${install_directory}. Pick Home, or fix the drive and re-run setup."
fi
else
log_debug "Copying install tree into $install_directory"
fi
UPGRADE_SOURCE_ROOT="${UPGRADE_SOURCE_ROOT:-$APP_ROOT}"
while IFS='|' read -r src dest; do
[ -z "$src" ] && continue
[[ "$src" =~ ^# ]] && continue
dest_path="$install_directory/$dest"
dest_dir=$(dirname "$dest_path")
if [ ! -d "$dest_dir" ]; then
rc=0
run_with_timeout 15 mkdir -p "$dest_dir" || rc=$?
[ "$rc" -eq 124 ] && _copy_stall_error
[ "$rc" -eq 0 ] || log_error "Failed to create $dest_dir"
fi
if ! src_path=$(resolve_project_file "$src"); then
if [[ "$src" == compose/examples/* ]]; then
log_debug "Optional example file not found: $src"
continue
fi
log_error "Failed to locate source file: $src"
fi
log_debug " $src -> $dest_path"
rc=0
run_with_timeout 20 cp "$src_path" "$dest_path" || rc=$?
if [ "$rc" -eq 124 ]; then
_copy_stall_error
elif [ "$rc" -eq 0 ]; then
case "$src" in
scripts/*|bin/cli.sh|bin/config.sh|bin/setup.sh) chmod +x "$dest_path" ;;
esac
copied=$((copied + 1))
else
log_error "Failed to copy $src to $dest_path. Check permissions"
fi
done < <(list_managed_files)
log_debug "Copied ${copied} file(s) into $install_directory"
}
generate_env_file() {
local env_file="$install_directory/.env"
log_debug "Generating environment configuration..."
# Set VPN-related variables for envsubst (credentials go to secrets/)
if [ "${setup_vpn,,}" == "y" ]; then
VPN_ENABLED="y"
VPN_SERVICE="$vpn_service"
VPN_TYPE="$vpn_type"
WIREGUARD_ADDRESSES="${wireguard_addresses:-}"
if [ "${enable_port_forwarding,,}" = "y" ]; then
PORT_FORWARD_ONLY="on"
VPN_PORT_FORWARDING="on"
else
PORT_FORWARD_ONLY="off"
VPN_PORT_FORWARDING="off"
fi
else
VPN_ENABLED="n"
VPN_SERVICE=""
VPN_TYPE="openvpn"
WIREGUARD_ADDRESSES=""
PORT_FORWARD_ONLY="off"
VPN_PORT_FORWARDING="off"
fi
# Export all variables for envsubst
API_HOST="${API_HOST:-127.0.0.1}"
export PUID="$puid"
export PGID="$pgid"
export MEDIA_DIRECTORY="$media_directory"
export INSTALL_DIRECTORY="$install_directory"
export MEDIA_SERVICE="$media_service"
export TZ="$tz"
export VPN_ENABLED VPN_SERVICE VPN_TYPE
export WIREGUARD_ADDRESSES
export PORT_FORWARD_ONLY VPN_PORT_FORWARDING
export API_HOST
local env_template
if ! env_template=$(resolve_project_file ".env.example"); then
log_error "Failed to locate .env.example"
fi
# Explicit key list so envsubst does not pull in unrelated shell variables
envsubst '${PUID} ${PGID} ${MEDIA_DIRECTORY} ${INSTALL_DIRECTORY} ${MEDIA_SERVICE} ${TZ} ${VPN_ENABLED} ${VPN_SERVICE} ${VPN_TYPE} ${WIREGUARD_ADDRESSES} ${PORT_FORWARD_ONLY} ${VPN_PORT_FORWARDING} ${API_HOST}' \
< "$env_template" | tr -d '\r' > "$env_file" || \
log_error "Failed to generate .env file"
chmod 600 "$env_file"
log_debug "Environment configuration generated ($env_file)"
}
write_vpn_secrets() {
if [ "${setup_vpn,,}" != "y" ]; then
return 0
fi
local secrets_dir="$install_directory/secrets"
mkdir -p "$secrets_dir"
if [ "$vpn_type" = "openvpn" ]; then
echo -n "${vpn_user:-}" > "$secrets_dir/openvpn_user.txt"
echo -n "${vpn_password:-}" > "$secrets_dir/openvpn_password.txt"
# Create empty WireGuard secret so the secret definition always resolves
: > "$secrets_dir/wireguard_private_key.txt"
else
echo -n "${wireguard_private_key:-}" > "$secrets_dir/wireguard_private_key.txt"
# Create empty OpenVPN secrets so the secret definitions always resolve
: > "$secrets_dir/openvpn_user.txt"
: > "$secrets_dir/openvpn_password.txt"
fi
chmod 600 "$secrets_dir"/*.txt
log_success "VPN secrets written to $secrets_dir"
}
write_auth_secrets() {
local secrets_dir="$install_directory/secrets"
mkdir -p "$secrets_dir"
echo -n "${auth_username:-admin}" > "$secrets_dir/auth_username.txt"
echo -n "${auth_password:-}" > "$secrets_dir/auth_password.txt"
chmod 600 "$secrets_dir/auth_username.txt" "$secrets_dir/auth_password.txt"
log_success "Auth credentials written to $secrets_dir"
}
write_opensubtitles_secrets() {
local secrets_dir="$install_directory/secrets"
mkdir -p "$secrets_dir"
if [ "${opensubtitles_enabled:-n}" = "y" ] && [ -n "${opensubtitles_username:-}" ]; then
echo -n "${opensubtitles_username}" > "$secrets_dir/opensubtitles_username.txt"
echo -n "${opensubtitles_password:-}" > "$secrets_dir/opensubtitles_password.txt"
chmod 600 "$secrets_dir/opensubtitles_username.txt" "$secrets_dir/opensubtitles_password.txt"
log_success "OpenSubtitles.com credentials written to $secrets_dir"
else
# Clear stale creds if user opted out on re-run
rm -f "$secrets_dir/opensubtitles_username.txt" "$secrets_dir/opensubtitles_password.txt" 2>/dev/null || true
fi
}
write_qbittorrent_config() {
local qbit_config_dir="$install_directory/config/qbittorrent/qBittorrent"
mkdir -p "$qbit_config_dir"
local pbkdf2_hash
pbkdf2_hash=$(qbit_generate_pbkdf2 "${auth_password:-}")
if [ -n "$pbkdf2_hash" ]; then
if [ "${setup_vpn,,}" = "y" ]; then
cat > "$qbit_config_dir/qBittorrent.conf" << EOF
[BitTorrent]
Session\Interface=tun0
Session\InterfaceName=tun0
[Preferences]
WebUI\Username=${auth_username:-admin}
WebUI\Password_PBKDF2=$pbkdf2_hash
WebUI\Port=8081
EOF
log_success "Pre-configured qBittorrent credentials and tun0 bind"
else
cat > "$qbit_config_dir/qBittorrent.conf" << EOF
[Preferences]
WebUI\Username=${auth_username:-admin}
WebUI\Password_PBKDF2=$pbkdf2_hash
WebUI\Port=8081
EOF
log_success "Pre-configured qBittorrent credentials (bypassing WebAPI restrictions)"
fi
fi
}
validate_vpn_config() {
validate_vpn_secrets "$install_directory" "${setup_vpn,,}" "${VPN_TYPE:-openvpn}"
}
test_vpn_connection() {
# Profile is only needed for media services; Gluetun has no profile.
local profile="${media_service:-jellyfin}"
build_compose_args "$install_directory" "${setup_vpn,,}"
test_vpn_connection_shared run_docker compose "${COMPOSE_ARGS[@]}" --profile "$profile"
}
# Provisional host vars so Gluetun can start before the rest of the wizard.
# Final paths/user are collected after a successful tunnel test and may change.
set_provisional_vpn_test_defaults() {
username="${username:-$USER}"
puid="${puid:-$(id -u)}"
pgid="${pgid:-$(id -g)}"
install_directory="${install_directory:-$APP_DEFAULT_INSTALL_DIR}"
media_directory="${media_directory:-$APP_DEFAULT_MEDIA_DIR}"
if declare -F expand_path >/dev/null 2>&1; then
install_directory=$(expand_path "$install_directory")
media_directory=$(expand_path "$media_directory")
fi
media_service="${media_service:-jellyfin}"
media_service_port="${media_service_port:-8096}"
if [ -z "${tz:-}" ]; then
tz=$(cat /etc/timezone 2>/dev/null || timedatectl show -p Timezone -v 2>/dev/null | tail -1 || echo "UTC")
tz=${tz:-UTC}
fi
export username puid pgid install_directory media_directory media_service media_service_port tz
}
# Gate the rest of setup on a working tunnel. On failure, re-prompt credentials
# and retry instead of abandoning a half-finished install.
ensure_vpn_connection() {
if [ "${setup_vpn,,}" != "y" ]; then
return 0
fi
while true; do
if test_vpn_connection; then
VPN_ALREADY_VERIFIED=1
export VPN_ALREADY_VERIFIED
return 0
fi
echo
echo -e "${RED}VPN test failed — cannot continue setup without a working VPN.${NC}"
echo -e "${YELLOW}Download clients (qBittorrent, SABnzbd) require the VPN to function.${NC}"
echo
echo "Credentials live under $install_directory/secrets/ (not as passwords in .env)."
if [ "${vpn_type:-openvpn}" = "wireguard" ]; then
echo " WireGuard key: $install_directory/secrets/wireguard_private_key.txt"
echo " WG address: WIREGUARD_ADDRESSES in $install_directory/.env"
else
echo " OpenVPN user: $install_directory/secrets/openvpn_user.txt"
echo " OpenVPN pass: $install_directory/secrets/openvpn_password.txt"
fi
echo "Gluetun logs above usually show why the tunnel failed."
echo
local retry=""
read -p "Re-enter VPN credentials and try again? (Y/n) [Default = y]: " retry
retry=${retry:-y}
if [ "${retry,,}" != "y" ]; then
echo
echo "Setup stopped — VPN was not verified, so the rest of the wizard was not run."
echo "When you are ready, re-run setup and enter correct VPN details:"
local raw_base="${APP_REPO_URL:-https://github.com/soulis-1256/assemblrr}"
raw_base="${raw_base/github.com/raw.githubusercontent.com}/main"
echo " Linux / WSL2: bash <(curl -fsSL ${raw_base}/platform/linux/bootstrap.sh)"
echo " Windows: irm ${raw_base}/platform/windows/bootstrap.ps1 | iex"
if [ -f "$install_directory/setup.sh" ]; then
echo " On disk: bash $install_directory/setup.sh"
fi
echo
exit 1
fi
echo
log_info "Re-enter VPN credentials for ${vpn_service:-your provider} (${vpn_type:-openvpn})."
prompt_vpn_credentials
write_vpn_secrets
# WIREGUARD_ADDRESSES and related values live in .env
generate_env_file
done
}
# Right after VPN prompts: materialize a minimal install and prove the tunnel
# before asking about users, paths, media, etc.
early_vpn_gate() {
if [ "${setup_vpn,,}" != "y" ]; then
return 0
fi
echo
log_info "Testing VPN connection before the rest of setup..."
set_provisional_vpn_test_defaults
_ASSEMBLRR_PROVISIONAL_INSTALL="$install_directory"
export _ASSEMBLRR_PROVISIONAL_INSTALL
bootstrap_operator
generate_env_file
write_vpn_secrets
# Bind mounts must exist as the host user before Gluetun starts
prepare_install_dirs "$install_directory" "$puid" "$pgid"
ensure_vpn_connection
log_success "VPN is working — continuing with the rest of setup."
echo
}
write_runtime_config() {
local config_file="$install_directory/.${APP_NAME}-config"
cat > "$config_file" << EOF
INSTALL_DIRECTORY="$install_directory"
MEDIA_DIRECTORY="$media_directory"
MEDIA_SERVICE="$media_service"
VPN_ENABLED="${setup_vpn,,}"
VPN_TYPE="${vpn_type:-openvpn}"
TZ="$tz"
QUALITY_TIER="$(quality_tier)"
QUALITY_SOURCE="$(quality_source)"
SEERR_IS_4K="$(quality_seerr_is_4k)"
SUBTITLE_LANGUAGE="${subtitle_language:-}"
SETUP_MODE="${SETUP_MODE:-manual}"
EOF
chmod 600 "$config_file"
write_install_pointer "$install_directory" "$media_directory"
log_debug "Runtime config written to $config_file"
}
# build_compose_args is defined in lib/compose.sh
# Usage: build_compose_args "$install_directory" "${setup_vpn,,}"
install_cli() {
if [ ! -f "$install_directory/cli.sh" ]; then
log_warning "CLI source missing at $install_directory/cli.sh — skip install"
return 1
fi
write_install_pointer "$install_directory" "${media_directory:-}"
install_user_cli_wrapper
log_debug "CLI installed to $HOME/.local/bin/$APP_CLI_NAME"
}
# Materialize install tree + operator CLI as soon as we know the install path.
# Lets users run: assemblrr uninstall after aborting mid-setup.
# Callers still generate .env / secrets when those steps are ready.
bootstrap_operator() {
local quiet="${1:-}"
mkdir -p "$install_directory"
copy_configuration_files
write_runtime_config
if install_cli; then
if [ "$quiet" != "quiet" ]; then
log_success "Operator CLI ready: $HOME/.local/bin/$APP_CLI_NAME"
log_info "Abort anytime with: $APP_CLI_NAME uninstall"
log_info "If the shell says command not found: $HOME/.local/bin/$APP_CLI_NAME uninstall"
fi
fi
}
set_permissions() {
log_debug "Setting ownership for install and media directories..."
# Install bind-mount tree (idempotent; also fixes root-owned leftovers)
prepare_install_dirs "$install_directory" "$puid" "$pgid"
if [ ! -d "$media_directory" ]; then
mkdir -p "$media_directory" || log_error "Failed to create media directory $media_directory"
fi
ensure_owned "$media_directory" "$puid" "$pgid"
log_debug "Ownership set for $install_directory and $media_directory"
}
main() {
# Prevent running as root
if [[ "$EUID" = 0 ]]; then
log_error "${APP_DISPLAY_NAME} must run without sudo! Please run with regular permissions"
fi
# Setup logging
LOG_FILE="/tmp/${APP_NAME}-setup-$(date '+%Y%m%d-%H%M%S').log"
log_info "Setup log: $LOG_FILE"
log_info "Checking prerequisites..."
check_dependencies
refuse_setup_if_already_installed
offer_leftover_cleanup
# 1) VPN questions, then immediately test the tunnel (fail fast).
# VPN path bootstraps install tree + CLI early so uninstall works if you abort.
configure_vpn
early_vpn_gate
# 2) Only after VPN works (or is disabled): rest of the wizard
get_user_info
get_installation_paths
# No-VPN (or path change after provisional VPN defaults): ensure tree + CLI exist
bootstrap_operator quiet
configure_media_service
configure_quality_profile
configure_subtitle_language
configure_opensubtitles
configure_timezone
configure_auth
# ---- Installation summary / confirmation ----
echo
echo "========================================================"
echo " ${APP_DISPLAY_NAME} — Installation Summary"
echo "========================================================"
echo " User: $username (PUID=$puid, PGID=$pgid)"
echo " Install directory: $install_directory"
echo " Media directory: $media_directory"
if [ "${setup_vpn,,}" = "y" ]; then
echo " VPN: ENABLED ($vpn_service, $vpn_type)"
echo " Port forwarding: ${enable_port_forwarding:-n}"
else
echo " VPN: DISABLED"
fi
echo " Media service: $media_service (port $media_service_port)"
if [ "$QUALITY_TIER" = "any" ]; then
echo " Quality profile: any"
else
echo " Quality profile: $QUALITY_TIER / ${QUALITY_SOURCE:-bluray} (4K=$seerr_is_4k)"
fi
echo " Subtitle language: ${subtitle_language:-en (Bazarr default)}"
if [ "${opensubtitles_enabled:-n}" = "y" ]; then
echo " OpenSubtitles.com: yes (username=${opensubtitles_username})"
else
echo " OpenSubtitles.com: no"
fi
echo " Timezone: $tz"
echo " Service login: $auth_username"
echo "========================================================"
echo
if [ "${SETUP_MODE:-}" != "express" ]; then
while true; do
read -p "Proceed with installation? (Y/n) [Default = y]: " proceed
proceed=${proceed:-"y"}
if [ "${proceed,,}" = "y" ]; then
break
elif [ "${proceed,,}" = "n" ]; then
log_info "Installation cancelled by user"
exit 0
else
log_warning "Please answer y or n."
fi
done
fi
log_info "Configuring ${APP_DISPLAY_NAME} for user \"$username\" in \"$install_directory\"..."
# Refresh install tree for final paths/settings (may differ from provisional VPN test defaults)
copy_configuration_files
generate_env_file
write_vpn_secrets
# Final bind-mount skeleton + ownership before any further compose operations
prepare_install_dirs "$install_directory" "$puid" "$pgid"
# Re-check only if we never verified (VPN disabled skips; early gate already set the flag)
if [ "${setup_vpn,,}" = "y" ] && [ "${VPN_ALREADY_VERIFIED:-}" != "1" ]; then
ensure_vpn_connection
elif [ "${setup_vpn,,}" = "y" ]; then
log_debug "VPN already verified earlier — not re-testing."
fi
write_auth_secrets
write_opensubtitles_secrets
write_qbittorrent_config
write_runtime_config
install_cli
set_permissions
log_success "Install files ready — starting services..."
# Ensure mounts still exist/owned immediately before the full stack comes up
prepare_install_dirs "$install_directory" "$puid" "$pgid"
build_compose_args "$install_directory" "${setup_vpn,,}"
if ! compose_up_stack "$media_service"; then
echo
log_warning "Failed to start ${APP_DISPLAY_NAME} services"
echo
log_info "The operator CLI is already installed. From a WSL or Linux shell:"
log_info " $HOME/.local/bin/$APP_CLI_NAME status"
log_info " $HOME/.local/bin/$APP_CLI_NAME uninstall"
log_info "If '$APP_CLI_NAME' is not found, use that full path (or open a new terminal)."
echo
exit 1
fi
# Wire services (Radarr, Prowlarr, etc.)
local wiring_ok=1
if [ -f "$install_directory/config.sh" ]; then
if ! bash "$install_directory/config.sh"; then
wiring_ok=0
log_warning "Service wiring had critical failures. Containers are still running."
log_warning "Fix the issues above, then re-run setup or: $APP_CLI_NAME upgrade"
fi
fi
# Drop legacy name if an older install left it behind
rm -f "$install_directory/configure.sh" 2>/dev/null || true
echo
# Live dashboard once (status exits non-zero when something is unhealthy —
# that is still a successful print; do not fall back to a second URL list).
if [ -f "$install_directory/lib/services.sh" ]; then
# shellcheck source=/dev/null
source "$install_directory/lib/services.sh"
fi
if [ -f "$install_directory/cli.sh" ]; then
log_info "Service dashboard:"
bash "$install_directory/cli.sh" status || true
log_info "Re-check anytime: $APP_CLI_NAME status"
elif type running_services_location >/dev/null 2>&1; then
running_services_location
log_info "After install, run: $APP_CLI_NAME status"
fi
# Drop legacy cheat-sheet if an older setup left one behind
rm -f "$HOME/assemblrr_services.txt" 2>/dev/null || true
log_info "========================================================"
if [ "$wiring_ok" -eq 1 ]; then
log_success "All done! Enjoy ${APP_DISPLAY_NAME}!"
log_info "Install directory: $install_directory"
log_info "Docs: ${APP_REPO_URL}"
exit 0
fi
log_warning "Setup finished, but service wiring failed."
log_warning "Install directory: $install_directory"
log_warning "Fix issues, then re-run setup or: $APP_CLI_NAME upgrade"
log_info "Docs: ${APP_REPO_URL}"
exit 1
}
main "$@"