89 lines
3.0 KiB
Go
89 lines
3.0 KiB
Go
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})
|
||
|
|
}
|