This commit is contained in:
ponzischeme89
2026-08-20 15:06:00 +12:00
parent 549f9c5eed
commit f1164db2c5
52 changed files with 5441 additions and 158 deletions
+31 -4
View File
@@ -83,7 +83,7 @@ func (s *Server) handleHome(w http.ResponseWriter, r *http.Request, sess store.S
// four cards. See heroRevision.
heroRev := heroRevision(s.currentHeroPolicy(ctx), sess.EmbyUserID, now, s.heroLocation())
key := cache.UserKey(
sess.EmbyUserID,
viewerKeyOf(ctx, sess),
"home:v4:"+itoa(limit)+":s"+strconv.FormatBool(sonarrSchedule)+
":r"+strconv.FormatBool(radarrSchedule)+":h"+strconv.FormatBool(hero)+
":hr"+heroRev+":d"+sess.DeviceID,
@@ -135,7 +135,19 @@ func (s *Server) handleHome(w http.ResponseWriter, r *http.Request, sess store.S
}()
}
// The three rows that are answers about a *person* rather than about the library.
// For the account's own viewer they are Emby's, exactly as they always were; for a
// shadow viewer they are built from that viewer's own state, and Emby is asked only to
// describe the titles. The fan-out, the failure counting and the merge below are
// unchanged either way — this is a substitution of one fetch for another, not a second
// code path through the launcher.
viewer := viewerOf(ctx, sess)
shadow := !viewer.IsMain() && s.store != nil
run("resume", &out.ContinueWatching, func(ctx context.Context) (*emby.ItemsResult, error) {
if shadow {
return s.viewerContinueRow(ctx, cred, viewer.ID, limit)
}
return s.emby.ResumeItems(ctx, cred, rowParams(url.Values{
"Recursive": {"true"},
"MediaTypes": {"Video"},
@@ -143,6 +155,9 @@ func (s *Server) handleHome(w http.ResponseWriter, r *http.Request, sess store.S
}, fieldsContinue))
})
run("favourites", &out.Favorites, func(ctx context.Context) (*emby.ItemsResult, error) {
if shadow {
return s.viewerFavouritesRow(ctx, cred, viewer.ID, limit)
}
return s.emby.Items(ctx, cred, rowParams(url.Values{
"Filters": {"IsFavorite"},
"IncludeItemTypes": {"Movie,Series"},
@@ -153,6 +168,9 @@ func (s *Server) handleHome(w http.ResponseWriter, r *http.Request, sess store.S
}, fieldsRow))
})
run("nextup", &out.NextUp, func(ctx context.Context) (*emby.ItemsResult, error) {
if shadow {
return s.viewerNextUpRow(ctx, cred, viewer.ID, limit)
}
return s.emby.NextUp(ctx, cred, rowParams(url.Values{
"Limit": {itoa(limit)},
}, fieldsContinue))
@@ -163,7 +181,16 @@ func (s *Server) handleHome(w http.ResponseWriter, r *http.Request, sess store.S
wg.Add(1)
go func() {
defer wg.Done()
played, err := s.recentlyPlayedSeries(timing.WithLabel(ctx, "emby.recent"), cred)
// What orders the two halves of the merge. For a shadow viewer it is one grouped
// query over their own state rather than a lookback over the account's plays —
// the same question, asked of the system that holds the answer.
var played map[string]time.Time
var err error
if shadow {
played, err = s.store.ViewerWatchedSeries(ctx, viewer.ID, continuePlayLookback)
} else {
played, err = s.recentlyPlayedSeries(timing.WithLabel(ctx, "emby.recent"), cred)
}
if err != nil {
s.loggerFor(ctx).Warn("recently played lookup failed", "error", err)
return
@@ -578,7 +605,7 @@ func (s *Server) handleSearch(w http.ResponseWriter, r *http.Request, sess store
return
}
limit := queryInt(r, "limit", 40, 100)
key := cache.UserKey(sess.EmbyUserID, "search:"+itoa(limit)+":"+term+":d"+sess.DeviceID)
key := cache.UserKey(viewerKeyOf(ctx, sess), "search:"+itoa(limit)+":"+term+":d"+sess.DeviceID)
// Every search the tab performs is recorded here, before the cache is consulted, so a
// query answered from Redis counts the same as one that reached Emby. The client also
@@ -606,7 +633,7 @@ func (s *Server) handleSearch(w http.ResponseWriter, r *http.Request, sess store
return
}
items := s.personalizeSearch(ctx, sess, term, result.Items, limit)
s.decorateItemRatings(ctx, items)
s.decorateItems(ctx, items)
// Instant search fires a request per keystroke past the second character, so this is
// DEBUG: it is the record of what somebody was looking for when nothing was found,
// not something to carry in the normal log.