#!/usr/bin/env bash set -euo pipefail REPO_URL="https://dock-it.dev/Idea-Studios/Maintainarr.git" REPO_BRANCH="${MAINTAINARR_BRANCH:-main}" APP_USER="${MAINTAINARR_USER:-maintainarr}" INSTALL_ROOT="${MAINTAINARR_INSTALL_ROOT:-/opt/maintainarr}" SRC_DIR="$INSTALL_ROOT/src" BIN_DIR="$INSTALL_ROOT/bin" BIN_PATH="$BIN_DIR/maintainarr" DATA_DIR="${MAINTAINARR_DATA_DIR:-/var/lib/maintainarr}" CONFIG_DIR="${MAINTAINARR_CONFIG_DIR:-/etc/maintainarr}" ENV_FILE="$CONFIG_DIR/maintainarr.env" SYSTEMD_UNIT="/etc/systemd/system/maintainarr.service" UPDATE_SCRIPT="/usr/local/bin/maintainarr-update" CRON_FILE="/etc/cron.d/maintainarr-update" GO_VERSION="${MAINTAINARR_GO_VERSION:-1.26.4}" INSTALL_COMMAND="curl -fsSL https://dock-it.dev/Idea-Studios/Maintainarr/raw/branch/main/install.sh | sudo bash" log() { printf '[maintainarr] %s\n' "$*" } fail() { printf '[maintainarr] ERROR: %s\n' "$*" >&2 exit 1 } require_root() { if [[ "${EUID}" -ne 0 ]]; then fail "Run this installer as root, for example: curl -fsSL ... | sudo bash" fi } command_exists() { command -v "$1" >/dev/null 2>&1 } apt_package_installed() { dpkg -s "$1" >/dev/null 2>&1 } version_ge() { [[ "$(printf '%s\n%s\n' "$2" "$1" | sort -V | head -n1)" == "$2" ]] } detect_arch() { case "$(uname -m)" in x86_64|amd64) echo "amd64" ;; aarch64|arm64) echo "arm64" ;; *) fail "Unsupported architecture: $(uname -m)" ;; esac } pkg_install() { if command_exists apt-get; then export DEBIAN_FRONTEND=noninteractive apt-get install -y "$@" return fi if command_exists dnf; then dnf install -y "$@" return fi if command_exists yum; then yum install -y "$@" return fi if command_exists pacman; then pacman -Sy --noconfirm "$@" return fi if command_exists zypper; then zypper --non-interactive install "$@" return fi if command_exists apk; then apk add --no-cache "$@" return fi fail "No supported package manager found" } install_base_packages() { if command_exists apt-get; then local packages=() local package for package in ca-certificates curl git tar cron; do if ! apt_package_installed "$package"; then packages+=("$package") fi done if [[ "${#packages[@]}" -eq 0 ]]; then log "System packages already present: ca-certificates curl git tar cron" return fi log "Installing missing system packages: ${packages[*]}" export DEBIAN_FRONTEND=noninteractive apt-get update apt-get install -y "${packages[@]}" return fi if command_exists dnf; then pkg_install ca-certificates curl git tar cronie return fi if command_exists yum; then pkg_install ca-certificates curl git tar cronie return fi if command_exists pacman; then pkg_install ca-certificates curl git tar cronie return fi if command_exists zypper; then pkg_install ca-certificates curl git tar cron return fi if command_exists apk; then pkg_install ca-certificates curl git tar dcron return fi fail "Unable to install prerequisites for this distribution" } install_go() { local current_version="" if command_exists go; then current_version="$(go version | awk '{print $3}' | sed 's/^go//')" elif [[ -x /usr/local/go/bin/go ]]; then current_version="$(/usr/local/go/bin/go version | awk '{print $3}' | sed 's/^go//')" fi if [[ -n "$current_version" ]] && version_ge "$current_version" "$GO_VERSION"; then log "Go $current_version already satisfies the requirement" return fi local arch arch="$(detect_arch)" local archive="/tmp/go${GO_VERSION}.linux-${arch}.tar.gz" log "Installing Go $GO_VERSION" curl -fsSL "https://go.dev/dl/go${GO_VERSION}.linux-${arch}.tar.gz" -o "$archive" rm -rf /usr/local/go tar -C /usr/local -xzf "$archive" ln -sf /usr/local/go/bin/go /usr/local/bin/go ln -sf /usr/local/go/bin/gofmt /usr/local/bin/gofmt } tty_available() { [[ -r /dev/tty && -w /dev/tty ]] } prompt_yes_no() { local prompt="$1" local default_answer="$2" local reply="" local suffix="[y/N]" if [[ "$default_answer" == "y" ]]; then suffix="[Y/n]" fi if ! tty_available; then [[ "$default_answer" == "y" ]] return fi while true; do printf '%s %s ' "$prompt" "$suffix" > /dev/tty IFS= read -r reply < /dev/tty || true reply="${reply:-$default_answer}" case "$reply" in y|Y|yes|YES) return 0 ;; n|N|no|NO) return 1 ;; *) printf 'Please answer yes or no.\n' > /dev/tty ;; esac done } ensure_user() { if id -u "$APP_USER" >/dev/null 2>&1; then return fi local nologin_shell nologin_shell="$(command -v nologin || true)" nologin_shell="${nologin_shell:-/usr/sbin/nologin}" useradd --system --home "$INSTALL_ROOT" --create-home --shell "$nologin_shell" "$APP_USER" } clone_or_update_repo() { mkdir -p "$INSTALL_ROOT" "$BIN_DIR" if [[ -d "$SRC_DIR/.git" ]]; then log "Updating source in $SRC_DIR" git -C "$SRC_DIR" fetch --tags origin git -C "$SRC_DIR" checkout "$REPO_BRANCH" git -C "$SRC_DIR" pull --ff-only origin "$REPO_BRANCH" return fi if [[ -f "$SRC_DIR/go.mod" && -d "$SRC_DIR/cmd/maintainarr" ]]; then log "Existing Maintainarr source found in $SRC_DIR; skipping download" return fi log "Cloning source into $SRC_DIR" git clone --branch "$REPO_BRANCH" --single-branch "$REPO_URL" "$SRC_DIR" } build_binary() { log "Building Maintainarr" mkdir -p "$BIN_DIR" ( cd "$SRC_DIR" export PATH="/usr/local/go/bin:/usr/local/bin:$PATH" export CGO_ENABLED=0 go build -o "$BIN_PATH" ./cmd/maintainarr ) chmod 755 "$BIN_PATH" } random_hex() { local bytes="$1" od -An -N"$bytes" -tx1 /dev/urandom | tr -d ' \n' } env_value() { local key="$1" awk -F= -v target="$key" '$1 == target { sub(/^[^=]+=*/, "", $0); print $0; exit }' "$ENV_FILE" } extract_port() { local addr="$1" if [[ "$addr" =~ :([0-9]+)$ ]]; then echo "${BASH_REMATCH[1]}" return fi echo "8080" } detect_local_ip() { local ip="" if command_exists hostname; then ip="$(hostname -I 2>/dev/null | awk '{print $1}')" fi if [[ -z "$ip" ]] && command_exists ip; then ip="$(ip route get 1.1.1.1 2>/dev/null | awk '/src/ {for (i=1;i<=NF;i++) if ($i=="src") {print $(i+1); exit}}')" fi if [[ -z "$ip" ]]; then ip="127.0.0.1" fi echo "$ip" } print_summary() { local local_ip="$1" local port="$2" local auto_updates="$3" local start_on_boot="$4" local addr base_url addr="$(env_value "MAINTAINARR_ADDR")" base_url="$(env_value "MAINTAINARR_BASE_URL")" printf '\n' printf 'Maintainarr install complete\n' printf 'Local URL: http://%s:%s\n' "$local_ip" "$port" printf 'Base URL: %s\n' "$base_url" printf '\n' printf 'Settings\n' printf 'MAINTAINARR_ADDR=%s\n' "$addr" printf 'MAINTAINARR_DB_PATH=%s\n' "$(env_value "MAINTAINARR_DB_PATH")" printf 'MAINTAINARR_LOG_ARCHIVE_DIR=%s\n' "$(env_value "MAINTAINARR_LOG_ARCHIVE_DIR")" printf 'MAINTAINARR_ORG_NAME=%s\n' "$(env_value "MAINTAINARR_ORG_NAME")" printf 'MAINTAINARR_BASE_URL=%s\n' "$base_url" printf 'MAINTAINARR_THEME=%s\n' "$(env_value "MAINTAINARR_THEME")" printf 'MAINTAINARR_THEME_MODE=%s\n' "$(env_value "MAINTAINARR_THEME_MODE")" printf 'MAINTAINARR_REFRESH_CRON=%s\n' "$(env_value "MAINTAINARR_REFRESH_CRON")" printf 'AUTO_UPDATES=%s\n' "$auto_updates" printf 'START_ON_BOOT=%s\n' "$start_on_boot" printf '\n' printf 'Paths\n' printf 'Config: %s\n' "$ENV_FILE" printf 'Data: %s\n' "$DATA_DIR" printf 'Source: %s\n' "$SRC_DIR" printf 'Binary: %s\n' "$BIN_PATH" printf '\n' printf 'Re-run installer\n' printf '%s\n' "$INSTALL_COMMAND" } write_env_file() { mkdir -p "$CONFIG_DIR" "$DATA_DIR/log-archives" if [[ -f "$ENV_FILE" ]]; then log "Keeping existing environment file at $ENV_FILE" return fi local session_key local encryption_key session_key="$(random_hex 32)" encryption_key="$(random_hex 16)" cat > "$ENV_FILE" < "$UPDATE_SCRIPT" <&2 exit 1 fi git -C "\$SRC_DIR" fetch --tags origin git -C "\$SRC_DIR" checkout "\$REPO_BRANCH" git -C "\$SRC_DIR" pull --ff-only origin "\$REPO_BRANCH" ( cd "\$SRC_DIR" export CGO_ENABLED=0 go build -o "\$BIN_PATH" ./cmd/maintainarr ) chown "\$APP_USER:\$APP_USER" "\$BIN_PATH" if command -v systemctl >/dev/null 2>&1 && systemctl is-enabled --quiet maintainarr 2>/dev/null; then systemctl restart maintainarr fi EOF chmod 755 "$UPDATE_SCRIPT" } write_systemd_service() { cat > "$SYSTEMD_UNIT" < "$CRON_FILE" <> /var/log/maintainarr-update.log 2>&1 EOF chmod 644 "$CRON_FILE" if command_exists systemctl; then if systemctl list-unit-files | grep -q '^cron\.service'; then systemctl enable --now cron >/dev/null 2>&1 || true elif systemctl list-unit-files | grep -q '^crond\.service'; then systemctl enable --now crond >/dev/null 2>&1 || true fi fi log "Installed daily update job in $CRON_FILE" } main() { local auto_updates="disabled" local start_on_boot="disabled" local addr port local_ip require_root install_base_packages install_go ensure_user clone_or_update_repo write_env_file build_binary write_update_script mkdir -p "$DATA_DIR" chown -R "$APP_USER:$APP_USER" "$INSTALL_ROOT" "$DATA_DIR" chown root:"$APP_USER" "$ENV_FILE" if prompt_yes_no "Enable automatic daily updates with cron?" "n"; then configure_auto_updates auto_updates="enabled" else rm -f "$CRON_FILE" log "Skipped cron auto-update setup" fi if prompt_yes_no "Start Maintainarr at boot with systemd?" "y"; then enable_start_on_boot start_on_boot="enabled" else log "Skipped boot-start setup" log "Run it manually with: $BIN_PATH" fi addr="$(env_value "MAINTAINARR_ADDR")" port="$(extract_port "$addr")" local_ip="$(detect_local_ip)" print_summary "$local_ip" "$port" "$auto_updates" "$start_on_boot" } main "$@"