feat(plex): sort direction, genre any/all, multi-person filters; fix back-nav + player stop

UAT follow-ups on the Plex filter epic:
- Sort now has an asc/desc toggle (sort_dir), applied to any sort field.
- Genre multi-select gains an Any/All mode (genre_mode: OR vs AND containment).
- Director/actor/studio become multi-value: people AND (titles featuring all selected),
  studios OR; clicking them on the info page stacks (unions) instead of replacing, and
  the sidebar shows each as a removable 'Active' chip.
- fix(history): clicking a metadata filter on the info page now pushes a fresh grid
  entry instead of history.back(), so browser Back returns to the info page rather than
  leaving the Plex module.
- fix(player): fully tear down the <video> + hls on unmount and guard late play() calls,
  so backing out of a just-started video no longer leaves audio playing in the background.
i18n en/hu/de (match any/all, sort direction).
This commit is contained in:
npeter83 2026-07-06 00:15:31 +02:00
parent eefd7e3abd
commit 1982cfa7b9
10 changed files with 160 additions and 60 deletions

View file

@ -193,6 +193,7 @@ def browse(
offset: int = 0,
limit: int = Query(default=40, ge=1, le=100),
genres: str | None = None,
genre_mode: str = "any",
content_ratings: str | None = None,
year_min: int | None = None,
year_max: int | None = None,
@ -200,9 +201,10 @@ def browse(
duration_min: int | None = None,
duration_max: int | None = None,
added_within: str | None = None,
director: str | None = None,
actor: str | None = None,
studio: str | None = None,
directors: str | None = None,
actors: str | None = None,
studios: str | None = None,
sort_dir: str = "desc",
user: User = Depends(current_user),
db: Session = Depends(get_db),
) -> dict:
@ -230,9 +232,10 @@ def browse(
else: # all — hide only the explicitly hidden
query = query.filter(or_(st.status.is_(None), st.status != "hidden"))
# --- Metadata filters (movie libraries; @> containment is GIN-indexed) ---
genre_any = _csv(genres)
if genre_any:
query = query.filter(or_(*[PlexItem.genres.contains([g]) for g in genre_any]))
gsel = _csv(genres)
if gsel:
conds = [PlexItem.genres.contains([g]) for g in gsel]
query = query.filter(and_(*conds) if genre_mode == "all" else or_(*conds))
crs = _csv(content_ratings)
if crs:
query = query.filter(PlexItem.content_rating.in_(crs))
@ -249,12 +252,13 @@ def browse(
cutoff = _added_cutoff(added_within)
if cutoff is not None:
query = query.filter(PlexItem.added_at >= cutoff)
if director:
query = query.filter(PlexItem.directors.contains([director]))
if actor:
query = query.filter(PlexItem.cast_names.contains([actor]))
if studio:
query = query.filter(PlexItem.studio == studio)
for d in _csv(directors): # AND — titles this whole set directed
query = query.filter(PlexItem.directors.contains([d]))
for a in _csv(actors): # AND — titles featuring all these people
query = query.filter(PlexItem.cast_names.contains([a]))
stds = _csv(studios)
if stds: # OR — a title has one studio
query = query.filter(PlexItem.studio.in_(stds))
else:
query = query.filter(PlexShow.library_id == lib.id)
@ -268,18 +272,21 @@ def browse(
if tsq is not None:
order = [func.ts_rank(model.search_vector, tsq).desc()]
elif sort == "title":
order = [func.lower(model.title).asc()]
elif sort == "year" and model is PlexItem:
order = [PlexItem.year.desc().nullslast()]
elif sort == "rating" and model is PlexItem:
order = [PlexItem.rating.desc().nullslast()]
elif sort == "duration" and model is PlexItem:
order = [PlexItem.duration_s.desc().nullslast()]
elif sort == "release" and model is PlexItem:
order = [PlexItem.originally_available_at.desc().nullslast()]
else: # added — newest first
order = [model.added_at.desc().nullslast()]
else:
if sort == "title":
col = func.lower(model.title)
elif sort == "year" and model is PlexItem:
col = PlexItem.year
elif sort == "rating" and model is PlexItem:
col = PlexItem.rating
elif sort == "duration" and model is PlexItem:
col = PlexItem.duration_s
elif sort == "release" and model is PlexItem:
col = PlexItem.originally_available_at
else: # added
col = model.added_at
asc = sort_dir == "asc"
order = [(col.asc() if asc else col.desc()).nullslast()]
order.append(model.id.desc())
rows = query.order_by(*order).offset(offset).limit(limit).all()