Command your home media server from your phone. π΄ββ οΈ
A mobile-first, installable web app that unifies the *arr stack β Sonarr, Radarr, Prowlarr, Bazarr and qBittorrent β behind one dark, pirate-themed interface.
Brand: a black-bear pirate captain β deep-sea-blue + midnight-black UI, treasure-gold accents, mutiny-red for destructive actions, Pirata One display type. Docker service names stay
blackbeard-*for continuity with the running stack.
The app, in six areas:
- Add β search and add movies (Radarr) and series (Sonarr) with full quality / monitor options. Results show the IMDb/TMDb rating and an "In library" badge for titles you already have. A Person mode searches actors & directors (TMDb) and lists their filmography.
- Trending β what's hot to grab: Trending (this week), Popular, For You (recommendations from your Radarr/Sonarr history via TMDb), and Watched (un-hide). One-tap add, a "hide" button to mark titles seen, and titles already watched in Jellyfin are auto-hidden. Already-owned titles are flagged.
- Upcoming β monitored titles awaiting release: movies with their digital/physical/ cinema dates and series episodes by air date, each with an "in X days" countdown (from the Radarr/Sonarr calendars).
- Downloads β live state of qBittorrent torrents, Sonarr/Radarr import queues and Bazarr wanted-subtitle counts, auto-refreshing every 5s. Includes a one-tap "Search wanted subtitles" that runs Bazarr's missing-subtitle tasks.
- Library β browse everything in Radarr/Sonarr (size on disk, filterable). When Jellyfin is connected: Continue watching + Recently added rows, a Watched badge and a "watched only" filter (to find what's safe to delete). Delete a title removes it from the *arr and, optionally, its files from disk (confirmation dialog + "delete files" checkbox).
- Settings & Diagnostics β configure each service (keys persist in
config.json, shown as "Saved β"), test connections, optional auto-cleanup (remove a finished torrent once it hits a ratio or a max seed time β whichever first β freeing space; skips anything still importing), and inspect health, versions, disk space, indexer status, providers, warnings, container logs/restart.
Mobile-first and installable: open it on your phone and Add to Home Screen to run it fullscreen like a native app (PWA manifest, no input-focus zoom, no overscroll bounce).
βββββββββββββββββββ
phone βββββββΊ β blackbeard-web β (React + Vite, served by nginx)
ββββββββββ¬ββββββββββ
β /api/* (reverse-proxied)
ββββββββββΌββββββββββ
β blackbeard-api β (Node + Express)
ββββββββββ¬ββββββββββ
ββββββββββββ¬βββββββ΄ββββ¬βββββββββββ¬ββββββββββββββ
Sonarr Radarr Prowlarr Bazarr qBittorrent
The backend is the only thing that holds API keys β the frontend never sees them.
The browser calls /api/..., and the backend makes the authenticated call to each
service. No database: everything is read live from the service APIs. The only persisted
state is config.json (URLs + keys), editable entirely from the Settings tab.
- Backend: Node.js + Express (ESM), native
fetch,dockerodefor container control. - Frontend: React + Vite + Tailwind CSS,
lucide-reacticons. - Config: a single
config.jsonfile (seeded from env vars, then UI-authoritative). - Deploy: two Docker containers joined to the existing
servarr_defaultnetwork.
Two terminals.
Backend (defaults to port 3000):
cd backend
npm install
npm run dev # node --watch src/index.jsOn first run it creates backend/config.json. You can leave it empty and fill keys via
the UI, or pre-seed via env vars (see below).
Frontend (Vite dev server on port 5173, proxies /api to the backend):
cd frontend
npm install
npm run devOpen http://localhost:5173. To point the dev proxy at a non-default backend:
BACKEND_URL=http://localhost:3000 npm run devThe docker-compose.yml is meant to be brought up alongside the existing servarr stack.
It joins the external servarr_default network so it can reach the other containers
by name.
cd blackbeard
docker compose up -d --buildThen open http://192.168.1.134:8085 (the web container publishes port 8085).
What the compose file does:
- blackbeard-api β the backend. Mounts
./configfor the persistedconfig.json, and mounts the Docker socket so the "Restart container" / "Logs" diagnostics work. - blackbeard-web β nginx serving the built frontend, reverse-proxying
/apitoblackbeard-api:3000.
Network name. This assumes your stack's network is
servarr_default. Confirm withdocker network ls. If it differs, change thenetworksblock at the bottom ofdocker-compose.yml.
Docker socket. Mounting
/var/run/docker.sockgrants the backend container control over Docker. That's required for restart/logs and is acceptable on a trusted LAN. Remove that volume line if you don't want it β the rest of the app still works, and the buttons simply disable themselves.
Open the app β Settings tab. For each service set the URL and key, hit Test
connection, then Save settings. Inside Docker the default URLs use container names
(http://sonarr:8989, etc.); from a dev machine on the LAN use http://192.168.1.134:<port>.
Where to find each API key:
| Service | Where to get the key |
|---|---|
| Sonarr | Settings β General β API Key |
| Radarr | Settings β General β API Key |
| Prowlarr | Settings β General β API Key |
| Bazarr | Settings β General β API Key (header is X-API-KEY) |
| qBittorrent | No API key β uses the Web UI username + password (default user admin) |
| TMDb | themoviedb.org β account Settings β API β API Key (v3). Needed for Trending |
| Jellyfin | Jellyfin β Dashboard β API Keys. URL is usually host.docker.internal:8096 or the LAN IP. Powers watch state |
Secrets are write-only from the UI: the backend returns whether a key is set, never the value. Leaving a key field blank on save keeps the existing one.
If you'd rather pre-fill the first config.json, set any of these on the backend before
its first start (also listed, commented, in docker-compose.yml):
PORT, CONFIG_PATH, DOCKER_SOCKET
SONARR_URL, SONARR_API_KEY, SONARR_CONTAINER
RADARR_URL, RADARR_API_KEY, RADARR_CONTAINER
PROWLARR_URL, PROWLARR_API_KEY, PROWLARR_CONTAINER
BAZARR_URL, BAZARR_API_KEY, BAZARR_CONTAINER
QBITTORRENT_URL, QBITTORRENT_USERNAME, QBITTORRENT_PASSWORD, QBITTORRENT_CONTAINER
TMDB_URL, TMDB_API_KEY
JELLYFIN_URL, JELLYFIN_API_KEY, JELLYFIN_USER_ID
Once you save in the UI, config.json wins and the env seeds are ignored.
All endpoints are under /api. The frontend uses these; you can also call them directly.
| Method | Path | Purpose |
|---|---|---|
| GET | /api/search?type=movie|series&term=... |
Lookup via Radarr/Sonarr |
| GET | /api/search/person?q=... |
Search people (actors/directors) via TMDb |
| GET | /api/search/person/:id |
A person's movies + series (filmography) |
| GET | /api/add/quality-profiles?type=movie|series |
List quality profiles |
| GET | /api/add/root-folders?type=movie|series |
List root folders |
| POST | /api/add |
Add { type, item, options } |
options (movie): qualityProfileId, rootFolderPath?, monitored, minimumAvailability, searchOnAdd.
options (series): qualityProfileId, rootFolderPath?, monitor, seasonFolder, seriesType, searchOnAdd.
| Method | Path | Purpose |
|---|---|---|
| GET | /api/downloads |
Torrents + Sonarr/Radarr queues + Bazarr wanted |
| POST | /api/downloads/torrents/:hash/pause |
Pause a torrent |
| POST | /api/downloads/torrents/:hash/resume |
Resume a torrent |
| DELETE | /api/downloads/torrents/:hash?deleteFiles=... |
Remove a torrent (optionally files) |
| POST | /api/downloads/bazarr/search-wanted |
Run Bazarr's "search missing subtitles" tasks |
| Method | Path | Purpose |
|---|---|---|
| GET | /api/pipeline?movieDays=365&episodeDays=90 |
Monitored, not-yet-available movies + episodes from the Radarr/Sonarr calendars, sorted by date |
| Method | Path | Purpose |
|---|---|---|
| GET | /api/trending?mode=trending|popular |
Trending-this-week or popular movies + series (TMDb) |
| GET | /api/trending/recommended |
"For You" β TMDb recommendations seeded from your Radarr/Sonarr library |
| POST | /api/trending/seen |
Hide an item { type, tmdbId } (persisted) |
| DELETE | /api/trending/seen |
Un-hide an item { type, tmdbId } |
| Method | Path | Purpose |
|---|---|---|
| GET | /api/library |
All Radarr movies + Sonarr series (size + watched) |
| GET | /api/library/ids |
Owned TMDb ids (to flag "in library" elsewhere) |
| DELETE | /api/library/movie/:id?deleteFiles=true |
Delete a movie from Radarr (and disk) |
| DELETE | /api/library/series/:id?deleteFiles=true |
Delete a series from Sonarr (and disk) |
| Method | Path | Purpose |
|---|---|---|
| GET | /api/jellyfin |
Continue watching + recently added for the user |
| GET | /api/jellyfin/image/:id |
Poster proxy (Jellyfin is LAN-only; streamed for remote) |
| Method | Path | Purpose |
|---|---|---|
| GET | /api/settings |
Current config (secrets masked) |
| POST | /api/settings |
Save { services: {...} } (blank secrets kept) |
| POST | /api/settings/test |
Test a service { service } |
| Method | Path | Purpose |
|---|---|---|
| GET | /api/diagnostics |
Health, versions, disk, indexers, providers, warnings |
| GET | /api/diagnostics/logs/:service?tail= |
Docker logs (needs socket) |
| POST | /api/diagnostics/restart/:service |
Restart container (needs socket) |
:service is one of sonarr, radarr, prowlarr, bazarr, qbittorrent.
Git is the source of truth; secrets never go in git β they live only in
config/config.json on the host (git-ignored), entered through the Settings tab.
Typical loop, editing from any machine and running Docker on the home server (e.g. a Mac mini):
# on your laptop
git clone git@github.com:<you>/blackbear.git
cd blackbear
# ...edit, then:
git commit -am "feat: ..." && git push
# deploy to the server (SSHes in, pulls, rebuilds)
./scripts/deploy.sh
# or override the target:
BLACKBEAR_HOST=user@host BLACKBEAR_DIR=/srv/blackbear ./scripts/deploy.shFor local development without the server, run the dev servers (backend/ npm run dev
and frontend/ npm run dev) and point them at the live services over the LAN.
Recommended upgrades:
- Tailscale β SSH to the server and reach the web UI by a stable name from anywhere,
no port-forwarding. Replace the LAN IP in
deploy.shwith the Tailscale host. - CI images (GHCR) β a GitHub Action can build and push the two images to
ghcr.ioon every push tomain; the server then justdocker compose pull && up -dinstead of building locally. Faster deploys and the server needs no source checkout.
β οΈ Blackbear has no built-in auth and can restart containers (Docker socket). Never expose it to the internet unprotected. The setup below puts it behind Cloudflare Access, which authenticates you before any traffic reaches the app.
A cloudflared service ships in docker-compose.yml under the cloudflare profile, so
it only runs when you ask for it. You need a domain managed in Cloudflare.
-
Create the tunnel. Cloudflare Zero Trust dashboard β Networks β Tunnels β Create a tunnel β Cloudflared β name it
blackbear. On the Docker tab, copy the token and put it in a local.env(git-ignored β see.env.example):CLOUDFLARE_TUNNEL_TOKEN=eyJ...your-token... -
Map a public hostname. Still in the tunnel config, add a Public Hostname:
- Subdomain
blackbear, your domain, path empty - Service: HTTP β
blackbeard-web:80
- Subdomain
-
Start the tunnel (alongside the stack):
docker compose --profile cloudflare up -d
Tip: add
COMPOSE_PROFILES=cloudflareto your.envso plaindocker compose up -d(andscripts/deploy.sh) always bring the tunnel up β no need to pass the flag each time. -
Gate it with Access. Zero Trust β Access β Applications β Add an application β Self-hosted β domain
blackbear.yourdomain.com. Add a policy: Allow, with a ruleEmails β your@email. Pick One-time PIN (or Google) as the login method.
Now https://blackbear.yourdomain.com prompts you to log in (email code), then serves the
app β UI and /api both, through the single hostname. The LAN port 8085 keeps working at
home; the tunnel is purely for outside access.
Tip: add your phone-friendly login method and you can still Add to Home Screen the tunnel URL for a native-feeling app away from home.
- No auth. Intended for the LAN, to live behind Tailscale later. Auth is a v2 item.
- Resilient by design. If a service is offline or misconfigured, its panel shows an error but the rest of the app keeps working β one dead Prowlarr won't sink the ship.
- qBittorrent 5.0 renamed
pause/resumetostop/start; Blackbear tries the new endpoints and falls back to the old ones automatically.
Yo ho ho and a bottle of rum. π΄ββ οΈ

