Files
peter 533571f572 refactor(plex): the unit's tables move into plex/models
PlexLibrary, PlexShow, PlexSeason, PlexItem, PlexPerson, PlexCollection, PlexState, PlexLink,
PlexPlaylist and PlexPlaylistItem leave app/models.py for app/plex/models.py; app.tables registers
them. Every import is re-pointed; no schema change.
2026-10-19 21:42:07 +02:00

329 lines
13 KiB
Python

"""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 User
from app.plex.models import (
PlexCollection,
PlexItem,
PlexLibrary,
PlexPlaylist,
PlexPlaylistItem,
PlexSeason,
PlexShow,
PlexState,
)
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):
"""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):
"""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ó." # 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):
"""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"),
]