Skip to main content

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 — see docs/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 serve through 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 to main (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.6.64

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for nazgarr 0.6.64
File Size Uploaded
nazgarr-0.6.64.tar.gz 1.2 MB Details

Built distribution (wheel)

Table of built distributions (wheels) for nazgarr 0.6.64
File Interpreter ABI Platform
nazgarr-0.6.64-py3-none-any.whl Python 3 none any Details

Total release size: 2.4 MB

Release files / nazgarr-0.6.64.tar.gz

Download URL nazgarr-0.6.64.tar.gz
Size 1.2 MB
Tags Source
SHA-256 checksum
How to use checksums
7e5653d0d637e60776f6da0191b996d178b2eafc2318961babda5e411d00b045
BLAKE2b-256 checksum
How to use checksums
03a3cc24d6b9960d6b2cb4fab54befef67872860206d9b2dcab610a2dc86d2fe
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

Release files / nazgarr-0.6.64-py3-none-any.whl

Download URL nazgarr-0.6.64-py3-none-any.whl
Size 1.2 MB
Tags Python 3
SHA-256 checksum
How to use checksums
1f2a47d8f1b20e78b38c9e0873a56cb636996277cf2b62fadaf947fff9d8d204
BLAKE2b-256 checksum
How to use checksums
b3c5ac85cf6d8701f54068fc73291f9ea15122a1f8b98175b93917c9c3895485
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

Release history Release notifications | RSS feed

0.8.9

2 release files

0.8.5

2 release files

0.8.2

2 release files

0.8.0

2 release files

0.7.5

2 release files

0.7.2

2 release files

0.7.0

2 release files

This release

0.6.64 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page