"""Capture the two docs images that are NOT screenshots of a Shortlist screen. Same gate and the same technique as `test_screenshots.py` (skipped unless `SHOTS_DIR` is set, so CI never runs it), but the subject is different: these compose an HTML page and photograph that, because neither picture is a Shortlist screen. SHOTS_DIR=docs/images .venv/bin/python -m pytest tests/e2e/test_marketing_assets.py -m e2e --no-cov -n0 - `plex-picked-for-you.jpg` is NOT captured here. It needs no browser and no Plex, so it lives in `tests/unit/test_hero_image.py` — which also means its checks run on every commit rather than only when someone regenerates it. Its source screenshot is `assets/plex-shelf-source.jpg`. - `two-account.webp` two accounts' Plex Home hubs, side by side, read through their OWN server tokens. Every poster on it is the fake PMS's real answer to that account's token, so the picture cannot claim a privacy result the harness doesn't have. - `social-preview.jpg` the og:image card. No live data at all, so it needs no Plex fixture. """ from __future__ import annotations import html import io import os import re from pathlib import Path from xml.etree import ElementTree import httpx import pytest from PIL import Image from playwright.sync_api import Browser from tests.e2e.conftest import ShortlistApp, build_real_rows pytestmark = pytest.mark.e2e SHOTS_DIR = os.environ.get("SHOTS_DIR") skip_unless_capturing = pytest.mark.skipif(not SHOTS_DIR, reason="set SHOTS_DIR to capture screenshots") #: The docs site's own tokens (docs/assets/css/main.css). Duplicated rather than imported because #: these images are rendered by a browser that never loads the site's stylesheet — keeping the hex #: values here, named the same, is what makes a drift visible when someone re-reads both files. PALETTE = """ --bg: #08080a; --surface: #121216; --surface-2: #191920; --border: rgba(255, 255, 255, 0.09); --text: #f4f4f5; --muted: #94949f; --amber: #e5a00d; --amber-bright: #f5c04a; """ #: Tiles per shelf on the two-account image. A row holds far more titles than this; showing them #: all at a readable size needs a canvas nobody will open, and the point of the picture is that the #: two accounts hold DIFFERENT titles, not how many each holds. TILES = 5 def _shot_path(name: str) -> Path: out = Path(SHOTS_DIR) / name out.parent.mkdir(parents=True, exist_ok=True) return out def _rows_visible_to(app: ShortlistApp, account_id: int) -> list[tuple[int, str, list[dict[str, str]]]]: """Every Shortlist row on that account's own Plex Home, with the items inside it. Asked of the fake PMS with `X-Plex-Token: server-`, which is the same call `test_privacy_uninstall_e2e.py` uses to prove nobody sees anyone else's row. Reading it any other way (Shortlist's own API, the owner's token) would produce a picture of what Shortlist believes rather than of what Plex answers. Returns: One `(rating_key, row title, tiles)` per visible row, in hub order. A tile carries the item's key, title, year and artwork path: both pictures draw posters, and the key is what lets the hero assert it is not advertising something the viewer already watched. """ rows: list[tuple[int, str, list[dict[str, str]]]] = [] for hub in app.plex_hubs_as(account_id): key = str(hub.get("key") or "") match = re.search(r"/library/collections/(\d+)", key) if match is None: continue # Continue Watching and friends: not ours, and not what this picture is about response = httpx.get( f"{app.pms_url}{key}", headers={"X-Plex-Token": f"server-{account_id}"}, timeout=30, ) response.raise_for_status() container = ElementTree.fromstring(response.text) # Plex serves movies as