"Delete from server" read identically for an episode, a season and a
whole show: same menu label, same dialog title, same red button, and a
body that named nothing. The menu header did not disambiguate either,
because MediaItem.displayTitle collapses an episode to its show name.
A reporter deleted a whole series from the detail hero's ⋮ believing it
acted on the episode he had highlighted, and the confirmation gave him
nothing to catch it with. Every one of those strings now names the kind,
and the body names the exact item — show, season and episode number, and
episode title.
Deleting a single item also destroyed files the confirmation never
mentioned: a Plex multi-episode file (S01E01-E03.mkv) takes its other
episodes with it, and a split item takes every part. The dialog now
reports that up front and, on success, emits deletion events for the
siblings the server destroyed so their rows do not linger.
The scope behind that warning is only asserted when it is established.
MediaItem.allPartFiles drops parts with no path, so a non-empty set
proves nothing about the ones it filtered out; a version is trusted only
when every part reports a file. A browse row that omits paths is missing
evidence rather than proof of a distinct file, so both the target and
each candidate sibling fall back to the detail endpoint before any
conclusion — otherwise a thin row, including the file-less part
PlexMappers fabricates for an empty payload, would look like a server
that withholds paths. When the answer cannot be established the dialog
says so in an error-tinted block and its button reads "Delete anyway",
separating a transient probe failure from a server that never sends
paths. It deliberately does not refuse: Plex withholds paths from
restricted users the server itself authorizes to delete, so failing
closed would take the feature away from them permanently.
Probing a season stays bounded in both directions. Siblings resolve one
at a time, so a season of thin rows cannot fan out a detail request per
episode, and expiry cancels the walk rather than merely abandoning it —
`Future.timeout` completes the future the caller awaits but leaves the
work behind it running, which would resume on the next sibling once the
outstanding request answered. A cooperative flag is checked before each
lookup, so at most the one already in flight outlives the deadline; the
neutral client exposes no abort handle for item lookups, so that one
cannot be recalled.
The spinner covering the probe was only barrierDismissible, which does
not stop system back. Back dismissed it and the cleanup pop then closed
the screen underneath, dropping the user out of the detail page
mid-flow. It now traps back, matching the non-dismissible contract its
own doc claims, which also repairs the log uploader and the file-info
sheet.
Coverage splits by what each layer owns. The dialog, its copy and the
DELETE wiring are backend-neutral and stay in the menu widget tests.
Plex — the backend multi-episode files actually come from — gets the
resolver over a real PlexClient and a mocked transport: a row with no
media at all, scope recovered from /library/metadata/{id}, siblings and
paths from /children, a Part that names no file, a sibling whose path
never resolves, the request count a sixty-episode thin season may cost,
and the rating key the DELETE carries. Those are plain async tests
because the Plex metadata cache is a real database whose I/O the widget
tester's fake clock never drives. Deadline behaviour needs the opposite,
so it is pinned separately under fakeAsync against a gated fake client,
with no wall-clock waiting anywhere.
close #1781
93 lines
4.8 KiB
Dart
93 lines
4.8 KiB
Dart
/// Centralized HTTP timeout constants for both backends. The same
|
|
/// [MediaServerHttpClient] wrapper is used by Plex and Jellyfin clients —
|
|
/// timeouts are kept here so the budgets per phase are visible at a
|
|
/// glance.
|
|
class MediaServerTimeouts {
|
|
static const connect = Duration(seconds: 10);
|
|
|
|
static const receive = Duration(seconds: 120);
|
|
|
|
/// Whole-request deadline for home `/hubs` startup calls. These endpoints can
|
|
/// be slow while Plex wakes idle disks or a CDN-fronted Jellyfin runs a cold
|
|
/// query, but should not block forever.
|
|
///
|
|
/// Deliberately a *single* budget rather than a retry ladder. `Client.send`
|
|
/// resolves when response headers arrive, so this budget covers the server's
|
|
/// think time, not just the socket connect — a slow-but-alive query trips it.
|
|
/// Replaying that request makes the server re-run the same expensive query
|
|
/// from scratch, so the old `[10s, 5s, 2.5s]` ladder turned an 11s answer
|
|
/// into a 17.5s empty row (#1784). See [retryTransientMediaServerCall].
|
|
static const homeHubDeadline = Duration(seconds: 15);
|
|
|
|
/// Whole-request deadline for per-library home hub rows
|
|
/// (`/hubs/sections/{id}`, Jellyfin `/Items/Latest`). These can be slower
|
|
/// than the top-level home hub call on remote servers. Same single-budget
|
|
/// rationale as [homeHubDeadline] — it replaced `[10s, 8s, 5s]`, whose 23s
|
|
/// worst case was the dominant cold-start stall in #1784.
|
|
static const libraryHubDeadline = Duration(seconds: 20);
|
|
|
|
/// Timeout for probing a cached/preferred endpoint (used in
|
|
/// [PlexServer.findBestWorkingConnection]).
|
|
static const preferredEndpointProbe = Duration(milliseconds: 1500);
|
|
|
|
/// How long the cached/preferred endpoint probe gets to answer before the
|
|
/// full candidate race starts alongside it. A healthy cached endpoint
|
|
/// answers well inside this window and wins deterministically; a stale one
|
|
/// (e.g. a cached LAN address probed from outside the LAN) only delays
|
|
/// discovery by this much instead of the full [preferredEndpointProbe].
|
|
static const preferredEndpointHeadStart = Duration(milliseconds: 300);
|
|
|
|
/// Timeout for the connection race where all candidates are tested in
|
|
/// parallel (used in [PlexServer.findBestWorkingConnection]).
|
|
static const connectionRace = Duration(seconds: 2);
|
|
|
|
/// Per-server connection watchdog ceiling. The discovery path is no longer
|
|
/// strictly serial (cached probe overlaps the race; the HTTPS upgrade runs
|
|
/// off the critical path), so this is a generous upper bound rather than a
|
|
/// sum of phases.
|
|
static const perServerConnect = Duration(milliseconds: 6500);
|
|
|
|
/// HTTP timeout for the live-TV tune POST. Matches Plex web's value — the
|
|
/// default 10s connect budget is too tight on Fire-TV cold starts.
|
|
static const tune = Duration(seconds: 30);
|
|
|
|
static const plexTvConnect = Duration(seconds: 15);
|
|
|
|
static const plexTvReceive = Duration(seconds: 10);
|
|
|
|
/// Authenticated health probe timeout. Health sweeps await every server, so
|
|
/// a stale Plex endpoint must not hold the whole sweep for [receive].
|
|
static const plexProbe = Duration(seconds: 8);
|
|
|
|
/// Probe + token-validate timeout — Jellyfin servers respond fast on
|
|
/// `/System/Info/Public` and `/Users/Me`.
|
|
static const jellyfinProbe = Duration(seconds: 8);
|
|
|
|
/// Per-item delete-permission probe. Shorter than [jellyfinProbe] because it
|
|
/// blocks a context menu from opening: a server that is nominally online but
|
|
/// hung must not hold the menu for a health-sweep budget. Unlike the other
|
|
/// values here it is also applied as a whole-request deadline by the caller
|
|
/// (the per-request budget covers the connect and receive phases
|
|
/// individually), and expiry fails closed — no delete entry — so the ceiling
|
|
/// only ever costs an entry, never safety.
|
|
static const jellyfinDeletePermission = Duration(seconds: 3);
|
|
|
|
/// Whole-probe deadline for establishing what a server-side delete will
|
|
/// destroy (see `resolveDeleteImpact`). Generous compared to
|
|
/// [jellyfinDeletePermission] for two reasons: it runs behind a modal
|
|
/// spinner *after* the user asked to delete, not while a menu is opening,
|
|
/// and it may fan out one detail request per sibling episode when browse
|
|
/// rows omit file paths.
|
|
///
|
|
/// Unlike the permission probe, expiry cannot fail closed — Plex never
|
|
/// sends file paths to restricted users, so refusing the delete would
|
|
/// permanently remove a feature the server itself authorizes. Expiry
|
|
/// instead downgrades the confirmation to its explicit "scope unverified"
|
|
/// form.
|
|
static const deleteImpactProbe = Duration(seconds: 10);
|
|
|
|
/// Best-effort `/Sessions/Logout` timeout — short because the call is
|
|
/// fire-and-forget; the token is removed locally regardless.
|
|
static const jellyfinSignOut = Duration(seconds: 5);
|
|
}
|