A minuscule Newznab-compatible server that exposes a local directory of .nzb files to AIOStreams, Prowlarr, or anything else that can query a Newznab indexer.
It is for setups where NZBs already exist locally — for example alongside nzbdavex/AIOStreams — but your search client still needs something that behaves like an indexer.
- Serves Newznab
caps,search,tvsearch,movie, andgetover/api.GET /also returns caps. - Recursively scans a local NZB directory; keeps only parsed metadata in memory. The
.nzbfiles on disk are the source of truth — no database. - Parses filenames with
guessitto infer movie/episode metadata and category. - Reads NZB XML segment sizes so results report the release payload size, not the
.nzbfile size. - Emits Newznab RSS with category, size, file count, title, year, season, episode, resolution, source, codec, release group, and more when available.
The example compose file pulls the published image from GitHub Container Registry and mounts ./nzbs into the container as /nzbs:
cp .env.example .env # then set a real API_KEY (see Configuration)
docker compose -f docker-compose.example.yml up -dOr run it without compose:
docker run -d --name nzbserver -p 8000:8000 \
-e API_KEY="$(openssl rand -hex 32)" \
-v /srv/nzbs:/nzbs:ro \
ghcr.io/needforseed1/localnzbs:latest(docker run pulls the image automatically; use docker pull ghcr.io/needforseed1/localnzbs:latest to fetch updates.)
- Default image:
ghcr.io/needforseed1/localnzbs:latest. To build locally, replace theimage:line withbuild: .. - Change the volume and
8000:8000port lines if your NZBs live elsewhere or port 8000 is taken. - The container runs as
1000:1000. Change theuser:line if a different user/group should read the shared directory.
The host side of the ./nzbs mount must be the same directory where nzbdave/nzbdavex saves downloaded NZBs. Mount that one host directory into both containers:
services:
nzbserver:
volumes:
- /srv/nzbs:/nzbs:ro
nzbdavex:
volumes:
- /srv/nzbs:/output-nzbs:rwLocalNZBs only reads the directory, so mount it read-only with :ro.
The container-side paths may differ, but the host path must match (/srv/nzbs above). Configure nzbdavex to save NZBs into /output-nzbs; keep LocalNZBs on NZB_DIR=/nzbs.
Requires Python 3.11+.
python3 -m venv .venv
. .venv/bin/activate
pip install -e .
API_KEY=$(openssl rand -hex 32) NZB_DIR=/path/to/nzbs uvicorn nzbserver.app:app --host 0.0.0.0 --port 8000Then query:
http://localhost:8000/api?t=caps&apikey=YOUR_KEY
http://localhost:8000/api?t=search&q=some.title&apikey=YOUR_KEY
http://localhost:8000/api?t=get&id=ITEM_ID&apikey=YOUR_KEY
| Variable | Default | Description |
|---|---|---|
NZB_DIR |
/nzbs |
Directory containing .nzb files. Scanned recursively. |
PROVIDER_NAME |
LocalNZBs |
Name exposed to clients as the provider/indexer. |
API_KEY |
change-me |
Required. /api returns 403 until this is changed from change-me. Requests must then include apikey. |
BASE_URL |
request host | Public base URL used in generated download links. |
REFRESH_SECONDS |
10 |
Minimum seconds between directory rescans. |
UI_PASSWORD |
(unset) | Password for the web UI at /ui. The UI is disabled until this is set. |
TRASH_DIR |
<NZB_DIR>/.trash |
Where deleted NZBs are moved. Excluded from indexing. |
HOST |
0.0.0.0 in Docker |
Docker command host binding. |
PORT |
8000 in Docker |
Docker command port. |
.env.example ships API_KEY at a placeholder value (change-me). The server treats that placeholder as "not set" and fails closed:
API_KEYleft atchange-me(or empty):/apireturns403until you set a real key. No open access.
Generate a real secret with:
openssl rand -hex 32Put the value in .env (or the container environment), then give the same API_KEY to Prowlarr/AIOStreams.
Note:
GET /returns caps without an API key. It exposes only the provider name (no NZB data) and is convenient as a health probe.
A built-in web interface lives under /ui for browsing indexed NZBs, inspecting per-release metadata, viewing stats, and (mass-)deleting releases.
- Disabled by default. It fails closed until
UI_PASSWORDis set — same model asAPI_KEY. Sign in at/ui/loginwith that password. - Auth is separate from
API_KEY: the indexer key is for Prowlarr/AIOStreams,UI_PASSWORDis for the browser. The session is stored in a signed cookie. - Browse (
/ui/): filter by query, category, or media type; sortable columns; paginated; per-row and select-all checkboxes for bulk actions. - Stats (
/ui/stats): totals, breakdown by category/resolution/release group, largest and most-recent releases. - Delete moves to trash, it does not unlink. Selected
.nzbfiles are moved toTRASH_DIR(default<NZB_DIR>/.trash), which is excluded from indexing, so they vanish from search results but remain recoverable on disk. Empty the trash directory yourself when you are sure.
Requires a writable mount. Deleting writes to the NZB directory, so the volume must be mounted
:rw(the example compose file does this). The same directory is shared with nzbdave/nzbdavex and AIOStreams — deleting an NZB here removes the release those tools rely on. If you only want browsing/stats with no risk of modification, keep the mount:ro; delete actions will then fail and the on-disk files stay untouched.
Add LocalNZBs anywhere that accepts a Newznab source:
- URL:
http://host:8000/api - API Path: leave as default if Prowlarr asks
- API Key: the
API_KEYyou set (required) - Categories: enable Movies and/or TV
Searches return locally saved NZBs whose filenames match the requested title. TV searches also filter on season/episode when the client sends them.
For best results, add LocalNZBs to AIOStreams directly as a Newznab source, and keep Prowlarr for remote Usenet indexers only.
That split keeps two independent cache paths: AIOStreams can cache slow remote Prowlarr searches while querying LocalNZBs through the direct Newznab path, which you can disable so newly saved local NZBs appear immediately (see Caching).
Routing LocalNZBs through the Prowlarr addon instead caches local and remote results together. AIOStreams can then keep serving a stale Prowlarr result that was cached before nzbdavex saved the local NZB.
Disable LocalNZBs in the AIOStreams Prowlarr addon selection, then add LocalNZBs to Ranked Stream Expressions with a high score, for example 100000:
/* LocalNZBs */ indexer(type(streams, 'usenet'), 'LocalNZBs')
This makes matching LocalNZBs streams sort above normal remote results.
Also add this stream expression under Filters → Stream Expression → Excluded:
pin(indexer(type(streams, 'usenet'), 'LocalNZBs'), 'top')
The pin() expression belongs in Excluded because it marks matching Usenet streams as pinned without removing them. Do not put this expression in a required/included filter, or it can act like a filter instead of just pinning.
LocalNZBs must match the indexer name shown in AIOStreams results. By default, that comes from PROVIDER_NAME=LocalNZBs. If you rename the provider, update the expression to match the displayed indexer name.
If local NZBs appear only after restarting AIOStreams, its Prowlarr search cache may have stored a result before nzbdavex saved the local NZB. Set these in the AIOStreams .env file or container environment, not in LocalNZBs:
BUILTIN_PROWLARR_SEARCH_CACHE_TTL=900
BUILTIN_NAB_SEARCH_CACHE_TTL=0BUILTIN_PROWLARR_SEARCH_CACHE_TTL=900— keep remote searches cached 15 min: short enough for new releases, light on remote indexers.BUILTIN_NAB_SEARCH_CACHE_TTL=0— direct Newznab searches against LocalNZBs stay fresh, so newly saved NZBs appear on the next search.
Recreate AIOStreams after changing these so the old in-memory cache is dropped: docker compose up -d.
Metadata comes from the NZB filename, not the NZB contents or any external provider. Release-style names work best:
Some.Movie.2024.1080p.WEB-DL.x265-GROUP.nzb
Some.Show.S02E05.2160p.WEB-DL.x265-GROUP.nzb
If the title/year/season/episode are not recognizable from the filename, the item can still surface in broad search, but Prowlarr matching is weaker.