"""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.plex.cards import display_summary, display_title 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", summary="English only.", tagline="English only tagline.", genres=["Comedy"], # no HU twin, never reached by a sync since: no genres_hu 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_a_genre_the_hungarian_list_offers_in_english_still_matches(db, monkeypatch, override_dep): """E30 review: a row the sync has not given Hungarian genres yet is listed under its English word (the facets read coalesce(genres_hu, genres)); filtering by that word must find it.""" user = _seed(db) facets = _get(db, user, monkeypatch, override_dep, "/api/plex/facets?scope=movie&lang=hu") assert "Comedy" in {g["value"] for g in facets.json()["genres"]} resp = _get( db, user, monkeypatch, override_dep, "/api/plex/library?scope=movie&lang=hu&genres=Comedy" ) assert _titles(resp) == {"EN Only"} def test_a_title_without_a_hungarian_twin_reads_in_english(db, monkeypatch, override_dep): user = _seed(db) body = _get(db, user, monkeypatch, override_dep, "/api/plex/item/m2?lang=hu").json() assert (body["title"], body["summary"], body["tagline"]) == ( "EN Only", "English only.", "English only tagline.", ) row = PlexItem(title="T", summary="S") assert (display_title(row, "hu"), display_summary(row, "hu")) == ("T", "S") row.title_hu, row.summary_hu = "HT", "HS" assert (display_title(row, "hu"), display_summary(row, "hu")) == ("HT", "HS") assert (display_title(row, "en"), display_summary(row, "en")) == ("T", "S") 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), ] ) # TMDB extras: m2 recommends m1 (held) and a title we lack; m1, watched, anchors the recs rail. m1.guid = "tmdb:1" m1.tmdb_enriched_at = datetime(2024, 1, 1, tzinfo=timezone.utc) m1.recommendations = [{"tmdb_id": 2, "title": "EN Only", "media_type": "movie"}] m2 = db.query(PlexItem).filter_by(rating_key="m2").one() m2.guid = "tmdb:2" m2.tmdb_enriched_at = datetime(2024, 1, 1, tzinfo=timezone.utc) m2.recommendations = [ {"tmdb_id": 1, "title": "English Title", "media_type": "movie"}, {"tmdb_id": 77, "title": "Elsewhere", "media_type": "movie"}, ] db.add( PlexState( user_id=user.id, item_id=m1.id, status="watched", watched_at=datetime(2024, 3, 1, tzinfo=timezone.utc), ) ) 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"), ] # A recommendation we hold is named as the library names it; the rail's anchor too. recs = get("/api/plex/item/m2?lang=hu")["tmdb"]["recommendations"] assert [(r["title"], r["in_library"]) for r in recs] == [ ("Magyar Cím", True), ("Elsewhere", False), ] byw = get("/api/plex/discover?scope=both&lang=hu")["because_you_watched"] assert byw["anchor"]["title"] == "Magyar Cím" 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"), ]