package ebooks import ( "database/sql" "errors" "log/slog" "net/http" "strconv" "github.com/mithral/vellum/internal/auth" "github.com/mithral/vellum/internal/catalog" "github.com/mithral/vellum/internal/httpx" ) // Handlers serves ebook-specific endpoints. type Handlers struct { db *sql.DB log *slog.Logger auth *auth.Authenticator } // NewHandlers builds the ebook HTTP layer. func NewHandlers(db *sql.DB, log *slog.Logger, a *auth.Authenticator) *Handlers { return &Handlers{db: db, log: log, auth: a} } // Register mounts the routes. func (h *Handlers) Register(mux *http.ServeMux) { mux.Handle("GET /api/v1/items/{id}/ebook", h.auth.Require(h.handleMetadata)) } // handleMetadata parses the EPUB on demand and returns its metadata. // // Parsing on demand rather than at scan time keeps the scanner fast and means // a fix to the parser applies immediately, without a rescan. func (h *Handlers) handleMetadata(w http.ResponseWriter, r *http.Request, u auth.User) { id, err := strconv.ParseInt(r.PathValue("id"), 10, 64) if err != nil || id <= 0 { httpx.WriteError(w, http.StatusBadRequest, "invalid item id") return } it, err := catalog.LoadFor(r.Context(), h.db, u, id) if errors.Is(err, catalog.ErrNotFound) { httpx.WriteError(w, http.StatusNotFound, "item not found") return } if err != nil { h.log.Error("load item", "err", err) httpx.WriteError(w, http.StatusInternalServerError, "internal server error") return } if it.Kind != Kind { httpx.WriteError(w, http.StatusBadRequest, "item is not an ebook") return } md, err := Parse(it.AbsPath()) if err != nil { h.log.Warn("parse epub", "item", it.ID, "err", err) httpx.WriteError(w, http.StatusUnprocessableEntity, "could not parse epub") return } // Fall back to the scanner's title when the OPF has none. if md.Title == "" { md.Title = it.Title } httpx.WriteJSON(w, http.StatusOK, map[string]any{"metadata": md}) }