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

@ -798,15 +798,18 @@ def facets(
query = query.filter(model.duration_s >= pp["duration_min"])
if with_dur and pp.get("duration_max") is not None:
query = query.filter(model.duration_s <= pp["duration_max"])
if show in ("watched", "in_progress", "unwatched"):
st = aliased(PlexState)
query = query.outerjoin(st, and_(st.item_id == model.id, st.user_id == uid))
if show == "watched":
query = query.filter(st.status == "watched")
elif show == "in_progress":
query = query.filter(st.position_seconds > 0, or_(st.status.is_(None), st.status != "watched"))
else:
query = query.filter(or_(st.status.is_(None), st.status.notin_(["watched", "hidden"])))
# Always join watch-state and mirror the grid's status branches (unified_library) exactly —
# hidden movies never appear in the grid, so they must not inflate facet counts/bounds either.
st = aliased(PlexState)
query = query.outerjoin(st, and_(st.item_id == model.id, st.user_id == uid))
if show == "watched":
query = query.filter(st.status == "watched")
elif show == "in_progress":
query = query.filter(st.position_seconds > 0, or_(st.status.is_(None), st.status != "watched"))
elif show == "unwatched":
query = query.filter(or_(st.status.is_(None), st.status.notin_(["watched", "hidden"])))
else:
query = query.filter(or_(st.status.is_(None), st.status != "hidden"))
else:
if show in ("watched", "in_progress", "unwatched"):
agg = _show_agg_subq(db, uid).subquery()
@ -1389,7 +1392,9 @@ def show_detail(
meta = plex.metadata(sh.rating_key) or {}
rich = _rich_meta(meta)
related = _related_show_cards(db, user.id, plex.related(sh.rating_key), exclude=sh.id)
except (PlexError, PlexNotConfigured):
except Exception:
# Best-effort enrichment only — the local episode list is already assembled, so any live-Plex
# failure (not configured, HTTP/JSON shape, _rich_meta parse error) must degrade, not 500.
pass
return {
@ -1943,8 +1948,8 @@ def item_detail(
"text": codec not in _BITMAP_SUBS,
}
)
except (PlexError, PlexNotConfigured):
pass # metadata extras are best-effort; playback works without them
except Exception:
pass # metadata extras are best-effort; any live-Plex failure must degrade, not 500 — playback works without them
prev_id, next_id = _episode_nav(db, it)
show = db.get(PlexShow, it.show_id) if it.kind == "episode" and it.show_id else None