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:
@@ -0,0 +1,31 @@
|
||||
"""add biography_hu to plex_people — TMDB's Hungarian biography (E30 S4)
|
||||
|
||||
Revision ID: 0092_plex_person_biography_hu
|
||||
Revises: 0091_plex_tagline_hu
|
||||
Create Date: 2026-09-29
|
||||
|
||||
The person page showed a Hungarian reader the English biography although TMDB has a Hungarian one for
|
||||
most people. The person fetch now appends `translations` and keeps the Hungarian biography beside the
|
||||
English one. Already-enriched people are un-stamped, so their next page open refetches them once (the
|
||||
same lazy path as a first open) and fills it; until then the English biography stands in.
|
||||
"""
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
revision: str = "0092_plex_person_biography_hu"
|
||||
down_revision: str | None = "0091_plex_tagline_hu"
|
||||
branch_labels: str | None = None
|
||||
depends_on: str | None = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.add_column("plex_people", sa.Column("biography_hu", sa.Text(), nullable=True))
|
||||
op.execute(
|
||||
"UPDATE plex_people SET tmdb_enriched_at = NULL "
|
||||
"WHERE tmdb_person_id IS NOT NULL AND tmdb_enriched_at IS NOT NULL"
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_column("plex_people", "biography_hu")
|
||||
@@ -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)
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -332,3 +332,74 @@ def test_person_detail_serves_bio_and_resolves_known_for(db, monkeypatch, overri
|
||||
assert j["also_known_as"] == ["Alt Name"]
|
||||
# Only the in-library known_for credit resolves to a card; the out-of-library one is dropped.
|
||||
assert [c["title"] for c in j["known_for"]] == ["Known Movie"]
|
||||
|
||||
|
||||
# --- E30 S4: the biography in the reader's language --------------------------------------------------
|
||||
# The same /person request appends `translations`; the Hungarian one is kept beside the instance-language
|
||||
# biography, and a Hungarian reader is served it, falling back to the English one when TMDB has none.
|
||||
|
||||
|
||||
def _translation(lang: str, bio: str) -> dict:
|
||||
return {"iso_639_1": lang, "iso_3166_1": lang.upper(), "data": {"biography": bio}}
|
||||
|
||||
|
||||
def test_parse_person_keeps_the_hungarian_biography():
|
||||
payload = {
|
||||
**PAYLOAD,
|
||||
"translations": {
|
||||
"translations": [
|
||||
_translation("de", "Ein Schauspieler."),
|
||||
_translation("hu", " Ünnepelt színész. "),
|
||||
]
|
||||
},
|
||||
}
|
||||
assert plex_tmdb.parse_person(payload)["biography_hu"] == "Ünnepelt színész."
|
||||
|
||||
|
||||
def test_parse_person_without_a_hungarian_biography():
|
||||
assert plex_tmdb.parse_person(PAYLOAD)["biography_hu"] is None
|
||||
blank = {**PAYLOAD, "translations": {"translations": [_translation("hu", " ")]}}
|
||||
assert plex_tmdb.parse_person(blank)["biography_hu"] is None
|
||||
|
||||
|
||||
def test_fetch_person_asks_for_the_translations():
|
||||
client = plex_tmdb.TmdbClient.__new__(plex_tmdb.TmdbClient)
|
||||
seen: list[dict] = []
|
||||
client._get = lambda path, params=None: seen.append(params or {}) or {} # type: ignore[method-assign]
|
||||
client.fetch_person(500)
|
||||
assert "translations" in seen[0]["append_to_response"].split(",")
|
||||
|
||||
|
||||
def test_person_detail_serves_the_biography_in_the_readers_language(db, monkeypatch, override_dep):
|
||||
from datetime import datetime, timezone
|
||||
|
||||
done = datetime(2024, 1, 1, tzinfo=timezone.utc)
|
||||
db.add(PlexLibrary(plex_key="7", title="Movies", kind="movie", enabled=True))
|
||||
db.add_all(
|
||||
[
|
||||
PlexPerson(
|
||||
name="Both P",
|
||||
tmdb_person_id=1,
|
||||
tmdb_enriched_at=done,
|
||||
biography="English bio.",
|
||||
biography_hu="Magyar életrajz.",
|
||||
),
|
||||
PlexPerson(
|
||||
name="Only EN P", tmdb_person_id=2, tmdb_enriched_at=done, biography="Only English."
|
||||
),
|
||||
]
|
||||
)
|
||||
user = User(email="biolang@example.com")
|
||||
db.add(user)
|
||||
db.commit()
|
||||
|
||||
def bio(name: str, lang: str) -> str | None:
|
||||
resp = _get(db, user, monkeypatch, override_dep, f"/api/plex/person/{name}?lang={lang}")
|
||||
assert resp.status_code == 200, resp.text
|
||||
return resp.json()["biography"]
|
||||
|
||||
assert bio("Both P", "hu") == "Magyar életrajz."
|
||||
assert bio("Both P", "en") == "English bio."
|
||||
assert (
|
||||
bio("Only EN P", "hu") == "Only English."
|
||||
) # TMDB has no Hungarian one: the English stands in
|
||||
|
||||
Reference in New Issue
Block a user