diff --git a/ct/flaresolverr.sh b/ct/flaresolverr.sh new file mode 100644 index 00000000..de4e5fbd --- /dev/null +++ b/ct/flaresolverr.sh @@ -0,0 +1,59 @@ +#!/usr/bin/env bash +source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVED/main/misc/build.func) +# Copyright (c) 2021-2026 tteck +# Author: tteck (tteckster) | Co-Author: remz1337 +# License: MIT | https://github.com/community-scripts/ProxmoxVED/raw/main/LICENSE +# Source: https://github.com/FlareSolverr/FlareSolverr + +APP="FlareSolverr" +var_tags="${var_tags:-proxy}" +var_cpu="${var_cpu:-2}" +var_ram="${var_ram:-2048}" +var_disk="${var_disk:-4}" +var_os="${var_os:-debian}" +var_version="${var_version:-13}" +var_unprivileged="${var_unprivileged:-1}" + +header_info "$APP" +variables +color +catch_errors + +function update_script() { + header_info + check_container_storage + check_container_resources + + if [[ ! -f /etc/systemd/system/flaresolverr.service ]]; then + msg_error "No ${APP} Installation Found!" + exit + fi + if [[ $(grep -E '^VERSION_ID=' /etc/os-release) == *"12"* ]]; then + msg_error "Wrong Debian version detected!" + msg_error "You must upgrade your LXC to Debian Trixie before updating." + exit + fi + if check_for_gh_release "flaresolverr" "FlareSolverr/FlareSolverr"; then + msg_info "Stopping service" + systemctl stop flaresolverr + msg_ok "Stopped service" + + rm -rf /opt/flaresolverr + fetch_and_deploy_gh_release "flaresolverr" "FlareSolverr/FlareSolverr" "prebuild" "latest" "/opt/flaresolverr" "flaresolverr_linux_x64.tar.gz" + + msg_info "Starting service" + systemctl start flaresolverr + msg_ok "Started service" + msg_ok "Updated successfully!" + fi + exit +} + +start +build_container +description + +msg_ok "Completed successfully!\n" +echo -e "${CREATING}${GN}${APP} setup has been successfully initialized!${CL}" +echo -e "${INFO}${YW} Access it using the following URL:${CL}" +echo -e "${TAB}${GATEWAY}${BGN}http://${IP}:8191${CL}" diff --git a/ct/gluetun.sh b/ct/gluetun.sh new file mode 100644 index 00000000..361e48b3 --- /dev/null +++ b/ct/gluetun.sh @@ -0,0 +1,61 @@ +#!/usr/bin/env bash +source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVED/main/misc/build.func) +# Copyright (c) 2021-2026 community-scripts ORG +# Author: MickLesk (CanbiZ) +# License: MIT | https://github.com/community-scripts/ProxmoxVED/raw/main/LICENSE +# Source: https://github.com/qdm12/gluetun + +APP="Gluetun" +var_tags="${var_tags:-vpn;wireguard;openvpn}" +var_cpu="${var_cpu:-2}" +var_ram="${var_ram:-2048}" +var_disk="${var_disk:-8}" +var_os="${var_os:-debian}" +var_version="${var_version:-13}" +var_unprivileged="${var_unprivileged:-1}" +var_tun="${var_tun:-yes}" + +header_info "$APP" +variables +color +catch_errors + +function update_script() { + header_info + check_container_storage + check_container_resources + + if [[ ! -f /usr/local/bin/gluetun ]]; then + msg_error "No ${APP} Installation Found!" + exit + fi + + if check_for_gh_release "gluetun" "qdm12/gluetun"; then + msg_info "Stopping Service" + systemctl stop gluetun + msg_ok "Stopped Service" + + CLEAN_INSTALL=1 fetch_and_deploy_gh_release "gluetun" "qdm12/gluetun" "tarball" + + msg_info "Building Gluetun" + cd /opt/gluetun + $STD go mod download + CGO_ENABLED=0 $STD go build -trimpath -ldflags="-s -w" -o /usr/local/bin/gluetun ./cmd/gluetun/ + msg_ok "Built Gluetun" + + msg_info "Starting Service" + systemctl start gluetun + msg_ok "Started Service" + msg_ok "Updated successfully!" + fi + exit +} + +start +build_container +description + +msg_ok "Completed Successfully!\n" +echo -e "${CREATING}${GN}${APP} setup has been successfully initialized!${CL}" +echo -e "${INFO}${YW} Access it using the following URL:${CL}" +echo -e "${TAB}${GATEWAY}${BGN}http://${IP}:8000${CL}" diff --git a/ct/lidarr.sh b/ct/lidarr.sh new file mode 100644 index 00000000..0ae5996e --- /dev/null +++ b/ct/lidarr.sh @@ -0,0 +1,55 @@ +#!/usr/bin/env bash +source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVED/main/misc/build.func) +# Copyright (c) 2021-2026 tteck +# Author: tteck (tteckster) +# License: MIT | https://github.com/community-scripts/ProxmoxVED/raw/main/LICENSE +# Source: https://lidarr.audio/ | Github: https://github.com/Lidarr/Lidarr + +APP="Lidarr" +var_tags="${var_tags:-arr;torrent;usenet}" +var_cpu="${var_cpu:-2}" +var_ram="${var_ram:-1024}" +var_disk="${var_disk:-4}" +var_os="${var_os:-debian}" +var_version="${var_version:-13}" +var_unprivileged="${var_unprivileged:-1}" + +header_info "$APP" +variables +color +catch_errors + +function update_script() { + header_info + check_container_storage + check_container_resources + + if [[ ! -d /var/lib/lidarr/ ]]; then + msg_error "No ${APP} Installation Found!" + exit + fi + + if check_for_gh_release "lidarr" "Lidarr/Lidarr"; then + msg_info "Stopping service" + systemctl stop lidarr + msg_ok "Service stopped" + + fetch_and_deploy_gh_release "lidarr" "Lidarr/Lidarr" "prebuild" "latest" "/opt/Lidarr" "Lidarr.master*linux-core-x64.tar.gz" + chmod 775 /opt/Lidarr + + msg_info "Starting service" + systemctl start lidarr + msg_ok "Service started" + msg_ok "Updated successfully!" + fi + exit +} + +start +build_container +description + +msg_ok "Completed successfully!\n" +echo -e "${CREATING}${GN}${APP} setup has been successfully initialized!${CL}" +echo -e "${INFO}${YW} Access it using the following URL:${CL}" +echo -e "${TAB}${GATEWAY}${BGN}http://${IP}:8686${CL}" diff --git a/ct/prowlarr.sh b/ct/prowlarr.sh new file mode 100644 index 00000000..249a3356 --- /dev/null +++ b/ct/prowlarr.sh @@ -0,0 +1,54 @@ +#!/usr/bin/env bash +source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVED/main/misc/build.func) +# Copyright (c) 2021-2026 tteck +# Author: tteck (tteckster) +# License: MIT | https://github.com/community-scripts/ProxmoxVED/raw/main/LICENSE +# Source: https://prowlarr.com/ | Github: https://github.com/Prowlarr/Prowlarr + +APP="Prowlarr" +var_tags="${var_tags:-arr}" +var_cpu="${var_cpu:-2}" +var_ram="${var_ram:-1024}" +var_disk="${var_disk:-4}" +var_os="${var_os:-debian}" +var_version="${var_version:-13}" +var_unprivileged="${var_unprivileged:-1}" + +header_info "$APP" +variables +color +catch_errors + +function update_script() { + header_info + check_container_storage + check_container_resources + if [[ ! -d /var/lib/prowlarr/ ]]; then + msg_error "No ${APP} Installation Found!" + exit + fi + if check_for_gh_release "prowlarr" "Prowlarr/Prowlarr"; then + msg_info "Stopping Service" + systemctl stop prowlarr + msg_ok "Stopped Service" + + rm -rf /opt/Prowlarr + fetch_and_deploy_gh_release "prowlarr" "Prowlarr/Prowlarr" "prebuild" "latest" "/opt/Prowlarr" "Prowlarr.master*linux-core-x64.tar.gz" + chmod 775 /opt/Prowlarr + + msg_info "Starting Service" + systemctl start prowlarr + msg_ok "Started Service" + msg_ok "Updated successfully!" + fi + exit +} + +start +build_container +description + +msg_ok "Completed successfully!\n" +echo -e "${CREATING}${GN}${APP} setup has been successfully initialized!${CL}" +echo -e "${INFO}${YW} Access it using the following URL:${CL}" +echo -e "${TAB}${GATEWAY}${BGN}http://${IP}:9696${CL}" diff --git a/ct/qbittorrent.sh b/ct/qbittorrent.sh new file mode 100644 index 00000000..63245083 --- /dev/null +++ b/ct/qbittorrent.sh @@ -0,0 +1,58 @@ +#!/usr/bin/env bash +source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVED/main/misc/build.func) +# Copyright (c) 2021-2026 community-scripts ORG +# Author: tteck (tteckster) | Co-Author: Slaviša Arežina (tremor021) +# License: MIT | https://github.com/community-scripts/ProxmoxVED/raw/main/LICENSE +# Source: https://www.qbittorrent.org/ | Github: https://github.com/qbittorrent/qBittorrent + +APP="qBittorrent" +var_tags="${var_tags:-torrent}" +var_cpu="${var_cpu:-2}" +var_ram="${var_ram:-2048}" +var_disk="${var_disk:-8}" +var_os="${var_os:-debian}" +var_version="${var_version:-13}" +var_unprivileged="${var_unprivileged:-1}" + +header_info "$APP" +variables +color +catch_errors + +function update_script() { + header_info + check_container_storage + check_container_resources + if [[ ! -f /etc/systemd/system/qbittorrent-nox.service ]]; then + msg_error "No ${APP} Installation Found!" + exit + fi + if [[ ! -f ~/.qbittorrent ]]; then + msg_error "Please create new qBittorrent LXC. Updating from v4.x to v5.x is not supported!" + exit + fi + if check_for_gh_release "qbittorrent" "userdocs/qbittorrent-nox-static"; then + msg_info "Stopping Service" + systemctl stop qbittorrent-nox + msg_ok "Stopped Service" + + rm -f /opt/qbittorrent/qbittorrent-nox + fetch_and_deploy_gh_release "qbittorrent" "userdocs/qbittorrent-nox-static" "singlefile" "latest" "/opt/qbittorrent" "x86_64-qbittorrent-nox" + mv /opt/qbittorrent/qbittorrent /opt/qbittorrent/qbittorrent-nox + + msg_info "Starting Service" + systemctl start qbittorrent-nox + msg_ok "Started Service" + msg_ok "Updated successfully!" + fi + exit +} + +start +build_container +description + +msg_ok "Completed successfully!\n" +echo -e "${CREATING}${GN}${APP} setup has been successfully initialized!${CL}" +echo -e "${INFO}${YW} Access it using the following URL:${CL}" +echo -e "${TAB}${GATEWAY}${BGN}http://${IP}:8090${CL}" diff --git a/ct/radarr.sh b/ct/radarr.sh new file mode 100644 index 00000000..dc575038 --- /dev/null +++ b/ct/radarr.sh @@ -0,0 +1,56 @@ +#!/usr/bin/env bash +source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVED/main/misc/build.func) +# Copyright (c) 2021-2026 tteck +# Author: tteck (tteckster) +# License: MIT | https://github.com/community-scripts/ProxmoxVED/raw/main/LICENSE +# Source: https://radarr.video/ | Github: https://github.com/Radarr/Radarr + +APP="Radarr" +var_tags="${var_tags:-arr}" +var_cpu="${var_cpu:-2}" +var_ram="${var_ram:-1024}" +var_disk="${var_disk:-4}" +var_os="${var_os:-debian}" +var_version="${var_version:-13}" +var_unprivileged="${var_unprivileged:-1}" + +header_info "$APP" +variables +color +catch_errors + +function update_script() { + header_info + check_container_storage + check_container_resources + + if [[ ! -d /var/lib/radarr/ ]]; then + msg_error "No ${APP} Installation Found!" + exit + fi + + if check_for_gh_release "Radarr" "Radarr/Radarr"; then + msg_info "Stopping Service" + systemctl stop radarr + msg_ok "Stopped Service" + + rm -rf /opt/Radarr + fetch_and_deploy_gh_release "Radarr" "Radarr/Radarr" "prebuild" "latest" "/opt/Radarr" "Radarr.master*linux-core-x64.tar.gz" + chmod 775 /opt/Radarr + + msg_info "Starting Service" + systemctl start radarr + msg_ok "Started Service" + msg_ok "Updated successfully!" + fi + exit +} + +start +build_container +description + +msg_ok "Completed successfully!\n" +echo -e "${CREATING}${GN}${APP} setup has been successfully initialized!${CL}" +echo -e "${INFO}${YW} Access it using the following URL:${CL}" +echo -e "${TAB}${GATEWAY}${BGN}http://${IP}:7878${CL}" diff --git a/ct/rdtclient.sh b/ct/rdtclient.sh new file mode 100644 index 00000000..fed85ae3 --- /dev/null +++ b/ct/rdtclient.sh @@ -0,0 +1,63 @@ +#!/usr/bin/env bash +source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVED/main/misc/build.func) +# Copyright (c) 2021-2026 tteck +# Author: tteck (tteckster) +# License: MIT | https://github.com/community-scripts/ProxmoxVED/raw/main/LICENSE +# Source: https://github.com/rogerfar/rdt-client + +APP="RDTClient" +var_tags="${var_tags:-torrent}" +var_cpu="${var_cpu:-1}" +var_ram="${var_ram:-1024}" +var_disk="${var_disk:-4}" +var_os="${var_os:-debian}" +var_version="${var_version:-13}" +var_unprivileged="${var_unprivileged:-1}" + +header_info "$APP" +variables +color +catch_errors + +function update_script() { + header_info + check_container_storage + check_container_resources + if [[ ! -d /opt/rdtc/ ]]; then + msg_error "No ${APP} Installation Found!" + exit + fi + if check_for_gh_release "rdt-client" "rogerfar/rdt-client"; then + msg_info "Stopping Service" + systemctl stop rdtc + msg_ok "Stopped Service" + + msg_info "Creating backup" + mkdir -p /opt/rdtc-backup + cp -R /opt/rdtc/appsettings.json /opt/rdtc-backup/ + msg_ok "Backup created" + + fetch_and_deploy_gh_release "rdt-client" "rogerfar/rdt-client" "prebuild" "latest" "/opt/rdtc" "RealDebridClient.zip" + cp -R /opt/rdtc-backup/appsettings.json /opt/rdtc/ + if dpkg-query -W aspnetcore-runtime-9.0 >/dev/null 2>&1; then + $STD apt remove --purge -y aspnetcore-runtime-9.0 + ensure_dependencies aspnetcore-runtime-10.0 + fi + rm -rf /opt/rdtc-backup + + msg_info "Starting Service" + systemctl start rdtc + msg_ok "Started Service" + msg_ok "Updated successfully!" + fi + exit +} + +start +build_container +description + +msg_ok "Completed successfully!\n" +echo -e "${CREATING}${GN}${APP} setup has been successfully initialized!${CL}" +echo -e "${INFO}${YW} Access it using the following URL:${CL}" +echo -e "${TAB}${GATEWAY}${BGN}http://${IP}:6500${CL}" diff --git a/ct/sabnzbd.sh b/ct/sabnzbd.sh new file mode 100644 index 00000000..db510c0b --- /dev/null +++ b/ct/sabnzbd.sh @@ -0,0 +1,70 @@ +#!/usr/bin/env bash +source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVED/main/misc/build.func) +# Copyright (c) 2021-2026 tteck +# Author: tteck (tteckster) | Co-Author: MickLesk (CanbiZ) +# License: MIT | https://github.com/community-scripts/ProxmoxVED/raw/main/LICENSE +# Source: https://sabnzbd.org/ | Github: https://github.com/sabnzbd/sabnzbd + +APP="SABnzbd" +var_tags="${var_tags:-downloader}" +var_cpu="${var_cpu:-2}" +var_ram="${var_ram:-2048}" +var_disk="${var_disk:-5}" +var_os="${var_os:-debian}" +var_version="${var_version:-13}" +var_unprivileged="${var_unprivileged:-1}" + +header_info "$APP" +variables +color +catch_errors + +function update_script() { + header_info + check_container_storage + check_container_resources + + if par2 --version | grep -q "par2cmdline-turbo"; then + fetch_and_deploy_gh_release "par2cmdline-turbo" "animetosho/par2cmdline-turbo" "prebuild" "latest" "/usr/bin/" "*-linux-amd64.zip" + fi + + if [[ ! -d /opt/sabnzbd ]]; then + msg_error "No ${APP} Installation Found!" + exit + fi + if check_for_gh_release "sabnzbd-org" "sabnzbd/sabnzbd"; then + PYTHON_VERSION="3.13" setup_uv + systemctl stop sabnzbd + cp -r /opt/sabnzbd /opt/sabnzbd_backup_$(date +%s) + fetch_and_deploy_gh_release "sabnzbd-org" "sabnzbd/sabnzbd" "prebuild" "latest" "/opt/sabnzbd" "SABnzbd-*-src.tar.gz" + + # Always ensure venv exists + if [[ ! -d /opt/sabnzbd/venv ]]; then + msg_info "Migrating SABnzbd to uv virtual environment" + $STD uv venv --clear /opt/sabnzbd/venv + msg_ok "Created uv venv at /opt/sabnzbd/venv" + fi + + # Always check and fix service file if needed + if [[ -f /etc/systemd/system/sabnzbd.service ]] && grep -q "ExecStart=python3 SABnzbd.py" /etc/systemd/system/sabnzbd.service; then + sed -i "s|ExecStart=python3 SABnzbd.py|ExecStart=/opt/sabnzbd/venv/bin/python SABnzbd.py|" /etc/systemd/system/sabnzbd.service + systemctl daemon-reload + msg_ok "Updated SABnzbd service to use uv venv" + fi + $STD uv pip install --upgrade pip --python=/opt/sabnzbd/venv/bin/python + $STD uv pip install -r /opt/sabnzbd/requirements.txt --python=/opt/sabnzbd/venv/bin/python + + systemctl start sabnzbd + msg_ok "Updated successfully!" + fi + exit +} + +start +build_container +description + +msg_ok "Completed successfully!\n" +echo -e "${CREATING}${GN}${APP} setup has been successfully initialized!${CL}" +echo -e "${INFO}${YW} Access it using the following URL:${CL}" +echo -e "${TAB}${GATEWAY}${BGN}http://${IP}:7777${CL}" diff --git a/ct/seerr.sh b/ct/seerr.sh new file mode 100644 index 00000000..6c7b2abc --- /dev/null +++ b/ct/seerr.sh @@ -0,0 +1,167 @@ +#!/usr/bin/env bash +source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVED/main/misc/build.func) +# Copyright (c) 2021-2026 community-scripts ORG +# Author: CrazyWolf13 +# License: MIT | https://github.com/community-scripts/ProxmoxVED/raw/main/LICENSE +# Source: https://docs.seerr.dev/ | Github: https://github.com/seerr-team/seerr + +APP="Seerr" +var_tags="${var_tags:-media}" +var_cpu="${var_cpu:-4}" +var_ram="${var_ram:-4096}" +var_disk="${var_disk:-12}" +var_os="${var_os:-debian}" +var_version="${var_version:-13}" +var_unprivileged="${var_unprivileged:-1}" + +header_info "$APP" +variables +color +catch_errors + +function update_script() { + header_info + check_container_storage + check_container_resources + + if [[ ! -d /opt/seerr && ! -d /opt/jellyseerr && ! -d /opt/overseerr ]]; then + msg_error "No ${APP} Installation Found!" + exit + fi + + # Start Migration from Jellyseerr + if [[ -f /etc/systemd/system/jellyseerr.service ]]; then + msg_info "Stopping Jellyseerr" + $STD systemctl stop jellyseerr || true + $STD systemctl disable jellyseerr || true + [ -f /etc/systemd/system/jellyseerr.service ] && rm -f /etc/systemd/system/jellyseerr.service + msg_ok "Stopped Jellyseerr" + + msg_info "Creating Backup (Patience)" + tar -czf /opt/jellyseerr_backup_$(date +%Y%m%d_%H%M%S).tar.gz -C /opt jellyseerr + msg_ok "Created Backup" + + msg_info "Migrating Jellyseerr to seerr" + [ -d /opt/jellyseerr ] && mv /opt/jellyseerr /opt/seerr + [ -d /etc/jellyseerr ] && mv /etc/jellyseerr /etc/seerr + [ -f /etc/seerr/jellyseerr.conf ] && mv /etc/seerr/jellyseerr.conf /etc/seerr/seerr.conf + cat </etc/systemd/system/seerr.service +[Unit] +Description=Seerr Service +Wants=network-online.target +After=network-online.target + +[Service] +EnvironmentFile=/etc/seerr/seerr.conf +Environment=NODE_ENV=production +Type=exec +Restart=on-failure +WorkingDirectory=/opt/seerr +ExecStart=/usr/bin/node dist/index.js + +[Install] +WantedBy=multi-user.target +EOF + systemctl daemon-reload + systemctl enable -q --now seerr + msg_ok "Migrated Jellyserr to Seerr" + fi + # END Jellyseerr Migration + + # Start Migration from Overseerr + if [[ -f /etc/systemd/system/overseerr.service ]]; then + msg_info "Stopping Overseerr" + $STD systemctl stop overseerr || true + $STD systemctl disable overseerr || true + [ -f /etc/systemd/system/overseerr.service ] && rm -f /etc/systemd/system/overseerr.service + msg_ok "Stopped Overseerr" + + msg_info "Creating Backup (Patience)" + tar -czf /opt/overseerr_backup_$(date +%Y%m%d_%H%M%S).tar.gz -C /opt overseerr + msg_ok "Created Backup" + + msg_info "Migrating Overseerr to seerr" + [ -d /opt/overseerr ] && mv /opt/overseerr /opt/seerr + mkdir -p /etc/seerr + cat </etc/seerr/seerr.conf +## Seerr's default port is 5055, if you want to use both, change this. +## specify on which port to listen +PORT=5055 + +## specify on which interface to listen, by default seerr listens on all interfaces +#HOST=127.0.0.1 + +## Uncomment if you want to force Node.js to resolve IPv4 before IPv6 (advanced users only) +# FORCE_IPV4_FIRST=true +EOF + cat </etc/systemd/system/seerr.service +[Unit] +Description=Seerr Service +Wants=network-online.target +After=network-online.target + +[Service] +EnvironmentFile=/etc/seerr/seerr.conf +Environment=NODE_ENV=production +Type=exec +Restart=on-failure +WorkingDirectory=/opt/seerr +ExecStart=/usr/bin/node dist/index.js + +[Install] +WantedBy=multi-user.target +EOF + systemctl daemon-reload + systemctl enable -q --now seerr + msg_ok "Migrated Overseerr to Seerr" + fi + # END Overseerr Migration + + if check_for_gh_release "seerr" "seerr-team/seerr"; then + msg_info "Stopping Service" + systemctl stop seerr + msg_ok "Stopped Service" + + msg_info "Creating Backup" + cp -a /opt/seerr/config /opt/seerr_backup + msg_ok "Created Backup" + + CLEAN_INSTALL=1 fetch_and_deploy_gh_release "seerr" "seerr-team/seerr" "tarball" + + ensure_dependencies build-essential python3-setuptools + + msg_info "Updating PNPM Version" + pnpm_desired=$(grep -Po '"pnpm":\s*"\K[^"]+' /opt/seerr/package.json) + NODE_VERSION="22" NODE_MODULE="pnpm@$pnpm_desired" setup_nodejs + msg_ok "Updated PNPM Version" + + msg_info "Updating Seerr" + cd /opt/seerr + rm -rf dist .next node_modules + export CYPRESS_INSTALL_BINARY=0 + $STD pnpm install --frozen-lockfile + export NODE_OPTIONS="--max-old-space-size=3072" + $STD pnpm build + msg_ok "Updated Seerr" + + msg_info "Restoring Backup" + rm -rf /opt/seerr/config + mv /opt/seerr_backup /opt/seerr/config + msg_ok "Restored Backup" + + msg_info "Starting Service" + systemctl start seerr + msg_ok "Started Service" + msg_ok "Updated successfully!" + fi + exit +} + +start +build_container +description + +msg_ok "Completed successfully!\n" +echo -e "${CREATING}${GN}${APP} setup has been successfully initialized!${CL}" +echo -e "${INFO}${YW} Access it using the following URL:${CL}" +echo -e "${TAB}${GATEWAY}${BGN}http://${IP}:5055${CL}" diff --git a/ct/slskd.sh b/ct/slskd.sh new file mode 100644 index 00000000..1b1b983a --- /dev/null +++ b/ct/slskd.sh @@ -0,0 +1,100 @@ +#!/usr/bin/env bash +source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVED/main/misc/build.func) +# Copyright (c) 2021-2026 community-scripts ORG +# Author: vhsdream +# License: MIT | https://github.com/community-scripts/ProxmoxVED/raw/main/LICENSE +# Source: https://github.com/slskd/slskd/, https://github.com/mrusse/soularr + +APP="slskd" +var_tags="${var_tags:-arr;p2p}" +var_cpu="${var_cpu:-1}" +var_ram="${var_ram:-512}" +var_disk="${var_disk:-4}" +var_os="${var_os:-debian}" +var_version="${var_version:-13}" +var_unprivileged="${var_unprivileged:-1}" + +header_info "$APP" +variables +color +catch_errors + +function update_script() { + header_info + check_container_storage + check_container_resources + + if [[ ! -d /opt/slskd ]]; then + msg_error "No Slskd Installation Found!" + exit + fi + + if check_for_gh_release "Slskd" "slskd/slskd"; then + msg_info "Stopping Service(s)" + systemctl stop slskd + [[ -f /etc/systemd/system/soularr.service ]] && systemctl stop soularr.timer soularr.service + msg_ok "Stopped Service(s)" + + msg_info "Backing up config" + cp /opt/slskd/config/slskd.yml /opt/slskd.yml.bak + msg_ok "Backed up config" + + CLEAN_INSTALL=1 fetch_and_deploy_gh_release "Slskd" "slskd/slskd" "prebuild" "latest" "/opt/slskd" "slskd-*-linux-x64.zip" + + msg_info "Restoring config" + mv /opt/slskd.yml.bak /opt/slskd/config/slskd.yml + + # Migrate 0.25.0 breaking config key renames + sed -i 's/^global:/transfers:/' /opt/slskd/config/slskd.yml + sed -i 's/^integration:/integrations:/' /opt/slskd/config/slskd.yml + msg_ok "Restored config" + + msg_info "Starting Service(s)" + systemctl start slskd + [[ -f /etc/systemd/system/soularr.service ]] && systemctl start soularr.timer + msg_ok "Started Service(s)" + msg_ok "Updated Slskd successfully!" + fi + [[ -d /opt/soularr ]] && if check_for_gh_release "Soularr" "mrusse/soularr"; then + if systemctl is-active soularr.timer >/dev/null; then + msg_info "Stopping Timer and Service" + systemctl stop soularr.timer soularr.service + msg_ok "Stopped Timer and Service" + fi + + msg_info "Backing up Soularr config" + cp /opt/soularr/config.ini /opt/soularr_config.ini.bak + cp /opt/soularr/run.sh /opt/soularr_run.sh.bak + msg_ok "Backed up Soularr config" + + PYTHON_VERSION="3.11" setup_uv + CLEAN_INSTALL=1 fetch_and_deploy_gh_release "Soularr" "mrusse/soularr" "tarball" "latest" "/opt/soularr" + msg_info "Updating Soularr" + cd /opt/soularr + $STD uv venv -c venv + $STD source venv/bin/activate + $STD uv pip install -r requirements.txt + deactivate + msg_ok "Updated Soularr" + + msg_info "Restoring Soularr config" + mv /opt/soularr_config.ini.bak /opt/soularr/config.ini + mv /opt/soularr_run.sh.bak /opt/soularr/run.sh + msg_ok "Restored Soularr config" + + msg_info "Starting Soularr Timer" + systemctl restart soularr.timer + msg_ok "Started Soularr Timer" + msg_ok "Updated Soularr successfully!" + fi + exit +} + +start +build_container +description + +msg_ok "Completed successfully!\n" +echo -e "${CREATING}${GN}${APP} setup has been successfully initialized!${CL}" +echo -e "${INFO}${YW} Access it using the following URL:${CL}" +echo -e "${TAB}${GATEWAY}${BGN}http://${IP}:5030${CL}" diff --git a/ct/sonarr.sh b/ct/sonarr.sh new file mode 100644 index 00000000..0688e504 --- /dev/null +++ b/ct/sonarr.sh @@ -0,0 +1,54 @@ +#!/usr/bin/env bash +source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVED/main/misc/build.func) +# Copyright (c) 2021-2026 tteck +# Author: tteck (tteckster) +# License: MIT | https://github.com/community-scripts/ProxmoxVED/raw/main/LICENSE +# Source: https://sonarr.tv/ | Github: https://github.com/Sonarr/Sonarr + +APP="Sonarr" +var_tags="${var_tags:-arr}" +var_cpu="${var_cpu:-2}" +var_ram="${var_ram:-1024}" +var_disk="${var_disk:-4}" +var_os="${var_os:-debian}" +var_version="${var_version:-13}" +var_unprivileged="${var_unprivileged:-1}" + +header_info "$APP" +variables +color +catch_errors + +function update_script() { + header_info + check_container_storage + check_container_resources + + if [[ ! -d /var/lib/sonarr/ ]]; then + msg_error "No ${APP} Installation Found!" + exit + fi + + if check_for_gh_release "Sonarr" "Sonarr/Sonarr"; then + msg_info "Stopping Service" + systemctl stop sonarr + msg_ok "Stopped Service" + + CLEAN_INSTALL=1 fetch_and_deploy_gh_release "Sonarr" "Sonarr/Sonarr" "prebuild" "latest" "/opt/Sonarr" "Sonarr.main.*.linux-x64.tar.gz" + + msg_info "Starting Service" + systemctl start sonarr + msg_ok "Started Service" + msg_ok "Updated successfully!" + fi + exit +} + +start +build_container +description + +msg_ok "Completed successfully!\n" +echo -e "${CREATING}${GN}${APP} setup has been successfully initialized!${CL}" +echo -e "${INFO}${YW} Access it using the following URL:${CL}" +echo -e "${TAB}${GATEWAY}${BGN}http://${IP}:8989${CL}" diff --git a/install/flaresolverr-install.sh b/install/flaresolverr-install.sh new file mode 100644 index 00000000..e260b515 --- /dev/null +++ b/install/flaresolverr-install.sh @@ -0,0 +1,62 @@ +#!/usr/bin/env bash + +# Copyright (c) 2021-2026 tteck +# Author: tteck (tteckster) +# Co-Author: remz1337 +# License: MIT | https://github.com/community-scripts/ProxmoxVED/raw/main/LICENSE +# Source: https://github.com/FlareSolverr/FlareSolverr + +source /dev/stdin <<<"$FUNCTIONS_FILE_PATH" +color +verb_ip6 +catch_errors +setting_up_container +network_check +update_os + +msg_info "Installing Dependencies" +$STD apt-get install -y \ + apt-transport-https \ + xvfb +msg_ok "Installed Dependencies" + +msg_info "Installing Chrome" +setup_deb822_repo \ + "google-chrome" \ + "https://dl.google.com/linux/linux_signing_key.pub" \ + "https://dl.google.com/linux/chrome/deb/" \ + "stable" +$STD apt update +$STD apt install -y google-chrome-stable +# remove google-chrome.list added by google-chrome-stable +if [ -f /etc/apt/sources.list.d/google-chrome.list ]; then + rm /etc/apt/sources.list.d/google-chrome.list +fi +msg_ok "Installed Chrome" + +fetch_and_deploy_gh_release "flaresolverr" "FlareSolverr/FlareSolverr" "prebuild" "latest" "/opt/flaresolverr" "flaresolverr_linux_x64.tar.gz" + +msg_info "Creating Service" +cat </etc/systemd/system/flaresolverr.service +[Unit] +Description=FlareSolverr +After=network.target +[Service] +SyslogIdentifier=flaresolverr +Restart=always +RestartSec=5 +Type=simple +Environment="LOG_LEVEL=info" +Environment="CAPTCHA_SOLVER=none" +WorkingDirectory=/opt/flaresolverr +ExecStart=/opt/flaresolverr/flaresolverr +TimeoutStopSec=30 +[Install] +WantedBy=multi-user.target +EOF +systemctl enable -q --now flaresolverr +msg_ok "Created Service" + +motd_ssh +customize +cleanup_lxc diff --git a/install/gluetun-install.sh b/install/gluetun-install.sh new file mode 100644 index 00000000..a249bd3b --- /dev/null +++ b/install/gluetun-install.sh @@ -0,0 +1,96 @@ +#!/usr/bin/env bash + +# Copyright (c) 2021-2026 community-scripts ORG +# Author: MickLesk (CanbiZ) +# License: MIT | https://github.com/community-scripts/ProxmoxVED/raw/main/LICENSE +# Source: https://github.com/qdm12/gluetun + +source /dev/stdin <<<"$FUNCTIONS_FILE_PATH" +color +verb_ip6 +catch_errors +setting_up_container +network_check +update_os + +msg_info "Installing Dependencies" +$STD apt install -y \ + openvpn \ + wireguard-tools \ + iptables +msg_ok "Installed Dependencies" + +msg_info "Configuring iptables" +$STD update-alternatives --set iptables /usr/sbin/iptables-legacy +$STD update-alternatives --set ip6tables /usr/sbin/ip6tables-legacy +ln -sf /usr/sbin/openvpn /usr/sbin/openvpn2.6 +msg_ok "Configured iptables" + +setup_go + +fetch_and_deploy_gh_release "gluetun" "qdm12/gluetun" "tarball" + +msg_info "Building Gluetun" +cd /opt/gluetun +$STD go mod download +CGO_ENABLED=0 $STD go build -trimpath -ldflags="-s -w" -o /usr/local/bin/gluetun ./cmd/gluetun/ +msg_ok "Built Gluetun" + +msg_info "Configuring Gluetun" +mkdir -p /opt/gluetun-data +touch /etc/alpine-release +ln -sf /opt/gluetun-data /gluetun +cat </opt/gluetun-data/.env +VPN_SERVICE_PROVIDER=custom +VPN_TYPE=openvpn +OPENVPN_CUSTOM_CONFIG=/opt/gluetun-data/custom.ovpn +OPENVPN_USER= +OPENVPN_PASSWORD= +OPENVPN_PROCESS_USER=root +PUID=0 +PGID=0 +HTTP_CONTROL_SERVER_ADDRESS=:8000 +HTTPPROXY=off +SHADOWSOCKS=off +PPROF_ENABLED=no +PPROF_BLOCK_PROFILE_RATE=0 +PPROF_MUTEX_PROFILE_RATE=0 +PPROF_HTTP_SERVER_ADDRESS=:6060 +FIREWALL_ENABLED_DISABLING_IT_SHOOTS_YOU_IN_YOUR_FOOT=on +HEALTH_SERVER_ADDRESS=127.0.0.1:9999 +DNS_UPSTREAM_RESOLVERS=cloudflare +LOG_LEVEL=info +STORAGE_FILEPATH=/gluetun/servers.json +PUBLICIP_FILE=/gluetun/ip +VPN_PORT_FORWARDING_STATUS_FILE=/gluetun/forwarded_port +TZ=UTC +EOF +msg_ok "Configured Gluetun" + +msg_info "Creating Service" +cat </etc/systemd/system/gluetun.service +[Unit] +Description=Gluetun VPN Client +After=network.target + +[Service] +Type=simple +User=root +WorkingDirectory=/opt/gluetun-data +EnvironmentFile=/opt/gluetun-data/.env +UnsetEnvironment=USER +ExecStartPre=/bin/sh -c 'rm -f /etc/openvpn/target.ovpn' +ExecStart=/usr/local/bin/gluetun +Restart=on-failure +RestartSec=5 +AmbientCapabilities=CAP_NET_ADMIN + +[Install] +WantedBy=multi-user.target +EOF +systemctl enable -q --now gluetun +msg_ok "Created Service" + +motd_ssh +customize +cleanup_lxc diff --git a/install/lidarr-install.sh b/install/lidarr-install.sh new file mode 100644 index 00000000..53d0c137 --- /dev/null +++ b/install/lidarr-install.sh @@ -0,0 +1,53 @@ +#!/usr/bin/env bash + +# Copyright (c) 2021-2026 tteck +# Author: tteck (tteckster) +# License: MIT | https://github.com/community-scripts/ProxmoxVED/raw/main/LICENSE +# Source: https://lidarr.audio/ | Github: https://github.com/Lidarr/Lidarr + +source /dev/stdin <<<"$FUNCTIONS_FILE_PATH" +color +verb_ip6 +catch_errors +setting_up_container +network_check +update_os + +msg_info "Installing Dependencies" +$STD apt install -y \ + sqlite3 \ + libchromaprint-tools \ + mediainfo +msg_ok "Installed Dependencies" + +fetch_and_deploy_gh_release "lidarr" "Lidarr/Lidarr" "prebuild" "latest" "/opt/Lidarr" "Lidarr.master*linux-core-x64.tar.gz" + +msg_info "Configuring Lidarr" +mkdir -p /var/lib/lidarr/ +chmod 775 /var/lib/lidarr/ +chmod 775 /opt/Lidarr +msg_ok "Configured Lidarr" + +msg_info "Creating Service" +cat </etc/systemd/system/lidarr.service +[Unit] +Description=Lidarr Daemon +After=syslog.target network.target + +[Service] +UMask=0002 +Type=simple +ExecStart=/opt/Lidarr/Lidarr -nobrowser -data=/var/lib/lidarr/ +TimeoutStopSec=20 +KillMode=process +Restart=on-failure + +[Install] +WantedBy=multi-user.target +EOF +systemctl enable -q --now lidarr +msg_ok "Created Service" + +motd_ssh +customize +cleanup_lxc diff --git a/install/prowlarr-install.sh b/install/prowlarr-install.sh new file mode 100644 index 00000000..3c835404 --- /dev/null +++ b/install/prowlarr-install.sh @@ -0,0 +1,49 @@ +#!/usr/bin/env bash + +# Copyright (c) 2021-2026 tteck +# Author: tteck (tteckster) +# License: MIT | https://github.com/community-scripts/ProxmoxVED/raw/main/LICENSE +# Source: https://prowlarr.com/ | Github: https://github.com/Prowlarr/Prowlarr + +source /dev/stdin <<<"$FUNCTIONS_FILE_PATH" +color +verb_ip6 +catch_errors +setting_up_container +network_check +update_os + +msg_info "Installing Dependencies" +$STD apt install -y sqlite3 +msg_ok "Installed Dependencies" + +fetch_and_deploy_gh_release "prowlarr" "Prowlarr/Prowlarr" "prebuild" "latest" "/opt/Prowlarr" "Prowlarr.master*linux-core-x64.tar.gz" + +msg_info "Configuring Prowlarr" +mkdir -p /var/lib/prowlarr/ +chmod 775 /var/lib/prowlarr/ /opt/Prowlarr +msg_ok "Configured Prowlarr" + +msg_info "Creating Service" +cat </etc/systemd/system/prowlarr.service +[Unit] +Description=Prowlarr Daemon +After=syslog.target network.target + +[Service] +UMask=0002 +Type=simple +ExecStart=/opt/Prowlarr/Prowlarr -nobrowser -data=/var/lib/prowlarr/ +TimeoutStopSec=20 +KillMode=process +Restart=on-failure + +[Install] +WantedBy=multi-user.target +EOF +systemctl enable -q --now prowlarr +msg_ok "Created Service" + +motd_ssh +customize +cleanup_lxc diff --git a/install/qbittorrent-install.sh b/install/qbittorrent-install.sh new file mode 100644 index 00000000..905d08dc --- /dev/null +++ b/install/qbittorrent-install.sh @@ -0,0 +1,56 @@ +#!/usr/bin/env bash + +# Copyright (c) 2021-2026 community-scripts ORG +# Author: tteck (tteckster) | Co-Author: Slaviša Arežina (tremor021) +# License: MIT | https://github.com/community-scripts/ProxmoxVED/raw/main/LICENSE +# Source: https://www.qbittorrent.org/ | Github: https://github.com/qbittorrent/qBittorrent + +source /dev/stdin <<<"$FUNCTIONS_FILE_PATH" +color +verb_ip6 +catch_errors +setting_up_container +network_check +update_os + +fetch_and_deploy_gh_release "qbittorrent" "userdocs/qbittorrent-nox-static" "singlefile" "latest" "/opt/qbittorrent" "x86_64-qbittorrent-nox" + +msg_info "Setup qBittorrent-nox" +mv /opt/qbittorrent/qbittorrent /opt/qbittorrent/qbittorrent-nox +mkdir -p ~/.config/qBittorrent/ +cat <~/.config/qBittorrent/qBittorrent.conf +[LegalNotice] +Accepted=true + +[Preferences] +WebUI\Password_PBKDF2="@ByteArray(amjeuVrF3xRbgzqWQmes5A==:XK3/Ra9jUmqUc4RwzCtrhrkQIcYczBl90DJw2rT8DFVTss4nxpoRhvyxhCf87ahVE3SzD8K9lyPdpyUCfmVsUg==)" +WebUI\Port=8090 +WebUI\UseUPnP=false +WebUI\Username=admin + +[Network] +PortForwardingEnabled=false +EOF +msg_ok "Setup qBittorrent-nox" + +msg_info "Creating Service" +cat </etc/systemd/system/qbittorrent-nox.service +[Unit] +Description=qBittorrent client +After=network.target + +[Service] +Type=simple +User=root +ExecStart=/opt/qbittorrent/qbittorrent-nox +Restart=always + +[Install] +WantedBy=multi-user.target +EOF +systemctl enable -q --now qbittorrent-nox +msg_ok "Created Service" + +motd_ssh +customize +cleanup_lxc diff --git a/install/radarr-install.sh b/install/radarr-install.sh new file mode 100644 index 00000000..3ac0072c --- /dev/null +++ b/install/radarr-install.sh @@ -0,0 +1,49 @@ +#!/usr/bin/env bash + +# Copyright (c) 2021-2026 tteck +# Author: tteck (tteckster) +# License: MIT | https://github.com/community-scripts/ProxmoxVED/raw/main/LICENSE +# Source: https://radarr.video/ | Github: https://github.com/Radarr/Radarr + +source /dev/stdin <<<"$FUNCTIONS_FILE_PATH" +color +verb_ip6 +catch_errors +setting_up_container +network_check +update_os + +msg_info "Installing Dependencies" +$STD apt install -y sqlite3 +msg_ok "Installed Dependencies" + +fetch_and_deploy_gh_release "Radarr" "Radarr/Radarr" "prebuild" "latest" "/opt/Radarr" "Radarr.master*linux-core-x64.tar.gz" + +msg_info "Configuring Radarr" +mkdir -p /var/lib/radarr/ +chmod 775 /var/lib/radarr/ /opt/Radarr/ +msg_ok "Configured Radarr" + +msg_info "Creating Service" +cat </etc/systemd/system/radarr.service +[Unit] +Description=Radarr Daemon +After=syslog.target network.target + +[Service] +UMask=0002 +Type=simple +ExecStart=/opt/Radarr/Radarr -nobrowser -data=/var/lib/radarr/ +TimeoutStopSec=20 +KillMode=process +Restart=on-failure + +[Install] +WantedBy=multi-user.target +EOF +systemctl enable -q --now radarr +msg_ok "Created Service" + +motd_ssh +customize +cleanup_lxc diff --git a/install/rdtclient-install.sh b/install/rdtclient-install.sh new file mode 100644 index 00000000..45af689b --- /dev/null +++ b/install/rdtclient-install.sh @@ -0,0 +1,52 @@ +#!/usr/bin/env bash + +# Copyright (c) 2021-2026 tteck +# Author: tteck (tteckster) +# License: MIT | https://github.com/community-scripts/ProxmoxVED/raw/main/LICENSE +# Source: https://github.com/rogerfar/rdt-client + +source /dev/stdin <<<"$FUNCTIONS_FILE_PATH" +color +verb_ip6 +catch_errors +setting_up_container +network_check +update_os + +msg_info "Installing Dependencies" +setup_deb822_repo \ + "microsoft" \ + "https://packages.microsoft.com/keys/microsoft-2025.asc" \ + "https://packages.microsoft.com/debian/13/prod/" \ + "trixie" +$STD apt install -y aspnetcore-runtime-10.0 +msg_ok "Installed Dependencies" + +fetch_and_deploy_gh_release "rdt-client" "rogerfar/rdt-client" "prebuild" "latest" "/opt/rdtc" "RealDebridClient.zip" + +msg_info "Setting up rdtclient" +cd /opt/rdtc +mkdir -p data/{db,downloads} +sed -i 's#/data/db/#/opt/rdtc&#g' /opt/rdtc/appsettings.json +msg_ok "Configured rdtclient" + +msg_info "Creating Service" +cat </etc/systemd/system/rdtc.service +[Unit] +Description=RdtClient Service + +[Service] +WorkingDirectory=/opt/rdtc +ExecStart=/usr/bin/dotnet RdtClient.Web.dll +SyslogIdentifier=RdtClient +User=root + +[Install] +WantedBy=multi-user.target +EOF +systemctl enable -q --now rdtc +msg_ok "Created Service" + +motd_ssh +customize +cleanup_lxc diff --git a/install/sabnzbd-install.sh b/install/sabnzbd-install.sh new file mode 100644 index 00000000..bf35f0be --- /dev/null +++ b/install/sabnzbd-install.sh @@ -0,0 +1,69 @@ +#!/usr/bin/env bash + +# Copyright (c) 2021-2026 tteck +# Author: tteck (tteckster) | Co-Author: MickLesk (CanbiZ) +# License: MIT | https://github.com/community-scripts/ProxmoxVED/raw/main/LICENSE +# Source: https://sabnzbd.org/ | Github: https://github.com/sabnzbd/sabnzbd + +source /dev/stdin <<<"$FUNCTIONS_FILE_PATH" +color +verb_ip6 +catch_errors +setting_up_container +network_check +update_os + +msg_info "Installing Dependencies" +$STD apt install -y \ + par2 \ + p7zip-full +msg_ok "Installed Dependencies" + +PYTHON_VERSION="3.13" setup_uv + +msg_info "Setup Unrar" +cat </etc/apt/sources.list.d/non-free.sources +Types: deb +URIs: http://deb.debian.org/debian/ +Suites: trixie +Components: non-free +Signed-By: /usr/share/keyrings/debian-archive-keyring.gpg +EOF +$STD apt update +$STD apt install -y unrar +msg_ok "Setup Unrar" + +fetch_and_deploy_gh_release "sabnzbd-org" "sabnzbd/sabnzbd" "prebuild" "latest" "/opt/sabnzbd" "SABnzbd-*-src.tar.gz" + +msg_info "Installing SABnzbd" +$STD uv venv --clear /opt/sabnzbd/venv +$STD uv pip install -r /opt/sabnzbd/requirements.txt --python=/opt/sabnzbd/venv/bin/python +msg_ok "Installed SABnzbd" + +read -r -p "Would you like to install par2cmdline-turbo? " prompt +if [[ "${prompt,,}" =~ ^(y|yes)$ ]]; then + mv /usr/bin/par2 /usr/bin/par2.old + fetch_and_deploy_gh_release "par2cmdline-turbo" "animetosho/par2cmdline-turbo" "prebuild" "latest" "/usr/bin/" "*-linux-amd64.zip" +fi + +msg_info "Creating Service" +cat </etc/systemd/system/sabnzbd.service +[Unit] +Description=SABnzbd +After=network.target + +[Service] +WorkingDirectory=/opt/sabnzbd +ExecStart=/opt/sabnzbd/venv/bin/python SABnzbd.py -s 0.0.0.0:7777 +Restart=always +User=root + +[Install] +WantedBy=multi-user.target +EOF +systemctl enable -q --now sabnzbd +msg_ok "Created Service" + +motd_ssh +customize +cleanup_lxc diff --git a/install/seerr-install.sh b/install/seerr-install.sh new file mode 100644 index 00000000..c33a6f27 --- /dev/null +++ b/install/seerr-install.sh @@ -0,0 +1,69 @@ +#!/usr/bin/env bash + +# Copyright (c) 2021-2026 community-scripts ORG +# Author: CrazyWolf13 +# License: MIT | https://github.com/community-scripts/ProxmoxVED/raw/main/LICENSE +# Source: https://docs.seerr.dev/ | Github: https://github.com/seerr-team/seerr + +source /dev/stdin <<<"$FUNCTIONS_FILE_PATH" +color +verb_ip6 +catch_errors +setting_up_container +network_check +update_os + +msg_info "Installing Dependencies" +$STD apt install -y \ + build-essential \ + python3-setuptools +msg_ok "Installed Dependencies" + +fetch_and_deploy_gh_release "seerr" "seerr-team/seerr" "tarball" +pnpm_desired=$(grep -Po '"pnpm":\s*"\K[^"]+' /opt/seerr/package.json) +NODE_VERSION="22" NODE_MODULE="pnpm@$pnpm_desired" setup_nodejs + +msg_info "Installing Seerr (Patience)" +export CYPRESS_INSTALL_BINARY=0 +cd /opt/seerr +$STD pnpm install --frozen-lockfile +export NODE_OPTIONS="--max-old-space-size=3072" +$STD pnpm build +mkdir -p /etc/seerr/ +cat </etc/seerr/seerr.conf +## Seerr's default port is 5055, if you want to use both, change this. +## specify on which port to listen +PORT=5055 + +## specify on which interface to listen, by default seerr listens on all interfaces +HOST=0.0.0.0 + +## Uncomment if you want to force Node.js to resolve IPv4 before IPv6 (advanced users only) +# FORCE_IPV4_FIRST=true +EOF +msg_ok "Installed Seerr" + +msg_info "Creating Service" +cat </etc/systemd/system/seerr.service +[Unit] +Description=Seerr Service +Wants=network-online.target +After=network-online.target + +[Service] +EnvironmentFile=/etc/seerr/seerr.conf +Environment=NODE_ENV=production +Type=exec +Restart=on-failure +WorkingDirectory=/opt/seerr +ExecStart=/usr/bin/node dist/index.js + +[Install] +WantedBy=multi-user.target +EOF +systemctl enable -q --now seerr +msg_ok "Created Service" + +motd_ssh +customize +cleanup_lxc diff --git a/install/slskd-install.sh b/install/slskd-install.sh new file mode 100644 index 00000000..d490937d --- /dev/null +++ b/install/slskd-install.sh @@ -0,0 +1,119 @@ +#!/usr/bin/env bash + +# Copyright (c) 2021-2026 community-scripts ORG +# Author: vhsdream +# License: MIT | https://github.com/community-scripts/ProxmoxVED/raw/main/LICENSE +# Source: https://github.com/slskd/slskd/, https://github.com/mrusse/soularr + +source /dev/stdin <<<"$FUNCTIONS_FILE_PATH" +color +verb_ip6 +catch_errors +setting_up_container +network_check +update_os + +fetch_and_deploy_gh_release "Slskd" "slskd/slskd" "prebuild" "latest" "/opt/slskd" "slskd-*-linux-x64.zip" + +msg_info "Configuring Slskd" +JWT_KEY=$(openssl rand -base64 44) +SLSKD_API_KEY=$(openssl rand -base64 44) +cp /opt/slskd/config/slskd.example.yml /opt/slskd/config/slskd.yml +sed -i \ + -e '/web:/,/cidr/s/^# //' \ + -e '/https:/,/port: 5031/s/false/true/' \ + -e '/port: 5030/,/socket/s/,.*$//' \ + -e '/content_path:/,/authentication/s/false/true/' \ + -e "\|api_keys|,\|cidr|s|/opt/soularr/run.sh +#!/usr/bin/env bash + +if ps aux | grep "[s]oularr.py" >/dev/null; then + echo "Soularr is already running. Exiting..." >&2 + exit 1 +fi + +# Remove stale lock file from previous ungraceful exit +rm -f "/opt/soularr/.soularr.lock" + +source /opt/soularr/venv/bin/activate +uv run python3 -u /opt/soularr/soularr.py --config-dir /opt/soularr 2>&1 +EOF + chmod +x /opt/soularr/run.sh + deactivate + msg_ok "Installed Soularr" +fi + +msg_info "Creating Service" +cat </etc/systemd/system/slskd.service +[Unit] +Description=Slskd Service +After=network.target +Wants=network.target + +[Service] +WorkingDirectory=/opt/slskd +ExecStart=/opt/slskd/slskd --config /opt/slskd/config/slskd.yml +Restart=always + +[Install] +WantedBy=multi-user.target +EOF + +if [[ -d /opt/soularr ]]; then + cat </etc/systemd/system/soularr.timer +[Unit] +Description=Soularr service timer +RefuseManualStart=no +RefuseManualStop=no + +[Timer] +Persistent=true +# run every 10 minutes +OnCalendar=*-*-* *:0/10:00 +Unit=soularr.service + +[Install] +WantedBy=timers.target +EOF + + cat </etc/systemd/system/soularr.service +[Unit] +Description=Soularr service +After=network.target slskd.service + +[Service] +Type=simple +WorkingDirectory=/opt/soularr +ExecStart=/bin/bash -c /opt/soularr/run.sh + +[Install] +WantedBy=multi-user.target +EOF + msg_warn "Add your Lidarr API key to Soularr in '/opt/soularr/config.ini', then run 'systemctl enable --now soularr.timer'" +fi +systemctl enable -q --now slskd +msg_ok "Created Services" + +motd_ssh +customize +cleanup_lxc diff --git a/install/sonarr-install.sh b/install/sonarr-install.sh new file mode 100644 index 00000000..4027fb2a --- /dev/null +++ b/install/sonarr-install.sh @@ -0,0 +1,45 @@ +#!/usr/bin/env bash + +# Copyright (c) 2021-2026 tteck +# Author: tteck (tteckster) +# License: MIT | https://github.com/community-scripts/ProxmoxVED/raw/main/LICENSE +# Source: https://sonarr.tv/ | Github: https://github.com/Sonarr/Sonarr + +source /dev/stdin <<<"$FUNCTIONS_FILE_PATH" +color +verb_ip6 +catch_errors +setting_up_container +network_check +update_os + +msg_info "Installing Dependencies" +$STD apt install -y sqlite3 +msg_ok "Installed Dependencies" + +fetch_and_deploy_gh_release "Sonarr" "Sonarr/Sonarr" "prebuild" "latest" "/opt/Sonarr" "Sonarr.main.*.linux-x64.tar.gz" +mkdir -p /var/lib/sonarr/ +chmod 775 /var/lib/sonarr/ + +msg_info "Creating Service" +cat </etc/systemd/system/sonarr.service +[Unit] +Description=Sonarr Daemon +After=syslog.target network.target + +[Service] +Type=simple +ExecStart=/opt/Sonarr/Sonarr -nobrowser -data=/var/lib/sonarr/ +TimeoutStopSec=20 +KillMode=process +Restart=on-failure + +[Install] +WantedBy=multi-user.target +EOF +systemctl enable -q --now sonarr +msg_ok "Created Service" + +motd_ssh +customize +cleanup_lxc diff --git a/json/arr-stack.json b/json/arr-stack.json new file mode 100644 index 00000000..fa4978d4 --- /dev/null +++ b/json/arr-stack.json @@ -0,0 +1,40 @@ +{ + "name": "ARR-Stack", + "slug": "arr-stack", + "categories": [ + 12 + ], + "date_created": "2026-01-18", + "type": "ct", + "updateable": true, + "privileged": false, + "interface_port": 3010, + "documentation": "community-scripts/ProxmoxVED/blob/main/tools/arr-stack.sh", + "website": "https://community-scripts.org", + "logo": "https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/ProxmoxVE.webp", + "description": "Proxmox VE Helper Scripts for setting up a stack of applications for use with Prowlarr, Sonarr, Radarr, Lidarr, Seerr, and qBittorrent.", + "install_methods": [ + { + "type": "default", + "script": "tools/arr-stack.sh", + "config_path": "", + "resources": { + "cpu": 1, + "ram": 1, + "hdd": 2, + "os": "Debian", + "version": "13" + } + } + ], + "default_credentials": { + "username": null, + "password": null + }, + "notes": [ + { + "text": "This script will prompt you to select the applications you want to install and configure the network settings.", + "type": "info" + } + ] +} diff --git a/tools/arr-stack.sh b/tools/arr-stack.sh new file mode 100644 index 00000000..6ffe7735 --- /dev/null +++ b/tools/arr-stack.sh @@ -0,0 +1,867 @@ +#!/usr/bin/env bash + +# Copyright (c) 2021-2026 community-scripts ORG +# Author: community-scripts +# License: MIT | https://github.com/community-scripts/ProxmoxVED/raw/main/LICENSE + +source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVED/main/misc/core.func) +source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVED/main/misc/tools.func) + +set -eEo pipefail + +color +formatting +icons + +msg_info() { echo -e "${INFO:-[i]} ${YW}${1}${CL}"; } +msg_ok() { echo -e "${CM:-[ok]} ${GN}${1}${CL}"; } +msg_warn() { echo -e "${YW}[WARN]${CL} ${1}"; } +msg_error() { echo -e "${CROSS:-[x]} ${RD}${1}${CL}"; } +msg_step() { echo -e "${BL}==>${CL} ${1}"; } + +var_container_storage="${var_container_storage:-}" +var_template_storage="${var_template_storage:-}" +var_bridge="${var_bridge:-}" +var_gateway="${var_gateway:-}" +var_cidr="${var_cidr:-24}" +var_start_ctid="${var_start_ctid:-}" +var_repo="${var_repo:-ProxmoxVED}" +SUMMARY_FILE="${SUMMARY_FILE:-/root/arr-stack-summary.txt}" + +BACKTITLE="Proxmox VE Helper Scripts — arr Stack" + +TEMP_DIR=$(mktemp -d) +trap 'rm -rf "$TEMP_DIR"' EXIT + +declare -A CTID_BY_SLUG +declare -A IP_BY_SLUG +declare -A PORT_BY_SLUG +declare -A APIKEY_BY_SLUG +declare -A USER_BY_SLUG +declare -A PASS_BY_SLUG +declare -A SCRIPT_BY_SLUG +declare -A IMPL_BY_SLUG +declare -A KIND_BY_SLUG +declare -A ARR_API_VER_BY_SLUG +declare -A NAME_BY_SLUG +declare -A CONFIG_CONTRACT_BY_SLUG + +SELECTED_ARRS="" +SELECTED_CLIENTS="" +ORDERED_SLUGS=() +INSTALLED_SLUGS=() +WIRING_RESULTS=() +WIRING_FAILURES=() + +SYNC_CATEGORIES_SONARR='[5000,5010,5020,5030,5040,5045,5050]' +SYNC_CATEGORIES_RADARR='[2000,2010,2020,2030,2040,2045,2050,2060]' +SYNC_CATEGORIES_LIDARR='[3000,3010,3020,3030,3040]' + +header_info() { + clear + cat <<"EOF" + _ _ + __ _ _ __ _ __ ___| |_ __ _ ___| | __ + / _` | '__| '__|____ / __| __/ _` |/ __| |/ / + | (_| | | | | |_____|\__ \ || (_| | (__| < + \__,_|_| |_| |___/\__\__,_|\___|_|\_\ + +EOF +} + +check_root() { + if [[ $EUID -ne 0 ]]; then + msg_error "Run this script as root." + exit 1 + fi +} + +check_pve_tools() { + local missing=() + for cmd in pct pvesh pvesm; do + command -v "$cmd" >/dev/null 2>&1 || missing+=("$cmd") + done + if (( ${#missing[@]} > 0 )); then + msg_error "Missing Proxmox VE tools: ${missing[*]}. Run this on a PVE node." + exit 1 + fi +} + +wait_for_port() { + local ip=$1 port=$2 timeout=${3:-60} elapsed=0 + while ! (echo > "/dev/tcp/${ip}/${port}") >/dev/null 2>&1; do + sleep 2 + elapsed=$((elapsed + 2)) + if (( elapsed >= timeout )); then return 1; fi + done + return 0 +} + +is_valid_ipv4() { + local ip=$1 + [[ "$ip" =~ ^([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})$ ]] || return 1 + local a=${BASH_REMATCH[1]} b=${BASH_REMATCH[2]} c=${BASH_REMATCH[3]} d=${BASH_REMATCH[4]} + (( a <= 255 && b <= 255 && c <= 255 && d <= 255 )) || return 1 + return 0 +} + +seed_catalog() { + while IFS='|' read -r slug script port impl apiver kind name contract; do + [[ -z "$slug" ]] && continue + SCRIPT_BY_SLUG[$slug]="$script" + PORT_BY_SLUG[$slug]="$port" + IMPL_BY_SLUG[$slug]="$impl" + ARR_API_VER_BY_SLUG[$slug]="$apiver" + KIND_BY_SLUG[$slug]="$kind" + NAME_BY_SLUG[$slug]="$name" + CONFIG_CONTRACT_BY_SLUG[$slug]="$contract" + done <<'EOF' +prowlarr|prowlarr.sh|9696||v1|indexer|Prowlarr| +sonarr|sonarr.sh|8989|Sonarr|v3|arr|Sonarr|SonarrSettings +radarr|radarr.sh|7878|Radarr|v3|arr|Radarr|RadarrSettings +lidarr|lidarr.sh|8686|Lidarr|v1|arr|Lidarr|LidarrSettings +seerr|seerr.sh|5055||-|requests|Seerr| +qbittorrent|qbittorrent.sh|8090|QBittorrent|-|client|qBittorrent|QBittorrentSettings +sabnzbd|sabnzbd.sh|7777|Sabnzbd|-|client|SABnzbd|SabnzbdSettings +EOF +} + +pick_storage() { + if [[ -n "$var_container_storage" ]]; then + msg_info "Container storage (from env): ${var_container_storage}" + else + local options=() row name type + while IFS= read -r row; do + name=$(awk '{print $1}' <<<"$row") + type=$(awk '{print $2}' <<<"$row") + [[ -z "$name" ]] && continue + options+=("$name" "$type") + done < <(pvesm status -content rootdir 2>/dev/null | awk 'NR>1') + + if (( ${#options[@]} == 0 )); then + msg_error "No PVE storage with content 'rootdir' available." + exit 1 + fi + + if (( ${#options[@]} == 2 )); then + var_container_storage="${options[0]}" + msg_info "Container storage (only option): ${var_container_storage}" + else + var_container_storage=$(whiptail --backtitle "$BACKTITLE" \ + --title "Container Storage" \ + --menu "Pick a PVE storage for the container rootfs:" 20 70 10 \ + "${options[@]}" 3>&1 1>&2 2>&3) || exit 0 + fi + fi + + if [[ -z "$var_template_storage" ]]; then + var_template_storage=$(pvesm status -content vztmpl 2>/dev/null \ + | awk 'NR>1 && $1=="local" {print $1; exit}') + [[ -z "$var_template_storage" ]] && var_template_storage=$(pvesm status -content vztmpl 2>/dev/null \ + | awk 'NR>1 {print $1; exit}') + fi + [[ -n "$var_template_storage" ]] && msg_info "Template storage: ${var_template_storage}" +} + +pick_network_defaults() { + if [[ -z "$var_bridge" ]]; then + local options=() b + while IFS= read -r b; do + [[ -n "$b" ]] && options+=("$b" "") + done < <(awk '/^iface vmbr/ {print $2}' /etc/network/interfaces 2>/dev/null) + + if (( ${#options[@]} == 0 )); then + options=("vmbr0" "") + fi + + var_bridge=$(whiptail --backtitle "$BACKTITLE" \ + --title "Network Bridge" \ + --menu "Pick the Linux bridge for all containers:" 15 60 6 \ + "${options[@]}" 3>&1 1>&2 2>&3) || exit 0 + fi + + while [[ -z "$var_gateway" ]] || ! is_valid_ipv4 "$var_gateway"; do + var_gateway=$(whiptail --backtitle "$BACKTITLE" \ + --title "Gateway" \ + --inputbox "IPv4 gateway for the container subnet:" 10 60 \ + "${var_gateway:-}" 3>&1 1>&2 2>&3) || exit 0 + if ! is_valid_ipv4 "$var_gateway"; then + whiptail --backtitle "$BACKTITLE" --title "Invalid" \ + --msgbox "Not a valid IPv4 address: ${var_gateway}" 8 60 + var_gateway="" + fi + done + + while true; do + var_cidr=$(whiptail --backtitle "$BACKTITLE" \ + --title "CIDR Mask" \ + --inputbox "Network mask (1-32, e.g. 24):" 10 60 \ + "${var_cidr:-24}" 3>&1 1>&2 2>&3) || exit 0 + if [[ "$var_cidr" =~ ^[0-9]+$ ]] && (( var_cidr >= 1 && var_cidr <= 32 )); then + break + fi + whiptail --backtitle "$BACKTITLE" --title "Invalid" \ + --msgbox "CIDR must be an integer between 1 and 32." 8 60 + done + + msg_info "Bridge ${var_bridge} | gateway ${var_gateway} | mask /${var_cidr}" +} + +pick_apps() { + while true; do + local choice + choice=$(whiptail --backtitle "$BACKTITLE" \ + --title "Pick *arr Apps" \ + --checklist "Prowlarr is always installed. Pick additional apps:" 16 70 6 \ + "sonarr" "Sonarr (TV)" ON \ + "radarr" "Radarr (Movies)" ON \ + "lidarr" "Lidarr (Music)" OFF \ + "seerr" "Seerr (Requests)" OFF \ + 3>&1 1>&2 2>&3) || exit 0 + + SELECTED_ARRS=$(echo "$choice" | tr -d '"') + + if [[ -z "$SELECTED_ARRS" ]]; then + if whiptail --backtitle "$BACKTITLE" --title "Confirm" \ + --yesno "You picked no *arr apps. Only Prowlarr will be installed and there will be nothing to wire. Continue anyway?" 10 70; then + return + fi + continue + fi + return + done +} + +pick_clients() { + local choice + choice=$(whiptail --backtitle "$BACKTITLE" \ + --title "Pick Download Clients" \ + --checklist "Optional download clients to install + wire:" 14 70 4 \ + "qbittorrent" "qBittorrent (Torrents)" ON \ + "sabnzbd" "SABnzbd (Usenet)" OFF \ + 3>&1 1>&2 2>&3) || exit 0 + + SELECTED_CLIENTS=$(echo "$choice" | tr -d '"') +} + +compute_ordered_slugs() { + ORDERED_SLUGS=("prowlarr") + local s + for s in $SELECTED_ARRS; do + [[ "$s" == "seerr" ]] && continue + ORDERED_SLUGS+=("$s") + done + for s in $SELECTED_CLIENTS; do + ORDERED_SLUGS+=("$s") + done + for s in $SELECTED_ARRS; do + [[ "$s" == "seerr" ]] && ORDERED_SLUGS+=("seerr") + done +} + +pick_ip_mode_and_ips() { + local mode + mode=$(whiptail --backtitle "$BACKTITLE" \ + --title "IP Entry Mode" \ + --menu "How would you like to enter IP addresses?" 14 70 2 \ + "list" "Enter all IPs at once (space- or comma-separated)" \ + "one_by_one" "Prompt per container" \ + 3>&1 1>&2 2>&3) || exit 0 + + if [[ "$mode" == "list" ]]; then + _collect_ips_list_mode + else + _collect_ips_one_by_one + fi +} + +_collect_ips_list_mode() { + local expected_n=${#ORDERED_SLUGS[@]} + local hint="" s + for s in "${ORDERED_SLUGS[@]}"; do hint+=" ${s}"$'\n'; done + + while true; do + local raw + raw=$(whiptail --backtitle "$BACKTITLE" \ + --title "Enter ${expected_n} IPv4 addresses" \ + --inputbox "Enter ${expected_n} IPs separated by spaces or commas, in this order:"$'\n\n'"${hint}" \ + 22 78 "" 3>&1 1>&2 2>&3) || exit 0 + + local normalized="${raw//,/ }" + local -a ips=() + # shellcheck disable=SC2206 + ips=( $normalized ) + + if (( ${#ips[@]} != expected_n )); then + whiptail --backtitle "$BACKTITLE" --title "Wrong count" \ + --msgbox "Expected ${expected_n} IPs, got ${#ips[@]}. Please re-enter." 8 60 + continue + fi + + local ok=1 i + for i in "${!ips[@]}"; do + if ! is_valid_ipv4 "${ips[$i]}"; then + whiptail --backtitle "$BACKTITLE" --title "Invalid" \ + --msgbox "Entry $((i+1)) is not a valid IPv4: ${ips[$i]}" 8 60 + ok=0; break + fi + if [[ "${ips[$i]}" == "$var_gateway" ]]; then + whiptail --backtitle "$BACKTITLE" --title "Invalid" \ + --msgbox "Entry $((i+1)) collides with the gateway: ${ips[$i]}" 8 60 + ok=0; break + fi + done + (( ok == 0 )) && continue + + local dup + dup=$(printf '%s\n' "${ips[@]}" | sort | uniq -d | head -n1) + if [[ -n "$dup" ]]; then + whiptail --backtitle "$BACKTITLE" --title "Duplicate IP" \ + --msgbox "IP appears more than once: ${dup}" 8 60 + continue + fi + + for i in "${!ORDERED_SLUGS[@]}"; do + IP_BY_SLUG[${ORDERED_SLUGS[$i]}]=${ips[$i]} + done + return + done +} + +_collect_ips_one_by_one() { + local slug ip running="" + for slug in "${ORDERED_SLUGS[@]}"; do + while true; do + ip=$(whiptail --backtitle "$BACKTITLE" \ + --title "IP for ${slug}" \ + --inputbox "Enter IPv4 for ${slug}.${running:+$'\n\nAlready assigned:'}${running}" \ + 16 60 "" 3>&1 1>&2 2>&3) || exit 0 + + if ! is_valid_ipv4 "$ip"; then + whiptail --backtitle "$BACKTITLE" --title "Invalid" \ + --msgbox "Not a valid IPv4: ${ip}" 8 60 + continue + fi + if [[ "$ip" == "$var_gateway" ]]; then + whiptail --backtitle "$BACKTITLE" --title "Invalid" \ + --msgbox "Collides with the gateway: ${ip}" 8 60 + continue + fi + local dup=0 other + for other in "${IP_BY_SLUG[@]}"; do + [[ "$other" == "$ip" ]] && { dup=1; break; } + done + if (( dup )); then + whiptail --backtitle "$BACKTITLE" --title "Duplicate" \ + --msgbox "Already used by another container: ${ip}" 8 60 + continue + fi + + IP_BY_SLUG[$slug]=$ip + running+=$'\n '"${slug} -> ${ip}" + break + done + done +} + +pick_start_ctid() { + local default_start + if [[ -n "$var_start_ctid" ]]; then + default_start="$var_start_ctid" + else + default_start=$(pvesh get /cluster/nextid 2>/dev/null || echo "100") + fi + + local start + start=$(whiptail --backtitle "$BACKTITLE" \ + --title "Starting CTID" \ + --inputbox "Starting Container ID (in-use IDs are skipped):" 10 60 \ + "$default_start" 3>&1 1>&2 2>&3) || exit 0 + + if ! [[ "$start" =~ ^[0-9]+$ ]]; then + msg_error "Invalid CTID: $start" + exit 1 + fi + + local id=$start s + for s in "${ORDERED_SLUGS[@]}"; do + while pct status "$id" >/dev/null 2>&1; do + id=$((id + 1)) + (( id > 999999 )) && { msg_error "Ran out of CTID space."; exit 1; } + done + CTID_BY_SLUG[$s]=$id + id=$((id + 1)) + done +} + +confirm_summary() { + local lines="" s + for s in "${ORDERED_SLUGS[@]}"; do + lines+=" $(printf '%-12s ctid=%-5s ip=%-16s port=%s' \ + "$s" "${CTID_BY_SLUG[$s]}" "${IP_BY_SLUG[$s]}" "${PORT_BY_SLUG[$s]}")"$'\n' + done + + local body="About to create these containers and wire them together:"$'\n\n'"${lines}"$'\n'"Storage: ${var_container_storage} | Bridge: ${var_bridge} | Gateway: ${var_gateway} | Mask: /${var_cidr}" + + whiptail --backtitle "$BACKTITLE" --title "Confirm" \ + --yesno "$body" 22 78 || { msg_warn "User cancelled."; exit 0; } +} + +orphan_report() { + if (( ${#INSTALLED_SLUGS[@]} == 0 )); then return; fi + msg_error "Containers already created (to clean up, run):" + local s + for s in "${INSTALLED_SLUGS[@]}"; do + echo " pct stop ${CTID_BY_SLUG[$s]} && pct destroy ${CTID_BY_SLUG[$s]} # ${s}" + done +} + +install_loop() { + local total=${#ORDERED_SLUGS[@]} idx=0 + local s script_file ip ctid port + + for s in "${ORDERED_SLUGS[@]}"; do + idx=$((idx + 1)) + ip="${IP_BY_SLUG[$s]}" + ctid="${CTID_BY_SLUG[$s]}" + port="${PORT_BY_SLUG[$s]}" + script_file="$TEMP_DIR/${s}.sh" + + msg_step "[${idx}/${total}] Downloading ct/${s}.sh" + curl -fsSL \ + "https://raw.githubusercontent.com/community-scripts/${var_repo}/main/ct/${s}.sh" \ + -o "$script_file" + + if [[ ! -s "$script_file" ]]; then + msg_error "Empty/failed download for ${s}" + orphan_report + exit 1 + fi + + msg_step "[${idx}/${total}] Installing ${s} -> ctid=${ctid} ip=${ip}/${var_cidr}" + if env \ + MODE=generated mode=generated PHS_SILENT=1 \ + var_ctid="$ctid" \ + var_hostname="$s" \ + var_brg="$var_bridge" \ + var_net="${ip}/${var_cidr}" \ + var_gateway="$var_gateway" \ + var_container_storage="$var_container_storage" \ + var_template_storage="$var_template_storage" \ + bash "$script_file"; then + INSTALLED_SLUGS+=("$s") + msg_ok "Installed ${s}" + else + msg_error "Install failed for ${s} (ctid=${ctid})." + orphan_report + exit 1 + fi + + if [[ "${KIND_BY_SLUG[$s]}" == "arr" || "${KIND_BY_SLUG[$s]}" == "indexer" ]]; then + msg_info "Waiting for ${s} to listen on ${port}..." + if ! wait_for_port "$ip" "$port" 90; then + msg_warn "${s} did not open ${port} within 90s; will retry during key extraction." + fi + fi + done +} + +extract_arr_key() { + local slug=$1 ctid=$2 ip=$3 port=$4 + local config_dir="/var/lib/${slug}/config.xml" + + msg_info "Waiting for ${slug} on ${ip}:${port}..." + wait_for_port "$ip" "$port" 240 || { msg_error "${slug} never opened ${port}"; return 1; } + + local i + for ((i=0; i<60; i++)); do + if pct exec "$ctid" -- test -f "$config_dir" 2>/dev/null; then break; fi + sleep 2 + done + + local key + key=$(pct exec "$ctid" -- sed -n 's:.*\([^<]*\).*:\1:p' "$config_dir" 2>/dev/null | head -n1 || true) + if [[ -z "$key" ]]; then + msg_error "Failed to extract API key for ${slug} (config: ${config_dir})" + return 1 + fi + APIKEY_BY_SLUG[$slug]="$key" + msg_ok "${slug} apikey extracted (${key:0:6}…)" +} + +extract_sabnzbd_key() { + local ctid=$1 ip=$2 + + msg_info "Waiting for sabnzbd on ${ip}:7777..." + wait_for_port "$ip" 7777 240 || { msg_warn "sabnzbd never opened 7777"; return 1; } + + local ini="" candidate + for candidate in /opt/sabnzbd/sabnzbd.ini /root/.sabnzbd/sabnzbd.ini /etc/sabnzbd/sabnzbd.ini; do + if pct exec "$ctid" -- test -f "$candidate" 2>/dev/null; then + ini="$candidate"; break + fi + done + if [[ -z "$ini" ]]; then + msg_warn "Could not locate sabnzbd.ini inside ctid ${ctid}; SABnzbd will need manual setup." + return 1 + fi + + local key="" i + for ((i=0; i<60; i++)); do + key=$(pct exec "$ctid" -- awk -F' *= *' '/^api_key/ {print $2; exit}' "$ini" 2>/dev/null || true) + [[ -n "$key" ]] && break + sleep 2 + done + + if [[ -z "$key" ]]; then + msg_warn "sabnzbd api_key not yet written. Open the web wizard once at http://${ip}:7777 and rerun wiring." + return 1 + fi + APIKEY_BY_SLUG[sabnzbd]="$key" + msg_ok "sabnzbd apikey extracted (${key:0:6}…)" +} + +wait_and_extract_keys() { + msg_step "Extracting credentials & API keys" + local s ctid ip port tmp + for s in "${ORDERED_SLUGS[@]}"; do + ctid="${CTID_BY_SLUG[$s]}" + ip="${IP_BY_SLUG[$s]}" + port="${PORT_BY_SLUG[$s]}" + case "${KIND_BY_SLUG[$s]}" in + indexer|arr) + extract_arr_key "$s" "$ctid" "$ip" "$port" || true + ;; + client) + if [[ "$s" == "qbittorrent" ]]; then + USER_BY_SLUG[qbittorrent]="admin" + PASS_BY_SLUG[qbittorrent]="adminadmin" + tmp=$(pct exec "$ctid" -- bash -c "journalctl -u qbittorrent-nox --no-pager 2>/dev/null | grep -i 'temporary password' | tail -n1" 2>/dev/null || true) + if [[ -n "$tmp" ]]; then + msg_warn "qBittorrent journalctl mentioned a temporary password — see summary." + PASS_BY_SLUG[qbittorrent]="" + fi + elif [[ "$s" == "sabnzbd" ]]; then + extract_sabnzbd_key "$ctid" "$ip" || true + fi + ;; + requests) + msg_warn "Seerr requires the web first-run wizard. URL + keys will be in the summary." + ;; + esac + done +} + +record_wiring() { WIRING_RESULTS+=("$1"); } +record_failure() { WIRING_FAILURES+=("$1"); } + +api_post() { + local url=$1 apikey=$2 payload=$3 label=$4 + local resp status="" + resp=$(curl -fsS --max-time 30 --retry 2 \ + -H "X-Api-Key: $apikey" \ + -H "Content-Type: application/json" \ + -X POST "$url" -d "$payload" \ + -w '\n__HTTP__%{http_code}' 2>&1) || status="curl_fail" + + local code="" + if [[ "$resp" =~ __HTTP__([0-9]+)$ ]]; then + code="${BASH_REMATCH[1]}" + fi + + if [[ "$status" == "curl_fail" || -z "$code" || "$code" -ge 400 ]]; then + record_failure "${label} FAIL (http ${code:-?})" + msg_warn "${label} failed (http ${code:-?})" + return 1 + fi + record_wiring "${label} OK" + msg_ok "${label}" +} + +probe_lidarr_api_version() { + if [[ -z "${APIKEY_BY_SLUG[lidarr]:-}" ]]; then return; fi + local ip="${IP_BY_SLUG[lidarr]}" key="${APIKEY_BY_SLUG[lidarr]}" + if curl -fsS --max-time 10 -H "X-Api-Key: $key" \ + "http://${ip}:8686/api/v3/system/status" >/dev/null 2>&1; then + ARR_API_VER_BY_SLUG[lidarr]="v3" + msg_info "Lidarr supports /api/v3 — using v3 for wiring." + fi +} + +wire_arrs_into_prowlarr() { + local prowlarr_ip="${IP_BY_SLUG[prowlarr]}" + local prowlarr_key="${APIKEY_BY_SLUG[prowlarr]:-}" + if [[ -z "$prowlarr_key" ]]; then + msg_warn "Skipping Prowlarr wiring — no Prowlarr API key." + return + fi + + local s sync_cats payload + for s in $SELECTED_ARRS; do + [[ "$s" == "seerr" ]] && continue + local key="${APIKEY_BY_SLUG[$s]:-}" + if [[ -z "$key" ]]; then + record_failure "Prowlarr -> ${NAME_BY_SLUG[$s]} FAIL (no apikey)" + continue + fi + + case "$s" in + sonarr) sync_cats="$SYNC_CATEGORIES_SONARR" ;; + radarr) sync_cats="$SYNC_CATEGORIES_RADARR" ;; + lidarr) sync_cats="$SYNC_CATEGORIES_LIDARR" ;; + *) sync_cats='[]' ;; + esac + + payload=$(jq -n \ + --arg name "${NAME_BY_SLUG[$s]}" \ + --arg impl "${IMPL_BY_SLUG[$s]}" \ + --arg contract "${CONFIG_CONTRACT_BY_SLUG[$s]}" \ + --arg prowlarr_url "http://${prowlarr_ip}:9696" \ + --arg base_url "http://${IP_BY_SLUG[$s]}:${PORT_BY_SLUG[$s]}" \ + --arg apikey "$key" \ + --argjson sync_cats "$sync_cats" \ + '{ + name: $name, + syncLevel: "fullSync", + implementation: $impl, + implementationName: $impl, + configContract: $contract, + tags: [], + fields: [ + { name: "prowlarrUrl", value: $prowlarr_url }, + { name: "baseUrl", value: $base_url }, + { name: "apiKey", value: $apikey }, + { name: "syncCategories", value: $sync_cats } + ] + }') + + api_post "http://${prowlarr_ip}:9696/api/v1/applications" \ + "$prowlarr_key" "$payload" \ + "Prowlarr -> ${NAME_BY_SLUG[$s]}" || true + done +} + +wire_clients_into_arrs() { + local arr client arr_key arr_ip arr_port api_ver category_field category_name payload url sab_key + + for arr in $SELECTED_ARRS; do + [[ "$arr" == "seerr" ]] && continue + arr_key="${APIKEY_BY_SLUG[$arr]:-}" + if [[ -z "$arr_key" ]]; then + msg_warn "Skipping download-client wiring for ${arr} — no API key." + continue + fi + arr_ip="${IP_BY_SLUG[$arr]}" + arr_port="${PORT_BY_SLUG[$arr]}" + api_ver="${ARR_API_VER_BY_SLUG[$arr]}" + + case "$arr" in + sonarr) category_field="tvCategory"; category_name="tv-sonarr" ;; + radarr) category_field="movieCategory"; category_name="radarr" ;; + lidarr) category_field="musicCategory"; category_name="lidarr" ;; + esac + + for client in $SELECTED_CLIENTS; do + url="http://${arr_ip}:${arr_port}/api/${api_ver}/downloadclient" + + if [[ "$client" == "qbittorrent" ]]; then + payload=$(jq -n \ + --arg host "${IP_BY_SLUG[qbittorrent]}" \ + --argjson port 8090 \ + --arg user "${USER_BY_SLUG[qbittorrent]}" \ + --arg pass "${PASS_BY_SLUG[qbittorrent]}" \ + --arg category_field "$category_field" \ + --arg category_name "$category_name" \ + '{ + enable: true, protocol: "torrent", priority: 1, + name: "qBittorrent", + implementation: "QBittorrent", + implementationName: "qBittorrent", + configContract: "QBittorrentSettings", + tags: [], + fields: [ + { name: "host", value: $host }, + { name: "port", value: $port }, + { name: "useSsl", value: false }, + { name: "username", value: $user }, + { name: "password", value: $pass }, + { name: $category_field, value: $category_name } + ] + }') + api_post "$url" "$arr_key" "$payload" \ + "${NAME_BY_SLUG[$arr]} -> qBittorrent" || true + + elif [[ "$client" == "sabnzbd" ]]; then + sab_key="${APIKEY_BY_SLUG[sabnzbd]:-}" + if [[ -z "$sab_key" ]]; then + record_failure "${NAME_BY_SLUG[$arr]} -> SABnzbd FAIL (no sab apikey)" + continue + fi + payload=$(jq -n \ + --arg host "${IP_BY_SLUG[sabnzbd]}" \ + --argjson port 7777 \ + --arg apikey "$sab_key" \ + --arg category_field "$category_field" \ + --arg category_name "$category_name" \ + '{ + enable: true, protocol: "usenet", priority: 1, + name: "SABnzbd", + implementation: "Sabnzbd", + implementationName: "SABnzbd", + configContract: "SabnzbdSettings", + tags: [], + fields: [ + { name: "host", value: $host }, + { name: "port", value: $port }, + { name: "apiKey", value: $apikey }, + { name: "useSsl", value: false }, + { name: $category_field, value: $category_name } + ] + }') + api_post "$url" "$arr_key" "$payload" \ + "${NAME_BY_SLUG[$arr]} -> SABnzbd" || true + fi + done + done +} + +wire_apis() { + msg_step "Wiring apps together via HTTP APIs" + probe_lidarr_api_version + wire_arrs_into_prowlarr + wire_clients_into_arrs + + if [[ " $SELECTED_ARRS " == *" seerr "* ]]; then + record_wiring "Seerr -> (manual via web wizard)" + msg_warn "Seerr can't be wired headlessly. URLs and keys are in the summary." + fi +} + +write_summary() { + msg_step "Writing summary" + local now host + now=$(date '+%Y-%m-%d %H:%M:%S %Z') + host=$(hostname) + + local -a lines=() + lines+=( "============================================================" ) + lines+=( " arr Stack — Provisioning Summary" ) + lines+=( " Generated: ${now}" ) + lines+=( " Host: ${host}" ) + lines+=( "============================================================" ) + lines+=( "" ) + lines+=( "[Shared settings]" ) + lines+=( " Bridge: ${var_bridge}" ) + lines+=( " Gateway: ${var_gateway}" ) + lines+=( " CIDR: /${var_cidr}" ) + lines+=( " CT storage: ${var_container_storage}" ) + lines+=( " Template: ${var_template_storage}" ) + lines+=( "" ) + + lines+=( "[Containers]" ) + local s + for s in "${ORDERED_SLUGS[@]}"; do + lines+=( "$(printf ' %-12s ctid=%-5s ip=%-16s url=http://%s:%s' \ + "$s" "${CTID_BY_SLUG[$s]}" "${IP_BY_SLUG[$s]}" "${IP_BY_SLUG[$s]}" "${PORT_BY_SLUG[$s]}")" ) + done + lines+=( "" ) + + lines+=( "[Credentials & API keys]" ) + for s in "${ORDERED_SLUGS[@]}"; do + case "${KIND_BY_SLUG[$s]}" in + indexer|arr) + if [[ -n "${APIKEY_BY_SLUG[$s]:-}" ]]; then + lines+=( "$(printf ' %-12s apikey: %s' "$s" "${APIKEY_BY_SLUG[$s]}")" ) + else + lines+=( "$(printf ' %-12s apikey: (not extracted)' "$s")" ) + fi + ;; + client) + if [[ "$s" == "qbittorrent" ]]; then + lines+=( "$(printf ' %-12s user: %s' "$s" "${USER_BY_SLUG[qbittorrent]:-admin}")" ) + lines+=( "$(printf ' %-12s pass: %s (CHANGE THIS!)' "" "${PASS_BY_SLUG[qbittorrent]:-adminadmin}")" ) + elif [[ "$s" == "sabnzbd" ]]; then + if [[ -n "${APIKEY_BY_SLUG[sabnzbd]:-}" ]]; then + lines+=( "$(printf ' %-12s apikey: %s' "$s" "${APIKEY_BY_SLUG[sabnzbd]}")" ) + else + lines+=( "$(printf ' %-12s apikey: (open web wizard at http://%s:7777 once)' "$s" "${IP_BY_SLUG[sabnzbd]}")" ) + fi + fi + ;; + requests) + lines+=( "$(printf ' %-12s (set during first-run web wizard)' "$s")" ) + ;; + esac + done + lines+=( "" ) + + lines+=( "[Wired automatically]" ) + if (( ${#WIRING_RESULTS[@]} == 0 )); then + lines+=( " (nothing)" ) + else + local w + for w in "${WIRING_RESULTS[@]}"; do lines+=( " ${w}" ); done + fi + lines+=( "" ) + + lines+=( "[Wiring failures]" ) + if (( ${#WIRING_FAILURES[@]} == 0 )); then + lines+=( " (none)" ) + else + local f + for f in "${WIRING_FAILURES[@]}"; do lines+=( " ${f}" ); done + fi + lines+=( "" ) + + lines+=( "[Manual steps still required]" ) + lines+=( " 1. Prowlarr: add indexers (none ship by default)." ) + lines+=( " 2. Sonarr/Radarr/Lidarr: set root folders and at least one quality profile." ) + if [[ " $SELECTED_CLIENTS " == *" qbittorrent "* ]]; then + lines+=( " 3. qBittorrent: change admin password (default admin/adminadmin)." ) + fi + if [[ " $SELECTED_ARRS " == *" seerr "* ]]; then + lines+=( " 4. Seerr: open http://${IP_BY_SLUG[seerr]}:5055, complete the first-run wizard, then add:" ) + for s in $SELECTED_ARRS; do + [[ "$s" == "seerr" ]] && continue + [[ "$s" == "lidarr" ]] && continue + lines+=( " ${NAME_BY_SLUG[$s]} at http://${IP_BY_SLUG[$s]}:${PORT_BY_SLUG[$s]} apikey: ${APIKEY_BY_SLUG[$s]:-}" ) + done + fi + lines+=( "" ) + lines+=( "Summary written to ${SUMMARY_FILE} (chmod 600)." ) + lines+=( "============================================================" ) + + local body + body=$(printf '%s\n' "${lines[@]}") + + echo + echo "$body" + + ( umask 077; printf '%s\n' "$body" > "$SUMMARY_FILE" ) + chmod 600 "$SUMMARY_FILE" 2>/dev/null || true + + msg_ok "Wrote ${SUMMARY_FILE}" +} + +main() { + header_info + check_root + check_pve_tools + ensure_dependencies curl whiptail jq + seed_catalog + pick_storage + pick_network_defaults + pick_apps + pick_clients + compute_ordered_slugs + pick_ip_mode_and_ips + pick_start_ctid + confirm_summary + install_loop + wait_and_extract_keys + wire_apis + write_summary + msg_ok "arr-stack provisioning finished." +} + +main "$@"