feat(plex): actor filter AND/OR toggle (default OR) (S2)
Multiple selected actors now combine as OR (any) by default — click several cast and see films featuring ANY of them — instead of the old hardcoded AND that emptied the grid. An AND (all) toggle in the ACTIVE filter section (shown for 2+ actors, mirroring the genre match toggle) narrows to films with ALL of them. Backend actor_mode param mirrors genre_mode. Verified: OR John Goodman|Douglas M. Griffin = 11 films; AND = 1 (10 Cloverfield Lane). (Plex cast-explore S2)
This commit is contained in:
@@ -373,8 +373,10 @@ def _meta_filter_conds(model, p: dict) -> list:
|
||||
conds.append(model.added_at >= cutoff)
|
||||
for d in _csv(p.get("directors")): # AND
|
||||
conds.append(model.directors.contains([d]))
|
||||
for a in _csv(p.get("actors")): # AND
|
||||
conds.append(model.cast_names.contains([a]))
|
||||
alist = _csv(p.get("actors"))
|
||||
if alist: # AND (all) or OR (any) — default OR, mirroring genre_mode
|
||||
ac = [model.cast_names.contains([a]) for a in alist]
|
||||
conds.append(and_(*ac) if p.get("actor_mode") == "all" else or_(*ac))
|
||||
stds = _csv(p.get("studios"))
|
||||
if stds: # OR
|
||||
conds.append(model.studio.in_(stds))
|
||||
@@ -405,6 +407,7 @@ class LibraryFilters:
|
||||
added_within: str | None = None
|
||||
directors: str | None = None
|
||||
actors: str | None = None
|
||||
actor_mode: str = "any" # how multiple actors combine: any (OR, default) | all (AND)
|
||||
studios: str | None = None
|
||||
collection: str | None = None
|
||||
|
||||
@@ -415,7 +418,8 @@ class LibraryFilters:
|
||||
"year_max": self.year_max, "rating_min": self.rating_min,
|
||||
"duration_min": self.duration_min, "duration_max": self.duration_max,
|
||||
"added_within": self.added_within, "directors": self.directors,
|
||||
"actors": self.actors, "studios": self.studios, "collection": self.collection,
|
||||
"actors": self.actors, "actor_mode": self.actor_mode,
|
||||
"studios": self.studios, "collection": self.collection,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -211,6 +211,23 @@ export default function PlexSidebar({
|
||||
onRemove={() => patch({ directors: filters.directors.filter((x) => x !== d) })}
|
||||
/>
|
||||
))}
|
||||
{filters.actors.length > 1 && (
|
||||
<div className="w-full inline-flex overflow-hidden rounded-lg border border-border text-[11px]">
|
||||
{(["any", "all"] as const).map((m) => (
|
||||
<button
|
||||
key={m}
|
||||
onClick={() => patch({ actorMode: m })}
|
||||
className={`px-2 py-0.5 transition ${
|
||||
(filters.actorMode ?? "any") === m
|
||||
? "bg-accent text-accent-fg"
|
||||
: "text-muted hover:bg-surface"
|
||||
}`}
|
||||
>
|
||||
{t(`plex.filter.match.${m}`)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{filters.actors.map((a) => (
|
||||
<RemovableChip
|
||||
key={`a:${a}`}
|
||||
|
||||
@@ -840,7 +840,8 @@ export interface PlexFilters {
|
||||
durationMax?: number | null;
|
||||
addedWithin?: string; // "" | "24h" | "1w" | "1m" | "6m" | "1y"
|
||||
directors: string[]; // AND — titles featuring ALL selected
|
||||
actors: string[]; // AND — titles featuring ALL selected
|
||||
actors: string[]; // OR (any) by default, or AND (all) via actorMode
|
||||
actorMode?: "any" | "all"; // how multiple actors combine (default any = OR)
|
||||
studios: string[]; // OR — from any selected studio
|
||||
collection?: string | null; // a single Plex collection (rating_key)
|
||||
collectionTitle?: string | null; // its title, for the active-filter chip
|
||||
@@ -882,6 +883,7 @@ function appendPlexFilters(u: URLSearchParams, f: PlexFilters): void {
|
||||
if (f.addedWithin) u.set("added_within", f.addedWithin);
|
||||
if (f.directors?.length) u.set("directors", f.directors.join(","));
|
||||
if (f.actors?.length) u.set("actors", f.actors.join(","));
|
||||
if (f.actorMode === "all") u.set("actor_mode", "all");
|
||||
if (f.studios?.length) u.set("studios", f.studios.join(","));
|
||||
if (f.collection) u.set("collection", f.collection);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user