From 9a449f8f5de0686851af57bcc38c26b9f4850a00 Mon Sep 17 00:00:00 2001 From: Anthony Ettinger Date: Wed, 19 Aug 2026 14:36:04 +0000 Subject: [PATCH] Queue a whole podcast from its own page, not just a topic MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit "Queue all" existed and only a topic could reach it. QueueAll.jsx was wired into TopicPlayer alone, and /api/queue spoke add-topic and remove-topic and nothing else — so a show's own page offered a play control on every episode and no way to take the show. Fifty episodes, fifty buttons, which is not a feature. The component already did the right thing, so it names a feed as well as a topic rather than being copied: same form, same endpoint, same toggle, and one extra branch deciding which query the server re-runs when the form comes back. A second component would have drifted. What the button acts on is the one thing that had to be got right. It names the feed rather than carrying its episodes — a form of fifty guids is four kilobytes a stale tab can replay — so the page and the endpoint each work the answer out separately, and would disagree the moment either changed its mind. They call playableEntries over the same query at the same limit instead, which is also why the page's literal 50 became FEED_QUEUE_LIMIT: the count on the button and the rows the press adds are now the same set by construction. Blog posts are left out rather than queued to Read. The control says play, and quietly filling somebody's reading queue with fifty essays is not what they pressed; a feed with nothing playable draws no button at all, which is every blog in the directory. Verified against production data: the podcast page renders "Queue all 10" posting add-feed to the Listen lane, a blog page renders nothing, and the topic player still posts add-topic exactly as before. Co-Authored-By: Claude Opus 5 (1M context) --- apps/web/src/app/QueueAll.jsx | 29 +++++++++++-- apps/web/src/app/[slug]/page.jsx | 31 +++++++++++++- apps/web/src/app/api/queue/route.js | 30 +++++++++++++- apps/web/src/lib/queue.js | 64 +++++++++++++++++++++++++++++ apps/web/test/queue.test.js | 63 +++++++++++++++++++++++++++- 5 files changed, 209 insertions(+), 8 deletions(-) diff --git a/apps/web/src/app/QueueAll.jsx b/apps/web/src/app/QueueAll.jsx index 1260fde..0956375 100644 --- a/apps/web/src/app/QueueAll.jsx +++ b/apps/web/src/app/QueueAll.jsx @@ -20,8 +20,14 @@ import { LANE_LABEL } from '../lib/queue.js'; * undoing it, and it should undo *this* rather than empty the lane and take the * rest of the reader's queue with it. * + * Names either a topic or a single feed. Both post the same shape to the same + * endpoint and differ only in which query the server re-runs, so a feed page + * gets the control it was missing without a second component drifting away from + * this one. + * * @param {{ - * topic: string, + * topic?: string|null, + * feed?: string|null, * group?: string|null, * total: number, * queued: number, @@ -29,9 +35,20 @@ import { LANE_LABEL } from '../lib/queue.js'; * next: string, * }} props */ -export default function QueueAll({ topic, group = null, total, queued, lanes, next }) { +export default function QueueAll({ + topic = null, + feed = null, + group = null, + total, + queued, + lanes, + next, +}) { if (total === 0) return null; + // Which playlist this is, and therefore which pair of actions it posts. + const scope = feed ? 'feed' : 'topic'; + // "All of it" rather than "every single one": a playlist whose entries are // already in the queue for other reasons should not offer to add them again. const all = queued >= total; @@ -46,8 +63,12 @@ export default function QueueAll({ topic, group = null, total, queued, lanes, ne return (
- - + + {feed ? ( + + ) : ( + + )} {group && }