Files
enki 1b7b70426c feat: Phase 0 bootstrap — kindexr boots, migrates, serves /health
- config: koanf-based loading (defaults → YAML → KINDEXR_ env vars)
- db: embedded SQLite migrations with BEGIN/END-aware statement splitter
- server: chi router, GET /health returns JSON stats
- cmd/kindexr: graceful SIGTERM shutdown
- cmd/kindexr-cli: stub
- deploy: systemd unit, example config, nginx snippet
- all packages covered by race-clean tests
2026-05-16 18:45:15 -07:00

3.5 KiB

Publishing (Writer side)

nzbstr's writer side (Phase 4+) watches a download client for completed downloads and publishes them as NIP-35 kind 2003 events to configured Nostr relays.

Overview

qBittorrent / Transmission / watch_dir
          |
          v  (poll completed downloads in tagged category)
   nzbstr publisher
          |
          v  (build kind 2003 event)
   NIP-46 bunker signer  OR  local nsec
          |
          v  (WebSocket publish)
   outbox_relays (e.g. wss://sovbit.host)
          |
          v  (also stored locally)
   local SQLite DB

Configuration

Enable the publisher in config.yaml:

publisher:
  enabled: true
  signer:
    mode: "bunker"
    bunker_uri: "bunker://..."
  outbox_relays:
    - "wss://sovbit.host"
    - "wss://relay.damus.io"
  client:
    type: "qbittorrent"
    qbittorrent:
      url: "http://127.0.0.1:8080"
      username: "admin"
      password: "${QBIT_PASSWORD}"
      category: "publish-nostr"
  enrich_before_publish: true

Signing modes

Run a self-hosted NIP-46 bunker (e.g. nsecbunker) on a trusted machine. nzbstr connects as a client and requests signatures. Your nsec never touches the publishing machine.

signer:
  mode: "bunker"
  bunker_uri: "bunker://<pubkey>?relay=wss://relay.example.com&secret=<secret>"

ncryptsec (NIP-49)

Encrypted private key stored on disk. nzbstr prompts for the passphrase at startup.

signer:
  mode: "ncryptsec"
  ncryptsec: "ncryptsec1..."

Unencrypted private key. Only use on air-gapped or fully trusted machines.

signer:
  mode: "local"
  nsec: "nsec1..."

Set the nsec value via environment variable instead of writing it to the config file:

NZBSTR_PUBLISHER_SIGNER_NSEC=nsec1... nzbstr --config /etc/nzbstr/config.yaml

Download clients

qBittorrent

nzbstr polls the qBittorrent Web API for torrents in the configured category (publish-nostr by default). Only torrents whose state is pausedUP (seeding completed) or uploading are considered.

Tag your torrents in qBittorrent with the publish category to opt them in. This prevents accidentally publishing everything in your client.

watch_dir (Phase 4+)

Drop .torrent files into the configured watch directory. nzbstr picks them up, parses the metainfo, builds the event, and publishes. Useful for scripted workflows.

Event construction

For each completed download:

  1. Parse .torrent metainfo: extract info-hash, title, file list, tracker list, total size.
  2. If enrich_before_publish: true, run the title through the title parser and optionally TMDB lookup to backfill imdb_id, tmdb_id, tvdb_id, season, episode, quality, source.
  3. Build a kind 2003 event with the appropriate x, title, file, tracker, i, and t tags.
  4. Sign via the configured signer.
  5. Publish to all outbox_relays.
  6. Store in local SQLite as if ingested (so it appears in your own Torznab results immediately).

Security notes

  • NIP-46 bunker on a public-facing seedbox is the right answer security-wise. Run a self-hosted bunker on a trusted internal machine with nzbstr as a client. Your nsec never leaves the bunker.
  • If you use local mode, use ProtectSystem=strict in the systemd unit (already set) and ensure the config file has mode 0640 and is owned by the nzbstr user.
  • The category filter in qBittorrent config is important — do not set it to a category that contains torrents you didn't deliberately choose to publish.