Self-hosted BitTorrent + arr-stack + catalog-update pipeline targeting
nullstone (Debian 13). Replaces the legacy onyx -> rsync -> import
round-trip.
Contents:
- README.md headline + ASCII architecture diagram + quickstart
- CLAUDE.md project rules (mirrors beta-flix style)
- .gitignore secrets dirs (.env, gluetun, qbt config, ssh keys)
- .gitleaksignore allowlist nullstone LAN addr + Tailscale CGNAT
- docs/architecture.md the plan in detail (gluetun + qbt + arr + catalog)
- docs/migration.md onyx-qbt -> nullstone-qbt runbook (3 phases)
- docs/trackers.md tracker schema + IP-pinning + ratio notes (user-curated)
- compose/docker-compose.yml gluetun v3.40 + qbt 5.0.5 (netns=gluetun) +
sonarr/radarr/prowlarr (hotio) + betaflix-catalog
- compose/.env.example documented env-var template (no secrets)
- compose/traefik/arr.yml file-provider for qbt/sonarr/radarr/prowlarr
.s8n.ru subdomains, LAN+TS only via
trusted-only@file + authentik-forwardauth@file
- catalog/catalog.py Flask service, ~340 LoC, /sonarr + /radarr +
/healthz; pulls beta-flix, inserts alphabetic
row into MEDIA-LIST.md, writes run log, commits
+ pushes as obsidian-ai. Idempotent via
payload-hash cache.
- catalog/Dockerfile python:3.12-slim + git + tini
- catalog/requirements.txt flask + jinja2 + requests + gitpython + pyyaml (pinned)
- catalog/templates/*.j2 run log + catalog row Jinja templates
- catalog/README.md service docs
- scripts/migrate-onyx.sh phase-2 helper (rsync + .torrent ship, dry-run by default)
- scripts/add-tracker.sh Prowlarr API helper
- scripts/killswitch-test.sh gluetun kill-switch verification (3 steps)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
109 lines
3.7 KiB
Bash
Executable file
109 lines
3.7 KiB
Bash
Executable file
#!/usr/bin/env bash
|
|
# scripts/migrate-onyx.sh — Phase 2 migration helper.
|
|
#
|
|
# Usage:
|
|
# ./migrate-onyx.sh <source-dir-on-onyx> <target-dir-on-nullstone>
|
|
#
|
|
# Example:
|
|
# ./migrate-onyx.sh "$HOME/Downloads/qbt/" \
|
|
# /home/user/media/_downloads/complete/
|
|
#
|
|
# What it does:
|
|
# 1. rsync <source-dir> → user@nullstone:<target-dir> over LAN.
|
|
# 2. Copies onyx's .torrent + .fastresume files to /tmp/qbt-migrate/
|
|
# on nullstone (you mass-add them via qbt webui afterwards).
|
|
# 3. Prints a checklist of remaining manual steps.
|
|
#
|
|
# Pre-reqs:
|
|
# - run from onyx (the SOURCE machine).
|
|
# - ssh user@nullstone reachable on LAN.
|
|
# - nullstone qbt stack already up (Phase 1 complete) — check with
|
|
# `bash killswitch-test.sh` first.
|
|
|
|
set -euo pipefail
|
|
|
|
SRC="${1:-}"
|
|
DST="${2:-}"
|
|
NULLSTONE="${NULLSTONE_SSH:-user@192.168.0.100}"
|
|
DRY_RUN="${DRY_RUN:-1}"
|
|
|
|
usage() {
|
|
cat <<EOF
|
|
Usage: $0 <source-dir> <target-dir-on-nullstone>
|
|
|
|
Env:
|
|
NULLSTONE_SSH default: user@192.168.0.100
|
|
DRY_RUN default: 1 (rsync --dry-run). Set DRY_RUN=0 to actually copy.
|
|
|
|
Example (dry-run):
|
|
$0 "\$HOME/Downloads/qbt/" /home/user/media/_downloads/complete/
|
|
|
|
Example (real):
|
|
DRY_RUN=0 $0 "\$HOME/Downloads/qbt/" /home/user/media/_downloads/complete/
|
|
EOF
|
|
exit 2
|
|
}
|
|
|
|
[ -z "$SRC" ] || [ -z "$DST" ] && usage
|
|
[ -d "$SRC" ] || { echo "Source dir not found: $SRC" >&2; exit 1; }
|
|
|
|
QBT_BACKUP="$HOME/.local/share/qBittorrent/BT_backup"
|
|
[ -d "$QBT_BACKUP" ] || { echo "qBittorrent BT_backup dir missing: $QBT_BACKUP" >&2; exit 1; }
|
|
|
|
RSYNC_FLAGS=(-av --info=progress2 --partial --human-readable)
|
|
if [ "$DRY_RUN" = "1" ]; then
|
|
RSYNC_FLAGS+=(--dry-run)
|
|
echo "=== DRY-RUN — no data will be copied. Set DRY_RUN=0 to run for real. ==="
|
|
fi
|
|
|
|
echo "=== Step 1/3: rsync data files to nullstone ==="
|
|
rsync "${RSYNC_FLAGS[@]}" "$SRC" "$NULLSTONE:$DST"
|
|
|
|
echo
|
|
echo "=== Step 2/3: ship .torrent + .fastresume to nullstone /tmp/qbt-migrate/ ==="
|
|
ssh "$NULLSTONE" "mkdir -p /tmp/qbt-migrate"
|
|
if [ "$DRY_RUN" = "1" ]; then
|
|
echo "(dry-run) would scp $QBT_BACKUP/*.torrent $QBT_BACKUP/*.fastresume → $NULLSTONE:/tmp/qbt-migrate/"
|
|
else
|
|
scp -q "$QBT_BACKUP"/*.torrent "$QBT_BACKUP"/*.fastresume "$NULLSTONE:/tmp/qbt-migrate/"
|
|
fi
|
|
|
|
echo
|
|
echo "=== Step 3/3: remaining manual steps ==="
|
|
cat <<EOF
|
|
|
|
Manual steps on the nullstone qbt webui (https://qbt.s8n.ru):
|
|
|
|
1. "Add Torrent" → multi-select all files in /tmp/qbt-migrate/*.torrent
|
|
Save path: $DST
|
|
[ ] UNCHECK "Start torrent" — we want them queued, not auto-resumed.
|
|
|
|
2. Select all newly-added torrents → right-click → "Force recheck"
|
|
qbt will hash-match files in $DST → mark each at 100% → start seeding.
|
|
|
|
3. Check tracker status per torrent. Private trackers may reject the new
|
|
source IP (Proton exit). See docs/trackers.md for the per-tracker
|
|
mitigation playbook.
|
|
|
|
4. On onyx (THIS machine): pause torrents one-by-one as nullstone takes
|
|
over each. Do NOT stop onyx-qbt entirely until every torrent shows
|
|
seeding on nullstone for 24h with zero tracker errors.
|
|
|
|
Fastresume path-rewrite (optional, only if save_path drift breaks recheck):
|
|
|
|
ssh $NULLSTONE 'python3 - <<PYEOF
|
|
import os, re, sys
|
|
from pathlib import Path
|
|
src_prefix = "/home/admin/Downloads/qbt" # onyx path
|
|
dst_prefix = "/downloads/complete" # nullstone path inside qbt container
|
|
for fr in Path("/tmp/qbt-migrate").glob("*.fastresume"):
|
|
data = fr.read_bytes()
|
|
if src_prefix.encode() in data:
|
|
new = data.replace(src_prefix.encode(), dst_prefix.encode())
|
|
fr.write_bytes(new)
|
|
print("rewrote:", fr.name)
|
|
PYEOF'
|
|
|
|
(Only run if force-recheck fails — qbt 5 usually handles re-pathing via
|
|
the UI's "Save path" field on add.)
|
|
EOF
|