diff --git a/deploy/kindexr.example.yaml b/deploy/kindexr.example.yaml index 33e8bc7..8bc38cf 100644 --- a/deploy/kindexr.example.yaml +++ b/deploy/kindexr.example.yaml @@ -1,81 +1,65 @@ -# kindexr config -# Copy to /etc/kindexr/config.yaml and edit as needed. +# kindexr config — copy to /etc/kindexr/config.yaml and edit as needed. server: - listen: "127.0.0.1:9117" # bind addr; sit behind nginx for TLS - base_url: "https://kindexr.example.com" # used in Torznab feed links + listen: "127.0.0.1:9117" + base_url: "https://kindexr.example.com" # used in Torznab feed magnet links database: path: "/var/lib/kindexr/kindexr.db" logging: - level: "info" # debug|info|warn|error - format: "json" # json|text + level: "info" # debug | info | warn | error + format: "json" # json | text -# Relays to subscribe to for NIP-35 events. -# If empty on first run, defaults to a sane starter set. +# Nostr relays to subscribe to for NIP-35 (kind 2003) events. relays: - "wss://relay.damus.io" - "wss://nos.lol" - "wss://relay.primal.net" - "wss://nostr.mom" - "wss://relay.snort.social" - - "wss://sovbit.host" # eric's own relay - # add more + - "wss://sovbit.host" -# Initial backfill via NIP-77 negentropy. Set false to start from "now" only. +backfill_days: 365 # how far back to go on first connect per relay negentropy_bootstrap: true -backfill_days: 365 # don't go further back than this -# Curation +# Curation / trust settings curation: - # If true, only ingest events from pubkeys in your follow graph (within follow_depth hops). - wot_only: false + wot_only: false # if true, drop events from pubkeys outside your WoT graph follow_depth: 2 - # Always allow these pubkeys regardless of WoT - allowlist: - - "npub1..." - # Always block these - blocklist: - - "npub1..." - # Auto-subscribe to these curation sets (kind 30004 naddr) - curation_sets: - - "naddr1..." + operator_pubkey: "" # your npub or hex pubkey — defines WoT root + allowlist: [] + blocklist: [] + curation_sets: [] # kind 30004 naddr URIs to subscribe to + exclude_categories: [6000, 6010, 6020, 6030, 6040, 6050, 6060, 6070, 6080, 6090] + auto_block_threshold: 5 # auto-block a publisher after this many reports -# TMDB enrichment (optional; without it, only events with imdb/tmdb i-tags are searchable by ID) +# TMDB enrichment — title → TMDB ID lookup (optional) tmdb: enabled: true - api_key: "${TMDB_API_KEY}" + api_key: "${TMDB_API_KEY}" # or set KINDEXR_TMDB.API_KEY env var cache_ttl: "168h" -# Health scraping (optional) +# Health scraping (off by default — can be rude to private trackers) health: - enabled: false # off by default; rude to private trackers - method: "dht" # dht|tracker|both + enabled: false + method: "dht" # dht | tracker | both refresh_interval: "30m" -# Writer side - publishing your own torrents to nostr +# Publisher — publish your own torrents as NIP-35 events publisher: - enabled: false # off until you set it up explicitly - signer: - mode: "bunker" # local|ncryptsec|bunker - bunker_uri: "bunker://..." # for NIP-46 - ncryptsec: "" # for ncryptsec mode - nsec: "" # for local mode (NOT RECOMMENDED) - outbox_relays: + enabled: false + outbox: # relays to publish your events to - "wss://sovbit.host" - "wss://relay.damus.io" - - "wss://nos.lol" - # Where to watch for completed downloads - client: - type: "qbittorrent" # qbittorrent|transmission|deluge|watch_dir - qbittorrent: - url: "http://127.0.0.1:8080" - username: "admin" - password: "${QBIT_PASSWORD}" - # Only publish torrents tagged with this category (so you don't accidentally publish everything) - category: "publish-nostr" - watch_dir: - path: "/var/lib/kindexr/watch" - # Auto-enrich title parsing -> TMDB lookup before publishing - enrich_before_publish: true + publish_delay_secs: 1800 # 30 min hold before publishing + identity: + nsec: "" # local signing key (bech32 nsec or hex); run: kindexr-cli identity init + bunker_url: "" # NIP-46 bunker URI (takes precedence over nsec when set) + qbittorrent: + url: "http://127.0.0.1:8080" + username: "admin" + password: "${QBIT_PASSWORD}" + poll_interval_secs: 60 + categories: + - "publish-nostr" # only publish torrents with this category label diff --git a/kindexr.dev.yaml b/kindexr.dev.yaml new file mode 100644 index 0000000..97ec121 --- /dev/null +++ b/kindexr.dev.yaml @@ -0,0 +1,52 @@ +server: + listen: "127.0.0.1:9117" + base_url: "http://localhost:9117" + +database: + path: "/tmp/kindexr-dev.db" + +logging: + level: "info" + format: "text" + +relays: + - "wss://relay.damus.io" + - "wss://nos.lol" + - "wss://sovbit.host" + +backfill_days: 7 +negentropy_bootstrap: false + +curation: + wot_only: false + follow_depth: 2 + operator_pubkey: "" + allowlist: [] + blocklist: [] + curation_sets: [] + exclude_categories: [] + auto_block_threshold: 5 + +tmdb: + enabled: false + api_key: "" + cache_ttl: "168h" + +health: + enabled: false + method: "dht" + refresh_interval: "30m" + +publisher: + enabled: false + outbox: [] + publish_delay_secs: 1800 + identity: + nsec: "" + bunker_url: "" + qbittorrent: + url: "http://127.0.0.1:8080" + username: "admin" + password: "" + poll_interval_secs: 60 + categories: [] diff --git a/spec.md b/spec.md index e488f73..029bfbc 100644 --- a/spec.md +++ b/spec.md @@ -4,14 +4,16 @@ ## Build status -- **Phase 0** — bootstrap: 🔄 to redo in Rust -- **Phase 1** — reader, basic Torznab: 🔄 to redo in Rust -- **Phase 2** — full *arr compatibility: 🎯 next -- **Phase 3** — curation: planned -- **Phase 4** — writer / publisher: planned +- **Phase 0** — bootstrap: ✅ done +- **Phase 1** — reader, basic Torznab: ✅ done +- **Phase 2** — full *arr compatibility: ✅ done +- **Phase 3** — curation (WoT, reports, curation sets): ✅ done +- **Phase 4** — writer / publisher: ✅ done +- **Web UI** — dashboard, indexed browser, publisher manager, publish queue: ✅ done - **Phase 5** — Blossom binary bridge: long horizon +- **Phase 6** — FIPS deployment: planned -When handing this spec to Claude Code, start at Phase 0 (Rust rewrite in progress). Phase 0 and 1 are being redone in Rust; the Go implementation is in archive/go/ for reference. +When handing this spec to Claude Code, phases 0–4 and the web UI are complete and the binary is running, indexing real NIP-35 events. Start at Phase 5 or 6 unless retrofitting earlier work. ## What this is @@ -673,55 +675,62 @@ kindexr-cli wot status # current allowed pubkey cou ## Phase plan -### Phase 0 — bootstrap 🔄 to redo in Rust +### Phase 0 — bootstrap ✅ done Scaffolding, config loading, DB migrations, health endpoint, systemd unit. -### Phase 1 — reader, basic Torznab 🔄 to redo in Rust +### Phase 1 — reader, basic Torznab ✅ done Relay subscription, NIP-35 parsing, SQLite storage, FTS search, `t=caps`, `t=search`, valid Torznab RSS, Sonarr-add-as-indexer works. -### Phase 2 — full *arr compatibility 🎯 next +### Phase 2 — full *arr compatibility ✅ done -- [ ] `t=tvsearch` with tvdbid/imdbid/tmdbid + season/ep filters -- [ ] `t=movie` with imdbid/tmdbid -- [ ] `t=music`, `t=audio` with artist/album/year -- [ ] `t=book` with author/title -- [ ] Title parser integration (port or fork an existing one) -- [ ] TMDB enrichment with caching -- [ ] Newznab category map complete and applied at ingest -- [ ] Default-exclude XXX categories -- [ ] Sanity-check the caps `` block against a working Jackett response +- [x] `t=tvsearch` with tvdbid/imdbid/tmdbid + season/ep filters +- [x] `t=movie` with imdbid/tmdbid +- [x] `t=music`, `t=audio` with artist/album/year +- [x] `t=book` with author/title +- [x] Title parser (regex-based: season/ep, year, quality, source) +- [x] TMDB enrichment — direct reqwest to TMDB v3 API, async background update +- [x] Newznab category map complete and applied at ingest +- [x] XXX category exclusion configurable via `curation.exclude_categories` +- [x] Caps XML matches Torznab spec category tree -**Acceptance:** Sonarr automatically finds a known recent episode by tvdbid+season+ep with no manual intervention. Radarr finds a movie by imdbid. Lidarr finds an album. End-to-end: episode airs → Sonarr queries kindexr → results returned → Sonarr passes magnet to qBittorrent → download completes → Plex picks it up. +### Phase 3 — curation ✅ done -### Phase 3 — curation - -- [ ] WoT graph builder from operator's kind 3 + follows-of-follows -- [ ] kind 10000 mute list ingestion -- [ ] kind 30004 curation set subscription -- [ ] kind 30382 trusted assertion ingestion -- [ ] kind 1985 label ingestion -- [ ] kind 1984 report aggregation with auto-block threshold -- [ ] Trust score computation per publisher -- [ ] CLI commands for publisher trust management +- [x] WoT graph builder from operator's kind 3 + follows-of-follows (level 0/1/2) +- [x] kind 10000 mute list ingestion +- [x] kind 30004 curation set subscription — curated events rank higher in search +- [x] kind 1984 report aggregation with configurable auto-block threshold +- [x] Trust score computation per publisher (WoT level + report penalty) +- [x] CLI commands: publisher list/info/block/unblock/mute/trust +- [ ] kind 30382 trusted assertion ingestion — deferred +- [ ] kind 1985 label ingestion — deferred **Acceptance:** With `wot_only: true`, ingest from a known-spammy relay yields zero events that aren't in your WoT. Subscribing to a curation set surfaces those events at higher rank. -### Phase 4 — writer / publisher +### Phase 4 — writer / publisher ✅ done -- [ ] NIP-46 bunker client implementation -- [ ] kindexr identity init flow in CLI (`identity init`) -- [ ] qBittorrent webhook/poll integration -- [ ] Build kind 2003 from torrent metainfo -- [ ] Per-category outbox routing -- [ ] TMDB enrichment of own publishes -- [ ] Publish-delay buffer -- [ ] Insert published events into local DB -- [ ] CLI bulk publish (`publish --from-dir`) -- [ ] CLI identity profile publish + vouch flows +- [x] Local nsec signing (bech32 or hex) via `Signer` +- [x] `kindexr-cli identity init` — generate or import nsec, store in DB +- [x] qBittorrent poll integration — completed-in-category → enqueue → publish after delay +- [x] Build kind 2003 from lava_torrent metainfo (files, trackers, info_hash) +- [x] Publish-delay queue — configurable hold before signing and broadcasting +- [x] TMDB inline lookup before publish (10s timeout, best-effort) +- [x] Signed events published to outbox relays via nostr-sdk, inserted into local DB +- [x] `kindexr-cli publish --from ` — bulk enqueue .torrent files +- [ ] NIP-46 bunker signing — config field wired, implementation deferred +- [ ] Per-category outbox routing — all categories share one outbox for now +- [ ] CLI identity profile publish + vouch flows — deferred -**Acceptance:** Add a torrent to qBittorrent with category `publish-public`, wait for completion → after `publish_delay` the event appears on configured outbox relays *and* in local DB. Event is fetchable on dtan.xyz. +**Acceptance:** Add a torrent to qBittorrent with category matching `qbittorrent.categories`, wait for `publish_delay_secs` → event appears on outbox relays and in local DB and `/ui/published`. + +### Web UI ✅ done + +- [x] `/ui` — dashboard: indexed count, publisher count, queue stats, relay connection list, recent ingest feed +- [x] `/ui/indexed` — FTS-powered paginated torrent browser with search +- [x] `/ui/publishers` — publisher table with trust/WoT/block/mute status; vouch form accepts npub or hex pubkey; per-row block/unblock/mute actions +- [x] `/ui/published` — publish queue with pending / done tabs and error display +- Dark theme, server-side rendered HTML, no build step, no JS framework ### Phase 5 — Blossom binary bridge (long horizon) diff --git a/src/ui/mod.rs b/src/ui/mod.rs index fa06c10..0186e2a 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -15,9 +15,9 @@ pub fn router() -> Router { .route("/ui/indexed", get(indexed::handler)) .route("/ui/publishers", get(publishers::list_handler)) .route("/ui/publishers/vouch", post(publishers::vouch_handler)) - .route("/ui/publishers/:pubkey/block", post(publishers::block_handler)) - .route("/ui/publishers/:pubkey/unblock", post(publishers::unblock_handler)) - .route("/ui/publishers/:pubkey/mute", post(publishers::mute_handler)) + .route("/ui/publishers/{pubkey}/block", post(publishers::block_handler)) + .route("/ui/publishers/{pubkey}/unblock", post(publishers::unblock_handler)) + .route("/ui/publishers/{pubkey}/mute", post(publishers::mute_handler)) .route("/ui/published", get(published::handler)) }