feat(podcasts): reverse order — position DESC, date-agnostic; single order helper
The 'Oldest first' order sorts by PUB DATE, which breaks on Scribl's migrated podiobooks feeds (messy/all-equal dates). New order value 'reverse' = feed position DESC with a date-DESC tiebreak — dates are not involved, so nothing can break. - services.OrderEpisodes: THE single order source of truth (§0 items 3+5a). The four surfaces (sequence JSON, reader chain context, SSR page list) previously carried three near-identical handwritten comparators — all now route through one generic helper. The SSR page list gains a SortDate key (lexical 2006-01-02) so the pub-date tiebreak is actually chronological; the old comparator compared 'Jan 2, 2006' display strings alphabetically (latent drift bug). - Dropdown becomes four options: Feed order / Feed order (reversed) / Oldest first (pub date) / Newest first (pub date). - Serial suggestion button renamed 'Reverse feed order' (sets reverse, not oldest) and its response out-of-band-swaps the order select (PodcastOrderSelectOOB — same mechanism as Download All's policy select), so the dropdown mirrors the server state immediately. - Validation: oneof=native reverse oldest newest everywhere. - Tests: reverse binds via the form wire; messy-dates sequence returns 3,2,1; existing reorder-safety stays green.
This commit is contained in:
1 parent
7c7c08748e
commit
e3675536aa
9 files changed
+708
-463
No files matched your search
@@ -11,12 +11,12 @@ package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"net/url"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
@@ -105,18 +105,18 @@ func createPodcastsLibrary(t *testing.T, setup *TestServerSetup, name string) (d
|
||||
func upsertTestEpisode(t *testing.T, setup *TestServerSetup, feed database.PodcastFeeds, guid, title string, position int) database.MediaItems {
|
||||
t.Helper()
|
||||
ep, err := setup.DB.UpsertPodcastEpisode(adminSeedCtx(t, setup), database.UpsertPodcastEpisodeParams{
|
||||
LibraryID: feed.LibraryID,
|
||||
Title: title,
|
||||
Authors: []string{},
|
||||
AuthorsSearch: []string{},
|
||||
FilePath: fmt.Sprintf("test-feed/%04d-%s.mp3", position, guid),
|
||||
Tags: []string{},
|
||||
SourceFeedID: feed.ID,
|
||||
EpisodeGuid: pgtype.Text{String: guid, Valid: true},
|
||||
FeedPosition: pgtype.Int4{Int32: int32(position), Valid: true},
|
||||
EnclosureUrl: pgtype.Text{String: "https://cdn.invalid/" + guid + ".mp3", Valid: true},
|
||||
Hosts: []string{},
|
||||
Guests: []string{},
|
||||
LibraryID: feed.LibraryID,
|
||||
Title: title,
|
||||
Authors: []string{},
|
||||
AuthorsSearch: []string{},
|
||||
FilePath: fmt.Sprintf("test-feed/%04d-%s.mp3", position, guid),
|
||||
Tags: []string{},
|
||||
SourceFeedID: feed.ID,
|
||||
EpisodeGuid: pgtype.Text{String: guid, Valid: true},
|
||||
FeedPosition: pgtype.Int4{Int32: int32(position), Valid: true},
|
||||
EnclosureUrl: pgtype.Text{String: "https://cdn.invalid/" + guid + ".mp3", Valid: true},
|
||||
Hosts: []string{},
|
||||
Guests: []string{},
|
||||
})
|
||||
require.NoError(t, err, "seed episode %q", title)
|
||||
return ep
|
||||
@@ -664,8 +664,6 @@ func adminScopeExec(t *testing.T, setup *TestServerSetup, adminID pgtype.UUID, s
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ---- task 1.9 residue: the htmx wire format is FORM-encoded (issue #2) ----
|
||||
|
||||
func TestPodcastOrderFormEncoding(t *testing.T) {
|
||||
@@ -695,10 +693,74 @@ func TestPodcastOrderFormEncoding(t *testing.T) {
|
||||
|
||||
assert.Equal(t, http.StatusOK, formPut("order=oldest"), "form-encoded order must bind")
|
||||
assert.Equal(t, http.StatusOK, formPut("order=native"), "native binds too")
|
||||
assert.Equal(t, http.StatusOK, formPut("order=reverse"), "reverse binds too (§0 item 3)")
|
||||
assert.Equal(t, http.StatusBadRequest, formPut(""), "missing order stays rejected (required)")
|
||||
assert.Equal(t, http.StatusBadRequest, formPut("order=sideways"), "invalid order stays rejected")
|
||||
}
|
||||
|
||||
// ---- §0 item 3: reverse = feed position DESC (no pub dates involved) ----
|
||||
|
||||
func TestPodcastReverseOrder(t *testing.T) {
|
||||
setup := setupTestServer(t)
|
||||
adminCtx := adminSeedCtx(t, setup)
|
||||
adminID := adminCtxValue(t, setup)
|
||||
lib, _ := createPodcastsLibrary(t, setup, "test ReverseOrder Podcasts")
|
||||
feed, err := setup.DB.CreatePodcastFeed(adminCtx, database.CreatePodcastFeedParams{
|
||||
LibraryID: lib.ID,
|
||||
FeedUrl: "https://feed.invalid/reverse-order.xml",
|
||||
Title: "Reverse Order Feed",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
feedIDStr := uuid.UUID(feed.ID.Bytes).String()
|
||||
|
||||
// Episodes with MESSY dates (the Scribl condition): all equal.
|
||||
// reverse must be driven by feed position alone — position 3 first.
|
||||
epA := upsertTestEpisode(t, setup, feed, "rr-1", "Reverse Alpha", 1)
|
||||
epB := upsertTestEpisode(t, setup, feed, "rr-2", "Reverse Beta", 2)
|
||||
epC := upsertTestEpisode(t, setup, feed, "rr-3", "Reverse Gamma", 3)
|
||||
adminScopeExec(t, setup, adminID, `
|
||||
UPDATE media_items SET
|
||||
download_state = 'downloaded',
|
||||
file_size = 1000,
|
||||
date_published = '2020-01-01T00:00:00Z'::timestamptz
|
||||
WHERE id IN ($1, $2, $3) AND source = 'podcast_episode'`, epA.ID, epB.ID, epC.ID)
|
||||
subscribeRaw(t, setup, uuid.UUID(adminID.Bytes), feed.ID)
|
||||
|
||||
// Set the override to reverse (the wire the "Reverse feed order"
|
||||
// button uses).
|
||||
formPut := func(body string) int {
|
||||
req, _ := http.NewRequest(http.MethodPut,
|
||||
setup.Server.URL+"/api/podcasts/feeds/"+feedIDStr+"/order",
|
||||
strings.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.Header.Set("Authorization", "Bearer "+setup.Token)
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
require.NoError(t, err)
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
return resp.StatusCode
|
||||
}
|
||||
require.Equal(t, http.StatusOK, formPut("order=reverse"))
|
||||
|
||||
// The SEQUENCE endpoint must return position 3, 2, 1 — driven by
|
||||
// feed position DESC, dates being all equal (the Scribl guard).
|
||||
_, body := podcastAuthed(t, setup, setup.Token, "GET",
|
||||
"/api/podcasts/feeds/"+feedIDStr+"/sequence", "")
|
||||
var seq struct {
|
||||
Order string `json:"order"`
|
||||
Episodes []struct {
|
||||
Title string `json:"title"`
|
||||
FeedPosition int `json:"feed_position"`
|
||||
} `json:"episodes"`
|
||||
}
|
||||
require.NoError(t, json.Unmarshal(body, &seq))
|
||||
assert.Equal(t, "reverse", seq.Order, "the endpoint echoes the effective order")
|
||||
require.Len(t, seq.Episodes, 3)
|
||||
assert.Equal(t, 3, seq.Episodes[0].FeedPosition, "PIN: reverse = position DESC, date-agnostic")
|
||||
assert.Equal(t, 1, seq.Episodes[2].FeedPosition)
|
||||
assert.Equal(t, "Reverse Gamma", seq.Episodes[0].Title,
|
||||
"position 3 first — a messy-date feed reorders by position, not dates")
|
||||
}
|
||||
|
||||
// ---- issue #8: page search hits EPISODE titles/descriptions too ----
|
||||
|
||||
func TestPodcastPageSearchEpisodes(t *testing.T) {
|
||||
|
||||
Reference in new issue
Block a user