Files
memby/server/internal/api/admin_requests.go
T

89 lines
3.0 KiB
Go
Raw Normal View History

2026-08-28 23:00:02 +12:00
package api
import (
"net/http"
"strings"
"github.com/ponzischeme89/memby/server/internal/store"
)
// The console's window on what the household has actually asked for.
//
// The overview already reports RequestUsage — a count and a last-asked time per viewer —
// which answers "who uses the feature" and nothing about "what did they ask for". This is
// the table underneath that number: every stored ask, newest first, with the person
// attached and the same derived status the viewer's own page shows, so an operator can see
// that three people are waiting on the same film or that a request has been stuck
// searching for a week.
const adminRequestsLimit = store.MediaRequestSweepLimit
// adminMediaRequest is one row of that table: an ask, who made it, and what has become of
// it. The status half is derived per read by decorateRequests, exactly as it is for the
// viewer, so the console and the television never disagree about a title's state.
type adminMediaRequest struct {
UserID string `json:"userId"`
Username string `json:"username,omitempty"`
myRequest
}
type adminRequestsResponse struct {
Requests []adminMediaRequest `json:"requests"`
}
func (s *Server) handleAdminRequests(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
owned, err := s.store.AllMediaRequests(ctx, adminRequestsLimit)
if err != nil {
s.loggerFor(ctx).Error("admin media requests read failed", "error", err)
writeError(w, http.StatusInternalServerError, "could not read media requests")
return
}
// An optional filter to what one viewer asked for, applied here rather than in SQL: the
// list is already bounded and small, and AllMediaRequests is the one read the ready
// sweep also uses.
if userID := strings.TrimSpace(r.URL.Query().Get("userId")); userID != "" {
filtered := owned[:0]
for _, req := range owned {
if req.UserID == userID {
filtered = append(filtered, req)
}
}
owned = filtered
}
stored := make([]store.MediaRequest, len(owned))
for i := range owned {
stored[i] = owned[i].MediaRequest
}
// One decoration pass for the whole household. The *arr catalogues it consults are
// cached for the day and shared, so this is the same cost as one viewer opening their
// page. The console is the operator's tool, so it always sees the full progress
// vocabulary.
cards := s.decorateRequests(ctx, stored, true)
// A name an ask cannot be attributed to costs the column and nothing else — the id
// still tells one requester from another, and the titles are the point of the page.
names := map[string]string{}
if users, err := s.store.KnownUsers(ctx); err == nil {
for _, user := range users {
if user.Username != "" {
names[user.ID] = user.Username
}
}
} else {
s.loggerFor(ctx).Warn("media request owners unresolved", "error", err)
}
rows := make([]adminMediaRequest, len(owned))
for i := range owned {
rows[i] = adminMediaRequest{
UserID: owned[i].UserID,
Username: names[owned[i].UserID],
myRequest: cards[i],
}
}
writeJSON(w, http.StatusOK, adminRequestsResponse{Requests: rows})
}