E5 S5: enriched Plex detail page (ratings + facts + keywords + crew)
Consumes the TMDB block the S2 engine already persists + serves (movie/show detail's `tmdb`), turning the movie info page into a Seerr-style enriched detail (page variant only — the player overlay stays lean): - Multi-source ratings row: IMDb (links out) · TMDb (with vote count) · Plex audience — whichever are present. Only the primary score (IMDb, else audience) carries the ratingMin filter, mirroring the backend rating column. - Facts panel: release · runtime · cert · budget · revenue · studios · countries. - Keyword chips → tap to filter the library. Backend: a new `keywords` filter on LibraryFilters + meta_filter_conds (`keywords @> [kw]`, both models), wired through PlexFilters (appendPlexFilters / plexFilterCount / active-chip / clear). - Full crew (TMDB key-crew) → tap a name to open their person page. Tests: test_plex_keyword_filter.py (3) + PlexInfo.dom.test.tsx (5, incl. the overlay-stays-lean guard). New i18n plex.info.facts.* / keywords / crew / audience (en+hu). Gate: vitest 337, backend lane 396.
This commit is contained in:
@@ -140,6 +140,8 @@ def meta_filter_conds(model, p: dict, lang: str = "en") -> list:
|
||||
stds = split_csv(p.get("studios"))
|
||||
if stds: # OR
|
||||
conds.append(model.studio.in_(stds))
|
||||
for kw in split_csv(p.get("keywords")): # AND — TMDB keyword chips from the detail page (E5 S5)
|
||||
conds.append(model.keywords.contains([kw]))
|
||||
if p.get("collection"):
|
||||
conds.append(model.collection_keys.contains([p["collection"]]))
|
||||
return conds
|
||||
@@ -171,6 +173,7 @@ class LibraryFilters:
|
||||
"any" # how multiple actors combine: any (OR, default) | all (AND)
|
||||
)
|
||||
studios: str | None = None
|
||||
keywords: str | None = None # TMDB keyword chips (E5 S5), CSV; language-neutral
|
||||
collection: str | None = None
|
||||
|
||||
def as_dict(self) -> dict:
|
||||
@@ -188,6 +191,7 @@ class LibraryFilters:
|
||||
"actors": self.actors,
|
||||
"actor_mode": self.actor_mode,
|
||||
"studios": self.studios,
|
||||
"keywords": self.keywords,
|
||||
"collection": self.collection,
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
"""E5 S5: the TMDB keyword filter. A keyword chip on the detail page sets ?keywords=<kw>, which
|
||||
narrows the unified library to rows whose persisted `keywords` JSONB array contains it. DB lane
|
||||
(`db` fixture) — skipped without Postgres.
|
||||
"""
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
import app.state as state
|
||||
from app.auth import current_user
|
||||
from app.db import get_db
|
||||
from app.main import app
|
||||
from app.models import PlexItem, PlexLibrary, User
|
||||
|
||||
|
||||
def _seed(db):
|
||||
lib = PlexLibrary(plex_key="1", title="Movies", kind="movie", enabled=True)
|
||||
db.add(lib)
|
||||
db.flush()
|
||||
db.add_all([
|
||||
PlexItem(rating_key="m1", library_id=lib.id, kind="movie", title="Heist Movie",
|
||||
keywords=["heist", "bank robbery"]),
|
||||
PlexItem(rating_key="m2", library_id=lib.id, kind="movie", title="Space Movie",
|
||||
keywords=["space", "aliens"]),
|
||||
PlexItem(rating_key="m3", library_id=lib.id, kind="movie", title="No Keywords Movie"),
|
||||
])
|
||||
user = User(email="kwfilter@example.com")
|
||||
db.add(user)
|
||||
db.commit()
|
||||
return user
|
||||
|
||||
|
||||
def _get(db, user, monkeypatch, path):
|
||||
monkeypatch.setattr(state, "_configured_cache", True)
|
||||
app.dependency_overrides[get_db] = lambda: db
|
||||
app.dependency_overrides[current_user] = lambda: user
|
||||
try:
|
||||
return TestClient(app).get(path)
|
||||
finally:
|
||||
app.dependency_overrides.clear()
|
||||
|
||||
|
||||
def _titles(resp):
|
||||
return {i["title"] for i in resp.json()["items"]}
|
||||
|
||||
|
||||
def test_keyword_filter_narrows_to_matching_rows(db, monkeypatch):
|
||||
user = _seed(db)
|
||||
resp = _get(db, user, monkeypatch, "/api/plex/library?scope=movie&keywords=heist")
|
||||
assert resp.status_code == 200, resp.text
|
||||
assert _titles(resp) == {"Heist Movie"}
|
||||
|
||||
|
||||
def test_no_keyword_filter_returns_all(db, monkeypatch):
|
||||
user = _seed(db)
|
||||
resp = _get(db, user, monkeypatch, "/api/plex/library?scope=movie")
|
||||
assert _titles(resp) == {"Heist Movie", "Space Movie", "No Keywords Movie"}
|
||||
|
||||
|
||||
def test_unknown_keyword_matches_nothing(db, monkeypatch):
|
||||
user = _seed(db)
|
||||
resp = _get(db, user, monkeypatch, "/api/plex/library?scope=movie&keywords=nonesuch")
|
||||
assert resp.json()["items"] == []
|
||||
Reference in New Issue
Block a user