diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-09-02 03:13:53 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-09-02 03:23:46 +0200 |
| commit | 12b6dc4dd3e009f2e844d87181800aa12d07a3a6 (patch) | |
| tree | c118ff1efee9f38de7579d4cb75ec6f4114e27d9 /packages/meshbay-node | |
| parent | f7917bdde37fe089485bb2b65c5504315dcc9c56 (diff) | |
| download | meshbay-12b6dc4dd3e009f2e844d87181800aa12d07a3a6.tar.gz | |
fix(hub): steady the show detail modal, and give a series its director
Opening a different season of the same show moved everything under the
synopsis, which is where the season control and the episode list are, so the
thing just clicked was no longer under the pointer.
- The synopsis is exactly three lines for a multi-season show, with a "read
more" link floated into the third line box (-webkit-line-clamp only ever
puts its ellipsis at the end of the last line and leaves no room after it).
Clamped from above and pinned from below to the same number: a constant,
not a range — a season summary runs two lines and the next one twelve, and
a band still reads as a jump. Whether three lines is all of it depends on
the modal's width, so it is measured in the browser and re-measured on a
resize.
- The cast is clamped to two lines.
- SeasonMenu replaces SeasonTabs: the tab row scrolled sideways once a show
had more seasons than fit, which is close to unusable on a phone. One
trigger reading "Season 5 · 1997" and a menu of every season with its
episode count, one row high whatever the season count.
- media_meta_resp.director was filled from the credits crew's job ==
"Director", a movie shape. TMDB's aggregate tv_credits crew is routinely
empty and never carries that job, so every show answered null and the modal
dropped the line. It now comes from created_by on the show details. Cached
show metadata keeps its null until TMDB_META_TTL_SECS expires or an
operator re-matches.
The facts line is joined rather than concatenated (a title with no rating
used to open with " · ") and carries the show's own year next to the
director; the selected season's air year moved onto the picker.
test_video_detail_measured.py asserts rectangles through layout_probe.py, not
declarations: the picker's offset inside its own modal body is the same pixel
either way, the synopsis and cast heights, where the read-more link lands,
and the open menu at 320 px. Each measured block sits in a whole-pixel-height
container, or two identical layouts an eighth of a pixel apart round to tops
one pixel apart. test_tmdb_show_director.py covers the credit.
docs/mediacenter.md §10.4.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014UtzVrzM7e2tG9fSpkR9ML
Diffstat (limited to 'packages/meshbay-node')
| -rw-r--r-- | packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py | 10 | ||||
| -rw-r--r-- | packages/meshbay-node/tests/test_tmdb_show_director.py | 101 |
2 files changed, 111 insertions, 0 deletions
diff --git a/packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py b/packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py index 64ba75c..1f069e7 100644 --- a/packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py +++ b/packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py @@ -3404,6 +3404,16 @@ class WebRTCPeerSession: director = next( (c.get("name") for c in (credits or {}).get("crew", []) if c.get("job") == "Director"), None) + else: + # A series has no single director, and `tv_credits`' crew is the + # aggregate one — routinely empty, and never a "Director" job. + # TMDB models the equivalent credit as `created_by` on the show + # itself, which is what its own page shows; several creators are + # ordinary, and they read as one line in the detail modal. + director = ", ".join( + name for name in + (c.get("name") for c in details.get("created_by") or []) + if name) or None runtime = details.get("runtime") if runtime is None and media_type == "tv": episode_run_times = details.get("episode_run_time") or [] diff --git a/packages/meshbay-node/tests/test_tmdb_show_director.py b/packages/meshbay-node/tests/test_tmdb_show_director.py new file mode 100644 index 0000000..cb31c2c --- /dev/null +++ b/packages/meshbay-node/tests/test_tmdb_show_director.py @@ -0,0 +1,101 @@ +""" +A series has a director credit in the detail modal too. + +`media_meta_resp.director` was filled from the credits crew's `job == +"Director"`, which is a movie shape. TMDB's aggregate `tv_credits` crew is +routinely empty and never carries that job, so every show answered with +`director: null` and the detail modal simply dropped the line — reported as +"I can't see the director of a series". + +What TMDB models for a show, and what its own page shows, is `created_by` on +the show details. Several creators is ordinary, so they join into one line. +""" + +import pytest + +from meshbay_node.transport.webrtc_server import WebRTCPeerSession + +pytestmark = pytest.mark.asyncio + + +class FakeTmdbClient: + """Details and credits, with no language fallback to get in the way.""" + + def __init__(self, details: dict, credits: dict | None = None): + self._details = details + self._credits = credits or {"cast": [], "crew": []} + + async def movie_details(self, tmdb_id, language=None): + return self._details + + async def tv_details(self, tmdb_id, language=None): + return self._details + + async def movie_credits(self, tmdb_id): + return self._credits + + async def tv_credits(self, tmdb_id): + return self._credits + + +def _details(**extra): + """Enough fields that the English per-field fallback never fires.""" + return {"name": "Some Show", "overview": "A synopsis.", + "poster_path": "/p.jpg", "genres": [{"name": "Drama"}], + "first_air_date": "1993-09-10", **extra} + + +async def test_a_show_reports_its_creator_as_the_director(): + client = FakeTmdbClient(_details(created_by=[{"name": "A Creator"}])) + + meta = await WebRTCPeerSession._tmdb_build_meta(client, "4087", "tv", {"id": 4087}) + + assert meta["director"] == "A Creator" + + +async def test_several_creators_read_as_one_line(): + client = FakeTmdbClient(_details( + created_by=[{"name": "A Creator"}, {"name": "Another Creator"}])) + + meta = await WebRTCPeerSession._tmdb_build_meta(client, "4087", "tv", {"id": 4087}) + + assert meta["director"] == "A Creator, Another Creator" + + +@pytest.mark.parametrize("created_by", [None, [], [{"name": ""}], [{}]]) +async def test_a_show_with_no_creator_credit_stays_none(created_by): + """None, not "" — the modal drops the line rather than printing a label + with nothing after it.""" + client = FakeTmdbClient(_details(created_by=created_by)) + + meta = await WebRTCPeerSession._tmdb_build_meta(client, "4087", "tv", {"id": 4087}) + + assert meta["director"] is None + + +async def test_an_empty_tv_crew_no_longer_decides_the_answer(): + """The old shape, kept as a regression: a show whose aggregate crew has a + Director entry is still described by its creators, and one whose crew is + empty — the usual case — is no longer left blank.""" + client = FakeTmdbClient( + _details(created_by=[{"name": "A Creator"}]), + credits={"cast": [], "crew": [{"job": "Director", "name": "An Episode Director"}]}) + + meta = await WebRTCPeerSession._tmdb_build_meta(client, "4087", "tv", {"id": 4087}) + + assert meta["director"] == "A Creator" + + +async def test_a_movie_still_takes_its_director_from_the_crew(): + """`created_by` does not exist on a movie; nothing about that path moves.""" + client = FakeTmdbClient( + {"title": "Some Film", "overview": "A synopsis.", "poster_path": "/p.jpg", + "genres": [{"name": "Thriller"}], "release_date": "2014-02-01"}, + credits={"cast": [{"name": "A Performer", "character": "Someone"}], + "crew": [{"job": "Editor", "name": "An Editor"}, + {"job": "Director", "name": "A Director"}]}) + + meta = await WebRTCPeerSession._tmdb_build_meta(client, "418517", "movie", {"id": 418517}) + + assert meta["director"] == "A Director" + assert meta["cast"] == [{"name": "A Performer", "character": "Someone"}] |