0.2.67 - Detail pages improvements
This commit is contained in:
@@ -0,0 +1,101 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/ponzischeme89/memby/server/internal/cache"
|
||||
"github.com/ponzischeme89/memby/server/internal/store"
|
||||
)
|
||||
|
||||
// extrasResponse is the Extras tab: featurettes, deleted scenes, interviews and the local
|
||||
// trailer, as Emby's own item JSON so the television's single BaseItem model decodes it.
|
||||
type extrasResponse struct {
|
||||
Items []json.RawMessage `json:"items"`
|
||||
}
|
||||
|
||||
// handleExtras joins the two lists Emby keeps an item's supplementary video in.
|
||||
//
|
||||
// It has to be a join, because Emby answers the question in two places and neither includes
|
||||
// the other: `SpecialFeatures` holds the featurettes and deleted scenes, `LocalTrailers`
|
||||
// holds the trailer, and a detail page asking only one of them shows an Extras tab missing
|
||||
// the thing most titles that have anything actually have.
|
||||
//
|
||||
// Done here rather than on the television for the ordinary reason: in gateway mode the set
|
||||
// holds no Emby credential. Doing it here also means one request instead of two on the path
|
||||
// that matters — the tab is probed on every detail page open.
|
||||
func (s *Server) handleExtras(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, "extras:v1:"+itemID)
|
||||
if raw, err := s.cache.Get(ctx, key); err == nil {
|
||||
w.Header().Set("X-Memby-Cache", "hit")
|
||||
writeRaw(w, http.StatusOK, raw)
|
||||
return
|
||||
}
|
||||
|
||||
cred := credentials(sess)
|
||||
// Neither list failing is fatal, and only both failing is a failure worth reporting: a
|
||||
// title can perfectly well have a trailer and no special features, or the reverse, and
|
||||
// an Extras tab withheld because one of two lookups was unwell is the worse outcome.
|
||||
var items []json.RawMessage
|
||||
trailerErr := error(nil)
|
||||
if result, err := s.emby.LocalTrailers(ctx, cred, itemID); err == nil {
|
||||
items = append(items, result.Items...)
|
||||
} else {
|
||||
trailerErr = err
|
||||
}
|
||||
featureErr := error(nil)
|
||||
if result, err := s.emby.SpecialFeatures(ctx, cred, itemID); err == nil {
|
||||
items = append(items, result.Items...)
|
||||
} else {
|
||||
featureErr = err
|
||||
}
|
||||
if trailerErr != nil && featureErr != nil {
|
||||
s.writeUpstreamError(ctx, w, featureErr, "could not load extras")
|
||||
return
|
||||
}
|
||||
|
||||
items = dedupeExtras(items)
|
||||
body, err := json.Marshal(extrasResponse{Items: nonNilRaws(items)})
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "could not encode extras")
|
||||
return
|
||||
}
|
||||
// Cached for the ordinary item lifetime, empty answers included. Most of a library has
|
||||
// no extras at all and every detail page asks, so the "no" is the entry worth keeping.
|
||||
if err := s.cache.Set(ctx, key, body, s.cfg.ItemTTL); err != nil {
|
||||
s.loggerFor(ctx).Warn("extras cache write failed", "error", err)
|
||||
}
|
||||
w.Header().Set("X-Memby-Cache", "miss")
|
||||
writeRaw(w, http.StatusOK, body)
|
||||
}
|
||||
|
||||
// dedupeExtras drops the same file appearing in both lists, in first-seen order.
|
||||
//
|
||||
// A trailer filed as a special feature *and* as a local trailer is an ordinary way for a
|
||||
// library to be laid out, and the television renders these into a keyed grid — which throws
|
||||
// on a repeated key rather than merely looking wrong. Anything without an id is dropped for
|
||||
// the same reason.
|
||||
func dedupeExtras(items []json.RawMessage) []json.RawMessage {
|
||||
seen := make(map[string]struct{}, len(items))
|
||||
out := make([]json.RawMessage, 0, len(items))
|
||||
for _, raw := range items {
|
||||
var identified struct {
|
||||
ID string `json:"Id"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &identified); err != nil || identified.ID == "" {
|
||||
continue
|
||||
}
|
||||
if _, repeated := seen[identified.ID]; repeated {
|
||||
continue
|
||||
}
|
||||
seen[identified.ID] = struct{}{}
|
||||
out = append(out, raw)
|
||||
}
|
||||
return out
|
||||
}
|
||||
Reference in New Issue
Block a user