feat(podcasts): real-time episode list — byte progress bars, direct swaps (issues #1/#3)
WHY: downloads needed manual refreshes to see progress; the 204 + HX-Trigger event indirection missed its listener, and the worker buffered whole enclosures so no progress existed to show. - Worker streams enclosures to disk through a counting loop (64KiB chunks, scheme/redirect/size guards preserved) with an in-memory per-episode progress snapshot (received/total; total from Content-Length, seeded by the feed's declared length). Deliberately transient — the DB row state remains the durable truth. - Episode-list renderer seam: the router injects a closure into PodcastHandler (handlers can't import templates — MediaDetail cycle) so htmx download/retry/Download All/refresh/order/policy actions answer with the REFRESHED partial directly (200, hx-target= #episode-list) — no events to miss. Non-HX callers keep JSON; tests without a renderer keep 204+trigger. - Polling tightens to 2s while downloads are active; downloading rows render a percentage bar + MB counter (quote-free inline style per templ gotcha #8) or an indeterminate bar when the server sends no length. - Issue #3: Download All (button AND the add-feed checkbox) flips the CALLER's subscription policy to 'all' — per-user, never global (D6: files shared, policies not) — and the auto-download select out-of-band-swaps so the dropdown shows what the user is getting. They can turn it down afterwards; the flip is honest communication, not coercion (user-approved 2026-10-29). PINs: Download All with HX answers 200 + episode-list + oob select and flips newest → all; add-feed download_all sets policy all.
This commit is contained in:
1 parent
ffc80f7ba7
commit
30766020c8
8 files changed
+545
-57
No files matched your search
@@ -91,10 +91,23 @@ type PodcastEpisodeRow struct {
|
||||
Size string // human MB/GB when known
|
||||
PlayURL string // reader URL (audio reader serves episodes like audiobooks)
|
||||
CoverImage string
|
||||
ProgressPct float64
|
||||
Downloaded bool
|
||||
DownloadState string // pending|queued|downloading|downloaded|failed
|
||||
DownloadError string
|
||||
|
||||
// Playback progress (reading_progress; the row badge) — reuses the
|
||||
// ProgressPct field below when the row is downloading? No: keep the
|
||||
// two concerns separate (issue #1).
|
||||
ListenPct float64
|
||||
|
||||
// Live byte progress for rows in the downloading state (issue #1):
|
||||
// the worker's in-flight snapshot. ProgressKnown=false renders the
|
||||
// indeterminate bar; ProgressPct (0-100) drives the width when the
|
||||
// total is authoritative.
|
||||
ProgressKnown bool
|
||||
ProgressPct float64
|
||||
ProgressReceived int64
|
||||
ProgressTotal int64
|
||||
}
|
||||
|
||||
// StateBadge renders the canonical state label.
|
||||
@@ -172,6 +185,10 @@ type PodcastDetailData struct {
|
||||
Order string // native | oldest | newest (the caller's override)
|
||||
IsAdmin bool
|
||||
IsSerial bool // serial/audiobook feeds get the oldest-first suggestion (D5)
|
||||
// SubscriberCount (issue #7): drives the last-subscriber
|
||||
// unsubscribe flow — the sole subscriber who is an admin gets the
|
||||
// three-choice modal (unsubscribe / remove feed / cancel).
|
||||
SubscriberCount int
|
||||
}
|
||||
|
||||
// podcastInt renders an int for templates.
|
||||
|
||||
Reference in new issue
Block a user