fix(plex): the recs rail's anchor and a held recommendation are named in the reader's language (E30 S1b)

Found in the S1b UAT: 'Mert megnézted: Batman Forever' kept the English title, and
the info page's TMDB recommendations kept TMDB's English titles even for titles we
hold. A held recommendation is now named as the library names it, in the reader's
language; one we lack keeps TMDB's title.
This commit is contained in:
2026-09-29 02:25:36 +02:00
parent 632e39d458
commit f6298d29af
5 changed files with 51 additions and 11 deletions
+13 -5
View File
@@ -21,6 +21,7 @@ from sqlalchemy.orm import Session
from app import logs, sysconfig
from app.config import settings
from app.models import PlexItem, PlexPerson, PlexShow
from app.plex.cards import display_title
log = logs.get_logger(__name__)
# Politeness throttle: TMDB tolerates ~50 req/s; ~33/s leaves comfortable headroom for a shared host.
@@ -406,10 +407,11 @@ def resolve_library_guids(
return movies, shows
def resolve_recs(db: Session, recs: list[dict] | None) -> list[dict]:
def resolve_recs(db: Session, recs: list[dict] | None, *, lang: str) -> list[dict]:
"""Tag each recommendation with whether it's in OUR library (matched by guid `tmdb:<id>`, bucketed
by media_type — see resolve_library_guids) and, if so, its rating_key — so the client can deep-link
the ones we have and merely display the rest."""
the ones we have and merely display the rest. One we have is named as the library names it, in
`lang` (E30); one we lack keeps the title TMDB gave."""
if not recs:
return []
movies, shows = resolve_library_guids(db, recs)
@@ -417,13 +419,19 @@ def resolve_recs(db: Session, recs: list[dict] | None) -> list[dict]:
for r in recs:
g = f"tmdb:{r.get('tmdb_id')}"
row = shows.get(g) if r.get("media_type") == "tv" else movies.get(g)
named = {"title": display_title(row, lang)} if row is not None else {}
out.append(
{**r, "in_library": row is not None, "rating_key": row.rating_key if row else None}
{
**r,
**named,
"in_library": row is not None,
"rating_key": row.rating_key if row else None,
}
)
return out
def serve_block(db: Session, row) -> dict | None:
def serve_block(db: Session, row, *, lang: str) -> dict | None:
"""The TMDB extras for a movie/show detail response, served from the row (no live TMDB call).
None until the row has been TMDB-enriched (the client then shows nothing extra)."""
if row.tmdb_enriched_at is None:
@@ -438,7 +446,7 @@ def serve_block(db: Session, row) -> dict | None:
"crew": row.crew or [],
"studios": row.studios or [],
"production_countries": row.production_countries or [],
"recommendations": resolve_recs(db, row.recommendations),
"recommendations": resolve_recs(db, row.recommendations, lang=lang),
"backdrops": row.backdrops or [],
}
+2 -2
View File
@@ -211,7 +211,7 @@ def show_detail(
"episode_count": show_roll["episode_count"],
"collection_keys": sh.collection_keys or [],
# TMDB enrichment (E5 S2), served from the row.
"tmdb": plex_tmdb.serve_block(db, sh),
"tmdb": plex_tmdb.serve_block(db, sh, lang=lang),
},
"seasons": [_season_block(se, by_season.get(se.id, [])) for se in seasons],
"related": related,
@@ -517,5 +517,5 @@ def item_detail(
"next_id": next_id,
"collections": _collection_strips(db, it, user.id, lang),
# TMDB enrichment (E5 S2), served from the row — movies only (episodes aren't enriched).
"tmdb": plex_tmdb.serve_block(db, it) if it.kind == "movie" else None,
"tmdb": plex_tmdb.serve_block(db, it, lang=lang) if it.kind == "movie" else None,
}
+5 -2
View File
@@ -313,7 +313,7 @@ def _because_you_watched(
)
if anchor is None:
return None
resolved = resolve_recs(db, anchor.recommendations)
resolved = resolve_recs(db, anchor.recommendations, lang=lang)
# Preserve TMDB's recommendation order, keep only in-library hits, and drop the anchor itself.
rks = [
r["rating_key"]
@@ -362,7 +362,10 @@ def _because_you_watched(
items.append(scards[rk])
if not items:
return None
return {"anchor": {"id": anchor.rating_key, "title": anchor.title}, "items": items}
return {
"anchor": {"id": anchor.rating_key, "title": display_title(anchor, lang)},
"items": items,
}
@router.get("/discover")
+27
View File
@@ -230,6 +230,25 @@ def _seed_rails(db):
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
@@ -259,6 +278,14 @@ def test_every_card_rail_serves_the_readers_language(db, monkeypatch, override_d
("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"]
+4 -2
View File
@@ -255,13 +255,13 @@ def test_serve_block_none_until_enriched_and_resolves_recs(db):
db.add_all([sequel, fresh])
db.commit()
assert tmdb.serve_block(db, fresh) is None # not enriched yet
assert tmdb.serve_block(db, fresh, lang="en") is None # not enriched yet
tmdb.apply_tmdb_detail(fresh, tmdb.parse_tmdb(MOVIE_PAYLOAD, "movie"))
fresh.tmdb_enriched_at = datetime.now(timezone.utc)
db.commit()
block = tmdb.serve_block(db, fresh)
block = tmdb.serve_block(db, fresh, lang="en")
assert block is not None
assert block["budget"] == 6000000
assert block["keywords"] == ["mafia", "crime family"]
@@ -409,6 +409,7 @@ def test_resolve_recs_matches_by_media_type_not_bare_id(db):
{"tmdb_id": 500, "title": "Movie 500", "media_type": "movie"},
{"tmdb_id": 900, "title": "Show 900", "media_type": "tv"},
],
lang="en",
)
}
assert miss[500]["in_library"] is False and miss[500]["rating_key"] is None
@@ -423,6 +424,7 @@ def test_resolve_recs_matches_by_media_type_not_bare_id(db):
{"tmdb_id": 900, "media_type": "movie"},
{"tmdb_id": 500, "media_type": "tv"},
],
lang="en",
)
}
assert hit[900]["in_library"] is True and hit[900]["rating_key"] == "m900"