Metadata-Version: 2.4
Name: nazgarr
Version: 0.7.0
Summary: Media library, seeding folders, hardlinks, torrent clients and tracker uploads in one place.
License-Expression: GPL-3.0-only
Project-URL: Homepage, https://github.com/lktorrentz/nazgarr
Requires-Python: >=3.12
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: fastapi
Requires-Dist: uvicorn[standard]
Requires-Dist: sqlalchemy
Requires-Dist: apscheduler
Requires-Dist: httpx
Requires-Dist: qbittorrent-api
Requires-Dist: pymediainfo
Requires-Dist: guessit
Requires-Dist: torf
Requires-Dist: ffmpeg-python
Requires-Dist: pyimgbox
Requires-Dist: jinja2
Requires-Dist: python-multipart
Requires-Dist: pyyaml
Requires-Dist: cryptography
Requires-Dist: pyjwt
Requires-Dist: packaging>=24
Dynamic: license-file

# 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/`](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`](docs/SPEC.md) §10 for the UI design and [`frontend/README.md`](frontend/README.md) for frontend-specific dev instructions.

- **Functional/architectural spec**: [`docs/SPEC.md`](docs/SPEC.md)
- **DB schema**: [`docs/schema.sql`](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`](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](https://trash-guides.info/) 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`](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](https://www.themoviedb.org/settings/api) (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)

```bash
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:

```bash
openssl rand -base64 32 | tr '+/' '-_'
```

Edit `docker-compose.yml` if your library isn't at `./data` relative to the compose file, then:

```bash
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`](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:

```bash
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**:

```bash
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`](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](https://pipx.pypa.io), and `mediainfo` and `ffmpeg` from your package manager:

```bash
sudo apt install pipx mediainfo ffmpeg        # Debian/Ubuntu
brew install pipx media-info ffmpeg            # macOS
```

Install it from [PyPI](https://pypi.org/project/nazgarr/):

```bash
pipx install nazgarr
```

The same package is also attached to every [stable release](https://github.com/lktorrentz/nazgarr/releases/latest) (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`):

```bash
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:

```bash
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](https://github.com/winsw/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):

```bash
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`):

```bash
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:

```bash
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:

```bash
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:

```bash
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`](docs/SPEC.md) §6 for what `search_by_tmdb` needs and §9 for uploads:

```bash
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:

```bash
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:**

```bash
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:

```bash
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](docs/SDK.md), and [examples/nazgarr-ntfy](examples/nazgarr-ntfy) for a complete plugin.

## Local development

```bash
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):

```bash
./.venv/bin/ruff check .
./.venv/bin/pytest -q
```

### Frontend

```bash
cd frontend
npm install
npm run dev   # http://localhost:5173, proxies /api to the backend above (see vite.config.ts)
```

See [`frontend/README.md`](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`](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.
