The person page now leads with a TMDB bio, lazily fetched the first time it's
opened (off the S2-linked `tmdb_person_id`) and then served from the row:
- Migration 0072: ADD-only nullable person columns on plex_people — biography,
birthday, deathday, place_of_birth, known_for_department, also_known_as,
known_for ([{tmdb_id, media_type, title}]), tmdb_enriched_at.
- tmdb.py: TmdbClient.fetch_person (/person/{id}+combined_credits), parse_person
(bio + a popularity-ranked, deduped known_for), apply_person, enrich_person
(lazy one-shot, key+id guarded, commits, 404 stamps the watermark).
- people.py person_detail: loads the full person row, lazily enriches (best-effort
— never fails the page), serves the bio fields + resolves known_for to library
rows by guid (in-library only, popularity order).
- FE PlexPersonView: bio/birth-facts/aka hero + a "Known for" Carousel rail above
the existing Movies/Shows filmography; PlexPersonDetail type + i18n (en+hu).
Tests: test_plex_person_bio.py (6: parse / lazy-enrich guards / serve) + extended
PlexPersonView.dom.test.tsx + the unknown-name contract in test_plex_person.py.
Browser-E2E'd on Jim Carrey (live TMDB enrich → bio 1937 chars, born 1962-01-17
Newmarket, Known-for resolved). Gate: vitest 337, backend lane 402.
128 lines
4.9 KiB
Python
128 lines
4.9 KiB
Python
"""DB-backed regression (E5 S4): GET /api/plex/person/{name} returns a cast/crew member's
|
|
filmography — movies + shows in the ENABLED libraries whose cast OR directors include the name,
|
|
each carrying the caller's watch-state, plus the cached headshot. Also covers `_content_libs`
|
|
scoping (disabled libraries excluded) and the kind filter (episodes are not movies).
|
|
|
|
Uses the DB lane (`db` fixture) — skipped when no Postgres is reachable.
|
|
"""
|
|
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,
|
|
PlexPerson,
|
|
PlexShow,
|
|
PlexState,
|
|
User,
|
|
)
|
|
from app.routes.plex._shared import _content_libs
|
|
|
|
NAME = "Robert De Niro"
|
|
|
|
|
|
def _get(db, user, monkeypatch, path):
|
|
monkeypatch.setattr(state, "_configured_cache", True) # pass the request gate
|
|
app.dependency_overrides[get_db] = lambda: db
|
|
app.dependency_overrides[current_user] = lambda: user
|
|
try:
|
|
return TestClient(app).get(path) # no lifespan (skip scheduler/HLS sweep)
|
|
finally:
|
|
app.dependency_overrides.clear()
|
|
|
|
|
|
def test_person_detail_returns_cast_and_director_credits(db, monkeypatch):
|
|
movies_lib = PlexLibrary(plex_key="1", title="Movies", kind="movie", enabled=True)
|
|
shows_lib = PlexLibrary(plex_key="2", title="Shows", kind="show", enabled=True)
|
|
off_lib = PlexLibrary(plex_key="3", title="Off", kind="movie", enabled=False)
|
|
db.add_all([movies_lib, shows_lib, off_lib])
|
|
db.flush()
|
|
|
|
heat = PlexItem(
|
|
rating_key="m1", library_id=movies_lib.id, kind="movie", title="Heat",
|
|
year=1995, cast_names=[NAME, "Al Pacino"],
|
|
)
|
|
# Director-only credit: the name is NOT in cast_names, only directors → the OR must still catch it.
|
|
directed = PlexItem(
|
|
rating_key="m2", library_id=movies_lib.id, kind="movie", title="A Bronx Tale",
|
|
year=1993, cast_names=["Chazz Palminteri"], directors=[NAME],
|
|
)
|
|
# Excluded: a movie in a DISABLED library.
|
|
hidden = PlexItem(
|
|
rating_key="m3", library_id=off_lib.id, kind="movie", title="Hidden",
|
|
year=2000, cast_names=[NAME],
|
|
)
|
|
# Excluded: an episode (kind != "movie") that happens to list the name.
|
|
episode = PlexItem(
|
|
rating_key="e1", library_id=movies_lib.id, kind="episode", title="An Episode",
|
|
year=2001, cast_names=[NAME],
|
|
)
|
|
show = PlexShow(rating_key="s1", library_id=shows_lib.id, title="A Show", cast_names=[NAME])
|
|
db.add_all([heat, directed, hidden, episode, show])
|
|
db.add(PlexPerson(name=NAME, photo_thumb="/img/deniro.jpg"))
|
|
db.flush()
|
|
|
|
user = User(email="person@example.com")
|
|
db.add(user)
|
|
db.flush()
|
|
db.add(PlexState(user_id=user.id, item_id=heat.id, status="watched"))
|
|
db.commit()
|
|
|
|
resp = _get(db, user, monkeypatch, f"/api/plex/person/{NAME.replace(' ', '%20')}")
|
|
assert resp.status_code == 200, resp.text
|
|
body = resp.json()
|
|
|
|
assert body["name"] == NAME
|
|
assert body["photo"] == "/img/deniro.jpg"
|
|
# Both the cast credit (Heat) and the director-only credit (A Bronx Tale), year desc; NOT the
|
|
# disabled-library movie and NOT the episode.
|
|
assert [m["title"] for m in body["movies"]] == ["Heat", "A Bronx Tale"]
|
|
assert body["movies"][0]["status"] == "watched" # from the caller's PlexState
|
|
assert body["movies"][1]["status"] == "new"
|
|
assert all(m["type"] == "movie" for m in body["movies"])
|
|
# The show credit.
|
|
assert [s["title"] for s in body["shows"]] == ["A Show"]
|
|
assert body["shows"][0]["type"] == "show"
|
|
|
|
|
|
def test_person_detail_unknown_name_is_empty(db, monkeypatch):
|
|
lib = PlexLibrary(plex_key="1", title="Movies", kind="movie", enabled=True)
|
|
db.add(lib)
|
|
db.flush()
|
|
db.add(PlexItem(rating_key="m1", library_id=lib.id, kind="movie", title="Solo", cast_names=["Nobody Else"]))
|
|
user = User(email="empty@example.com")
|
|
db.add(user)
|
|
db.commit()
|
|
|
|
resp = _get(db, user, monkeypatch, "/api/plex/person/Ghost%20Actor")
|
|
assert resp.status_code == 200, resp.text
|
|
body = resp.json()
|
|
# An unknown name has no plex_people row → no bio, no titles (E5 S6 added the bio fields, all null).
|
|
assert body == {
|
|
"name": "Ghost Actor",
|
|
"photo": None,
|
|
"biography": None,
|
|
"birthday": None,
|
|
"deathday": None,
|
|
"place_of_birth": None,
|
|
"known_for_department": None,
|
|
"also_known_as": [],
|
|
"known_for": [],
|
|
"movies": [],
|
|
"shows": [],
|
|
}
|
|
|
|
|
|
def test_content_libs_partitions_only_enabled(db):
|
|
db.add_all([
|
|
PlexLibrary(plex_key="1", title="Movies", kind="movie", enabled=True),
|
|
PlexLibrary(plex_key="2", title="Shows", kind="show", enabled=True),
|
|
PlexLibrary(plex_key="3", title="OffMovie", kind="movie", enabled=False),
|
|
])
|
|
db.commit()
|
|
movie_ids, show_ids = _content_libs(db)
|
|
assert len(movie_ids) == 1 and len(show_ids) == 1 # the disabled movie lib is excluded
|