This commit is contained in:
ponzischeme89
2026-08-16 12:13:51 +12:00
parent bd3732fba5
commit b9374baaf1
47 changed files with 2793 additions and 364 deletions
+55 -2
View File
@@ -94,8 +94,12 @@ type playbackReportResponse struct {
// Emby's own item JSON, forwarded verbatim like every other item the gateway returns, so
// the client decodes it into the same BaseItem it uses everywhere else.
type nextEpisodeResponse struct {
Item json.RawMessage `json:"item"`
Title string `json:"title"`
Item json.RawMessage `json:"item"`
Title string `json:"title"`
// StreamReady says whether this answer carries a negotiated stream. A client asking
// with `stream=0` gets identity, artwork and runtime and nothing that touches Emby's
// PlaybackInfo — see [handleNextEpisode] for why that separation exists.
StreamReady bool `json:"streamReady"`
URL string `json:"url"`
ResumePositionMs int64 `json:"resumePositionMs"`
Subtitles []playableSubtitle `json:"subtitles"`
@@ -302,6 +306,20 @@ func episodeCode(item emby.Summary) string {
//
// "Nothing follows this" is a normal answer, not a failure: a movie, a series finale and an
// unreadable series all come back as 404 and the client simply shows no banner.
//
// It answers in two shapes, and the separation is the whole reason auto-advance stopped
// showing a viewer the machinery. `stream=0` asks for **identity only** — the episode's
// item JSON, its title and its artwork — and touches nothing but the episode list. The
// full answer additionally negotiates PlaybackInfo, which mints a play session and, on a
// title that has to be transcoded, a transcode session at Emby.
//
// The television asks for the cheap shape when the episode it is watching *starts*, which
// is where the banner's picture and wording come from, and for the full one shortly before
// the hand-over. Doing both at once is what the player used to do, and it was wrong twice:
// a session negotiated forty minutes early has long since been torn down by the time it is
// used — so the transition failed, retried, and narrated all of it to the viewer — and the
// transcode it may have started sat there being paid for by a viewer who had not yet
// decided to watch it.
func (s *Server) handleNextEpisode(w http.ResponseWriter, r *http.Request, sess store.Session) {
ctx := r.Context()
itemID := r.PathValue("id")
@@ -355,6 +373,27 @@ func (s *Server) handleNextEpisode(w http.ResponseWriter, r *http.Request, sess
if series := strings.TrimSpace(seriesNameOf(raw)); series != "" && title != "" {
title = series + " " + title
}
// The metadata-only shape. Everything below this point is a PlaybackInfo negotiation,
// and a client that said it does not want one yet must not be given one anyway.
if !nextEpisodeWantsStream(r) {
s.playbackTitles.remember(next.ID, title)
s.loggerFor(ctx).Debug("next episode identified",
"title", title, "item", next.ID, "after_item", itemID)
writeJSON(w, http.StatusOK, nextEpisodeResponse{
Item: raw,
Title: title,
StreamReady: false,
ResumePositionMs: max64(next.UserData.PlaybackPositionTicks/ticksPerMillisecond, 0),
Subtitles: []playableSubtitle{},
SubtitlesEnabled: true,
TrickplayAvailable: s.trickplayEnabled(ctx),
SkipIntroAvailable: s.skipIntroEnabled(ctx),
EndCreditsAvailable: s.endCreditsEnabled(ctx),
})
return
}
subtitles, mediaSourceID, playSessionID, negotiatedURL, playMethod := s.playbackSubtitles(
ctx, cred, next.ID, next.UserData.PlaybackPositionTicks, nil, "", false,
s.effectivePlaybackCapabilities(ctx, sess),
@@ -380,6 +419,7 @@ func (s *Server) handleNextEpisode(w http.ResponseWriter, r *http.Request, sess
writeJSON(w, http.StatusOK, nextEpisodeResponse{
Item: raw,
Title: title,
StreamReady: true,
URL: streamURL,
ResumePositionMs: max64(next.UserData.PlaybackPositionTicks/ticksPerMillisecond, 0),
Subtitles: subtitles,
@@ -395,6 +435,19 @@ func (s *Server) handleNextEpisode(w http.ResponseWriter, r *http.Request, sess
})
}
// nextEpisodeWantsStream reads the client's `stream` parameter, and **defaults to yes**.
// An app built before the two-phase lookup existed sends nothing and expects the whole
// answer; reading a missing parameter as "identity only" would hand every one of those
// televisions a next-up banner with no stream behind it.
func nextEpisodeWantsStream(r *http.Request) bool {
switch strings.TrimSpace(strings.ToLower(r.URL.Query().Get("stream"))) {
case "0", "false", "no":
return false
default:
return true
}
}
func (s *Server) playbackSubtitles(
ctx context.Context, cred emby.Credentials, itemID string, startTicks int64,
subtitleIndex *int, currentPlaySessionID string, forceTranscode bool,