The Stremio addon
zurg can answer Stremio as an addon: the client asks for streams by IMDb id, zurg searches your Newznab indexers, and picking a stream pulls that release's NZB into the Usenet backend and plays it — the search-per-play model streamnzb had, on top of zurg's library, repair and archive streaming. Everything played this way also lands in the library, so it shows up in Plex like any other release.
Configuration
The addon needs the nzb provider (it is what serves the bytes) and at least
one indexer:
stremio:
enabled: true
# token: "" # generated and kept in data/stremio-token when empty
indexers:
- name: my-indexer
url: https://indexer.example
api_key: YOUR_KEY
- name: second-indexer
url: https://second-indexer.example
api_key: YOUR_KEY
api_path: /api/v1/api # for an indexer that 404s the default /api
All of it is on the config page under Stremio Addon — the switch, the
indexer list and the result cap — and the addon URL is shown there with a copy
button, built from the address you opened the dashboard on. That is the address
to use: see
On startup zurg logs the addon URL:
<zurg>/stremio/<token>/manifest.json
Paste it into Stremio (Addons → the search box takes a URL). The token in the path is the whole authorization — Stremio clients send no auth of their own, so the routes sit outside basic auth the way the SABnzbd endpoint's do. Treat the URL like a password: the play links it produces carry your indexer api keys, and anyone holding it can spend your indexer grabs and news-server connections.
Installing it on another device
Stremio only takes an addon over plain http on the computer Stremio itself
runs on. That means http://127.0.0.1 or http://localhost on the zurg machine.
Everywhere else it needs https. A TV or a phone or a second computer shows
failed to load manifest for an http:// address even when it can reach zurg
just fine. That includes a LAN address and a Tailscale address. The config page
says so next to the addon URL whenever you opened it over plain http on such an
address.
zurg does not serve https itself. Put something in front of it that does.
Then open the config page on the https address and copy the addon URL from
there. zurg builds the addon URL and every play link from the address each
request came in on. There is no public URL setting and base_url is not used
here. One zurg can serve a LAN player and a remote one at the same time.
Whatever sits in front has to tell zurg two things. The name comes from the
Host header or from X-Forwarded-Host. The scheme comes from
X-Forwarded-Proto: https. Without that header the addon loads but the play
links come back as http:// and playback fails instead. Each option below sends
both.
Stremio uses two paths. /stremio/ is the addon. /strm/e/ is where pressing
play sends the player for the video. Both work without zurg's password because
Stremio cannot send one. The token in the addon URL is what protects them. When
zurg goes on the internet publish only those two paths. Everything else is your
dashboard and your library.
Tailscale Serve
The simplest option when every device that runs Stremio can run Tailscale. zurg stays off the internet.
- In the Tailscale admin console turn on MagicDNS and HTTPS certificates.
-
Run this on the zurg machine. Use your own port if zurg is not on 9999.
tailscale serve --bg 9999 - Open
https://<machine>.<tailnet>.ts.net/config/and copy the addon URL from the Stremio Addon card.
Tailscale gets the certificate and sends both headers. tailscale serve reset
turns it off again.
A device that cannot run Tailscale can use Tailscale Funnel instead. It puts the same address on the internet. Publish only what Stremio needs.
tailscale funnel --bg --set-path /stremio http://127.0.0.1:9999/stremio
tailscale funnel --bg --set-path /strm/e http://127.0.0.1:9999/strm/e
The config page is not on that address. Copy the addon URL from the page as you
normally open it and swap the start for https://<machine>.<tailnet>.ts.net.
Tailscale limits how much bandwidth a Funnel gets. A 4K remux may stutter.
Your own reverse proxy
Already running a reverse proxy with a domain and a certificate? Add zurg to it.
Caddy keeps the Host header and sends X-Forwarded-Proto on its own.
This publishes the two Stremio paths and nothing else.
zurg.example.com {
handle /stremio/* {
reverse_proxy 127.0.0.1:9999
}
handle /strm/e/* {
reverse_proxy 127.0.0.1:9999
}
handle {
respond 404
}
}
nginx sends neither header unless you ask for both.
location ~ ^/(stremio|strm/e)/ {
proxy_pass http://127.0.0.1:9999;
proxy_set_header Host $http_host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_buffering off;
}
Put that inside the server block that already listens on 443 with your
certificate. proxy_buffering off lets the video stream straight through
rather than through nginx's buffers.
Cloudflare Tunnel
A named tunnel gives zurg an https address on a domain you have on Cloudflare
without opening a port. Cloudflare keeps the Host header and sends
X-Forwarded-Proto on its own. Route only the Stremio paths in the tunnel's
config.yml.
ingress:
- hostname: zurg.example.com
path: ^/(stremio|strm/e)/
service: http://127.0.0.1:9999
- service: http_status:404
Two things to know first. Cloudflare gives up on a request after 100 seconds. The first play of a release can take longer than that. Press play again and it starts. And Cloudflare's terms let it limit accounts that send a lot of video through its network without a paid plan. Every play goes through the tunnel.
What a play does
Picking a stream is a grab: zurg downloads the NZB from the indexer, drops it
into nzbs/ (through the same naming rules as the SABnzbd endpoint, so a
release grabbed twice — or already grabbed by an *arr — is found, not
duplicated), waits for the library to list it, and redirects the player into
the signed /strm/e/ endpoint that every .strm file already plays through:
ranged reads, account failover, RAR/7z interiors.
The first play of a release therefore takes longer than a debrid stream — the NZB has to be fetched and parsed, and an archive's interior read — typically a few seconds, up to a minute or two for an obfuscated post whose naming pass has to run. A play that outlasts the wait answers 503 with a Retry-After; pressing play again lands on the fast path. Every later play is immediate.
A release that lists and turns out to hold nothing playable — usually one
that has aged off the spool, every file marked broken by the news-server
check — is not the end of the click. The play URL carries the request it was
minted for, so zurg ranks the title's releases again and tries the rows after
the one picked, each through the same grab-and-list path; the first that plays
is what the player is redirected into. It walks the whole answer rather than
only the rows the list rendered: max_results is how much a person wants to
scroll, and letting it bound recovery too meant a title shown ten rows deep
could only ever try nine, however much the search had already found. The rows after the pick on purpose: they are the same resolution and
smaller, or the resolution below, never a bigger release the user passed over.
A fallback that is itself still ingesting answers the same 503, and pressing
play again resolves it — the refused release is refused at once and the
fallback is found on the fast path. Only that final refusal falls through; a
503 never does. Each try spends an indexer grab and leaves its release in the
library like any other grab, so the chain is bounded by a budget of twenty-five
seconds and a backstop of six tries. The budget is the governor because the
tries do not cost the same: a release that has aged off the spool is refused in
under a second, while one that ingests and opens an archive before turning out
to hold nothing can take most of the budget alone. When every row tried is
refused the answer is the 404 it always was. Each fallback is
logged at info with the release refused and the one tried next.
Measured against three other usenet-backed Stremio addons on 7 September 2026: stremio-addon-findings-2026-09-07.md. The short version is that the per-resolution cap fixed what the previous round found and coverage still halved, because a five-deep bucket has nowhere to go when its first playable entry is a release that has aged off the spool. The fallback above is the first fix from that round; the bucket is still five deep.
Search behaviour
-
Movies search
t=movie, episodest=tvsearchwithseason/ep, both byimdbid— as bare digits, because several indexers answer zero results for thettform. -
Each indexer is asked for
search_limitreleases (default 100). Every limit below chooses among what this asked for, so it is the ceiling on the answer. -
Indexers are searched in parallel; one refusing (a burst limit, a dead key) costs its results, not the list. Refusals are logged per indexer.
-
Results are deduplicated by release name, ranked resolution-first then size, and capped at
max_results(default 10) per resolution. The cap counts per resolution because the sort leads with 2160p: a popular title has more UHD releases under the size ceiling than any cap, so counting across the whole list answers every popular title with nothing but remuxes, whatever else the search found. -
What each resolution keeps is a spread of its sizes, not its largest few: evenly spaced picks from the largest release down to the smallest, both always included. Kept from the top, a 1080p tier of five is five remuxes between 19 and 30 GB, and the 4 GB encode a phone or a laptop direct-plays is exactly what the cap removes — measured on 7 September 2026, three of zurg's fifteen entries for The Shawshank Redemption were under 6 GB against forty such releases on the same indexers. The list stays size-descending within a resolution. A release whose size the indexer did not state has no place on that axis, so it only fills whatever room the sized releases leave.
-
Each stream's description carries the release name, its size, the indexer that found it, and how long ago it was posted —
3d,126d,2y— when the indexer said. Ageing off the news server was the most common reason a play failed in the 7 September round, and the post date is the one signal a viewer can steer around it with. -
Releases larger than
max_size_gb(default 40) are dropped before ranking — the resolution-first sort would otherwise put full-disc UHD remuxes at the top of every list. Sizes the indexer did not state are kept. The gate is applied when the list is rendered, so changing the ceiling takes effect on cached titles too. -
Results are cached on disk (
data/stremio-cache/, one file per title), so reopening a title costs no indexer calls — they are often day-quota'd, and Treasure Maps refuses after roughly six rapid ones. How long an answer lives scales with how much the search found, because a thin list is evidence the uploads are still arriving or an indexer was down: one result lives one hour, two live two, up to four; five or more is a settled answer and livescache_hours(default 24 — the only configurable rung). An empty answer is never cached. A cached stream list carries a refresh item at the bottom saying how old it is; opening that item (it opens in a browser, not the player) clears the cache for that title, and reopening the title searches fresh. The cache survives restarts and expired entries clean themselves up. -
Season 0 (specials) is refused rather than searched wrongly, and id-search coverage is the indexer's: a release an indexer only finds via text search will not appear.