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
@@ -0,0 +1,60 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
)
|
||||
|
||||
// Podcast episode ordering — THE single source of order truth (§0 item
|
||||
// 3 + item 5a, 2026-10-30). Every surface (sequence JSON, reader chain
|
||||
// context, SSR page list, download queueing) routes through
|
||||
// OrderEpisodes; per-surface sort blocks were the drift risk (three
|
||||
// near-identical comparators existed by hand).
|
||||
|
||||
// PodcastEpisodeOrder values:
|
||||
// - native: feed position ASC, date ASC tiebreak (fetch-maintained order)
|
||||
// - reverse: feed position DESC, date DESC tiebreak ("Reverse feed
|
||||
// order" — no pub dates involved, so Scribl's messy/all-equal
|
||||
// migrated podiobooks dates can't break it)
|
||||
// - oldest / newest: chronological by pub date string (2006-01-02 or
|
||||
// anything lexical-comparable), position as same-date tiebreak
|
||||
func ValidPodcastEpisodeOrder(o string) bool {
|
||||
return o == "native" || o == "reverse" || o == "oldest" || o == "newest"
|
||||
}
|
||||
|
||||
// OrderEpisodes sorts eps in place into the given order. Accessor
|
||||
// functions supply each episode's feed position and pub date key
|
||||
// (LEN lexical-comparable, e.g. RFC3339 or 2006-01-02).
|
||||
func OrderEpisodes[T any](eps []T, order string, pos func(T) int32, date func(T) string) {
|
||||
switch order {
|
||||
case "reverse":
|
||||
sort.SliceStable(eps, func(i, j int) bool {
|
||||
if pos(eps[i]) != pos(eps[j]) {
|
||||
return pos(eps[i]) > pos(eps[j])
|
||||
}
|
||||
return date(eps[i]) > date(eps[j])
|
||||
})
|
||||
case "oldest", "newest":
|
||||
sort.SliceStable(eps, func(i, j int) bool {
|
||||
ka := date(eps[i]) + episodePosKey(pos(eps[i]))
|
||||
kb := date(eps[j]) + episodePosKey(pos(eps[j]))
|
||||
if order == "newest" {
|
||||
return ka > kb
|
||||
}
|
||||
return ka < kb
|
||||
})
|
||||
default: // native
|
||||
sort.SliceStable(eps, func(i, j int) bool {
|
||||
if pos(eps[i]) != pos(eps[j]) {
|
||||
return pos(eps[i]) < pos(eps[j])
|
||||
}
|
||||
return date(eps[i]) < date(eps[j])
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// episodePosKey: zero-padded position so lexical date keys tiebreak
|
||||
// numerically.
|
||||
func episodePosKey(pos int32) string {
|
||||
return fmt.Sprintf("%06d", pos)
|
||||
}
|
||||
Reference in new issue
Block a user