feat(podcasts): chain playback + Continue section — sequence mode in the web player (phase 2 core)

D7 chain-by-default, delivered with minimal player surgery:

- ReaderMetadata gains PodcastSequence: the neighboring DOWNLOADED
  episodes in the caller's effective order (D5 override applied,
  feed-native fallback; computed in the reader route for
  source='podcast_episode' items — RLS already scoped the reads; gaps
  never enter the chain, invariant 6). ?single=1 escapes chaining (the
  'play single episode' affordance).
- Audio reader renders the sequence into body[data-json]
  (AudioReaderConfigJSON — books produce the identical shape minus the
  sequence key, so the reader path is unchanged for them) and an
  'Ep N/M' indicator + prev/next episode controls in the header.
- audio.ts: the last-track 'ended' handler chains into the next
  episode's reader page (final progress push precedes navigation);
  back button exits episodes to their feed page. Books are untouched —
  no sequence context, zero behavior change. Day-one chaining is
  page-navigation based (progress persists; position restores).

Continue aggregation (2.5): GetPodcastContinueForUser (in-progress
episodes, most-recent-first, ≥95% excluded) feeds a Continue section
on /podcasts — sorted most recently listened, cards show 'Ep N · title'
— ahead of All Feeds. Cards prefer the continue line over the
description.
This commit is contained in:
John O'Keefe committed 2026-10-29 16:25:44 -04:00
1 parent 2235b00c59
commit 28420d56ed
14 files changed
+740 -174

No files matched your search

+18
View File
@@ -312,6 +312,24 @@ type ReaderMetadata struct {
// KeepScreenAwake: user preference — the reader holds a Screen Wake
// Lock while open when true.
KeepScreenAwake bool `json:"keep_screen_awake"`
// PodcastSequence (plan D7/Phase 2): set ONLY for podcast episodes —
// the neighboring downloaded episodes in the caller's effective
// order. Powers chain-by-default playback (auto-advance across
// episodes) and the player's episode prev/next. nil for books.
PodcastSequence *PodcastSequenceContext `json:"podcast_sequence,omitempty"`
}
// PodcastSequenceContext is the episode-level chain payload rendered
// into the audio reader's config JSON.
type PodcastSequenceContext struct {
FeedURL string `json:"feed_url,omitempty"`
FeedTitle string `json:"feed_title,omitempty"`
PrevURL string `json:"prev_url,omitempty"`
NextURL string `json:"next_url,omitempty"`
NextTitle string `json:"next_title,omitempty"`
// EpisodeIndex/IndexTotal render the "Ep N of M" indicator (D7).
EpisodeIndex int `json:"episode_index,omitempty"`
IndexTotal int `json:"index_total,omitempty"`
}
type ReadingProgress struct {