fix(explore): find library copies through the Plex search index and report servers that could not answer
Explore showed "Not in your library" for titles the user owns. Two causes: Plex's /library/all?title= filter is an ordered word-prefix substring match, so `Oshi no Ko` never found a library titled `[Oshi no Ko]` and a romaji or localized title was unreachable once the first candidate hit; and a server that answered slower than the 10s header budget was treated as a dead endpoint, dropped from the wave, cached as a negative for ten minutes and cascaded through its stale LAN candidates. Plex title candidates now go through /hubs/search with includeGuids, the full-text index behind Plex's own search, which also covers originalTitle. Both backends search every candidate concurrently and union the id-verified copies instead of stopping at the first title that hit; the candidate cap becomes two title families so a sequel can reach its alternate title. Lookup requests run under a dedicated deadline with endpoint failover off. The aggregation layer reports per-server failures, the matcher only memoizes complete positive waves, and the detail screen shows "Couldn't check N servers" instead of claiming absence. close #2098
This commit is contained in:
@@ -26,6 +26,15 @@ class MediaServerTimeouts {
|
||||
/// worst case was the dominant cold-start stall in #1784.
|
||||
static const libraryHubDeadline = Duration(seconds: 20);
|
||||
|
||||
/// Whole-request deadline for the Explore reverse library lookup
|
||||
/// (`findByExternalIds`: Plex `/hubs/search` and `/library/all?guid=`,
|
||||
/// Jellyfin `/Items?SearchTerm=`). It runs behind an in-page progress row
|
||||
/// after a tap, so it can afford the same budget as a library hub row. Used
|
||||
/// with endpoint failover off: a large library that answers slowly is not a
|
||||
/// dead endpoint, and treating the timeout as one used to cascade the whole
|
||||
/// client through its stale LAN candidates (#2098).
|
||||
static const libraryLookup = Duration(seconds: 20);
|
||||
|
||||
/// Timeout for probing a cached/preferred endpoint (used in
|
||||
/// [PlexServer.findBestWorkingConnection]).
|
||||
static const preferredEndpointProbe = Duration(milliseconds: 1500);
|
||||
|
||||
@@ -60,21 +60,23 @@ String _normalize(String title) {
|
||||
/// title is the only candidate filter available and a sequel entry's own
|
||||
/// title — `You and I Are Polar Opposites Season 2` — never matches the parent
|
||||
/// show. Each input contributes itself plus its season-stripped form; the
|
||||
/// caller tries them in order and stops at the first candidate whose external
|
||||
/// ids verify, so a broader title can never widen what actually matches.
|
||||
/// backend searches every candidate concurrently and verifies external ids,
|
||||
/// so a candidate can only ever add genuine copies.
|
||||
///
|
||||
/// [limit] bounds the request fan-out, and 2 is deliberate: the entry's own
|
||||
/// title plus its season-stripped form matched 77 of 113 real sequel entries
|
||||
/// against a 267-show Plex library, where the unexpanded title alone matched
|
||||
/// 3. Raising it to 6 (adding romaji/native/synonym variants) reached only 81
|
||||
/// — four more entries for up to five more requests per lookup that finds
|
||||
/// nothing, which is the common case on a discovery tab. Two candidates cost
|
||||
/// the same two requests the single-title lookup already spent.
|
||||
/// [limit] is the request budget per server per lookup. A title is a key into
|
||||
/// the server's index, and a copy filed under a localized or romaji title is
|
||||
/// reachable only through that title (#2098), so the budget buys title
|
||||
/// *families*: 4 is the entry's own title and its first alternate, each with
|
||||
/// its stripped form. Against a 267-show Plex library, the own family alone
|
||||
/// matched 77 of 113 real sequel entries and six candidates reached 81; the
|
||||
/// second family is not for hit rate but for the copies the first family
|
||||
/// cannot name, and every request beyond it buys near nothing on the miss
|
||||
/// path that dominates a discovery tab.
|
||||
///
|
||||
/// Each title is emitted immediately followed by its stripped form rather than
|
||||
/// in two passes, so the cap can never spend every slot on unstripped titles
|
||||
/// and never try the one candidate that actually reaches the parent show.
|
||||
List<String> titleMatchCandidates(Iterable<String?> titles, {int limit = 2}) {
|
||||
List<String> titleMatchCandidates(Iterable<String?> titles, {int limit = 4}) {
|
||||
final out = <String>[];
|
||||
final seen = <String>{};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user