From 07ae1c0c71716a488801d4f8161a86b4f6442424 Mon Sep 17 00:00:00 2001 From: tim Date: Wed, 22 Jul 2026 11:59:51 -0700 Subject: [PATCH] Add SABnzbd for Usenet NZB search and download Wire Newshosting/Tweaknews via secrets-driven configure script, NZBgeek through Prowlarr, and /data/usenet paths for hardlinks with the Arrs. --- .env.example | 3 + .gitignore | 2 + README.md | 1 + ansible/group_vars/all.yml | 5 ++ docker-compose.yml | 18 +++- quickstart.md | 1 + scripts/configure-usenet.sh | 167 ++++++++++++++++++++++++++++++++++++ secrets/.env.example | 27 ++++++ 8 files changed, 223 insertions(+), 1 deletion(-) create mode 100755 scripts/configure-usenet.sh create mode 100644 secrets/.env.example diff --git a/.env.example b/.env.example index 9afff75..06681a3 100644 --- a/.env.example +++ b/.env.example @@ -73,6 +73,9 @@ OPENHANDS_MODEL=openai/local FLARESOLVERR_LOG_LEVEL=info FLARESOLVERR_CAPTCHA_SOLVER=none +# SABnzbd (Usenet) — credentials in secrets/.env, not here +SABNZBD_PORT=8085 + # Forgejo — self-hosted Git (web + SSH clone) FORGEJO_HTTP_PORT=3002 FORGEJO_SSH_PORT=2222 diff --git a/.gitignore b/.gitignore index 9505473..f4efcef 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,8 @@ models/bonsai/*.gguf models/bonsai/*.partial .openhands-state/ # secrets / local runtime +secrets/* +!secrets/.env.example **/wg0.conf wireguard/wg0.conf workspace/* diff --git a/README.md b/README.md index 5146854..31c0984 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,7 @@ for the **Beelink SER5 PRO** (Ryzen 7 7735HS + Radeon 680M). | Radarr | http://host:7878 | Movies | | Bazarr | http://host:6767 | Subtitles | | Lidarr | http://host:8686 | Music | +| SABnzbd | http://host:8085 | Usenet NZB downloads | | **Forgejo** | http://host:3002 | Self-hosted Git (SSH :2222) | | **Open WebUI** | http://host:3000 | Chat UI, RAG, tools, code interpreter | | **Bonsai API** | http://host:8081/v1 | OpenAI-compatible llama-server | diff --git a/ansible/group_vars/all.yml b/ansible/group_vars/all.yml index eb5b20d..1d0d839 100644 --- a/ansible/group_vars/all.yml +++ b/ansible/group_vars/all.yml @@ -185,6 +185,7 @@ ufw_lan_tcp_ports: - { port: 3001, comment: "OpenHands (optional)" } - { port: 3002, comment: "Forgejo HTTP" } - { port: 2222, comment: "Forgejo Git SSH" } + - { port: 8085, comment: "SABnzbd" } # WireGuard peer hostname for gluetun-endpoint-watch wireguard_endpoint_host: las-183-wg.whiskergalaxy.com @@ -231,3 +232,7 @@ media_subdirs: - torrents/movies - torrents/tv - torrents/music + - usenet/incomplete + - usenet/complete/movies + - usenet/complete/tv + - usenet/complete/music diff --git a/docker-compose.yml b/docker-compose.yml index b64850d..3eff718 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,7 +4,8 @@ name: arr-stack # Arr stack # gluetun WireGuard VPN client + firewall killswitch # qbittorrent torrent client, lives INSIDE gluetun's network namespace -# prowlarr indexer manager +# prowlarr indexer manager (torrents + NZB/Usenet indexers) +# sabnzbd Usenet NZB download client (Newshosting, Tweaknews, …) # sonarr TV radarr movies bazarr subtitles # jellyfin media server # @@ -80,6 +81,21 @@ services: - ${CONFIG_DIR}/qbittorrent:/config - ${DATA_DIR}:/data # single shared tree -> hardlinks + # ---------------------------------------------------------- usenet / NZB # + # SABnzbd talks to Usenet providers over SSL (no VPN required). + # Incomplete + complete under /data/usenet so Sonarr/Radarr can hardlink. + sabnzbd: + image: lscr.io/linuxserver/sabnzbd:latest + container_name: sabnzbd + restart: unless-stopped + networks: [arr-net] + ports: + - "${SABNZBD_PORT:-8085}:8080" + environment: *arr-env + volumes: + - ${CONFIG_DIR}/sabnzbd:/config + - ${DATA_DIR}:/data + # -------------------------------------------------------------- indexers # prowlarr: image: lscr.io/linuxserver/prowlarr:latest diff --git a/quickstart.md b/quickstart.md index c5f6123..8b720ee 100644 --- a/quickstart.md +++ b/quickstart.md @@ -68,6 +68,7 @@ LAN URLs (UFW allows **`192.168.8.0/24`**). Replace host if needed. | Radarr | http://192.168.8.123:7878 | | Bazarr | http://192.168.8.123:6767 | | Lidarr | http://192.168.8.123:8686 | +| SABnzbd | http://192.168.8.123:8085 | | Open WebUI | http://192.168.8.123:3000 | | Bonsai API | http://192.168.8.123:8081/v1 | | OpenHands | http://192.168.8.123:3001 | diff --git a/scripts/configure-usenet.sh b/scripts/configure-usenet.sh new file mode 100755 index 0000000..90f2c14 --- /dev/null +++ b/scripts/configure-usenet.sh @@ -0,0 +1,167 @@ +#!/usr/bin/env bash +# Bootstrap SABnzbd folders + Usenet servers from secrets/.env +# Run on the stack host after: docker compose up -d sabnzbd +# +# ./scripts/configure-usenet.sh +# SECRETS_FILE=./secrets/.env SAB_URL=http://127.0.0.1:8085 ./scripts/configure-usenet.sh + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +STACK_DIR="${STACK_DIR:-$(cd "$SCRIPT_DIR/.." && pwd)}" +SECRETS_FILE="${SECRETS_FILE:-$STACK_DIR/secrets/.env}" +SAB_URL="${SAB_URL:-http://127.0.0.1:8085}" + +log() { echo "[configure-usenet] $*"; } +die() { log "ERROR: $*"; exit 1; } + +[[ -f "$SECRETS_FILE" ]] || die "missing $SECRETS_FILE (see secrets/.env.example)" + +# shellcheck disable=SC1090 +set -a +# shellcheck disable=SC1091 +source "$SECRETS_FILE" +set +a + +# Accept both clean names and the keys already in secrets/.env +NZBGEEK_API_KEY="${NZBGEEK_API_KEY:-${NZBKEEPKEY:-}}" +NZBGEEK_URL="${NZBGEEK_URL:-${NZBGEEKAPI:-https://api.nzbgeek.info/}}" +NEWSHOSTING_HOST="${NEWSHOSTING_HOST:-${NEWSHOSTINGAPI:-news.newshosting.com}}" +NEWSHOSTING_PORT="${NEWSHOSTING_PORT:-${NEWSHOSTINGSSLPORT:-563}}" +[[ "$NEWSHOSTING_PORT" == "653" ]] && NEWSHOSTING_PORT=563 # common typo +NEWSHOSTING_USER="${NEWSHOSTING_USER:-${NEWSHOSTINGUSER:-}}" +NEWSHOSTING_PASS="${NEWSHOSTING_PASS:-${HNEWSHOSTINGPASS:-${NEWSHOSTINGPASS:-}}}" +NEWSHOSTING_CONNECTIONS="${NEWSHOSTING_CONNECTIONS:-20}" + +TWEAKNEWS_HOST="${TWEAKNEWS_HOST:-news.tweaknews.eu}" +TWEAKNEWS_PORT="${TWEAKNEWS_PORT:-563}" +TWEAKNEWS_USER="${TWEAKNEWS_USER:-${TWEAKNEWSUSER:-}}" +TWEAKNEWS_PASS="${TWEAKNEWS_PASS:-${TWEAKNEWSPASS:-}}" +TWEAKNEWS_CONNECTIONS="${TWEAKNEWS_CONNECTIONS:-10}" + +find_api_key() { + local f + for f in \ + "${CONFIG_DIR:-$STACK_DIR/config}/sabnzbd/sabnzbd.ini" \ + "$STACK_DIR/config/sabnzbd/sabnzbd.ini" \ + "/opt/stack/config/sabnzbd/sabnzbd.ini" \ + ; do + if [[ -f "$f" ]]; then + grep -E '^api_key[[:space:]]*=' "$f" | head -1 | cut -d= -f2- | tr -d '[:space:]' + return 0 + fi + done + # docker exec fallback + if command -v docker >/dev/null 2>&1; then + docker exec sabnzbd sh -c 'grep -E "^api_key" /config/sabnzbd.ini 2>/dev/null | head -1' \ + | cut -d= -f2- | tr -d '[:space:]' || true + fi +} + +log "Waiting for SABnzbd at $SAB_URL ..." +for i in $(seq 1 90); do + if curl -fsS "$SAB_URL/api?mode=version&output=json" >/dev/null 2>&1; then + break + fi + sleep 2 + [[ $i -eq 90 ]] && die "SABnzbd not reachable — is the container up? (docker compose up -d sabnzbd)" +done + +api_key="$(find_api_key)" +[[ -n "$api_key" ]] || die "no api_key yet — open $SAB_URL once to finish first-run, then re-run this script" + +sab_set() { + # set_config: section + keyword + value OR servers with name= + fields + curl -fsS -G "${SAB_URL}/api" \ + --data-urlencode "mode=set_config" \ + --data-urlencode "apikey=${api_key}" \ + --data-urlencode "output=json" \ + "$@" +} + +log "Folders: incomplete + complete under /data/usenet" +sab_set --data-urlencode "section=misc" --data-urlencode "keyword=download_dir" \ + --data-urlencode "value=/data/usenet/incomplete" >/dev/null +sab_set --data-urlencode "section=misc" --data-urlencode "keyword=complete_dir" \ + --data-urlencode "value=/data/usenet/complete" >/dev/null + +# Relative category folders under complete_dir +add_category() { + local name="$1" + sab_set \ + --data-urlencode "section=categories" \ + --data-urlencode "name=${name}" \ + --data-urlencode "dir=${name}" \ + --data-urlencode "pp=3" \ + >/dev/null || true +} +add_category movies +add_category tv +add_category music + +add_server() { + local name="$1" host="$2" port="$3" user="$4" pass="$5" conns="$6" + if [[ -z "$user" || -z "$pass" ]]; then + log "skip server '$name' (missing user/password in secrets)" + return 0 + fi + log "Usenet server: $name → $host:$port (ssl, ${conns} conn)" + # https://sabnzbd.org/wiki/configuration/5.0/api — servers via set_config + name= + sab_set \ + --data-urlencode "section=servers" \ + --data-urlencode "name=${name}" \ + --data-urlencode "displayline=${name}" \ + --data-urlencode "host=${host}" \ + --data-urlencode "port=${port}" \ + --data-urlencode "username=${user}" \ + --data-urlencode "password=${pass}" \ + --data-urlencode "connections=${conns}" \ + --data-urlencode "ssl=1" \ + --data-urlencode "enable=1" \ + --data-urlencode "priority=0" \ + --data-urlencode "timeout=60" \ + >/dev/null +} + +add_server "Newshosting" \ + "$NEWSHOSTING_HOST" "$NEWSHOSTING_PORT" \ + "$NEWSHOSTING_USER" "$NEWSHOSTING_PASS" \ + "$NEWSHOSTING_CONNECTIONS" + +add_server "Tweaknews" \ + "$TWEAKNEWS_HOST" "$TWEAKNEWS_PORT" \ + "$TWEAKNEWS_USER" "$TWEAKNEWS_PASS" \ + "$TWEAKNEWS_CONNECTIONS" + +log "Restart SABnzbd to pick up servers" +if command -v docker >/dev/null 2>&1 && docker ps --format '{{.Names}}' | grep -qx sabnzbd; then + docker restart sabnzbd >/dev/null + sleep 5 +fi + +log "" +log "=== SABnzbd ready ===" +log "UI: $SAB_URL" +log "API: (Config → General) — use this key in Prowlarr/*arr" +log "" +log "=== Prowlarr (NZBgeek) ===" +log "Indexers → Add Indexer → NZBgeek" +log " URL: ${NZBGEEK_URL}" +if [[ -n "$NZBGEEK_API_KEY" ]]; then + log " API Key: ${NZBGEEK_API_KEY}" +else + log " API Key: (set NZBKEEPKEY or NZBGEEK_API_KEY in secrets/.env)" +fi +log "" +log "Download Clients → Add → SABnzbd" +log " Host: sabnzbd Port: 8080 API Key: (from SABnzbd)" +log " (sync to apps so Sonarr/Radarr get SABnzbd too, or add per-app)" +log "" +log "=== Sonarr / Radarr / Lidarr ===" +log "Download Clients → SABnzbd → Host sabnzbd Port 8080" +log " Category: tv | movies | music" +log "Root folders still /data/media/{tv,movies,music} (hardlinks on same /data)" +log "" +if [[ -z "$TWEAKNEWS_USER" || -z "$TWEAKNEWS_PASS" ]]; then + log "NOTE: Tweaknews user/pass not in secrets — add TWEAKNEWS_USER / TWEAKNEWS_PASS and re-run." +fi diff --git a/secrets/.env.example b/secrets/.env.example new file mode 100644 index 0000000..2db0a9e --- /dev/null +++ b/secrets/.env.example @@ -0,0 +1,27 @@ +# Copy to secrets/.env and fill in. NEVER commit secrets/.env +# Used by scripts/configure-usenet.sh and as a personal password vault. + +# --- NZBgeek (indexer — wire in Prowlarr) --- +NZBGEEK_USERNAME= +NZBGEEK_EMAIL= +# API key from nzbgeek.info account (not the base URL) +NZBGEEK_API_KEY= +# Optional base URL (default https://api.nzbgeek.info/) +NZBGEEK_URL=https://api.nzbgeek.info/ + +# --- Newshosting (Usenet provider — SABnzbd server) --- +NEWSHOSTING_HOST=news.newshosting.com +NEWSHOSTING_PORT=563 +NEWSHOSTING_SSL=1 +NEWSHOSTING_USER= +NEWSHOSTING_PASS= +# Connections: start ~20 (plan-dependent) +NEWSHOSTING_CONNECTIONS=20 + +# --- Tweaknews (Usenet provider / fill-in block) --- +TWEAKNEWS_HOST=news.tweaknews.eu +TWEAKNEWS_PORT=563 +TWEAKNEWS_SSL=1 +TWEAKNEWS_USER= +TWEAKNEWS_PASS= +TWEAKNEWS_CONNECTIONS=10