App v0.2.26 and gateway 0.1.20
Client: seek controls, Bazarr subtitle download and cast panel in the player; MDBList ratings strip; episode and schedule detail pages; series pace estimate; what's new panel; install-permission onboarding step; synced per-profile preferences; Emby outage banner. Gateway: rebuilt admin console (one fragment per page), preference history and restore, merged Continue Watching, Emby health probe, subtitle selection and Bazarr download, structured request logging with per-request identity, and embedded build version. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
2675e6d82b
commit
4a4df7a73c
@@ -47,8 +47,8 @@ func (s *Server) handleItem(w http.ResponseWriter, r *http.Request, sess store.S
|
||||
return
|
||||
}
|
||||
// Version the entry when the detail contract grows so older cached payloads cannot
|
||||
// hide newly requested fields such as People.
|
||||
key := cache.UserKey(sess.EmbyUserID, "item:v3:"+itemID)
|
||||
// hide newly requested fields such as People or the stored ratings.
|
||||
key := cache.UserKey(sess.EmbyUserID, "item:v5:"+itemID)
|
||||
|
||||
if raw, err := s.cache.Get(ctx, key); err == nil {
|
||||
w.Header().Set("X-Memby-Cache", "hit")
|
||||
@@ -58,11 +58,16 @@ func (s *Server) handleItem(w http.ResponseWriter, r *http.Request, sess store.S
|
||||
|
||||
item, err := s.emby.Item(ctx, credentials(sess), itemID, fieldsDetail)
|
||||
if err != nil {
|
||||
s.writeUpstreamError(w, err, "could not load the item")
|
||||
s.writeUpstreamError(ctx, w, err, "could not load the item")
|
||||
return
|
||||
}
|
||||
// A detail page can then draw its ratings with the rest of the hero rather than
|
||||
// after a second request. Anything not yet stored still arrives on /ratings.
|
||||
decorated := []json.RawMessage{item}
|
||||
s.decorateItemRatings(ctx, decorated)
|
||||
item = decorated[0]
|
||||
if err := s.cache.Set(ctx, key, item, s.cfg.ItemTTL); err != nil {
|
||||
s.log.Warn("item cache write failed", "error", err)
|
||||
s.loggerFor(ctx).Warn("item cache write failed", "error", err)
|
||||
}
|
||||
w.Header().Set("X-Memby-Cache", "miss")
|
||||
writeRaw(w, http.StatusOK, item)
|
||||
@@ -94,7 +99,7 @@ func (s *Server) handleSeasonFinale(w http.ResponseWriter, r *http.Request, sess
|
||||
"ProviderIds,SeriesId,SeriesName,ParentIndexNumber,IndexNumber",
|
||||
)
|
||||
if err != nil {
|
||||
s.writeUpstreamError(w, err, "could not inspect the playing episode")
|
||||
s.writeUpstreamError(ctx, w, err, "could not inspect the playing episode")
|
||||
return
|
||||
}
|
||||
var current finaleEmbyItem
|
||||
@@ -107,7 +112,7 @@ func (s *Server) handleSeasonFinale(w http.ResponseWriter, r *http.Request, sess
|
||||
|
||||
seriesRaw, err := s.emby.Item(ctx, credentials(sess), current.SeriesID, "ProviderIds")
|
||||
if err != nil {
|
||||
s.log.Warn("season finale series metadata unavailable", "item", itemID, "error", err)
|
||||
s.loggerFor(ctx).Warn("season finale series metadata unavailable", "item", itemID, "error", err)
|
||||
s.writeSeasonFinaleResponse(ctx, key, empty, w)
|
||||
return
|
||||
}
|
||||
@@ -124,7 +129,7 @@ func (s *Server) handleSeasonFinale(w http.ResponseWriter, r *http.Request, sess
|
||||
|
||||
series, err := s.sonarr.Series(ctx)
|
||||
if err != nil {
|
||||
s.log.Warn("season finale Sonarr series unavailable", "item", itemID, "error", err)
|
||||
s.loggerFor(ctx).Warn("season finale Sonarr series unavailable", "item", itemID, "error", err)
|
||||
s.writeSeasonFinaleResponse(ctx, key, empty, w)
|
||||
return
|
||||
}
|
||||
@@ -141,7 +146,7 @@ func (s *Server) handleSeasonFinale(w http.ResponseWriter, r *http.Request, sess
|
||||
}
|
||||
episodes, err := s.sonarr.Episodes(ctx, sonarrSeriesID)
|
||||
if err != nil {
|
||||
s.log.Warn("season finale Sonarr episodes unavailable", "item", itemID, "error", err)
|
||||
s.loggerFor(ctx).Warn("season finale Sonarr episodes unavailable", "item", itemID, "error", err)
|
||||
s.writeSeasonFinaleResponse(ctx, key, empty, w)
|
||||
return
|
||||
}
|
||||
@@ -166,7 +171,7 @@ func (s *Server) writeSeasonFinaleResponse(
|
||||
return
|
||||
}
|
||||
if err := s.cache.Set(ctx, key, body, s.cfg.ItemTTL); err != nil {
|
||||
s.log.Warn("season finale cache write failed", "error", err)
|
||||
s.loggerFor(ctx).Warn("season finale cache write failed", "error", err)
|
||||
}
|
||||
writeRaw(w, http.StatusOK, body)
|
||||
}
|
||||
@@ -210,8 +215,10 @@ func (s *Server) handleSeriesEpisodes(w http.ResponseWriter, r *http.Request, se
|
||||
}
|
||||
|
||||
result, err := s.emby.Episodes(ctx, credentials(sess), seriesID, url.Values{
|
||||
"UserId": {sess.EmbyUserID},
|
||||
"Fields": {"Overview,RunTimeTicks,SeriesName,PrimaryImageAspectRatio"},
|
||||
"UserId": {sess.EmbyUserID},
|
||||
// PremiereDate is the one fact that tells two episodes of a list apart, so the
|
||||
// episode browser asks for it by name — Emby does not return it otherwise.
|
||||
"Fields": {"Overview,RunTimeTicks,SeriesName,PremiereDate,PrimaryImageAspectRatio"},
|
||||
"EnableUserData": {"true"},
|
||||
"EnableImages": {"true"},
|
||||
"EnableImageTypes": {"Primary,Thumb,Backdrop"},
|
||||
@@ -219,7 +226,7 @@ func (s *Server) handleSeriesEpisodes(w http.ResponseWriter, r *http.Request, se
|
||||
"Limit": {"1000"},
|
||||
})
|
||||
if err != nil {
|
||||
s.writeUpstreamError(w, err, "could not load series episodes")
|
||||
s.writeUpstreamError(ctx, w, err, "could not load series episodes")
|
||||
return
|
||||
}
|
||||
items := result.Items
|
||||
@@ -232,7 +239,7 @@ func (s *Server) handleSeriesEpisodes(w http.ResponseWriter, r *http.Request, se
|
||||
return
|
||||
}
|
||||
if err := s.cache.Set(ctx, key, body, s.cfg.ItemTTL); err != nil {
|
||||
s.log.Warn("series episodes cache write failed", "error", err)
|
||||
s.loggerFor(ctx).Warn("series episodes cache write failed", "error", err)
|
||||
}
|
||||
w.Header().Set("X-Memby-Cache", "miss")
|
||||
writeRaw(w, http.StatusOK, body)
|
||||
@@ -248,7 +255,7 @@ func (s *Server) handleTrailer(w http.ResponseWriter, r *http.Request, sess stor
|
||||
}
|
||||
result, err := s.emby.LocalTrailers(r.Context(), credentials(sess), itemID)
|
||||
if err != nil {
|
||||
s.writeUpstreamError(w, err, "could not load trailers")
|
||||
s.writeUpstreamError(r.Context(), w, err, "could not load trailers")
|
||||
return
|
||||
}
|
||||
if len(result.Items) == 0 {
|
||||
@@ -291,11 +298,11 @@ func (s *Server) setFlag(
|
||||
|
||||
userData, err := apply(itemID, req.Value)
|
||||
if err != nil {
|
||||
s.writeUpstreamError(w, err, "could not update the item")
|
||||
s.writeUpstreamError(r.Context(), 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)
|
||||
s.loggerFor(r.Context()).Warn("cache invalidation failed", "error", err)
|
||||
}
|
||||
if s.forYou != nil {
|
||||
s.forYou.MarkDirty(r.Context(), sess)
|
||||
|
||||
Reference in New Issue
Block a user