2e491e20c1
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.
70 lines
1.8 KiB
Go
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)
|
|
}
|