Files
siftlode/backend/tests/test_plex_lang.py
T
peter 632e39d458 feat(plex): every card rail in the reader's language; an episode in a poster rail stands as its show (E30 S1b)
The grid served Hungarian titles, but every other card surface (Discover rails, the
show page's episodes, search, collections, the person page, playlists, Home's resume
rail, hero and comfort shelf) served English. The card serializers now take a
required keyword-only lang (a forgotten call site fails instead of going English), and
each of those reads takes ?lang=hu, with the language in its query key. A Hungarian
search finds an episode by its Hungarian title too.

An episode on a poster rail showed its 16:9 still cropped into a poster, titled only
with the episode name; it now shows the show's poster and name, with S·E · title under
it (option A). Plex's generic season names (Season 1, Specials) are rebuilt from the
number in the reader's language; a season with a name of its own keeps it.

Home's comfort-shelf move wrote its optimistic order to the key without the language,
which nothing read any more; it writes the query's own key.
2026-09-29 02:12:32 +02:00

272 lines
10 KiB
Python

"""E5 S6: HU-overlay language serving. When ?lang=hu, the unified library, facets, genre filter and
detail endpoints serve the Hungarian `*_hu` columns (falling back to EN where a row has no HU twin);
?lang=en (the default) serves English. Also covers the imdb_rating IMDb-only / audience-score split.
DB lane (`db` fixture) — skipped without Postgres.
"""
from datetime import datetime, timezone
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 import sysconfig
from app.models import (
PlexCollection,
PlexItem,
PlexLibrary,
PlexPlaylist,
PlexPlaylistItem,
PlexSeason,
PlexShow,
PlexState,
User,
)
def _seed(db):
"""One EN movie library with a single movie that has both EN and HU metadata, plus a second movie
that is EN-only (no HU twin) to prove the coalesce fallback."""
lib = PlexLibrary(plex_key="1", title="Movies", kind="movie", enabled=True)
db.add(lib)
db.flush()
m1 = PlexItem(
rating_key="m1",
library_id=lib.id,
kind="movie",
title="English Title",
title_hu="Magyar Cím",
summary="English summary.",
summary_hu="Magyar összefoglaló.",
genres=["Drama"],
genres_hu=["Dráma", "Akció"],
tagline="English tagline.",
tagline_hu="Magyar mottó.",
rating=6.5,
imdb_rating=None,
year=2001,
added_at=datetime(2024, 1, 1, tzinfo=timezone.utc),
people_enriched_at=datetime(
2024, 1, 1, tzinfo=timezone.utc
), # serve from row, skip live enrich
)
m2 = PlexItem(
rating_key="m2",
library_id=lib.id,
kind="movie",
title="EN Only",
genres=["Comedy"], # no HU twin
year=2002,
added_at=datetime(2024, 1, 2, tzinfo=timezone.utc),
people_enriched_at=datetime(2024, 1, 2, tzinfo=timezone.utc),
)
db.add_all([m1, m2])
user = User(email="plexlang@example.com")
db.add(user)
db.commit()
return user
def _get(db, user, monkeypatch, override_dep, path):
monkeypatch.setattr(
state, "_configured_cache", True
) # the request gate 503s an unconfigured app
override_dep(get_db, db.info["request"])
override_dep(current_user, lambda: user)
client = TestClient(app) # no context manager: skip lifespan
return client.get(path)
def _titles(resp):
return {i["title"] for i in resp.json()["items"]}
def test_library_serves_active_language_titles(db, monkeypatch, override_dep):
user = _seed(db)
hu = _get(db, user, monkeypatch, override_dep, "/api/plex/library?scope=movie&lang=hu")
en = _get(db, user, monkeypatch, override_dep, "/api/plex/library?scope=movie&lang=en")
# HU serves the twin's Hungarian title; the EN-only row falls back to its English title.
assert _titles(hu) == {"Magyar Cím", "EN Only"}
assert _titles(en) == {"English Title", "EN Only"}
def test_facets_vocabulary_is_language_specific(db, monkeypatch, override_dep):
user = _seed(db)
hu = _get(db, user, monkeypatch, override_dep, "/api/plex/facets?scope=movie&lang=hu")
en = _get(db, user, monkeypatch, override_dep, "/api/plex/facets?scope=movie&lang=en")
hu_genres = {g["value"] for g in hu.json()["genres"]}
en_genres = {g["value"] for g in en.json()["genres"]}
assert "Dráma" in hu_genres and "Drama" not in hu_genres
assert "Drama" in en_genres and "Dráma" not in en_genres
def test_genre_filter_matches_active_language_vocabulary(db, monkeypatch, override_dep):
user = _seed(db)
# The HU genre chip filters the HU column; the same string against the EN column matches nothing.
hit = _get(
db, user, monkeypatch, override_dep, "/api/plex/library?scope=movie&lang=hu&genres=Dráma"
)
miss = _get(
db, user, monkeypatch, override_dep, "/api/plex/library?scope=movie&lang=en&genres=Dráma"
)
assert _titles(hit) == {"Magyar Cím"}
assert miss.json()["items"] == []
def test_a_genre_filter_saved_in_english_still_matches_in_hungarian(db, monkeypatch, override_dep):
"""E30: the HU column holds one Hungarian word per genre. A filter saved while the HU list still
offered "Action" is read through the same map, so it finds the "Akció" title instead of nothing."""
user = _seed(db)
resp = _get(
db, user, monkeypatch, override_dep, "/api/plex/library?scope=movie&lang=hu&genres=Action"
)
assert _titles(resp) == {"Magyar Cím"}
def test_item_detail_language_and_rating_split(db, monkeypatch, override_dep):
user = _seed(db)
resp = _get(db, user, monkeypatch, override_dep, "/api/plex/item/m1?lang=hu")
body = resp.json()
assert body["title"] == "Magyar Cím"
assert body["summary"] == "Magyar összefoglaló."
assert body["tagline"] == "Magyar mottó." # E30: the HU overlay's tagline
en = _get(db, user, monkeypatch, override_dep, "/api/plex/item/m1").json()
assert en["tagline"] == "English tagline."
# imdb_rating is IMDb-only (None here); the audience score is served separately as `rating`.
assert body["imdb_rating"] is None
assert body["rating"] == 6.5
def test_hu_search_still_finds_en_only_titles(db, monkeypatch, override_dep):
user = _seed(db)
# m2 ("EN Only") has no HU twin, so search_vector_hu carries no title tokens for it. HU-mode search
# must still match it via the EN vector (vector_match's union) — findable by its English title.
resp = _get(db, user, monkeypatch, override_dep, "/api/plex/library?scope=movie&lang=hu&q=Only")
assert "EN Only" in _titles(resp)
def test_rating_filter_prefers_imdb_over_audience(db, monkeypatch, override_dep):
lib = PlexLibrary(plex_key="1", title="Movies", kind="movie", enabled=True)
db.add(lib)
db.flush()
# imdb 8.0 but audience 4.0 → a rating_min of 7 must MATCH (filter the displayed best score,
# IMDb preferred), where filtering the raw audience column (4.0) would wrongly exclude it.
db.add(
PlexItem(
rating_key="r1",
library_id=lib.id,
kind="movie",
title="High IMDb",
imdb_rating=8.0,
rating=4.0,
year=2001,
added_at=datetime(2024, 1, 1, tzinfo=timezone.utc),
people_enriched_at=datetime(2024, 1, 1, tzinfo=timezone.utc),
)
)
user = User(email="rating@example.com")
db.add(user)
db.commit()
hit = _get(db, user, monkeypatch, override_dep, "/api/plex/library?scope=movie&rating_min=7")
assert "High IMDb" in _titles(hit)
miss = _get(db, user, monkeypatch, override_dep, "/api/plex/library?scope=movie&rating_min=9")
assert "High IMDb" not in _titles(miss)
def _seed_rails(db):
"""`_seed` plus what every card rail reads: a HU-overlaid show with a HU-overlaid episode in
progress, a cast name, a collection, and a playlist holding the movie and the episode."""
user = _seed(db)
m1 = db.query(PlexItem).filter_by(rating_key="m1").one()
m1.cast_names = ["Anna"]
m1.collection_keys = ["c1"]
db.add(PlexCollection(rating_key="c1", library_id=m1.library_id, title="Coll"))
tv = PlexLibrary(plex_key="2", title="TV Shows", kind="show", enabled=True)
db.add(tv)
db.flush()
show = PlexShow(
rating_key="s1",
library_id=tv.id,
title="English Show",
title_hu="Magyar Sorozat",
added_at=datetime(2024, 1, 3, tzinfo=timezone.utc),
)
db.add(show)
db.flush()
season = PlexSeason(rating_key="s1s1", show_id=show.id, title="Season 1", season_number=1)
db.add(season)
db.flush()
ep = PlexItem(
rating_key="e1",
library_id=tv.id,
kind="episode",
show_id=show.id,
season_id=season.id,
title="Pilot",
title_hu="Kezdet",
season_number=1,
episode_number=1,
duration_s=3000,
)
db.add(ep)
db.flush()
db.add(
PlexState(
user_id=user.id,
item_id=ep.id,
status="in_progress",
position_seconds=600,
progress_updated_at=datetime(2024, 2, 1, tzinfo=timezone.utc),
)
)
pl = PlexPlaylist(user_id=user.id, title="Mix")
db.add(pl)
db.flush()
db.add_all(
[
PlexPlaylistItem(playlist_id=pl.id, item_id=m1.id, position=0),
PlexPlaylistItem(playlist_id=pl.id, item_id=ep.id, position=1),
]
)
sysconfig.set_value(db, "plex_enabled", True)
db.commit()
return user, pl.id
def test_every_card_rail_serves_the_readers_language(db, monkeypatch, override_dep):
"""E30: the grid spoke Hungarian, but every other card surface (Discover rails, show page,
search, collection, person page, playlist, Home) served the English title to a HU reader."""
user, pid = _seed_rails(db)
def get(path):
return _get(db, user, monkeypatch, override_dep, path).json()
def titles(cards):
return {c["title"] for c in cards}
rails = get("/api/plex/discover?scope=both&lang=hu")
assert {"Magyar Cím", "Magyar Sorozat"} <= titles(rails["recently_added"])
assert "Magyar Sorozat" in titles(rails["continue_watching"])
assert titles(get("/api/plex/show/s1?lang=hu")["seasons"][0]["episodes"]) == {"Kezdet"}
found = get("/api/plex/library?scope=both&lang=hu&q=Kezdet")["episodes"]
assert [(e["title"], e["show_title"]) for e in found] == [("Kezdet", "Magyar Sorozat")]
assert titles(get("/api/plex/collections/c1?lang=hu")["items"]) == {"Magyar Cím"}
assert titles(get("/api/plex/person/Anna?lang=hu")["movies"]) == {"Magyar Cím"}
items = get(f"/api/plex/playlists/{pid}?lang=hu")["items"]
assert [(c["title"], c.get("show_title")) for c in items] == [
("Magyar Cím", None),
("Kezdet", "Magyar Sorozat"),
]
home = get("/api/discover?lang=hu")["continue_watching"]
plex = [c for c in home if c["source"] == "plex"]
assert [c["title"] for c in plex] == ["Magyar Sorozat"]
assert plex[0]["next"]["title"] == "Kezdet"
# EN (the default) still serves English on the same rails.
items = get(f"/api/plex/playlists/{pid}")["items"]
assert [(c["title"], c.get("show_title")) for c in items] == [
("English Title", None),
("Pilot", "English Show"),
]