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:
1 parent
2235b00c59
commit
28420d56ed
14 files changed
+740
-174
No files matched your search
@@ -6,6 +6,7 @@ package templates
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -42,6 +43,14 @@ type PodcastFeedCard struct {
|
||||
AutoDownload string
|
||||
AutoDownloadCount int
|
||||
Subscribed bool
|
||||
|
||||
// Continue aggregation (plan 2.5): the episode the user is midway
|
||||
// through, most recent first across feeds.
|
||||
HasContinue bool
|
||||
ContinueTitle string
|
||||
ContinueEpisode int
|
||||
ContinuePercent float64
|
||||
ContinueLastRead time.Time // section sort key (most recent first)
|
||||
}
|
||||
|
||||
// DisplayTitle renders the subscription display name when set (D9).
|
||||
@@ -138,7 +147,11 @@ func FormatEpisodeSize(bytes int64) string {
|
||||
|
||||
// PodcastsPageData carries everything the /podcasts page renders.
|
||||
type PodcastsPageData struct {
|
||||
Feeds []PodcastFeedCard
|
||||
// ContinueFeeds: the in-progress subset, most recently listened
|
||||
// first (plan 2.5) — split in the page builder (templ bodies can't
|
||||
// declare locals).
|
||||
ContinueFeeds []PodcastFeedCard
|
||||
Feeds []PodcastFeedCard
|
||||
Libraries []LibraryData // podcasts-type libraries (add-feed targets)
|
||||
Facets map[string][]string
|
||||
Query string
|
||||
@@ -168,3 +181,20 @@ func podcastTimeOrEmpty(t time.Time, valid bool, tz string) string {
|
||||
}
|
||||
return utils.FormatTimeInTimezone(t, tz)
|
||||
}
|
||||
|
||||
|
||||
// PodcastContinueFeeds returns the in-progress feeds, most recently
|
||||
// listened first (plan 2.5). ≥95% progress counts as finished and is
|
||||
// excluded (the continue query already applies that bound).
|
||||
func PodcastContinueFeeds(feeds []PodcastFeedCard) []PodcastFeedCard {
|
||||
out := make([]PodcastFeedCard, 0, len(feeds))
|
||||
for _, f := range feeds {
|
||||
if f.HasContinue {
|
||||
out = append(out, f)
|
||||
}
|
||||
}
|
||||
sort.SliceStable(out, func(i, j int) bool {
|
||||
return out[i].ContinueLastRead.After(out[j].ContinueLastRead)
|
||||
})
|
||||
return out
|
||||
}
|
||||
Reference in new issue
Block a user