0.2.82
This commit is contained in:
@@ -48,7 +48,7 @@ func (s *Server) handleItem(w http.ResponseWriter, r *http.Request, sess store.S
|
||||
writeError(w, http.StatusBadRequest, "item id is required")
|
||||
return
|
||||
}
|
||||
if raw, err := s.cache.Get(ctx, itemDetailKey(sess.EmbyUserID, itemID)); err == nil {
|
||||
if raw, err := s.cache.Get(ctx, itemDetailKey(viewerKeyOf(ctx, sess), itemID)); err == nil {
|
||||
w.Header().Set("X-Memby-Cache", "hit")
|
||||
writeRaw(w, http.StatusOK, raw)
|
||||
return
|
||||
@@ -88,7 +88,7 @@ func (s *Server) detailItem(
|
||||
// than after a second request. Anything not yet stored still arrives on
|
||||
// /ratings.
|
||||
decorated := []json.RawMessage{raw}
|
||||
s.decorateItemRatings(ctx, decorated)
|
||||
s.decorateItems(ctx, decorated)
|
||||
return decorated[0], nil
|
||||
})
|
||||
return item, err
|
||||
@@ -255,7 +255,7 @@ func (s *Server) handleSeriesEpisodes(w http.ResponseWriter, r *http.Request, se
|
||||
// Watching card is focused, and the page asks again when somebody presses. A
|
||||
// long-running show is a thousand records, so two of them is a real cost on the one
|
||||
// press that must feel free.
|
||||
key := cache.UserKey(sess.EmbyUserID, "series-episodes:"+seriesID)
|
||||
key := cache.UserKey(viewerKeyOf(ctx, sess), "series-episodes:"+seriesID)
|
||||
body, hit, err := s.cachedRead(ctx, key, s.cfg.ItemTTL,
|
||||
func(ctx context.Context) (json.RawMessage, error) {
|
||||
result, err := s.emby.Episodes(
|
||||
@@ -278,6 +278,11 @@ func (s *Server) handleSeriesEpisodes(w http.ResponseWriter, r *http.Request, se
|
||||
if items == nil {
|
||||
items = []json.RawMessage{}
|
||||
}
|
||||
// The ticks down the episode list are the clearest statement this app makes
|
||||
// about what somebody has seen, so they are the last place the account's
|
||||
// answer may be left standing. The cache key is the viewer's, so this is
|
||||
// stored per person rather than decorated on the way out.
|
||||
s.decorateViewerState(ctx, items)
|
||||
return json.Marshal(seriesEpisodesResponse{Items: items})
|
||||
})
|
||||
if err != nil {
|
||||
@@ -307,33 +312,78 @@ func (s *Server) handleTrailer(w http.ResponseWriter, r *http.Request, sess stor
|
||||
writeRaw(w, http.StatusOK, result.Items[0])
|
||||
}
|
||||
|
||||
// The four routes below are the whole of what Memby writes back to Emby about a person:
|
||||
// a favourite, a watched flag, a hidden resume item and a playback report. Each one now
|
||||
// asks who is watching first, because that is the entire promise of a shadow viewer — the
|
||||
// Emby account lends them the library and never learns what they did with it.
|
||||
|
||||
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) {
|
||||
s.setFlag(w, r, sess, func(viewer store.Viewer, itemID string, value bool) (json.RawMessage, error) {
|
||||
if !viewer.IsMain() {
|
||||
return s.setShadowFlag(r.Context(), viewer, itemID, func() error {
|
||||
return s.store.SetViewerFavourite(r.Context(), viewer.ID, itemID, value)
|
||||
})
|
||||
}
|
||||
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) {
|
||||
s.setFlag(w, r, sess, func(viewer store.Viewer, itemID string, value bool) (json.RawMessage, error) {
|
||||
if !viewer.IsMain() {
|
||||
return s.setShadowFlag(r.Context(), viewer, itemID, func() error {
|
||||
return s.store.SetViewerPlayed(r.Context(), viewer.ID, itemID, value)
|
||||
})
|
||||
}
|
||||
return s.emby.SetPlayed(r.Context(), credentials(sess), itemID, value)
|
||||
})
|
||||
}
|
||||
|
||||
// setShadowFlag applies a Memby-side mutation and answers in the shape Emby would have.
|
||||
//
|
||||
// Re-reading the row rather than describing the write is deliberate: the response is what
|
||||
// the television draws the card from, and a favourite pressed on a title that is also part
|
||||
// way through has to come back carrying the position as well as the heart.
|
||||
func (s *Server) setShadowFlag(
|
||||
ctx context.Context, viewer store.Viewer, itemID string, apply func() error,
|
||||
) (json.RawMessage, error) {
|
||||
if err := apply(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
state, err := s.store.ViewerStateFor(ctx, viewer.ID, itemID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return viewerUserData(state), nil
|
||||
}
|
||||
|
||||
func (s *Server) handleHideFromResume(w http.ResponseWriter, r *http.Request, sess store.Session) {
|
||||
itemID := r.PathValue("id")
|
||||
if itemID == "" {
|
||||
writeError(w, http.StatusBadRequest, "item id is required")
|
||||
return
|
||||
}
|
||||
userData, err := s.emby.HideFromResume(r.Context(), credentials(sess), itemID)
|
||||
viewer := s.activeViewer(r.Context(), sess, r)
|
||||
var userData json.RawMessage
|
||||
var err error
|
||||
if viewer.IsMain() {
|
||||
userData, err = s.emby.HideFromResume(r.Context(), credentials(sess), itemID)
|
||||
} else {
|
||||
userData, err = s.setShadowFlag(r.Context(), viewer, itemID, func() error {
|
||||
return s.store.HideViewerFromResume(r.Context(), viewer.ID, itemID)
|
||||
})
|
||||
}
|
||||
if err != nil {
|
||||
s.writeUpstreamError(r.Context(), w, err, "could not remove the item from Continue Watching")
|
||||
return
|
||||
}
|
||||
if err := s.cache.InvalidateUser(r.Context(), sess.EmbyUserID); err != nil {
|
||||
if err := s.cache.InvalidateUser(r.Context(), viewer.ID); err != nil {
|
||||
s.loggerFor(r.Context()).Warn("cache invalidation failed", "error", err)
|
||||
}
|
||||
if s.forYou != nil {
|
||||
// For You is built from the Emby account's own history, so a shadow viewer's press
|
||||
// says nothing about it. Marking it dirty would rebuild the main viewer's row out of
|
||||
// somebody else's choice.
|
||||
if s.forYou != nil && viewer.IsMain() {
|
||||
s.forYou.MarkDirty(r.Context(), sess)
|
||||
}
|
||||
writeRaw(w, http.StatusOK, userData)
|
||||
@@ -345,8 +395,9 @@ func (s *Server) setFlag(
|
||||
w http.ResponseWriter,
|
||||
r *http.Request,
|
||||
sess store.Session,
|
||||
apply func(itemID string, value bool) (json.RawMessage, error),
|
||||
apply func(viewer store.Viewer, itemID string, value bool) (json.RawMessage, error),
|
||||
) {
|
||||
viewer := s.activeViewer(r.Context(), sess, r)
|
||||
itemID := r.PathValue("id")
|
||||
if itemID == "" {
|
||||
writeError(w, http.StatusBadRequest, "item id is required")
|
||||
@@ -358,15 +409,18 @@ func (s *Server) setFlag(
|
||||
return
|
||||
}
|
||||
|
||||
userData, err := apply(itemID, req.Value)
|
||||
userData, err := apply(viewer, itemID, req.Value)
|
||||
if err != nil {
|
||||
s.writeUpstreamError(r.Context(), w, err, "could not update the item")
|
||||
return
|
||||
}
|
||||
if err := s.cache.InvalidateUser(r.Context(), sess.EmbyUserID); err != nil {
|
||||
if err := s.cache.InvalidateUser(r.Context(), viewer.ID); err != nil {
|
||||
s.loggerFor(r.Context()).Warn("cache invalidation failed", "error", err)
|
||||
}
|
||||
if s.forYou != nil {
|
||||
// For You is built from the Emby account's own history, so a shadow viewer's press
|
||||
// says nothing about it. Marking it dirty would rebuild the main viewer's row out of
|
||||
// somebody else's choice.
|
||||
if s.forYou != nil && viewer.IsMain() {
|
||||
s.forYou.MarkDirty(r.Context(), sess)
|
||||
}
|
||||
writeRaw(w, http.StatusOK, userData)
|
||||
|
||||
Reference in New Issue
Block a user