0.2.79 - Slow api fixes
This commit is contained in:
+119
-98
@@ -7,10 +7,12 @@ import (
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/ponzischeme89/memby/server/internal/cache"
|
||||
"github.com/ponzischeme89/memby/server/internal/sonarr"
|
||||
"github.com/ponzischeme89/memby/server/internal/store"
|
||||
"github.com/ponzischeme89/memby/server/internal/timing"
|
||||
)
|
||||
|
||||
type seriesEpisodesResponse struct {
|
||||
@@ -75,23 +77,21 @@ func itemDetailKey(userID, itemID string) string {
|
||||
func (s *Server) detailItem(
|
||||
ctx context.Context, sess store.Session, itemID string,
|
||||
) (json.RawMessage, error) {
|
||||
key := itemDetailKey(sess.EmbyUserID, itemID)
|
||||
if raw, err := s.cache.Get(ctx, key); err == nil {
|
||||
return raw, nil
|
||||
}
|
||||
item, err := s.emby.Item(ctx, credentials(sess), itemID, fieldsDetail)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 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.loggerFor(ctx).Warn("item cache write failed", "error", err)
|
||||
}
|
||||
return item, nil
|
||||
item, _, err := s.cachedRead(ctx, itemDetailKey(sess.EmbyUserID, itemID), s.cfg.ItemTTL,
|
||||
func(ctx context.Context) (json.RawMessage, error) {
|
||||
raw, err := s.emby.Item(
|
||||
timing.WithLabel(ctx, "emby.item"), credentials(sess), itemID, fieldsDetail)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 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{raw}
|
||||
s.decorateItemRatings(ctx, decorated)
|
||||
return decorated[0], nil
|
||||
})
|
||||
return item, err
|
||||
}
|
||||
|
||||
// handleSeasonFinale verifies an episode against Sonarr's complete season, including
|
||||
@@ -109,92 +109,113 @@ func (s *Server) handleSeasonFinale(w http.ResponseWriter, r *http.Request, sess
|
||||
writeJSON(w, http.StatusOK, empty)
|
||||
return
|
||||
}
|
||||
key := cache.UserKey(sess.EmbyUserID, "season-finale:v1:"+itemID)
|
||||
if raw, err := s.cache.Get(ctx, key); err == nil {
|
||||
writeRaw(w, http.StatusOK, raw)
|
||||
return
|
||||
}
|
||||
|
||||
currentRaw, err := s.emby.Item(
|
||||
ctx, credentials(sess), itemID,
|
||||
"ProviderIds,SeriesId,SeriesName,ParentIndexNumber,IndexNumber",
|
||||
)
|
||||
// Cached for the household rather than per viewer, and outside the namespace a
|
||||
// playback stop invalidates. Whether an episode closes its season is a fact about
|
||||
// the season; it carries no user data, and it was previously being thrown away by
|
||||
// the very event that most often precedes somebody asking for it — the stop report
|
||||
// at the end of the episode before.
|
||||
key := cache.MetadataKey("season-finale:v2:" + itemID)
|
||||
body, hit, err := s.cachedRead(ctx, key, s.cfg.ItemTTL,
|
||||
func(ctx context.Context) (json.RawMessage, error) {
|
||||
return json.Marshal(s.seasonFinale(ctx, sess, itemID))
|
||||
})
|
||||
if err != nil {
|
||||
s.writeUpstreamError(ctx, w, err, "could not inspect the playing episode")
|
||||
return
|
||||
}
|
||||
writeCached(w, hit, body)
|
||||
}
|
||||
|
||||
// seasonFinale is four upstream reads and only three of them are a chain.
|
||||
//
|
||||
// The episode has to be read before its series can be, and the series before Sonarr can
|
||||
// be searched for it — but Sonarr's series catalogue depends on none of that, and used
|
||||
// to be fetched fourth, in sequence, after both Emby lookups had returned. It is now
|
||||
// fetched beside them, which takes a leg off the wait for the ordinary case and all of
|
||||
// it for a household whose catalogue is still warm.
|
||||
//
|
||||
// Every failure below answers "not a finale" rather than an error. This decorates the
|
||||
// end of an episode; being unable to say is the same outcome as saying no, and a viewer
|
||||
// must never be shown a failure for it.
|
||||
func (s *Server) seasonFinale(
|
||||
ctx context.Context, sess store.Session, itemID string,
|
||||
) seasonFinaleResponse {
|
||||
empty := seasonFinaleResponse{}
|
||||
|
||||
var (
|
||||
catalogue []sonarr.Series
|
||||
catalogueErr error
|
||||
wg sync.WaitGroup
|
||||
)
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
catalogue, catalogueErr = s.sonarrSeriesCatalogue(timing.WithLabel(ctx, "sonarr.series"))
|
||||
}()
|
||||
// Whatever happens below, the catalogue read has to be waited for: it is running on
|
||||
// this request's context and returning without it would leave a goroutine writing to
|
||||
// variables the caller has moved on from.
|
||||
defer wg.Wait()
|
||||
|
||||
currentRaw, err := s.emby.Item(
|
||||
timing.WithLabel(ctx, "emby.episode"), credentials(sess), itemID,
|
||||
"ProviderIds,SeriesId,SeriesName,ParentIndexNumber,IndexNumber",
|
||||
)
|
||||
if err != nil {
|
||||
s.loggerFor(ctx).Warn("season finale episode unavailable", "item", itemID, "error", err)
|
||||
return empty
|
||||
}
|
||||
var current finaleEmbyItem
|
||||
if json.Unmarshal(currentRaw, ¤t) != nil ||
|
||||
!strings.EqualFold(current.Type, "Episode") || current.SeriesID == "" ||
|
||||
current.ParentIndexNumber <= 0 || current.IndexNumber <= 0 {
|
||||
s.writeSeasonFinaleResponse(ctx, key, empty, w)
|
||||
return
|
||||
return empty
|
||||
}
|
||||
|
||||
seriesRaw, err := s.emby.Item(ctx, credentials(sess), current.SeriesID, "ProviderIds")
|
||||
seriesRaw, err := s.emby.Item(
|
||||
timing.WithLabel(ctx, "emby.series"), credentials(sess), current.SeriesID, "ProviderIds")
|
||||
if err != nil {
|
||||
s.loggerFor(ctx).Warn("season finale series metadata unavailable", "item", itemID, "error", err)
|
||||
s.writeSeasonFinaleResponse(ctx, key, empty, w)
|
||||
return
|
||||
return empty
|
||||
}
|
||||
var seriesItem finaleEmbyItem
|
||||
if json.Unmarshal(seriesRaw, &seriesItem) != nil {
|
||||
s.writeSeasonFinaleResponse(ctx, key, empty, w)
|
||||
return
|
||||
return empty
|
||||
}
|
||||
tvdbID, err := strconv.Atoi(providerID(seriesItem.ProviderIDs, "tvdb"))
|
||||
if err != nil || tvdbID <= 0 {
|
||||
s.writeSeasonFinaleResponse(ctx, key, empty, w)
|
||||
return
|
||||
return empty
|
||||
}
|
||||
|
||||
series, err := s.sonarrSeriesCatalogue(ctx)
|
||||
if err != nil {
|
||||
s.loggerFor(ctx).Warn("season finale Sonarr series unavailable", "item", itemID, "error", err)
|
||||
s.writeSeasonFinaleResponse(ctx, key, empty, w)
|
||||
return
|
||||
wg.Wait()
|
||||
if catalogueErr != nil {
|
||||
s.loggerFor(ctx).Warn("season finale Sonarr series unavailable", "item", itemID, "error", catalogueErr)
|
||||
return empty
|
||||
}
|
||||
sonarrSeriesID := 0
|
||||
for _, candidate := range series {
|
||||
for _, candidate := range catalogue {
|
||||
if candidate.TVDBID == tvdbID {
|
||||
sonarrSeriesID = candidate.ID
|
||||
break
|
||||
}
|
||||
}
|
||||
if sonarrSeriesID == 0 {
|
||||
s.writeSeasonFinaleResponse(ctx, key, empty, w)
|
||||
return
|
||||
return empty
|
||||
}
|
||||
episodes, err := s.sonarr.Episodes(ctx, sonarrSeriesID)
|
||||
episodes, err := s.sonarr.Episodes(timing.WithLabel(ctx, "sonarr.episodes"), sonarrSeriesID)
|
||||
if err != nil {
|
||||
s.loggerFor(ctx).Warn("season finale Sonarr episodes unavailable", "item", itemID, "error", err)
|
||||
s.writeSeasonFinaleResponse(ctx, key, empty, w)
|
||||
return
|
||||
return empty
|
||||
}
|
||||
result := seasonFinaleResponse{
|
||||
SeasonFinale: isSeasonFinale(current.ParentIndexNumber, current.IndexNumber, episodes),
|
||||
if !isSeasonFinale(current.ParentIndexNumber, current.IndexNumber, episodes) {
|
||||
return empty
|
||||
}
|
||||
return seasonFinaleResponse{
|
||||
SeasonFinale: true,
|
||||
SeriesName: current.SeriesName,
|
||||
SeasonNumber: current.ParentIndexNumber,
|
||||
EpisodeNumber: current.IndexNumber,
|
||||
}
|
||||
if !result.SeasonFinale {
|
||||
result = empty
|
||||
}
|
||||
s.writeSeasonFinaleResponse(ctx, key, result, w)
|
||||
}
|
||||
|
||||
func (s *Server) writeSeasonFinaleResponse(
|
||||
ctx context.Context, key string, result seasonFinaleResponse, w http.ResponseWriter,
|
||||
) {
|
||||
body, err := json.Marshal(result)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "could not encode finale status")
|
||||
return
|
||||
}
|
||||
if err := s.cache.Set(ctx, key, body, s.cfg.ItemTTL); err != nil {
|
||||
s.loggerFor(ctx).Warn("season finale cache write failed", "error", err)
|
||||
}
|
||||
writeRaw(w, http.StatusOK, body)
|
||||
}
|
||||
|
||||
func providerID(ids map[string]string, wanted string) string {
|
||||
@@ -228,42 +249,42 @@ func (s *Server) handleSeriesEpisodes(w http.ResponseWriter, r *http.Request, se
|
||||
writeError(w, http.StatusBadRequest, "series id is required")
|
||||
return
|
||||
}
|
||||
// Per viewer, because the browser marks what has been watched — and coalesced,
|
||||
// because this is the largest request the television makes and it is made twice for
|
||||
// the same show as a matter of course: the launcher warms it while a Continue
|
||||
// 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)
|
||||
if raw, err := s.cache.Get(ctx, key); err == nil {
|
||||
w.Header().Set("X-Memby-Cache", "hit")
|
||||
writeRaw(w, http.StatusOK, raw)
|
||||
return
|
||||
}
|
||||
|
||||
result, err := s.emby.Episodes(ctx, credentials(sess), seriesID, url.Values{
|
||||
"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"},
|
||||
"ImageTypeLimit": {"1"},
|
||||
"Limit": {"1000"},
|
||||
})
|
||||
body, hit, err := s.cachedRead(ctx, key, s.cfg.ItemTTL,
|
||||
func(ctx context.Context) (json.RawMessage, error) {
|
||||
result, err := s.emby.Episodes(
|
||||
timing.WithLabel(ctx, "emby.episodes"), credentials(sess), seriesID, url.Values{
|
||||
"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"},
|
||||
"ImageTypeLimit": {"1"},
|
||||
"Limit": {"1000"},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items := result.Items
|
||||
if items == nil {
|
||||
items = []json.RawMessage{}
|
||||
}
|
||||
return json.Marshal(seriesEpisodesResponse{Items: items})
|
||||
})
|
||||
if err != nil {
|
||||
s.writeUpstreamError(ctx, w, err, "could not load series episodes")
|
||||
return
|
||||
}
|
||||
items := result.Items
|
||||
if items == nil {
|
||||
items = []json.RawMessage{}
|
||||
}
|
||||
body, err := json.Marshal(seriesEpisodesResponse{Items: items})
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "could not encode series episodes")
|
||||
return
|
||||
}
|
||||
if err := s.cache.Set(ctx, key, body, s.cfg.ItemTTL); err != nil {
|
||||
s.loggerFor(ctx).Warn("series episodes cache write failed", "error", err)
|
||||
}
|
||||
w.Header().Set("X-Memby-Cache", "miss")
|
||||
writeRaw(w, http.StatusOK, body)
|
||||
writeCached(w, hit, body)
|
||||
}
|
||||
|
||||
// handleTrailer answers with the item's first local trailer, or 404 when it has none.
|
||||
|
||||
Reference in New Issue
Block a user