# media-acquisition Self-hosted BitTorrent + arr-stack + canonical-import pipeline that lands torrents directly on **nullstone**, through a Proton WireGuard VPN with verified kill-switch, hardlinks files into the existing ARRFLIX library, and auto-commits catalog rows to `git.s8n.ru/s8n/beta-flix`. Replaces the legacy `onyx → rsync → nullstone` round-trip. ## Architecture ``` +-----------------+ | Proton VPN | | (WireGuard) | +--------+--------+ | wg0 v +------------+ indexer queries +-------------------+ torrent traffic | Prowlarr |-------------------->| gluetun |<------------------+ +-----+------+ (via netns) | kill-switch fw | | | +-------------------+ | | search ^ ^ ^ | v | | | | +------------+ grabs +----------+ | +----------+ | | Sonarr/ |----------->| qBittorrent (netns=gluetun) | | Radarr | | /home/user/media/_downloads/{incomplete,complete} +-----+------+ +-------------------------------------------------+ | | OnImport webhook (POST /sonarr or /radarr) v +--------------------+ | betaflix-catalog |--+ XFS reflink/hardlink into /home/user/media/{movies,tv} | (Flask, Python) | | +--------+-----------+ +--> Jellyfin (tv.s8n.ru) picks up new items | | git commit + push (obsidian-ai) v +-----------------------------+ | git.s8n.ru/s8n/beta-flix | | playbooks/import-media/ | | MEDIA-LIST.md (updated) | | runs/.md (new) | +-----------------------------+ ``` Single XFS filesystem at `/home/user/media` → hardlinks / reflinks are free. ## Quickstart ```bash # Clone on nullstone ssh user@nullstone git clone https://git.s8n.ru/s8n/media-acquisition.git /opt/docker/media-acquisition cd /opt/docker/media-acquisition/compose # Configure cp .env.example .env ${EDITOR:-vi} .env # fill in PVPN_WG_PRIVKEY, PVPN_WG_ADDRESSES, FORGEJO_PUSH_TOKEN, etc. # Bring up docker compose up -d # Verify VPN kill-switch (CRITICAL — do not skip) bash ../scripts/killswitch-test.sh # Sanity: pick a sacrificial legal torrent in qbt UI, confirm it lands in # /home/user/media/_downloads/complete/ and arr stack hardlinks it. ``` ## Layout ``` README.md This file. CLAUDE.md Project rules for Claude Code. docs/ architecture.md The plan in detail. Decision log + reasoning. migration.md onyx-qbt → nullstone-qbt migration runbook. trackers.md Tracker schema + IP-pinning notes (user fills in). compose/ docker-compose.yml Full stack: gluetun + qbt + sonarr + radarr + prowlarr + catalog. .env.example All env vars documented. traefik/arr.yml Traefik file-provider for *.s8n.ru subdomains (LAN+TS only). catalog/ catalog.py Flask webhook receiver → beta-flix catalog updater. Dockerfile python:3.12-slim base. requirements.txt Pinned versions. templates/ Jinja2 templates for run logs and catalog rows. README.md Catalog service docs. scripts/ migrate-onyx.sh Phase-2 migration: rsync + .torrent mass-add. add-tracker.sh Helper: add tracker to Prowlarr via API. killswitch-test.sh Verify gluetun blocks traffic when VPN drops. ``` ## Related - Plan: `docs/architecture.md` - Catalog target: `git.s8n.ru/s8n/beta-flix` (`playbooks/import-media/MEDIA-LIST.md`) - Jellyfin (consumer): `tv.s8n.ru` (`jellyfin-stock` container on nullstone) - Host docs: `ai-lab/SYSTEM.md` ## Status Scaffold. Live deploy pending VPN slot allocation + tracker IP-pinning review. Next step: fill in `compose/.env` and bring up gluetun + qbt only (no arr yet) to validate kill-switch.