package api import ( "encoding/json" "net/http" "github.com/ponzischeme89/memby/server/internal/cache" "github.com/ponzischeme89/memby/server/internal/store" ) type flagRequest struct { Value bool `json:"value"` } // handleItem serves full metadata for one item. The TV asks for this only after D-pad // focus settles, so it is worth caching for longer than a home row. func (s *Server) handleItem(w http.ResponseWriter, r *http.Request, sess store.Session) { ctx := r.Context() itemID := r.PathValue("id") if itemID == "" { writeError(w, http.StatusBadRequest, "item id is required") return } key := cache.UserKey(sess.EmbyUserID, "item:"+itemID) if raw, err := s.cache.Get(ctx, key); err == nil { w.Header().Set("X-Memby-Cache", "hit") writeRaw(w, http.StatusOK, raw) return } item, err := s.emby.Item(ctx, credentials(sess), itemID, fieldsDetail) if err != nil { s.writeUpstreamError(w, err, "could not load the item") return } if err := s.cache.Set(ctx, key, item, s.cfg.ItemTTL); err != nil { s.log.Warn("item cache write failed", "error", err) } w.Header().Set("X-Memby-Cache", "miss") writeRaw(w, http.StatusOK, item) } // handleTrailer answers with the item's first local trailer, or 404 when it has none. // The screensaver's Play action uses this before asking for a playback URL. func (s *Server) handleTrailer(w http.ResponseWriter, r *http.Request, sess store.Session) { itemID := r.PathValue("id") if itemID == "" { writeError(w, http.StatusBadRequest, "item id is required") return } result, err := s.emby.LocalTrailers(r.Context(), credentials(sess), itemID) if err != nil { s.writeUpstreamError(w, err, "could not load trailers") return } if len(result.Items) == 0 { writeError(w, http.StatusNotFound, "no trailer available") return } writeRaw(w, http.StatusOK, result.Items[0]) } func (s *Server) handleFavorite(w http.ResponseWriter, r *http.Request, sess store.Session) { s.setFlag(w, r, sess, func(itemID string, value bool) (json.RawMessage, error) { return s.emby.SetFavorite(r.Context(), credentials(sess), itemID, value) }) } func (s *Server) handlePlayed(w http.ResponseWriter, r *http.Request, sess store.Session) { s.setFlag(w, r, sess, func(itemID string, value bool) (json.RawMessage, error) { return s.emby.SetPlayed(r.Context(), credentials(sess), itemID, value) }) } // setFlag applies a user-data mutation and drops this user's cached views, so the next // home request reflects it rather than serving the row it just contradicted. func (s *Server) setFlag( w http.ResponseWriter, r *http.Request, sess store.Session, apply func(itemID string, value bool) (json.RawMessage, error), ) { itemID := r.PathValue("id") if itemID == "" { writeError(w, http.StatusBadRequest, "item id is required") return } var req flagRequest if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 4<<10)).Decode(&req); err != nil { writeError(w, http.StatusBadRequest, "malformed request body") return } userData, err := apply(itemID, req.Value) if err != nil { s.writeUpstreamError(w, err, "could not update the item") return } if err := s.cache.InvalidateUser(r.Context(), sess.EmbyUserID); err != nil { s.log.Warn("cache invalidation failed", "error", err) } writeRaw(w, http.StatusOK, userData) }