From f6298d29af987d6cc0a174b0d25962d7df9be93b Mon Sep 17 00:00:00 2001 From: npeter83 Date: Tue, 29 Sep 2026 02:25:36 +0200 Subject: [PATCH] fix(plex): the recs rail's anchor and a held recommendation are named in the reader's language (E30 S1b) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- backend/app/plex/tmdb.py | 18 +++++++++++++----- backend/app/routes/plex/detail.py | 4 ++-- backend/app/routes/plex/discover.py | 7 +++++-- backend/tests/test_plex_lang.py | 27 +++++++++++++++++++++++++++ backend/tests/test_plex_tmdb.py | 6 ++++-- 5 files changed, 51 insertions(+), 11 deletions(-) diff --git a/backend/app/plex/tmdb.py b/backend/app/plex/tmdb.py index f373d072..c0f32ed2 100644 --- a/backend/app/plex/tmdb.py +++ b/backend/app/plex/tmdb.py @@ -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:`, 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 [], } diff --git a/backend/app/routes/plex/detail.py b/backend/app/routes/plex/detail.py index c0798a56..47458af5 100644 --- a/backend/app/routes/plex/detail.py +++ b/backend/app/routes/plex/detail.py @@ -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, } diff --git a/backend/app/routes/plex/discover.py b/backend/app/routes/plex/discover.py index 5adc8f43..39b91033 100644 --- a/backend/app/routes/plex/discover.py +++ b/backend/app/routes/plex/discover.py @@ -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") diff --git a/backend/tests/test_plex_lang.py b/backend/tests/test_plex_lang.py index 0790f804..15465fe1 100644 --- a/backend/tests/test_plex_lang.py +++ b/backend/tests/test_plex_lang.py @@ -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"] diff --git a/backend/tests/test_plex_tmdb.py b/backend/tests/test_plex_tmdb.py index 640fd686..319a4a55 100644 --- a/backend/tests/test_plex_tmdb.py +++ b/backend/tests/test_plex_tmdb.py @@ -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"