fix(plex): address code-review findings (F1-F3, F5, F9)

- F1: refresh grid after playback/collection-edit — invalidate the renamed
  ["plex-library"] query key in PlexPlayer + PlexCollectionEditor (was stale
  ["plex-browse"], a dead no-op)
- F2: exclude hidden movies from facet counts/bounds — the movie facet base now
  always joins watch-state and mirrors unified_library's status branches, so
  facets match the visible grid exactly
- F3: show_detail/item_detail live-Plex enrichment degrades instead of 500ing —
  broaden the best-effort block to except Exception
- F5: don't re-run the heavy facet computation on sort-direction toggles — key
  the facets query only on fields plexFacets actually sends
- F9: optimistic watched-toggle also updates the search Episodes section and
  gives toggleEpisodeWatched an optimistic path (shared optimisticStatus helper)
This commit is contained in:
npeter83 2026-07-11 02:25:14 +02:00
parent 186fdbb0e5
commit 9f8e410033
5 changed files with 37 additions and 21 deletions

View file

@ -188,26 +188,33 @@ export default function PlexBrowse({
infoScrollRef.current = 0; // fresh info page starts at the top
sub.open({ kind: "info", id: card.id });
}
async function toggleWatched(card: PlexCard) {
const next = card.status === "watched" ? "new" : "watched";
// Optimistically flip the card in every cached library page so the quick toggle reacts instantly
// and reliably BOTH ways (mark and un-mark), then reconcile with the server.
// Optimistically flip a card's status in every cached library page — both the title grid (`items`)
// and the search "Episodes" section (`episodes`) — so the quick toggle reacts instantly and reliably
// BOTH ways (mark and un-mark). An id matches in only one array, so touching both is safe.
function optimisticStatus(id: string, next: string) {
qc.setQueriesData<InfiniteData<PlexUnifiedResult>>({ queryKey: ["plex-library"] }, (old) =>
old
? {
...old,
pages: old.pages.map((pg) => ({
...pg,
items: pg.items.map((it) => (it.id === card.id ? { ...it, status: next } : it)),
items: pg.items.map((it) => (it.id === id ? { ...it, status: next } : it)),
episodes: pg.episodes.map((ep) => (ep.id === id ? { ...ep, status: next } : ep)),
})),
}
: old,
);
}
async function toggleWatched(card: PlexCard) {
const next = card.status === "watched" ? "new" : "watched";
optimisticStatus(card.id, next);
await api.plexSetState(card.id, next).catch(() => {});
qc.invalidateQueries({ queryKey: ["plex-library"] });
}
async function toggleEpisodeWatched(ep: PlexCard) {
await api.plexSetState(ep.id, ep.status === "watched" ? "new" : "watched").catch(() => {});
const next = ep.status === "watched" ? "new" : "watched";
optimisticStatus(ep.id, next);
await api.plexSetState(ep.id, next).catch(() => {});
qc.invalidateQueries({ queryKey: ["plex-library"] });
}
// Apply a metadata filter (clicked on an info page / show hero / cast) then show the unified grid.

View file

@ -50,7 +50,7 @@ export default function PlexCollectionEditor({
const refresh = () => {
qc.invalidateQueries({ queryKey: ["plex-collections"] });
qc.invalidateQueries({ queryKey: ["plex-item", item.id] });
qc.invalidateQueries({ queryKey: ["plex-browse"] });
qc.invalidateQueries({ queryKey: ["plex-library"] });
onChanged();
};

View file

@ -489,7 +489,7 @@ export default function PlexPlayer({ itemId, onClose, queue }: Props) {
old ? { ...old, position_seconds: pos } : old,
);
}
qc.invalidateQueries({ queryKey: ["plex-browse"] });
qc.invalidateQueries({ queryKey: ["plex-library"] });
qc.invalidateQueries({ queryKey: ["plex-show"] });
};
}, [id, qc]);

View file

@ -64,8 +64,12 @@ export default function PlexSidebar({
}
// Facet values (genres/ratings/bounds) for the current scope, ADAPTED to the active filters
// (faceted search) — so picking one filter narrows what the others offer. Refetches on any change.
// Key only on the fields plexFacets actually sends — sortDir/collectionTitle don't reach /facets,
// so keying on the whole `filters` object would re-run the heavy facet computation on every
// sort-direction toggle (or chip-title change) for an identical request.
const { sortDir: _sd, collectionTitle: _ct, ...facetKey } = filters;
const facetsQ = useQuery({
queryKey: ["plex-facets", scope, show, filters],
queryKey: ["plex-facets", scope, show, facetKey],
queryFn: () => api.plexFacets(scope, filters, show),
enabled: !!scope,
placeholderData: (prev) => prev, // keep the old chips visible during the refetch (no flicker)