Nazgarr
A self-hosted tool that manages, in one place: your media library, your torrent seeding folders, the hardlink correspondence between them, the real state of your configured torrent clients (multi-client, cross-seed included), and publishing new uploads to trackers.
Not specific to Unraid or the *arr stack — it works with any layout of separate disks (no FUSE/RAID required), and Sonarr/Radarr/qBittorrent-style tools are optional integrations, never dependencies. The name is a stylistic nod to the *arr naming convention (like Bazarr/Prowlarr), nothing more.
Current status: the backend is complete and tested (filesystem/hardlink scan, multi-client torrent indexing, TMDB content identification, the matching/reseeding engine for both directions, scheduling, and the upload module) and exposed as a JSON API under /api/*. The web UI (React + shadcn/ui, frontend/) is functionally complete — Dashboard, Library (tree/grid/orphaned), Reseeding (review queue, runs), Upload (wizard + queue) and Configuration (disks, torrent clients, trackers, settings) are all implemented and served by the same container. The walkthrough below still drives the API directly (curl, a REST client, or your own scripts) since that works identically whether or not you're also using the UI — see docs/SPEC.md §10 for the UI design and frontend/README.md for frontend-specific dev instructions.
- Functional/architectural spec:
docs/SPEC.md - DB schema:
docs/schema.sql - Interactive API docs: once running,
http://<host>:8080/docs(Swagger UI, generated by FastAPI from the same code — the most convenient way to explore every endpoint and its exact request/response shape)
License: GPL-3.0 (see LICENSE).
Requirements
- Docker (recommended), or Python 3.12 to run it directly.
- A layout of one or more disks/mounts containing your media and torrent-client seeding folders. The TrashGuide convention (a single combined
torrents/+ media folder, shared with your download client and media manager so hardlinks resolve) is the simplest setup and the one assumed by the defaults below, but it is not required — seedocs/SPEC.md§2/§4 for the fully generic model. - Optional, only if you use the matching/reseeding engine, TMDB identification, or uploads: a TMDB API key (free), a UNIT3D-based tracker account with an API token, and a supported torrent client (qBittorrent, for now — see §5 of the spec for what's planned next).
Quick start (Docker Compose)
git clone https://github.com/lktorrentz/nazgarr.git
cd nazgarr
cp .env.example .env
Edit .env and set APP_SECRET_KEY — it encrypts credentials (tracker tokens, torrent client passwords) at rest in the database, and signs the login session token. Generate one with:
openssl rand -base64 32 | tr '+/' '-_'
Edit docker-compose.yml if your library isn't at ./data relative to the compose file, then:
docker compose up -d
On first visit, the web UI asks you to create the admin account (single user, JWT-based — never HTTP basic auth). Until you do, every page and API endpoint stays open, exactly like earlier versions with no login at all — so upgrading an existing instance never locks you out unannounced.
On first start, the container creates config/config.yaml from config.example.yaml if it doesn't already exist. Everything except disk_scan_root/data_dir (disks, media paths, trackers, torrent clients, thresholds, the schedule) lives in the app's own database and is configured through the API below — no file editing, no restart needed for any of it.
Check it's up:
curl http://localhost:8080/api/health
# {"status":"ok"}
Creating your account
Nazgarr is closed until you create its single administrator account. On the first start (and at every restart until the account exists) the container log prints a one-time setup code:
docker logs nazgarr
# No account yet: open Nazgarr and create it with this setup code: 3fT9-kQ2xY
Open the web UI, enter the code, a username and a password. Only someone who can read the container log can create the account, so nobody else on your network can take it over. To set the code yourself (for automated setups), use the NAZGARR_SETUP_CODE environment variable.
Unraid
A Community-Applications-style template is published at unraid/nazgarr-template.xml — add it as a custom template pointing at that raw GitHub URL, or download it and add it manually via "Add Container" → "Template" in the Unraid Docker UI. It follows the same TrashGuide layout as the compose file above (one combined /data mount).
Without Docker (Python package)
Every stable release also ships a Python package with the web UI already built inside, so neither Docker nor Node is needed. You need Python 3.12 or newer, pipx, and mediainfo and ffmpeg from your package manager:
sudo apt install pipx mediainfo ffmpeg # Debian/Ubuntu
brew install pipx media-info ffmpeg # macOS
Install it from PyPI:
pipx install nazgarr
The same package is also attached to every stable release (the .whl asset), if you prefer to install from there: pipx install <URL of the .whl>.
Set it up once, pointing --scan-root at the folder your disks live under (for example /mnt or /srv):
nazgarr init --scan-root /mnt
This writes config.yaml and a secret key (secret.key, readable only by you) to ~/.config/nazgarr (macOS: ~/Library/Application Support/Nazgarr). The database lives in ~/.local/share/nazgarr. Back up the secret key with the database: it encrypts the stored credentials, and without it they can't be read. init also tells you if mediainfo or ffmpeg are missing.
Run it in the foreground with nazgarr serve (--host, --port; default 0.0.0.0:8080), or install it as a service that starts at boot:
nazgarr install-service
systemctl --user daemon-reload && systemctl --user enable --now nazgarr # Linux (systemd)
sudo loginctl enable-linger $USER # Linux: start without logging in
launchctl load -w ~/Library/LaunchAgents/io.github.lktorrentz.nazgarr.plist # macOS (launchd)
The first start prints the one-time setup code in the service log (journalctl --user -u nazgarr on Linux, ~/Library/Logs/Nazgarr/nazgarr.log on macOS), as with Docker.
Things to know:
- Paths: Nazgarr sees your real filesystem, so there's no volume mapping. Disks are the real paths under
--scan-root. If your torrent client runs in a container while Nazgarr doesn't (or the reverse), set the client's root path per disk under Configuration → Torrent clients. - User: run it as the user that owns your media and torrent folders. Hardlinks need write access, and every folder of a disk must be on the same filesystem.
- One process only: never start it with several workers. The upload worker, the scheduler and the watched folder run inside the process.
- Updating:
pipx upgrade nazgarr, then restart the service. Only stable versions are published as a package; the test builds (:nightly) are Docker only. - Windows: not tested yet. Hardlinks work on NTFS; run
nazgarr servethrough WinSW or NSSM if you want to try it.
Release channels
Two image tags, pick one:
ghcr.io/lktorrentz/nazgarr:stable(same image as:latest): releases promoted by hand once they've been tested. This is the tag to use if you just want to run it, and the Unraid template uses it.ghcr.io/lktorrentz/nazgarr:nightly: a test build for every push tomain(published as a GitHub prerelease). It moves fast and may break.
Every version is also published as :X.Y.Z. Configuration → Application → Check for updates follows the channel you're on. On a stable version it only offers newer stable releases; on a test build it offers every newer build.
First run: registering a disk and scanning your library
There's no setup wizard yet — this is the sequence a future UI would drive, done by hand against the API.
Every call needs your login token. Get one once and reuse it (valid 30 days):
TOKEN=$(curl -s -X POST http://localhost:8080/api/auth/login -H 'Content-Type: application/json' \
-d '{"username": "admin", "password": "your-password"}' | python3 -c 'import json,sys; print(json.load(sys.stdin)["access_token"])')
and add -H "Authorization: Bearer $TOKEN" to the commands below.
1. Register a disk. Point root_path at wherever you mounted your combined torrents+media folder inside the container (/data by default, matching config.example.yaml):
curl -X POST http://localhost:8080/api/disks \
-H 'Content-Type: application/json' \
-d '{"label": "main", "root_path": "/data"}'
# -> {"id": 1, "label": "main", "root_path": "/data", "torrents_rel_path": null, "st_dev": ...}
2. Add a media path — a subfolder under the disk that holds actual media (movies or TV), so the scanner knows what to index and how to classify it:
curl -X POST http://localhost:8080/api/disks/1/media-paths \
-H 'Content-Type: application/json' \
-d '{"relative_path": "media/movies", "content_type": "movie"}'
Repeat for a TV path ("content_type": "tv") if you have one. The disk's torrent seeding folder (e.g. torrents/) doesn't need its own registration — it's discovered by the scan itself.
3. (Optional) Add a torrent client, so the scan can tell which seeding files are actually tracked vs. orphaned, and so the reseeding engine can add new torrents automatically:
curl -X POST http://localhost:8080/api/torrent-clients \
-H 'Content-Type: application/json' \
-d '{"label": "qbit", "adapter_type": "qbittorrent", "base_url": "http://qbittorrent:8080", "username": "admin", "password": "your-password"}'
# -> {"id": 1, ...}
curl -X POST http://localhost:8080/api/torrent-clients/1/test # verifies the connection
curl -X POST http://localhost:8080/api/torrent-clients/1/disks/1 # enables this client for disk 1
4. (Optional) Set your TMDB API key, so scanned files get identified and matched against a tracker's catalog:
curl -X PUT http://localhost:8080/api/settings/tmdb_api_key \
-H 'Content-Type: application/json' \
-d '{"value": "your-tmdb-api-key"}'
5. (Optional) Add a tracker, if you want automatic matching/reseeding or the upload module — see docs/SPEC.md §6 for what search_by_tmdb needs and §9 for uploads:
curl -X POST http://localhost:8080/api/trackers \
-H 'Content-Type: application/json' \
-d '{"label": "mytracker", "adapter_type": "unit3d", "base_url": "https://mytracker.example", "api_token": "your-api-token", "announce_url": "https://mytracker.example/announce/your-passkey"}'
6. Run a bulk import — scans every registered disk, resolves content via TMDB (if configured), indexes every enabled torrent client, runs the matching engine against every enabled tracker, and auto-executes anything above the confidence threshold:
curl -X POST http://localhost:8080/api/runs
# -> {"id": 1, "run_type": "bulk_import", "current_phase": "scanning", ...}
curl http://localhost:8080/api/runs/1 # poll until finished_at is set
7. Check the results:
curl http://localhost:8080/api/library/items # your library, grouped by resolved content
curl http://localhost:8080/api/dashboard # library health %, pending review, orphan/ignored counts
curl http://localhost:8080/api/reviews # matches below the auto-approve threshold, waiting on you
8. (Optional) Schedule it to run periodically instead of triggering it by hand every time:
curl -X PUT http://localhost:8080/api/schedule \
-H 'Content-Type: application/json' \
-d '{"cron": "0 */6 * * *"}' # every 6 hours, standard 5-field cron
Full endpoint reference: http://<host>:8080/docs.
Plugins, webhooks and API
Nazgarr can be extended with plugins (trackers, torrent clients, media resolvers, image hosts, notification services), sends signed webhooks for its events, and exposes its JSON API to scripts through API keys. See docs/SDK.md, and examples/nazgarr-ntfy for a complete plugin.
Local development
python3 -m venv .venv
./.venv/bin/pip install -r requirements.txt && ./.venv/bin/pip install -r requirements-dev.txt
cp config.example.yaml config.yaml # edit disk_scan_root/data_dir as needed
export APP_SECRET_KEY=$(openssl rand -base64 32 | tr '+/' '-_')
./.venv/bin/uvicorn nazgarr.main:app --reload --port 8080
Lint and tests (same checks as CI):
./.venv/bin/ruff check .
./.venv/bin/pytest -q
Frontend
cd frontend
npm install
npm run dev # http://localhost:5173, proxies /api to the backend above (see vite.config.ts)
See frontend/README.md for details (type generation from the backend's OpenAPI schema, build, structure). In production the built frontend is served by FastAPI itself from the same container — no separate Node process (see the Dockerfile's frontend-build stage and nazgarr/frontend.py).
Contributing
Issues and pull requests are welcome — docs/SPEC.md is the source of truth for design decisions already made; if something looks wrong or incomplete, open an issue rather than silently deviating from it.
Metadata
Release files for nazgarr 0.7.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| nazgarr-0.7.0.tar.gz | 1.3 MB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| nazgarr-0.7.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 2.5 MB
Release files / nazgarr-0.7.0.tar.gz
| Download URL | nazgarr-0.7.0.tar.gz |
|---|---|
| Size | 1.3 MB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
b7c81c20958d07cec581ee6cb09e623991f7e4e16672b92bfc4e8fb49b8fe46d
|
|
BLAKE2b-256 checksum How to use checksums |
2bbe46b535dc760365a71e7e19affc87a77067c80c39e90fa1cf1209781f9a4e
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 2, 2026.
Transparency logRelease files / nazgarr-0.7.0-py3-none-any.whl
| Download URL | nazgarr-0.7.0-py3-none-any.whl |
|---|---|
| Size | 1.2 MB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
f07fabcf6370d6c5d07344800cd7b8f726f8329c4dbf138dcc4f6e8f216c2e63
|
|
BLAKE2b-256 checksum How to use checksums |
0ee05db325936e9aa4e9352e6813e89fea0aa64f5b3387815efb69d2f93b6a37
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 2, 2026.
Transparency log