fix(home): stop Next Up repeating Continue Watching

Jellyfin's /Shows/NextUp defaults EnableResumable=true, which returns a
partially-watched episode as its own series' next up — precisely the
episode /Items/Resume already returns. Home's "Next Episode" row and the
TV landing's Next Up row therefore duplicated Continue Watching card for
card.

build_next_up_endpoint now sends EnableResumable=false, and because
servers predating that parameter ignore it, filterInProgressNextUpItems
also drops any next-up entry whose id appears in the resume list. It is
the mirror of DR-089 and sits beside it: presentation-layer de-duplication
over two lists the frontend already holds. The resume filter still reads
its frontier from the unfiltered Next Up list, so pruning in-progress
entries cannot resurrect a stale resume card.

The code changes were swept into 5e8efa25 by a concurrent `git add -A`;
this carries the remainder — DR-197 / JA-036 / UT-190..192, the
renumbering off the DR-196 collision that commit created, the regenerated
matrix, and the requirement-count guard.

TRACES: UR-059 | DR-197, JA-036 | UT-190, UT-191, UT-192
This commit is contained in:
2026-08-16 22:18:06 +02:00
parent 3b9a8ad695
commit be907b4945
8 changed files with 1229 additions and 1171 deletions
+9 -5
View File
@@ -870,12 +870,12 @@ fn build_latest_items_endpoint(user_id: &str, parent_id: &str, limit: Option<usi
/// the very episode `/Items/Resume` returns — so Continue Watching and Next Up
/// end up showing the same cards. Next Up should only ever offer episodes the
/// viewer has not started. Servers predating the parameter ignore it, which is
/// why the frontend also drops in-progress entries (DR-196).
/// why the frontend also drops in-progress entries (DR-197).
///
/// Pulled out of `get_next_up_episodes` so the query can be asserted without an
/// HTTP server, matching `build_favorites_endpoint`.
///
/// TRACES: UR-059 | DR-196, JA-036 | UT-190, UT-191
/// TRACES: UR-059 | DR-197, JA-036 | UT-190, UT-191
fn build_next_up_endpoint(user_id: &str, series_id: Option<&str>, limit: Option<usize>) -> String {
let mut endpoint = format!(
"/Shows/NextUp?UserId={}&Limit={}&EnableResumable=false&Fields=BackdropImageTags,ParentBackdropImageTags,UserData",
@@ -3497,7 +3497,7 @@ mod tests {
/// `/Items/Resume` already returns, so Continue Watching and Next Up render
/// the same cards.
///
/// TRACES: UR-059 | DR-196, JA-036 | UT-190
/// TRACES: UR-059 | DR-197, JA-036 | UT-190
#[test]
fn test_build_next_up_endpoint_excludes_resumable() {
let endpoint = build_next_up_endpoint("u1", None, Some(12));
@@ -3518,14 +3518,18 @@ mod tests {
/// UT-191 — a per-series Next Up query keeps the series filter.
///
/// TRACES: UR-059 | DR-196 | UT-191
/// TRACES: UR-059 | DR-197 | UT-191
#[test]
fn test_build_next_up_endpoint_scopes_to_series() {
let endpoint = build_next_up_endpoint("u1", Some("series-a"), None);
assert!(endpoint.contains("SeriesId=series-a"));
assert!(endpoint.contains("EnableResumable=false"));
assert!(endpoint.contains("Limit=16"), "default limit, got: {}", endpoint);
assert!(
endpoint.contains("Limit=16"),
"default limit, got: {}",
endpoint
);
}
/// UT-099 — a Jellyfin item's `UserData` reaches `MediaItem.user_data`.