diff --git a/backend/app/routes/plex.py b/backend/app/routes/plex.py index db007b5..af1a177 100644 --- a/backend/app/routes/plex.py +++ b/backend/app/routes/plex.py @@ -672,10 +672,15 @@ def facets( def per_table(model, ids, kind_movie: bool) -> None: if not ids: return - # Genres — exclude the genre filter itself so you can keep adding genres. + # Genres — in "any"/OR mode, exclude the genre filter itself so each offered genre is a valid + # OR-addition that only widens (keep adding). In "all"/AND mode, KEEP the filter applied so we + # offer only genres that CO-OCCUR on the current result set — a non-co-occurring genre would AND + # the result down to zero, i.e. a dead-end chip. (The already-selected genres still appear, since + # every matching title carries them, so they stay togglable.) + genre_pp = p if p.get("genre_mode") == "all" else {**p, "genres": None} expanded = wsq( db.query(func.jsonb_array_elements_text(model.genres).label("g")), - model, ids, {**p, "genres": None}, kind_movie, True, + model, ids, genre_pp, kind_movie, True, ).subquery() for g, c in db.query(expanded.c.g, func.count()).group_by(expanded.c.g).all(): genre_counts[g] = genre_counts.get(g, 0) + c @@ -711,6 +716,12 @@ def facets( per_table(PlexItem, movie_ids, True) per_table(PlexShow, show_ids, False) + # Always surface the actively-selected genres (even a zero-match AND combo, where they'd otherwise + # be absent): the sidebar renders genres solely from this list and hides the whole genre section — + # chips AND the Any/All toggle — when it's empty, which would trap the user with no per-chip way to + # undo a zero-result selection. Count 0 is fine (genre chips don't display counts). + for g in _csv(genres): + genre_counts.setdefault(g, 0) return { "genres": [{"value": g, "count": c} for g, c in sorted(genre_counts.items(), key=lambda kv: kv[0])], "content_ratings": [{"value": cr, "count": c} for cr, c in sorted(cr_counts.items(), key=lambda kv: -kv[1])],