Files
kindexr/internal/torznab/server.go
T
enki 2e491e20c1 feat: Phase 1 — Nostr reader + Torznab API
Reader (internal/nostr):
- Connects to all configured relays via WebSocket
- Subscribes to kind 2003 (NIP-35) events since (now - backfill_days)
- Parses x/title/file/tracker/i/t tags into DB rows
- Reconnects with exponential backoff (5s → 5min) on disconnect

Torznab API (internal/torznab):
- GET /api?t=caps  — XML capabilities doc (exact shape Sonarr expects)
- GET /api?t=search — FTS5 full-text search over title+description
- All other t= types (tvsearch, movie, etc.) route to the same search handler
- API key auth via ?apikey= query param; 401 XML error on missing/bad key
- Magnet links built from info_hash + event tracker tags

DB (internal/db/queries.go):
- InsertTorrent with upsert + related rows (trackers, tags, files)
- Search with FTS5 + optional category filter (parent cat expands to range)
- GetAPIKey, CreateAPIKey, UpsertRelay, UpdateRelaySync/LastEvent

CLI (cmd/kindexr-cli):
- apikey create --label <name> [--visibility all|wot|curated]

Health endpoint now reports live relays_connected count.
2026-05-16 19:19:49 -07:00

70 lines
1.8 KiB
Go

package torznab
import (
"encoding/xml"
"net/http"
"github.com/go-chi/chi/v5"
"git.utn.lol/enki/kindexr/internal/config"
"git.utn.lol/enki/kindexr/internal/db"
)
// Server handles Torznab API requests.
type Server struct {
cfg *config.Config
db *db.DB
version string
}
// New creates a new Torznab Server.
func New(cfg *config.Config, database *db.DB, version string) *Server {
return &Server{cfg: cfg, db: database, version: version}
}
// Mount registers Torznab routes under the given chi router at /api.
func (s *Server) Mount(r chi.Router) {
r.Group(func(r chi.Router) {
r.Use(s.authMiddleware)
r.Get("/api", s.apiHandler)
})
}
// apiHandler dispatches on the ?t= query parameter.
func (s *Server) apiHandler(w http.ResponseWriter, r *http.Request) {
switch r.URL.Query().Get("t") {
case "caps":
s.capsHandler(w, r)
case "search", "tvsearch", "movie", "music", "audio", "book":
s.searchHandler(w, r)
default:
writeError(w, http.StatusBadRequest, 202, "unknown function")
}
}
// authMiddleware enforces the apikey query parameter.
func (s *Server) authMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
key := r.URL.Query().Get("apikey")
if key == "" {
writeError(w, http.StatusUnauthorized, 100, "missing api key")
return
}
ak, err := s.db.GetAPIKey(r.Context(), key)
if err != nil || ak == nil {
writeError(w, http.StatusUnauthorized, 100, "invalid api key")
return
}
next.ServeHTTP(w, r)
})
}
func writeError(w http.ResponseWriter, status, code int, descr string) {
e := ErrorResponse{Code: code, Descr: descr}
out, _ := xml.MarshalIndent(e, "", " ")
w.Header().Set("Content-Type", "application/xml; charset=utf-8")
w.WriteHeader(status)
w.Write([]byte(xml.Header))
w.Write(out)
}