A self-hosted torrent streaming media server — search, stream a torrent to your browser before it finishes downloading, transcode on the fly with hardware acceleration, and watch (Safari included, via HLS). Go backend, React UI embedded in a single binary, runs in Docker.
JackUI started as a visual front-end for Jackett and grew into a full media server: find a release, start playing it while it downloads, and let the server transcode incompatible codecs to something your browser can play. No "wait for the download to finish", no separate transcoding box.
JackUI is an independent project — it is not affiliated with or endorsed by Jackett.
Note
The web UI is internationalised (react-i18next): Portuguese and English ship today (web/src/locales/{pt,en}.json). This README and the developer docs are in English.
┌──────────┐ magnet / .torrent
browser ──────► │ JackUI │ ──────────────────────────┐
(search) │ (Gin) │ ▼
└────┬─────┘ ┌──────────────────┐
│ GET /api/stream/:hash │ anacrolix/torrent │
browser ◄────────────┤ (HTTP + Range) │ BitTorrent swarm │
<video> ▲│ └────────┬─────────┘
││ HLS (.m3u8 + .ts) or raw MP4 Range │ pieces
│▼ ▼
┌─────────┐ incompatible codec? ┌───────────────┐
│ ffmpeg │ ◄──── HEVC/AV1/AC3/DTS ──────── │ disk cache │
│transcode│ NVENC / VAAPI / QSV / x264 │ (LRU evict) │
└─────────┘ └───────────────┘
The torrent is exposed as a seekable HTTP source with Range support, so ffmpeg (and the browser) can start reading from any offset before the download completes. Incompatible codecs are transcoded on demand; Safari is served HLS because its <video> rejects progressive MP4 over chunked transfer.
- Stream before download — torrent → HTTP with Range; playback starts on the first pieces. Disk cache with LRU eviction (favourites evicted last).
- On-demand transcode — HEVC/AV1/x265 → H.264 via GPU (NVENC/VAAPI/QSV) or libx264 fallback. Safari gets HLS; everyone else gets direct-play or progressive transcode. The GPU is bounded: a semaphore caps concurrent CUDA decoders (
JACKUI_MAX_GPU_TRANSCODES, default 3) and an extra session falls back to CPU-decode + NVENC-encode onCUDA_ERROR_OUT_OF_MEMORY, so playback never hard-fails for lack of VRAM. - Music player — audio playlists with gapless playback, selectable HLS audio track (torrent + local), cover art / chapters /
mediaSession(lock-screen + AirPlay on Safari/iOS), and a footer mini-player you can drag and expand. - Swarm health on cards —
SeedBadgeshows seeders + availability. A background tracker scrape (BEP 48, private trackers included) backs the count so an active torrent never sits at a misleading 0; results are cached. - Subtitles — embedded (ffmpeg probe), sidecar
.srt/.vttinside the torrent, and external (OpenSubtitles). Choice persists per file. - Discover & recommendations — a Discover page with weekly-trending titles, personalised recommendations seeded from your favourites + watch history (with dismiss), and a Music mode showing trending albums (keyless Apple RSS). Both seed the search on click.
- TMDB enrichment — posters + metadata on results and library (PostgreSQL cache, 30-day TTL).
- Per-torrent artwork — resolved and persisted by
info_hashthrough a fail-safe chain: embedded torrent image → TMDB poster → keyless web image search → captured frame. - AI title cleanup (optional) — an OpenAI-compatible chain (Groq / OpenRouter / Ollama) cleans raw release names before the TMDB lookup, with per-provider fallback + circuit breaker. A tunable benchmark (Settings → admin) scores providers on accuracy, latency and cost/energy, keeps history, and reorders the chain.
- Downloads queue — the internal worker treats a multi-file torrent as one unit: the scheduler counts a slot per torrent and steers the anacrolix file priorities (selected = download, the rest = cancel), so one big pack no longer hogs every slot or spikes CPU/RAM. The list groups one card per torrent; you can filter completed into Seeding vs On-disk and sort by speed / seeds / date / name / size / progress. Downloads land under a category folder by default, with a destination picker (incl. browse into mounts) and auto-seed of completed torrents (re-seeded in place from the cached metainfo). qBittorrent/Transmission clients are also supported.
*arrprovider — exposes a Transmission-RPC-compatible endpoint so Sonarr/Radarr/Prowlarr can use JackUI as their download client (opt-in). See docs/TRANSMISSION_RPC.md.- Local files — browse configured mounts; promote to the library (
LocalPromoteModal, batch + per-row). A downloaded video on local disk seeks instantly (http.ServeFile/sendfile), while remote/rclone mounts get read-ahead + a whole-file LRU disk cache. Local play falls back to HLS when the container/codec needs it. Continue Watching tracks local items too; the list filters by downloading / done. - Library extras — Playlists, Watchlists (cron + ntfy push, opt-in auto-download with quality filters), Continue Watching (resume position), and an Incognito toggle that skips history/library writes. A global "reveal hidden" curtain hides flagged items across the UI.
- Low-footprint mode — the HLS pipeline shuts ffmpeg down when the last viewer leaves (no 5-min survival), the UI pauses its polling when the tab is hidden, and a balanced runtime profile (Go
GOGC/GOMEMLIMIT/GOMAXPROCS+JACKUI_MAX_CONNS/JACKUI_PEERS_HIGH) keeps idle memory low on a home server. - Desktop app (optional) — an Electron wrapper bundling the Go server, with a status tray, magnet deep-links, and native downloads. See
electron/. - Auth — JWT on by default (opt out only with
JACKUI_AUTH_ENABLED=0+JACKUI_ALLOW_INSECURE_AUTH=1) with rotated refresh tokens, roles, MFA/passkeys, andAdminOnlyroutes (incl. admin password reset). - Observability — public
/status(version/commit/buildTime), Prometheus/api/metrics(admin JWT orJACKUI_METRICS_TOKEN), opt-in/debug/pprof(JACKUI_PPROF_ENABLED+ token/admin JWT), structured logs (JACKUI_LOG_FORMAT=json), and scheduled bandwidth windows for the streamer.
| Layer | Tech |
|---|---|
| Backend | Go 1.26, Gin, anacrolix/torrent |
| Transcode | ffmpeg (NVENC / VAAPI / QSV / libx264), HLS for Safari |
| Frontend | React 18 + TypeScript + Vite + TailwindCSS, embedded via //go:embed all:dist |
| Storage | PostgreSQL (JACKUI_DATABASE_URL, unified schema via internal/db); disk cache for torrent pieces |
| Deploy | Docker, single binary + embedded UI |
- Go 1.25+ and Node 18+ (for development/building).
- ffmpeg with the hardware encoders you intend to use (NVENC needs an NVIDIA GPU + the NVIDIA container toolkit; VAAPI/QSV need
/dev/dri). - Docker (for deployment) and a Jackett instance for search.
# 1. Backend (Go server on :8989, serves the embedded UI)
make dev-backend
# 2. Frontend with hot-reload (Vite on :5173, proxies the API to :8989)
make dev-frontend
# 3. Run the test suite
make testOpen http://localhost:5173 (dev) or http://localhost:8989 (the embedded build). Configure your Jackett URL + API key in Settings, then search.
Runtime config comes from config.yaml plus environment overrides (env wins). Key variables:
All variables below accept empty/unset as "use default". The canonical default is compiled in or read from config.yaml; env wins when set.
| Category | Variable | Default | Purpose |
|---|---|---|---|
| Core | JACKUI_PORT |
8989 |
HTTP listen port |
JACKETT_URL |
http://localhost:9117 |
Jackett search backend | |
JACKETT_API_KEY |
— | Jackett API key | |
JACKUI_BASE_URL |
— | Public app URL (e-mail links, optional) | |
JACKUI_CONTROL_TOKEN |
— | Static token for RPC control endpoint (port refresh) | |
| PostgreSQL | JACKUI_DATABASE_URL |
— | Required. DSN (postgres://...). Overrides PG_* below |
DATABASE_URL |
— | Same as above (alternative name) | |
JACKUI_PG_HOST |
localhost |
Host | |
JACKUI_PG_PORT |
5432 |
Port | |
JACKUI_PG_USER |
jackui |
User | |
JACKUI_PG_PASSWORD |
— | Password | |
JACKUI_PG_DB |
jackui |
Database name | |
JACKUI_PG_SSLMODE |
disable |
SSL mode | |
| Directories | JACKUI_STREAM_DIR |
./data/stream |
Piece cache + transcode/HLS temp (reconstructable) |
JACKUI_DOWNLOAD_DIR |
./data/downloads |
Completed download target (empty = stay in cache) | |
JACKUI_SHARED_DIR |
./data/shared |
Shared library: browsable mounts + promote target | |
JACKUI_EXTERNAL_MOUNTS |
— | Extra mount paths (rclone/NFS, comma-separated) | |
| Streamer perf | JACKUI_STREAM_MAX_GB |
50 |
Piece-cache size cap (LRU eviction) |
JACKUI_STREAM_DOWN_MBPS |
— | Download rate limit (MB/s) | |
JACKUI_STREAM_UP_MBPS |
— | Upload rate limit (MB/s) | |
JACKUI_READAHEAD_MB |
— | Read-ahead for sequential file serving (MiB) | |
JACKUI_STORAGE_BACKEND |
— | Storage backend driver (mmap etc.) |
|
JACKUI_HALF_OPEN |
— | Max half-open TCP connections | |
JACKUI_PIECE_HASHERS |
— | Piece hash checker workers | |
JACKUI_SEED_TRACKERS |
— | Extra announce trackers for seeding | |
| Local cache | JACKUI_LOCAL_READAHEAD_MB |
16 |
Read-ahead buffer for local/rclone mounts (MiB) |
JACKUI_LOCAL_CACHE_GB |
50 |
Dedicated pre-fetch cache (GiB, LRU) | |
JACKUI_HLS_VOD_MODE |
all |
VOD seekbar: off, hlsjs, or all |
|
JACKUI_HLS_MEDIA_RENDITIONS |
0 |
HLS master EXT-X-MEDIA subtitle renditions (Phase 2 M2b). Audio renditions (TYPE=AUDIO) are always on when the source has ≥2 tracks. Enable (1) only after Safari X-TIMESTAMP-MAP validation for WebVTT. |
|
| Peer/Torrent | JACKUI_MAX_CONNS |
— | Peer connections per torrent |
JACKUI_PEERS_HIGH |
— | Swarm high-water mark | |
JACKUI_PEER_PORT |
— | Fixed listen port (overrides forwarded port) | |
JACKUI_MAX_GPU_TRANSCODES |
3 |
Cap on concurrent HW decoders (0 = unlimited) |
|
JACKUI_MAX_UPLOAD_MB |
— | Max upload per torrent (MiB) | |
JACKUI_IDLE_MINUTES |
30 |
Drop torrent after N min idle (files stay) | |
JACKUI_METADATA_SECONDS |
60 |
Metadata fetch timeout | |
JACKUI_TRANSMISSION_RPC_ENABLED |
0 |
Expose Transmission-RPC *arr provider (opt-in) |
|
| Downloads | JACKUI_DL_MAX_ACTIVE |
— | Max active downloads per user |
JACKUI_DL_PER_USER_MAX |
— | Max concurrent downloads per user | |
JACKUI_DL_STALL_MIN |
— | Stall timeout (minutes) | |
JACKUI_DL_MAX_STALLS |
— | Max stalls before abort | |
JACKUI_DL_AGING_STEP_MIN |
— | Aging step interval (minutes) | |
JACKUI_DL_AGING_CAP |
— | Aging cap | |
JACKUI_DL_ROTATION |
— | Download rotation strategy | |
JACKUI_DL_AUTO_PROMOTE_ARR |
0 |
Promote *arr (Transmission-RPC) completed downloads into SharedDir/<category>/ (1/true; needs JACKUI_SHARED_DIR) |
|
| Auth | JACKUI_AUTH_ENABLED |
1 |
JWT auth on by default; set 0 only with JACKUI_ALLOW_INSECURE_AUTH=1 (dev/LAN) |
JACKUI_ADMIN_USERNAME |
admin |
Admin username | |
JACKUI_ADMIN_PASSWORD |
— | Admin password | |
JACKUI_JWT_SECRET |
— | JWT signing secret (≥32 bytes, required with auth) | |
JACKUI_METRICS_TOKEN |
— | Static token for Prometheus /api/metrics |
|
JACKUI_PPROF_ENABLED |
0 |
Expose /debug/pprof (opt-in; never anonymous) |
|
JACKUI_PPROF_TOKEN |
— | Static token for /debug/pprof (else admin JWT; without either the routes stay off) |
|
| Notifications | JACKUI_NTFY_TOPIC |
— | ntfy.sh topic for watchlist push |
JACKUI_NTFY_URL |
https://ntfy.sh |
ntfy server URL | |
JACKUI_NTFY_TOKEN |
— | ntfy auth token (optional) | |
| AI title ID | GROQ_API_KEY |
— | Groq API key |
OPENROUTER_API_KEY |
— | OpenRouter API key | |
GEMINI_API_KEY |
— | Google Gemini API key | |
OPENCODE_API_KEY |
— | OpenCode Zen API key | |
OLLAMA_BASE_URL |
— | Ollama endpoint URL | |
JACKUI_AI_ENABLED |
— | Auto-enabled when any key is present | |
JACKUI_AI_MAX_COST_PER_1M |
— | Max cost per 1M tokens (USD) | |
JACKUI_AI_KWH_PRICE |
— | Electricity price for local-model cost | |
JACKUI_AI_LOCAL_WATTS |
— | Local model power draw (watts) | |
| Metadata | TMDB_API_KEY |
— | TMDB v3 API key (posters + clean titles) |
OMDB_API_KEY |
— | OMDb API key (IMDb rating) | |
| SMTP | JACKUI_SMTP_HOST |
— | SMTP server host |
JACKUI_SMTP_PORT |
587 |
SMTP port | |
JACKUI_SMTP_USER |
— | SMTP user | |
JACKUI_SMTP_PASS |
— | SMTP password | |
JACKUI_SMTP_FROM |
— | Sender address | |
| Observability | JACKUI_LOG_FORMAT |
— | Structured logs: json (empty = text) |
State lives in PostgreSQL (JACKUI_DATABASE_URL). The piece cache (JACKUI_STREAM_DIR) is deliberately separate to reduce I/O contention on torrent pieces.
Low-footprint runtime tuning (Go GOGC/GOMEMLIMIT/GOMAXPROCS) is applied via the process environment, not config.yaml; production sets it in the deploy compose (see below).
JackUI deploys as a Docker container to a home server. It runs on a bridge shared with a reverse proxy (e.g. Nginx Proxy Manager), reachable at jackui:8989.
make dev-backend # go run ./cmd/server on :8989
make dev-frontend # Vite :5173 with API proxymake deploy-auto # ✅ default: auto-detects GPU (NVENC/VAAPI/QSV), no VPN
make deploy-auto-vpn # same, but routes through a gluetun VPN overlay (opt-in)deploy-auto is the standard make target (auto-detects the GPU, no VPN); -vpn adds a gluetun overlay (network_mode: container:gluetun).
Note
The author's own production instance currently runs behind gluetun (network_mode: container:gluetun, on the VPN's forwarded port — watchForwardedPort in cmd/server/main.go triggers a graceful restart to rebind when the port rotates), even though the no-VPN path is the documented default. Pick the mode that keeps your swarm healthy.
CI/CD runs on GitHub Actions (PR + main gates, CodeQL, release → GHCR + Trivy pre-push + SBOM), documented in docs/CICD.md. Images publish to ghcr.io/lgldsilva/jackui. Homelab deploy is still a hand-maintained compose on the server (docker compose up -d --force-recreate) — the release job only publishes the image. New env vars added to the repo's compose do not reach production by themselves; edit the server-side compose too.
A short map; the canonical reference is docs/ARCHITECTURE.md.
cmd/server/main.go Gin wiring: /api/* + SPA fallback + background workers
ui/embed.go //go:embed all:dist (the React build ships inside the binary)
web/src/ React app (PlayerProvider lives above the router)
internal/
streamer/ anacrolix: Add / FileReader / probe / cache / favourites
transcode/ ffmpeg pipeline + HLS sessions (seek-restart)
handlers/ HTTP handlers (search, config, stream, local, classify, …)
config/ jackett/ downloader/ downloads/ tmdb/ subtitles/ parser/ db/
auth/ history/ library/ playlists/ watchlist/ PostgreSQL stores (unified pool)
transmissionrpc/ Transmission-RPC compatibility layer (*arr provider)
middleware/ cross-cutting Gin middleware (incognito, media-token auth)
- Auth is on by default — JWT with rotated refresh tokens;
AdminOnlyguards sensitive routes. Running public is opt-in and explicit (JACKUI_AUTH_ENABLED=0+JACKUI_ALLOW_INSECURE_AUTH=1, dev/LAN only) — the boot fails closed otherwise. Media elements (<video>/<track>) can't send headers, so media routes accept a scoped?token=. - SSRF guard — torrent
.torrent/metadata fetches are restricted to the configured Jackett host. - The
*arrRPC surface is opt-in (JACKUI_TRANSMISSION_RPC_ENABLED, default off) and, when auth is enabled, requires Basic Auth. - Local-file browser is confined to explicitly configured, read-only mounts.
What JackUI does not do: it is not hardened for public-internet exposure without a reverse proxy + auth. Run it behind your proxy on a trusted network.
- i18n / multi-language UI — done: react-i18next ships Portuguese + English (
web/src/locales/). - HLS master playlist (Phase 2): multi-resolution
#EXT-X-STREAM-INF+TYPE=AUDIOwhen the source has ≥2 tracks.TYPE=SUBTITLESstays behindJACKUI_HLS_MEDIA_RENDITIONSuntil Safari WebVTT is validated. - Streamer reconciles pieces with already-downloaded files (play without re-downloading).
- Promote on the local-files page (
LocalPromoteModal— per-row + batch). - Durable state migrated to PostgreSQL (favorites, metadata cache, downloads, etc. no longer live in the piece-cache dir).
| Doc | What |
|---|---|
| docs/ARCHITECTURE.md | Canonical architecture + reading order for new contributors |
| docs/design-decisions.md | Why the tricky bits are the way they are (and what not to "fix") |
| docs/TRANSMISSION_RPC.md | Using JackUI as a Sonarr/Radarr/Prowlarr download client |
| docs/CICD.md | Build pipeline, quality gates, deploy |
| docs/RCLONE.md | Tuning rclone/Google-Drive mounts for streaming (cache, chunking, read-ahead) |
| docs/PERFORMANCE.md | N+1 backlog, on-demand profiling (/debug/pprof), benchmarks in CI |
| docs/RELIABILITY_PROGRAM_AUDIT.md | Closing record of the reliability program (issue #80): phase → PR → evidence |
JackUI is a neutral tool: it searches indexers you configure and streams/downloads content over the BitTorrent protocol. It does not host, index, or distribute any content by itself, and it ships with no indexers or trackers configured.
You are solely responsible for what you search for, download, stream, and share with it. Make sure your use complies with the laws of your jurisdiction and with the rights of content owners. The authors and contributors do not condone copyright infringement and provide this software without any warranty (see LICENSE).
MIT. JackUI is built on third-party components (and, as a Docker image, redistributes ffmpeg and the NVIDIA CUDA runtime) — see THIRD_PARTY-LICENSES.md.