0.2.84
This commit is contained in:
+79
-46
@@ -76,6 +76,13 @@ func (s *Server) handleHome(w http.ResponseWriter, r *http.Request, sess store.S
|
||||
radarrSchedule := s.radarrEnabled(r.Context()) && supportsRadarrSchedule(r)
|
||||
hero := supportsHomeHero(r)
|
||||
now := time.Now()
|
||||
featurePolicy := s.currentFeaturePolicy(ctx)
|
||||
continueDefinition, _ := knownFeature(featureContinueWatching)
|
||||
continueWatching := evaluateFeature(
|
||||
featurePolicy,
|
||||
continueDefinition,
|
||||
ProtocolVersion,
|
||||
).Enabled
|
||||
// The hero revision is part of the key rather than something to invalidate. An
|
||||
// operator's change therefore makes the entries built under the old policy simply
|
||||
// unreachable, and they age out on their own TTL — where the sweep it replaced dropped
|
||||
@@ -86,6 +93,8 @@ func (s *Server) handleHome(w http.ResponseWriter, r *http.Request, sess store.S
|
||||
viewerKeyOf(ctx, sess),
|
||||
"home:v4:"+itoa(limit)+":s"+strconv.FormatBool(sonarrSchedule)+
|
||||
":r"+strconv.FormatBool(radarrSchedule)+":h"+strconv.FormatBool(hero)+
|
||||
":c"+strconv.FormatBool(continueWatching)+
|
||||
":f"+strconv.FormatInt(featurePolicy.Revision, 10)+
|
||||
":hr"+heroRev+":d"+sess.DeviceID,
|
||||
)
|
||||
|
||||
@@ -104,6 +113,7 @@ func (s *Server) handleHome(w http.ResponseWriter, r *http.Request, sess store.S
|
||||
var (
|
||||
mu sync.Mutex
|
||||
failures int
|
||||
rowAttempts int
|
||||
out homeResponse
|
||||
sonarrRow *recommend.Row
|
||||
radarrRow *recommend.Row
|
||||
@@ -120,6 +130,7 @@ func (s *Server) handleHome(w http.ResponseWriter, r *http.Request, sess store.S
|
||||
// on rather than only that it was waiting on Emby. They are concurrent, so a single
|
||||
// summed `emby=` would be the one number that could not answer that.
|
||||
run := func(name string, dest *[]json.RawMessage, fetch func(context.Context) (*emby.ItemsResult, error)) {
|
||||
rowAttempts++
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
@@ -144,16 +155,18 @@ func (s *Server) handleHome(w http.ResponseWriter, r *http.Request, sess store.S
|
||||
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"},
|
||||
"Limit": {itoa(limit)},
|
||||
}, fieldsContinue))
|
||||
})
|
||||
if continueWatching {
|
||||
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"},
|
||||
"Limit": {itoa(limit)},
|
||||
}, fieldsContinue))
|
||||
})
|
||||
}
|
||||
run("favourites", &out.Favorites, func(ctx context.Context) (*emby.ItemsResult, error) {
|
||||
if shadow {
|
||||
return s.viewerFavouritesRow(ctx, cred, viewer.ID, limit)
|
||||
@@ -167,38 +180,42 @@ func (s *Server) handleHome(w http.ResponseWriter, r *http.Request, sess store.S
|
||||
"Limit": {itoa(limit)},
|
||||
}, 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))
|
||||
})
|
||||
if continueWatching {
|
||||
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))
|
||||
})
|
||||
}
|
||||
// What orders the Next Up half of Continue Watching. A failure here is not a failed
|
||||
// row: the merge falls back to putting the resume items first, which is the order
|
||||
// the launcher had before the two rows became one.
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
// 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
|
||||
}
|
||||
mu.Lock()
|
||||
seriesPlayed = played
|
||||
mu.Unlock()
|
||||
}()
|
||||
if continueWatching {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
// 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
|
||||
}
|
||||
mu.Lock()
|
||||
seriesPlayed = played
|
||||
mu.Unlock()
|
||||
}()
|
||||
}
|
||||
run("latest", &out.LatestMovies, func(ctx context.Context) (*emby.ItemsResult, error) {
|
||||
newReleaseDays := s.weightedConfig().NewReleaseDays
|
||||
if newReleaseDays < 1 {
|
||||
@@ -298,7 +315,7 @@ func (s *Server) handleHome(w http.ResponseWriter, r *http.Request, sess store.S
|
||||
wg.Wait()
|
||||
upstream()
|
||||
|
||||
if failures == 4 {
|
||||
if rowAttempts > 0 && failures == rowAttempts {
|
||||
writeError(w, http.StatusBadGateway, "could not reach the emby server")
|
||||
return
|
||||
}
|
||||
@@ -307,11 +324,15 @@ func (s *Server) handleHome(w http.ResponseWriter, r *http.Request, sess store.S
|
||||
// One row, not two: an episode finished a minute ago should be followed by the next
|
||||
// one at the front of Continue Watching rather than moving the show to a different
|
||||
// row. NextUp stays on the wire for televisions that predate the merge.
|
||||
out.ContinueWatching = mergeContinueWatching(out.ContinueWatching, out.NextUp, seriesPlayed)
|
||||
if continueWatching {
|
||||
out.ContinueWatching = mergeContinueWatching(out.ContinueWatching, out.NextUp, seriesPlayed)
|
||||
}
|
||||
// A current show with an episode today is more immediately useful than a long-lived
|
||||
// resume from an ended rewatch. Keep Emby's order inside both groups; this is a
|
||||
// promotion, not a replacement ranking for Continue Watching.
|
||||
out.ContinueWatching = prioritizeAiringTodayContinue(out.ContinueWatching, sonarrRow)
|
||||
if continueWatching {
|
||||
out.ContinueWatching = prioritizeAiringTodayContinue(out.ContinueWatching, sonarrRow)
|
||||
}
|
||||
|
||||
// Recommendations are read from their own long-lived cache. A miss means this
|
||||
// response ships without them and a rebuild starts in the background — the home
|
||||
@@ -321,7 +342,7 @@ func (s *Server) handleHome(w http.ResponseWriter, r *http.Request, sess store.S
|
||||
s.refreshRecommendationsInBackground(sess)
|
||||
}
|
||||
assemble := timing.Start(ctx, timing.StageRows)
|
||||
rows := baseRows(out)
|
||||
rows := visibleBaseRows(out, continueWatching)
|
||||
nearContinue := make([]recommend.Row, 0, len(forYouRows)+2)
|
||||
if len(forYouRows) > 0 {
|
||||
nearContinue = append(nearContinue, forYouRows...)
|
||||
@@ -337,9 +358,13 @@ func (s *Server) handleHome(w http.ResponseWriter, r *http.Request, sess store.S
|
||||
nearContinue = append(nearContinue, *radarrRow)
|
||||
}
|
||||
if len(nearContinue) > 0 {
|
||||
// Personalised discovery and the upcoming schedule are most useful immediately
|
||||
// after Continue Watching, before the broader library collections.
|
||||
rows = append(rows[:1], append(nearContinue, rows[1:]...)...)
|
||||
// Personalised discovery and the upcoming schedule are most useful at the start of
|
||||
// Home: after Continue Watching when it is present, before broader collections.
|
||||
insertAt := 0
|
||||
if continueWatching {
|
||||
insertAt = 1
|
||||
}
|
||||
rows = append(rows[:insertAt], append(nearContinue, rows[insertAt:]...)...)
|
||||
}
|
||||
out.Rows = append(rows, recommendations...)
|
||||
out.Rows = s.filterRecommendationPermissions(ctx, sess, out.Rows)
|
||||
@@ -733,6 +758,14 @@ func baseRows(h homeResponse) []recommend.Row {
|
||||
}
|
||||
}
|
||||
|
||||
func visibleBaseRows(h homeResponse, continueWatching bool) []recommend.Row {
|
||||
rows := baseRows(h)
|
||||
if continueWatching {
|
||||
return rows
|
||||
}
|
||||
return rows[1:]
|
||||
}
|
||||
|
||||
type homeForYouWindow struct {
|
||||
ID string
|
||||
Title string
|
||||
|
||||
Reference in New Issue
Block a user