package api import ( "context" "encoding/json" "net/http" "net/url" "github.com/ponzischeme89/memby/server/internal/cache" "github.com/ponzischeme89/memby/server/internal/store" ) type personFilmographyResponse struct { Items []json.RawMessage `json:"items"` } // handlePerson returns Emby's person item. PremiereDate and EndDate are the person's // birth and death dates; Overview is their biography. // // It is cached for the household rather than per viewer, and outside the namespace a // playback stop invalidates. Nothing about who Cillian Murphy is depends on who is // asking or on what they finished watching ten minutes ago, and filing it under the // viewer meant a cast page paid Emby again after every episode anybody in the house // completed. func (s *Server) handlePerson(w http.ResponseWriter, r *http.Request, sess store.Session) { ctx := r.Context() personID := r.PathValue("id") if personID == "" { writeError(w, http.StatusBadRequest, "person id is required") return } person, hit, err := s.cachedRead(ctx, cache.MetadataKey("person:v2:"+personID), s.cfg.ItemTTL, func(ctx context.Context) (json.RawMessage, error) { return s.emby.Item(ctx, credentials(sess), personID, "Overview,Genres,PrimaryImageAspectRatio") }) if err != nil { s.writeUpstreamError(ctx, w, err, "could not load the person") return } writeCached(w, hit, person) } // Filmography is kept separate from the biography so life dates can decorate the cast // row without also loading every cast member's credits. // // Unlike the biography this one is genuinely per viewer — it asks Emby for user data, so // the grid can mark what has been watched — and therefore stays where invalidation can // reach it. What it gains instead is coalescing: this is the most expensive query the // gateway makes of Emby, a recursive scan of the whole library filtered by person, and // three televisions opening the same cast card used to mean three of them. func (s *Server) handlePersonFilmography(w http.ResponseWriter, r *http.Request, sess store.Session) { ctx := r.Context() personID := r.PathValue("id") if personID == "" { writeError(w, http.StatusBadRequest, "person id is required") return } key := cache.UserKey(sess.EmbyUserID, "person-filmography:v1:"+personID) body, hit, err := s.cachedRead(ctx, key, s.cfg.ItemTTL, func(ctx context.Context) (json.RawMessage, error) { result, err := s.emby.Items(ctx, credentials(sess), url.Values{ "PersonIds": {personID}, "IncludeItemTypes": {"Movie,Series"}, "Recursive": {"true"}, "SortBy": {"ProductionYear,SortName"}, "SortOrder": {"Descending"}, "Limit": {"60"}, "Fields": {"Overview,ProductionYear,PrimaryImageAspectRatio"}, "EnableImages": {"true"}, "EnableImageTypes": {"Primary"}, "ImageTypeLimit": {"1"}, "EnableUserData": {"true"}, }) if err != nil { return nil, err } items := result.Items if items == nil { items = []json.RawMessage{} } return json.Marshal(personFilmographyResponse{Items: items}) }) if err != nil { s.writeUpstreamError(ctx, w, err, "could not load the person's filmography") return } writeCached(w, hit, body) }