feat(podcasts): Download All queues in the LISTENING order + add-modal order dropdown

Download All previously queued in feed-native order — 'getting the
last chapter first doesn't help me'. Now the queue follows the feed's
effective listening order (§0 item 5b): QueueFeedDownloadsAs reads the
CALLER's order override and runs episodes through services.OrderEpisodes
(same helper as the sequence/reader/page surfaces — the consolidation
from item 5a landed with the reverse-order commit). Queue tickets
(SEQUENCE-stamped, item 4) make that enqueue order the claim order, so
chapters land in the order you'll actually listen.

Add modal (§0 item 5c): with 'Download all episodes now' checked, a
'Download order' select appears (pure-CSS :checked sibling reveal — no
JS island, CSP-clean). Its choice is the feed's INITIAL order override
(reader_settings upsert; 'feed' = no row) AND the queue order for the
initial batch. Per-book by design: the override IS the download order.

Test TestPodcastAddModalDownloadOrder: add with download_all+reverse →
the override row exists AND persisted tickets run position 3→1. Tickets
are asserted (not live queued rows) because the shared dev server's
worker legitimately races this test's queue — tickets survive every
state flip.
This commit is contained in:
John O'Keefe committed 2026-10-30 10:26:00 -04:00
1 parent 0f3c4a8cec
commit 5c545534c2
7 files changed
+176 -9

No files matched your search

+44
View File
@@ -301,6 +301,15 @@ func (w *PodcastDownloadWorker) QueueEpisode(ctx context.Context, episodeID, fee
// QueueFeedDownloads implements "Download All" (D8): queue exactly the
// non-downloaded set — pending AND failed rows (retry is the same path).
func (w *PodcastDownloadWorker) QueueFeedDownloads(ctx context.Context, feedID pgtype.UUID) (int, error) {
return w.QueueFeedDownloadsAs(ctx, pgtype.UUID{}, feedID)
}
// QueueFeedDownloadsAs is Download All honoring the CALLER's listening
// order (§0 item 5b): episodes queue in the feed's effective order (the
// caller's override, feed-native fallback). Tickets (§0 item 4) make
// that queue order the download order — "getting the last chapter
// first doesn't help me". userID zero = no override (feed order).
func (w *PodcastDownloadWorker) QueueFeedDownloadsAs(ctx context.Context, userID, feedID pgtype.UUID) (int, error) {
actx, err := w.svc.adminCtx(ctx)
if err != nil {
return 0, err
@@ -313,6 +322,23 @@ func (w *PodcastDownloadWorker) QueueFeedDownloads(ctx context.Context, feedID p
if err != nil {
return 0, err
}
order := "native"
if userID.Valid {
order = w.orderOverrideFor(actx, userID, feedID)
}
// THE order helper (§0 item 5a) — same semantics as the sequence,
// reader chain, and page list; no per-surface sort drift.
OrderEpisodes(eps, order,
func(ep database.MediaItems) int32 { return ep.FeedPosition.Int32 },
func(ep database.MediaItems) string {
if ep.DatePublished.Valid {
return ep.DatePublished.Time.Format("2006-01-02")
}
return ""
},
)
queued := 0
for _, ep := range eps {
if _, err := w.db.SetPodcastEpisodeDownloadQueued(actx, ep.ID); err == nil {
@@ -323,6 +349,24 @@ func (w *PodcastDownloadWorker) QueueFeedDownloads(ctx context.Context, feedID p
return queued, nil
}
// orderOverrideFor reads a user's podcast_order override via the
// reader_settings KV (the D5 storage; native when absent/invalid).
func (w *PodcastDownloadWorker) orderOverrideFor(ctx context.Context, userID, feedID pgtype.UUID) string {
row, err := w.db.GetUserSettingByKey(ctx, database.GetUserSettingByKeyParams{
UserID: userID, SettingKey: "podcast_order:" + feedID.String(),
})
if err != nil {
return "native"
}
var v struct {
Order string `json:"order"`
}
if json.Unmarshal(row, &v) != nil || !ValidPodcastEpisodeOrder(v.Order) {
return "native"
}
return v.Order
}
// ApplyAutoPolicies unions subscriber policies for a feed (D6: most
// aggressive wins for what ARRIVES — deletion stays admin-only) and
// queues the target set, respecting the auto-retry attempt cap.