fix(library): podcasts list newest episode first

A Jellypod podcast listed its episodes alphabetically. The store pinned
SortBy=SortName onto every drill-down, which overrode the order the
channel plugin returns — and since Jellypod prefixes played episodes with
"[Played]", the name sort also clumped every heard episode at the top.

Which order a container's children take is domain knowledge, so it moves
to Rust: the caller names the container (GetItemsOptions.parentKind) and
default_listing_sort answers with the sort. A channel folder is
PremiereDate descending, every other container keeps SortName ascending,
and a caller naming no container still gets no SortBy, so the paths that
rely on the server's own order keep it. An explicit sort always wins.

ChannelFolderItem with is_folder now maps to MediaKind::ChannelFolder
instead of collapsing into Folder — while both were Folder there was
nothing to key the rule on. The offline leg of the cache/server race
applies the same order, so the cached list no longer flashes in name
order before the server's arrives.

TRACES: UR-007 | DR-257 | UT-229, UT-230, UT-231
This commit is contained in:
2026-08-23 18:38:24 +02:00
parent 2ff07bfa49
commit 231ffae626
15 changed files with 285 additions and 16 deletions
+23 -4
View File
@@ -1239,10 +1239,29 @@ impl MediaRepository for OfflineRepository {
let start_index = opts.start_index.unwrap_or(0);
// SortBy=Random is the only sort the landing pages rely on offline (the
// hero "surprise" pool); everything else keeps the stable name order.
let order_by = match opts.sort_by.as_deref() {
Some("Random") => "RANDOM()",
_ => "i.sort_name ASC, i.name ASC",
// hero "surprise" pool); PremiereDate is what a channel folder's
// children are listed by (DR-257), so the cached leg of the race agrees
// with the server's order instead of flashing a name-sorted list first.
// Everything else keeps the stable name order.
//
// Rows with no premiere date sort last rather than leading the list.
let default_sort = default_listing_sort(opts.parent_kind);
let sort_field = opts
.sort_by
.as_deref()
.or(default_sort.map(|(field, _)| field));
let descending = opts
.sort_order
.as_deref()
.or(default_sort.map(|(_, order)| order))
== Some("Descending");
let order_by = match sort_field {
Some("Random") => "RANDOM()".to_string(),
Some("PremiereDate") => format!(
"i.premiere_date IS NULL, i.premiere_date {}, i.sort_name ASC",
if descending { "DESC" } else { "ASC" }
),
_ => "i.sort_name ASC, i.name ASC".to_string(),
};
// Bind the type filter rather than interpolating it: `include_item_types`