package api import ( "context" "encoding/json" "net/http" "strings" "github.com/ponzischeme89/memby/server/internal/store" ) type heroAdminItem struct { ID string `json:"id"` Name string `json:"name"` Type string `json:"type"` Year int `json:"year,omitempty"` } type heroAdminPolicy struct { PinnedItems []heroAdminItem `json:"pinnedItems"` PrimeSubtitle string `json:"primeSubtitle"` } func adminHeroItem(raw json.RawMessage) (heroAdminItem, bool) { fact, ok := heroFactsOf(raw) if !ok || !fact.Playable || (!strings.EqualFold(fact.Type, "Movie") && !strings.EqualFold(fact.Type, "Series")) { return heroAdminItem{}, false } return heroAdminItem{ID: fact.ID, Name: fact.Name, Type: fact.Type, Year: heroYearOf(fact)}, true } func (s *Server) heroAdminPolicy(ctx context.Context) heroAdminPolicy { policy, err := s.store.HeroPolicy(ctx) if err != nil { s.loggerFor(ctx).Warn("hero policy unavailable to admin", "error", err) return heroAdminPolicy{PinnedItems: []heroAdminItem{}} } items, err := s.store.LibraryItemsByID(ctx, policy.PinnedItemIDs) if err != nil { s.loggerFor(ctx).Warn("pinned hero titles unavailable to admin", "error", err) return heroAdminPolicy{PinnedItems: []heroAdminItem{}} } byID := make(map[string]heroAdminItem, len(items)) for _, raw := range items { if item, ok := adminHeroItem(raw); ok { byID[item.ID] = item } } out := heroAdminPolicy{ PinnedItems: make([]heroAdminItem, 0, len(policy.PinnedItemIDs)), PrimeSubtitle: policy.PrimeSubtitle, } for _, id := range policy.PinnedItemIDs { if item, ok := byID[id]; ok { out.PinnedItems = append(out.PinnedItems, item) } } return out } func (s *Server) handleAdminHeroSearch(w http.ResponseWriter, r *http.Request) { items, err := s.store.SearchLibrary(r.Context(), r.URL.Query().Get("q"), 20) if err != nil { s.loggerFor(r.Context()).Error("hero library search failed", "error", err) writeError(w, http.StatusInternalServerError, "could not search the library") return } results := make([]heroAdminItem, 0, len(items)) for _, raw := range items { if item, ok := adminHeroItem(raw); ok { results = append(results, item) } } writeJSON(w, http.StatusOK, map[string]any{"items": results}) } func (s *Server) handleAdminHeroPolicy(w http.ResponseWriter, r *http.Request) { var request struct { PinnedItemIDs []string `json:"pinnedItemIds"` PrimeSubtitle string `json:"primeSubtitle"` } if err := json.NewDecoder(r.Body).Decode(&request); err != nil { writeError(w, http.StatusBadRequest, "invalid hero policy") return } ids := uniqueHeroIDs(request.PinnedItemIDs) if len(ids) > 4 { writeError(w, http.StatusBadRequest, "the hero can pin at most four titles") return } items, err := s.store.LibraryItemsByID(r.Context(), ids) if err != nil { s.loggerFor(r.Context()).Error("hero title validation failed", "error", err) writeError(w, http.StatusInternalServerError, "could not validate hero titles") return } valid := map[string]bool{} for _, raw := range items { if item, ok := adminHeroItem(raw); ok { valid[item.ID] = true } } for _, id := range ids { if !valid[id] { writeError(w, http.StatusBadRequest, "every pinned item must be a playable library film or series") return } } policy := store.HeroPolicy{PinnedItemIDs: ids, PrimeSubtitle: request.PrimeSubtitle} if err := s.store.SetHeroPolicy(r.Context(), policy); err != nil { s.loggerFor(r.Context()).Error("hero policy write failed", "error", err) writeError(w, http.StatusInternalServerError, "could not save hero policy") return } s.invalidateAllHomeCaches(r.Context()) writeJSON(w, http.StatusOK, map[string]any{"saved": true}) } func uniqueHeroIDs(ids []string) []string { seen := map[string]bool{} out := make([]string, 0, len(ids)) for _, id := range ids { id = strings.TrimSpace(id) if id == "" || seen[id] { continue } seen[id] = true out = append(out, id) } return out } func (s *Server) invalidateAllHomeCaches(ctx context.Context) { users, err := s.store.KnownUsers(ctx) if err != nil { s.loggerFor(ctx).Warn("hero cache invalidation could not list users", "error", err) return } for _, user := range users { if err := s.cache.InvalidateUser(ctx, user.ID); err != nil { s.loggerFor(ctx).Warn("hero cache invalidation failed", "user", user.ID, "error", err) } } }