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
+49
-1
@@ -171,9 +171,20 @@ export function endTimeLabel(
|
||||
|
||||
const PROGRESS_INTERVAL_MS = 30_000;
|
||||
|
||||
interface PodcastSequenceConfig {
|
||||
feed_url?: string;
|
||||
feed_title?: string;
|
||||
prev_url?: string;
|
||||
next_url?: string;
|
||||
next_title?: string;
|
||||
episode_index?: number;
|
||||
index_total?: number;
|
||||
}
|
||||
|
||||
interface AudioConfig {
|
||||
chapterEndpoint: string;
|
||||
progressEndpoint: string;
|
||||
podcastSequence?: PodcastSequenceConfig;
|
||||
}
|
||||
|
||||
function readConfig(): AudioConfig {
|
||||
@@ -419,10 +430,14 @@ Alpine.data("audioReader", () => ({
|
||||
});
|
||||
audio.addEventListener("ended", () => {
|
||||
// Natural end of track n: roll into n+1 and keep playing. At the
|
||||
// last track the book is done — nothing to advance into.
|
||||
// last track the book is done — nothing to advance into... unless
|
||||
// this is a podcast episode with a sequence (D7): chain into the
|
||||
// next downloaded episode's reader page.
|
||||
if (this.activeTrack < this.tracks.length - 1) {
|
||||
this.setActiveTrack(this.activeTrack + 1, 0, true);
|
||||
return;
|
||||
}
|
||||
this.goToNextEpisode(true);
|
||||
});
|
||||
audio.addEventListener("play", () => {
|
||||
this.playing = true;
|
||||
@@ -760,9 +775,42 @@ Alpine.data("audioReader", () => ({
|
||||
|
||||
backToDetail() {
|
||||
this.pushProgress(true);
|
||||
// Podcast episodes exit to their feed page (D7 sequence context);
|
||||
// books keep the book-detail exit.
|
||||
const seq = readConfig().podcastSequence;
|
||||
if (seq && seq.feed_url) {
|
||||
window.location.href = seq.feed_url;
|
||||
return;
|
||||
}
|
||||
window.location.href = `/media/${this.mediaItemId}`;
|
||||
},
|
||||
|
||||
// ---- podcast sequence chaining (D7) ----
|
||||
// Auto-advance across episodes: the last track's `ended` rolls into
|
||||
// the next episode's reader page (progress already pushed on
|
||||
// timeupdate; a final push fires before navigating). Books are
|
||||
// untouched — no sequence context, no behavior change.
|
||||
goToNextEpisode(finalPush) {
|
||||
const seq = readConfig().podcastSequence;
|
||||
if (!seq || !seq.next_url) return false;
|
||||
const go = () => {
|
||||
window.location.href = seq.next_url as string;
|
||||
};
|
||||
if (finalPush) {
|
||||
Promise.resolve(this.pushProgress(true)).then(go).catch(go);
|
||||
} else {
|
||||
go();
|
||||
}
|
||||
return true;
|
||||
},
|
||||
goToPrevEpisode() {
|
||||
const seq = readConfig().podcastSequence;
|
||||
if (seq && seq.prev_url) {
|
||||
this.pushProgress(true);
|
||||
window.location.href = seq.prev_url;
|
||||
}
|
||||
},
|
||||
|
||||
// ---- volume (settings strip) ----
|
||||
|
||||
volumeIcon(): "muted" | "low" | "mid" | "high" {
|
||||
|
||||
Reference in new issue
Block a user