perf(plex): batch bulk pushes, cache show enrichment; remove dead code + DRY (F6/F8/F10)

F6: coalesce whole-show/season Plex watch-pushes into ONE background task
(push_bulk_state_to_plex) that reuses a single DB session + keep-alive Plex
client, instead of scheduling one task (own session + HTTP client) per episode.

F8: cache a show's live Plex enrichment (metadata + related) per rating_key for
a short TTL and fetch the two calls in parallel; repeat opens skip the network.
Raw payloads are cached; per-user shaping stays out. An empty related list is not
cached (plex.related swallows a transient failure as [] — don't pin it for the TTL).

F10: remove dead code — the pre-unified /browse route + browse(), the /people
route + people() + _person_photo(), api.plexPeople, interface PlexPerson, and the
orphaned plex.people i18n block. DRY: appendPlexFilters() shared by plexLibrary +
plexFacets; one exported plexDetailUi.Filterable (was Fil + Filterable); PlexInfo
migrated onto the shared plexDetailUi hooks/menu (DetailCustomizeMenu gained
overlay + extra props; useDetailPrefs exposes savePref; PrefToggle exported).

Reviewed (high) → clean. F7 (facet aggregate collapse) deferred: self-exclusion
gives each sub-aggregate a distinct WHERE, and no measurement shows /facets slow.
This commit is contained in:
npeter83 2026-07-11 03:10:02 +02:00
parent 9f8e410033
commit b3af04a997
9 changed files with 180 additions and 450 deletions

View file

@ -1,4 +1,4 @@
import { lazy, Suspense, useEffect, useLayoutEffect, useRef, useState, type CSSProperties, type ReactNode } from "react";
import { lazy, Suspense, useEffect, useLayoutEffect, useRef, useState, type CSSProperties } from "react";
import { useTranslation } from "react-i18next";
import { useInfiniteQuery, useQuery, useQueryClient, type InfiniteData } from "@tanstack/react-query";
import {
@ -28,7 +28,7 @@ import {
} from "../lib/api";
import { useDebounced } from "../lib/useDebounced";
import { useHistorySubview } from "../lib/history";
import { DetailCustomizeMenu, useArtBackdrop, useDetailPrefs } from "../lib/plexDetailUi";
import { DetailCustomizeMenu, Filterable, useArtBackdrop, useDetailPrefs } from "../lib/plexDetailUi";
import PlexPlaylistAdd, { type PlexAddTarget } from "./PlexPlaylistAdd";
import PlexCollectionEditor from "./PlexCollectionEditor";
@ -553,16 +553,6 @@ function epLabel(ep?: PlexCard | null): string | undefined {
return ep.title;
}
// A metadata value that filters the unified grid when clicked (if onClick is wired), else plain text.
function Fil({ onClick, className, children }: { onClick?: () => void; className?: string; children: ReactNode }) {
if (!onClick) return <span className={className}>{children}</span>;
return (
<button onClick={onClick} className={`${className ?? ""} cursor-pointer hover:text-accent transition`}>
{children}
</button>
);
}
function BackBtn({ onBack, label }: { onBack: () => void; label: string }) {
return (
<button
@ -967,14 +957,14 @@ function PlexShowView({
<h1 className="text-2xl font-semibold leading-tight">{show.title}</h1>
<div className="mt-1 flex flex-wrap items-center gap-x-2 gap-y-0.5 text-sm text-muted">
{show.year != null && (
<Fil onClick={onFilter ? () => onFilter({ yearMin: show.year, yearMax: show.year }) : undefined}>
<Filterable onClick={onFilter ? () => onFilter({ yearMin: show.year, yearMax: show.year }) : undefined}>
{show.year}
</Fil>
</Filterable>
)}
{show.content_rating && (
<Fil onClick={onFilter ? () => onFilter({ contentRatings: [show.content_rating!] }) : undefined}>
<Filterable onClick={onFilter ? () => onFilter({ contentRatings: [show.content_rating!] }) : undefined}>
· {show.content_rating}
</Fil>
</Filterable>
)}
{show.season_count != null && <span>· {t("plex.seasons", { count: show.season_count })}</span>}
{show.imdb_rating != null && (
@ -990,13 +980,13 @@ function PlexShowView({
{show.genres.length > 0 && (
<div className="mt-2 flex flex-wrap gap-1.5">
{show.genres.map((g) => (
<Fil
<Filterable
key={g}
className="text-[11px] rounded-full bg-surface px-2 py-0.5 text-muted"
onClick={onFilter ? () => onFilter({ genres: [g] }) : undefined}
>
{g}
</Fil>
</Filterable>
))}
</div>
)}