Files
siftlode/backend/tests/test_plex_person.py
T
peter 705377c797 feat(plex): the people are local — bios and portraits fetched ahead, not on the click (E30 S6)
A person's TMDB bio was fetched only when their page was first opened, off the request path, so the first
open showed no bio and the page never asked again (2 of 18,202 linked people were ever enriched).

- people_enrich scheduler job (every 10 min, 300 per run): linked people's bios from TMDB and their
  portraits into the image cache, the most-credited people first. A failed TMDB lookup ends the batch.
- Portraits are resized through Plex's photo transcoder to a 320 box (~15 KB instead of ~170 KB; the
  person-image proxy uses the same cache key, and a full-size original is removed as its small twin lands).
- The person response says `enriching` while a bio is on its way (a person the job hasn't reached yet).
- "Known for" is ranked by vote count, without guest visits as themselves (talk shows had crowded out the
  films: Tom Cruise had 14 of 15), with directed works, from a 40-deep pool; the page shows up to 15.
  Migration 0093 un-stamps enriched people so the job refetches them.
- A phased job result (backfill, people_enrich) now counts as a change for the audit trail; before, an
  automatic backfill run that added videos was never audited.
- HU: the watch reconcile job reads "egyeztetés" (a leftover from the S5 sweep).
2026-09-30 02:19:54 +02:00

154 lines
5.2 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.plex.query import content_libs
NAME = "Robert De Niro"
def _get(db, user, monkeypatch, override_dep, path):
monkeypatch.setattr(state, "_configured_cache", True) # pass the request gate
override_dep(get_db, db.info["request"])
override_dep(current_user, lambda: user)
return TestClient(app).get(path) # no lifespan (skip scheduler/HLS sweep)
def test_person_detail_returns_cast_and_director_credits(db, monkeypatch, override_dep):
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, override_dep, 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, override_dep):
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, override_dep, "/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": [],
"enriching": False,
"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