mirror of
https://github.com/soulis-1256/assemblrr.git
synced 2026-08-28 09:01:47 +00:00
775 lines
28 KiB
Bash
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 "$@"
|