From 0e29eff7b74ed4e72796a1185430f8a69b83a1d0 Mon Sep 17 00:00:00 2001 From: John O'Keefe Date: Sun, 1 Nov 2026 11:46:14 -0500 Subject: [PATCH] feat(podcasts): unset listening order defaults to reversed (audiobook-first) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The per-feed listening order — the Order dropdown on the podcast page and player — resolved its UNSET state to "native": feed document order, which runs newest-first for classic RSS. This platform is audiobook- first, so native started every book at its LAST chapter (verified on live data: 7th Son position 1 = Bonus Episode #2, positions descending through the chapters). The unset fallback flips to "reverse" at every resolution site of that ONE setting — the podcast page's list + dropdown (podcastOrderForFeed), the player's feed chain (reader.go), and the download queue (orderOverrideFor) — so the dropdown shows reversed selected, the page renders in that order, and play/download follow. Stored overrides are untouched (native stays a valid savable choice); no ordering semantics changed; collections are untouched. Pins: the flipped native expectation is changelogged in contract_pins; new pins cover the sequence config carrying reverse, feed position 1 playing last, and the page dropdown rendering reverse selected for an unset feed. --- cmd/server/tests/podcast_test.go | 24 +++++++++++++++++++++--- docs/contract_pins.md | 19 +++++++++++++++++++ internal/router/podcast_page.go | 13 ++++++++++--- internal/router/reader.go | 7 +++++-- internal/services/podcast_worker.go | 14 ++++++++++---- 5 files changed, 65 insertions(+), 12 deletions(-) diff --git a/cmd/server/tests/podcast_test.go b/cmd/server/tests/podcast_test.go index 8f42749d..c3993113 100644 --- a/cmd/server/tests/podcast_test.go +++ b/cmd/server/tests/podcast_test.go @@ -531,13 +531,31 @@ func TestPodcastPlayerOrderControl(t *testing.T) { // match the escaped form. esc := func(s string) string { return strings.ReplaceAll(s, `"`, """) } - t.Run("context_carries_feed_source", func(t *testing.T) { + t.Run("unset_default_is_reversed", func(t *testing.T) { + // 2026-11-01: the UNSET per-feed order resolves to "reverse" + // (audiobook-first — feed document order runs newest-first, so + // native started a book at its last chapter). The player's + // sequence config carries it; the podcast page's dropdown shows + // it selected; ep1 (feed position 1 of 2) plays LAST. code, body := podcastAuthed(t, setup, setup.Token, "GET", ep1URL, "") require.Equal(t, http.StatusOK, code) assert.Contains(t, string(body), esc(`"source":"feed"`), "queue context must carry its owner (§10 R2)") assert.Contains(t, string(body), esc(`"feed_id":"`+feedID+`"`)) - assert.Contains(t, string(body), esc(`"order":"native"`)) - assert.Contains(t, string(body), esc(`"episode_index":1`)) + assert.Contains(t, string(body), esc(`"order":"reverse"`), + "unset order defaults to reversed (audiobook-first)") + assert.Contains(t, string(body), esc(`"episode_index":2`), + "feed position 1 plays last under the reversed default") + }) + + t.Run("podcast_page_dropdown_shows_reversed_when_unset", func(t *testing.T) { + code, body := podcastAuthed(t, setup, setup.Token, "GET", "/podcasts/"+feedID, "") + require.Equal(t, http.StatusOK, code) + html := string(body) + assert.Contains(t, html, `id="podcast-order-select"`, "the feed page renders its Order dropdown") + assert.Contains(t, html, `