feat(i18n): the person page's biography in Hungarian, from TMDB's translation (E30 S4)

The /person fetch appends translations and keeps TMDB's Hungarian biography beside the instance-language
one (new plex_people.biography_hu, migration 0092). A Hungarian reader is served it, the English one when
TMDB has none. Already-enriched people are un-stamped, so their next open refetches them once.
This commit is contained in:
2026-09-29 23:44:48 +02:00
parent a39504e617
commit 519fbe2a18
5 changed files with 125 additions and 3 deletions
+4
View File
@@ -1432,6 +1432,10 @@ class PlexPerson(Base, TimestampMixin, UpdatedAtMixin):
# no-op fetch so it isn't retried every open). `known_for` = top combined-credits [{tmdb_id,
# media_type, title}], resolved to library rows by guid at serve time (like recommendations). ---
biography: Mapped[str | None] = mapped_column(Text)
# TMDB's Hungarian biography (E30 S4), from the same request's `translations`; a Hungarian reader is
# served it, and the instance-language `biography` when TMDB has none. Birthplace, aliases and
# keywords have no Hungarian on TMDB, so they stay in the instance language.
biography_hu: Mapped[str | None] = mapped_column(Text)
birthday: Mapped[str | None] = mapped_column(String(10)) # "YYYY-MM-DD", served as-is
deathday: Mapped[str | None] = mapped_column(String(10))
place_of_birth: Mapped[str | None] = mapped_column(Text)
+15 -2
View File
@@ -132,8 +132,11 @@ class TmdbClient:
return cast("list[dict] | None", self._get("/configuration/countries"))
def fetch_person(self, person_id: int) -> dict | None:
"""One person's bio + combined credits (E5 S6), in a single append_to_response request."""
return self._get(f"/person/{person_id}", {"append_to_response": "combined_credits"})
"""One person's bio + combined credits (E5 S6) + the bio's translations (E30 S4), in a single
append_to_response request."""
return self._get(
f"/person/{person_id}", {"append_to_response": "combined_credits,translations"}
)
def guid_to_tmdb_id(guid: str | None, media_type: str, tmdb: TmdbClient) -> int | None:
@@ -524,6 +527,7 @@ def parse_person(payload: dict) -> dict:
break
return {
"biography": (payload.get("biography") or "").strip() or None,
"biography_hu": _translated_bio(payload, "hu"),
"birthday": payload.get("birthday") or None,
"deathday": payload.get("deathday") or None,
"place_of_birth": payload.get("place_of_birth") or None,
@@ -533,8 +537,17 @@ def parse_person(payload: dict) -> dict:
}
def _translated_bio(payload: dict, lang: str) -> str | None:
"""The `lang` biography from the appended `translations` (TMDB leaves an untranslated one blank)."""
for tr in (payload.get("translations") or {}).get("translations") or []:
if tr.get("iso_639_1") == lang:
return ((tr.get("data") or {}).get("biography") or "").strip() or None
return None
def apply_person(row: PlexPerson, parsed: dict) -> None:
row.biography = parsed["biography"]
row.biography_hu = parsed["biography_hu"]
row.birthday = parsed["birthday"]
row.deathday = parsed["deathday"]
row.place_of_birth = parsed["place_of_birth"]
+4 -1
View File
@@ -243,7 +243,10 @@ def person_detail(
return {
"name": name,
"photo": person.photo_thumb if person else None,
"biography": person.biography if person else None,
# The Hungarian biography for a Hungarian reader, the instance-language one when TMDB has none.
"biography": ((person.biography_hu if lang == "hu" else None) or person.biography)
if person
else None,
"birthday": person.birthday if person else None,
"deathday": person.deathday if person else None,
"place_of_birth": person.place_of_birth if person else None,