diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-09-02 10:00:23 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-09-02 10:00:23 +0200 |
| commit | 9f6797bfc4d8b55373cc6a957969f03443b5b29d (patch) | |
| tree | 1e9584d56c362fec78dd45a30195d86a0cd09b52 /packages/meshbay-hub/src | |
| parent | 9e3dfcb8229e0cb3d8e296acd09cf5e2acb9565e (diff) | |
| download | meshbay-9f6797bfc4d8b55373cc6a957969f03443b5b29d.tar.gz | |
fix(hub): a show opens on its first season, not its first thumbnail
Reported live: a show with a dozen seasons opened on season 6. Every season
was in the picker and none was missing — the default was wrong.
VideoDetailModal took it from `repEntry.season`. `repEntry` is the show's
"representative entry", which the poster grid picks as
`episodes.find((e) => e.thumb_hash) || episodes[0]`: the first episode that
has a thumbnail, so the card has a fallback frame when TMDB has no poster.
That is from the original Videos commit; the season tabs came later and read
the same entry as "the episode the reader is looking at", which it never was
on that path. Episodes are sorted by (season, episode), so a show whose first
five seasons had no thumbnail yet — a partial enrichment pass, or ffmpeg
failing on those files — hands back a season-6 episode.
`defaultSeason(show)` reads the season list and nothing else: the lowest
season present, specials only when there is nothing else, and the lowest
*number* rather than the first entry so it does not quietly depend on
buildSeasons keeping its sort. The effect's dependency on repEntry goes with
it — nothing in it reads that any more.
test_video_default_season.py runs the function in node. No input it takes can
carry a thumbnail, which is the point. docs/mediacenter.md §10.5.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014UtzVrzM7e2tG9fSpkR9ML
Diffstat (limited to 'packages/meshbay-hub/src')
| -rw-r--r-- | packages/meshbay-hub/src/meshbay_hub/static/video-app.js | 36 |
1 files changed, 26 insertions, 10 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/video-app.js b/packages/meshbay-hub/src/meshbay_hub/static/video-app.js index 2fa22d0..19b0f0f 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/video-app.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/video-app.js @@ -73,6 +73,28 @@ function buildSeasons(episodes) { .map(([season, seasonEpisodes]) => ({ season, episodes: seasonEpisodes })); } +// The season a show's detail modal opens on. +// +// Deliberately not the representative episode's. `repEntry` is picked for its +// *thumbnail* — `episodes.find((e) => e.thumb_hash)` in the poster grid, so +// the card has a fallback frame when TMDB has no poster — which makes its +// season an accident of which files the node has managed to thumbnail so far. +// Found live on a show whose early seasons had none: the modal opened on +// season 6. The two uses of "representative" were never the same thing, and +// only one of them is about what the reader is looking at. +// +// Specials first is not what anyone means by the beginning of a show, so +// season 0 wins only when it is all there is. +function defaultSeason(show) { + if (!show || !show.seasons.length) return null; + // The lowest number rather than the first entry: `buildSeasons` does sort + // ascending, but reading the answer off that ordering makes this quietly + // depend on a caller keeping it, and there is nothing to gain by that. + const numbers = show.seasons.map((s) => s.season); + const real = numbers.filter((n) => n !== 0); + return Math.min(...(real.length ? real : numbers)); +} + function groupVideoEntries(entries, videoRoot) { const movies = []; const showsByTitle = new Map(); @@ -686,17 +708,11 @@ function VideoDetailModal({ setRematching(false); }, [rematching, repEntry, transportRef]); - // Reset whenever a different file/show is opened in this same modal - // instance — repEntry/show change identity, selectedSeason must not - // silently keep pointing at whatever the previous show's season 4 was. + // Reset whenever a different show is opened in this same modal instance — + // `show` changes identity, and selectedSeason must not silently keep + // pointing at whatever the previous show's season 4 was. const [selectedSeason, setSelectedSeason] = useState(null); - useEffect(() => { - if (!show) { setSelectedSeason(null); return; } - const preferred = repEntry.season != null && show.seasons.some((s) => s.season === repEntry.season) - ? repEntry.season - : (show.seasons.find((s) => s.season !== 0) || show.seasons[0]).season; - setSelectedSeason(preferred); - }, [show, repEntry]); + useEffect(() => { setSelectedSeason(defaultSeason(show)); }, [show]); const showMultiSeason = Boolean(show && show.seasons.length > 1); const seasonMeta = useSeasonMeta( |