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)
|
conds.append(model.added_at >= cutoff)
|
||||||
for d in _csv(p.get("directors")): # AND
|
for d in _csv(p.get("directors")): # AND
|
||||||
conds.append(model.directors.contains([d]))
|
conds.append(model.directors.contains([d]))
|
||||||
for a in _csv(p.get("actors")): # AND
|
alist = _csv(p.get("actors"))
|
||||||
conds.append(model.cast_names.contains([a]))
|
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"))
|
stds = _csv(p.get("studios"))
|
||||||
if stds: # OR
|
if stds: # OR
|
||||||
conds.append(model.studio.in_(stds))
|
conds.append(model.studio.in_(stds))
|
||||||
@@ -405,6 +407,7 @@ class LibraryFilters:
|
|||||||
added_within: str | None = None
|
added_within: str | None = None
|
||||||
directors: str | None = None
|
directors: str | None = None
|
||||||
actors: str | None = None
|
actors: str | None = None
|
||||||
|
actor_mode: str = "any" # how multiple actors combine: any (OR, default) | all (AND)
|
||||||
studios: str | None = None
|
studios: str | None = None
|
||||||
collection: str | None = None
|
collection: str | None = None
|
||||||
|
|
||||||
@@ -415,7 +418,8 @@ class LibraryFilters:
|
|||||||
"year_max": self.year_max, "rating_min": self.rating_min,
|
"year_max": self.year_max, "rating_min": self.rating_min,
|
||||||
"duration_min": self.duration_min, "duration_max": self.duration_max,
|
"duration_min": self.duration_min, "duration_max": self.duration_max,
|
||||||
"added_within": self.added_within, "directors": self.directors,
|
"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) })}
|
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) => (
|
{filters.actors.map((a) => (
|
||||||
<RemovableChip
|
<RemovableChip
|
||||||
key={`a:${a}`}
|
key={`a:${a}`}
|
||||||
|
|||||||
@@ -840,7 +840,8 @@ export interface PlexFilters {
|
|||||||
durationMax?: number | null;
|
durationMax?: number | null;
|
||||||
addedWithin?: string; // "" | "24h" | "1w" | "1m" | "6m" | "1y"
|
addedWithin?: string; // "" | "24h" | "1w" | "1m" | "6m" | "1y"
|
||||||
directors: string[]; // AND — titles featuring ALL selected
|
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
|
studios: string[]; // OR — from any selected studio
|
||||||
collection?: string | null; // a single Plex collection (rating_key)
|
collection?: string | null; // a single Plex collection (rating_key)
|
||||||
collectionTitle?: string | null; // its title, for the active-filter chip
|
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.addedWithin) u.set("added_within", f.addedWithin);
|
||||||
if (f.directors?.length) u.set("directors", f.directors.join(","));
|
if (f.directors?.length) u.set("directors", f.directors.join(","));
|
||||||
if (f.actors?.length) u.set("actors", f.actors.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.studios?.length) u.set("studios", f.studios.join(","));
|
||||||
if (f.collection) u.set("collection", f.collection);
|
if (f.collection) u.set("collection", f.collection);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user