E5 S6 backend half (no migration — the *_hu / rich columns exist from S5a/S5b). Language-aware serving (?lang=hu serves the HU overlay twin, EN fallback per row): - query.py: column pickers title_col / genres_col / vector_col; field_recheck and meta_filter_conds take `lang`. DISPLAY/FILTER pickers coalesce(*_hu, *) (EN fallback); SEARCH pickers use the raw *_hu column mirroring the generated search_vector_hu (coalesce to '', not EN) so the field recheck stays a strict subset of the GIN vector (the S2 invariant). genres_col type_coerces the coalesce to JSONB so the GIN `.contains` survives. - browse.py: `lang` param on /library, /facets, /item, /show (validated to en|hu). unified_library title/vector/rank/field-scope + meta filters are language-aware for both the movie and show branches; facets unnest the active-language genre vocabulary (jsonb_typeof guard kept); detail endpoints serve HU title/summary/ genres/show_title with EN fallback. Episodes stay EN (not overlay-localized). IMDb-only rating (folded-in S5b review #5): - detail.py rich_detail: drop the audienceRating/rating fallback → imdb_rating is NULL when there's no imdb:// Rating entry (stop mislabelling a Plex audience score as IMDb). item_detail now also serves the `rating` column (audience score; show_detail already did) so the UI can show it when there's no IMDb score. Tests: test_plex_lang.py (HU title serving + EN fallback, language-specific facet vocabulary, genre-filter language matching, item_detail lang + rating split); test_plex_detail updated for IMDb-only. DB-lane suite 293 passed / 2 skipped; ruff clean.
97 lines
4.0 KiB
Python
97 lines
4.0 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.models import PlexItem, PlexLibrary, 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"],
|
|
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, path):
|
|
monkeypatch.setattr(state, "_configured_cache", True) # the request gate 503s an unconfigured app
|
|
app.dependency_overrides[get_db] = lambda: db
|
|
app.dependency_overrides[current_user] = lambda: user
|
|
try:
|
|
client = TestClient(app) # no context manager: skip lifespan
|
|
return client.get(path)
|
|
finally:
|
|
app.dependency_overrides.clear()
|
|
|
|
|
|
def _titles(resp):
|
|
return {i["title"] for i in resp.json()["items"]}
|
|
|
|
|
|
def test_library_serves_active_language_titles(db, monkeypatch):
|
|
user = _seed(db)
|
|
hu = _get(db, user, monkeypatch, "/api/plex/library?scope=movie&lang=hu")
|
|
en = _get(db, user, monkeypatch, "/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):
|
|
user = _seed(db)
|
|
hu = _get(db, user, monkeypatch, "/api/plex/facets?scope=movie&lang=hu")
|
|
en = _get(db, user, monkeypatch, "/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):
|
|
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, "/api/plex/library?scope=movie&lang=hu&genres=Dráma")
|
|
miss = _get(db, user, monkeypatch, "/api/plex/library?scope=movie&lang=en&genres=Dráma")
|
|
assert _titles(hit) == {"Magyar Cím"}
|
|
assert miss.json()["items"] == []
|
|
|
|
|
|
def test_item_detail_language_and_rating_split(db, monkeypatch):
|
|
user = _seed(db)
|
|
resp = _get(db, user, monkeypatch, "/api/plex/item/m1?lang=hu")
|
|
body = resp.json()
|
|
assert body["title"] == "Magyar Cím"
|
|
assert body["summary"] == "Magyar összefoglaló."
|
|
# 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
|