aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src/meshbay_hub/static/video-app.js
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/static/video-app.js')
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/video-app.js81
1 files changed, 63 insertions, 18 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 c0102e5..143cf4a 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/video-app.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/video-app.js
@@ -145,6 +145,7 @@ const _thumbBlobCache = new Map();
function MediaThumb({
thumbHash, transportRef, gekRef, alt, cls = 'video-thumb', onReady, emptyIcon = 'video',
+ reloadKey,
}) {
const [blobUrl, setBlobUrl] = useState(() => _thumbBlobCache.get(thumbHash) || null);
const [retryToken, setRetryToken] = useState(0);
@@ -188,7 +189,12 @@ function MediaThumb({
}
})();
return () => { cancelled = true; };
- }, [thumbHash, retryToken]);
+ // `reloadKey` — bumped by the caller when the transport behind `transportRef`
+ // was replaced (a search-page pool reconnect). Without it, a thumb whose
+ // first fetch hit a not-yet-connected transport and bailed above would stay
+ // an empty placeholder for good, since neither `thumbHash` nor `retryToken`
+ // changes when only the connection does.
+ }, [thumbHash, retryToken, reloadKey]);
if (!blobUrl) return html`<div class="${cls} video-thumb-empty"><${Icon} name=${emptyIcon} /></div>`;
return html`<img class=${cls} src=${blobUrl} alt=${alt || ''} loading="lazy" />`;
@@ -219,7 +225,13 @@ function bumpThumbGeneration() {
// would otherwise storm TMDB with its raw filename), so the client must
// refetch once the enriched fields arrive on an index delta — the fileId
// (a content hash) never changes, so nothing else would trigger it.
-function useMediaMeta(transportRef, fileId, active, enrichSig) {
+// `reloadKey` — an optional value the caller bumps when the transport behind
+// `transportRef` was swapped out (search-page's connection pool evicted and
+// later rebuilt this group's connection). The first fetch after a mount onto a
+// not-yet-connected transport returns early below; nothing else here would ever
+// re-run it, so the tile would sit on a spinner for good. Threading the group's
+// `_connGen` in as `reloadKey` is what unsticks it.
+function useMediaMeta(transportRef, fileId, active, enrichSig, reloadKey) {
const [meta, setMeta] = useState(null);
const [refetchToken, setRefetchToken] = useState(0);
@@ -244,7 +256,7 @@ function useMediaMeta(transportRef, fileId, active, enrichSig) {
} catch { if (!cancelled) setMeta({ confidence: 0 }); }
})();
return () => { cancelled = true; };
- }, [fileId, active, refetchToken, enrichSig]);
+ }, [fileId, active, refetchToken, enrichSig, reloadKey]);
return meta;
}
@@ -283,10 +295,24 @@ function useSeasonMeta(transportRef, tmdbId, season, active) {
// ── Mode A: poster grid ──────────────────────────────────────────────────────
-function PosterCard({ title, subtitle, repEntry, transportRef, gekRef, onOpen, groupKey, onMetaResolved }) {
+function PosterCard({
+ title, subtitle, repEntry, transportRef, gekRef, onOpen, groupKey, onMetaResolved, onNeedConn,
+}) {
const tRef = repEntry._tRef || transportRef;
const gRef = repEntry._gRef || gekRef;
- const meta = useMediaMeta(tRef, repEntry.id, true, repEntry.display_title || '');
+
+ // Search view only: ask for this group's connection the moment the tile
+ // actually mounts (it is inside a LazyTile, so that means "scrolled near").
+ // Groups the pre-connect walk did not reach light up here instead of never.
+ // A no-op in a single-group page — there is no groupId and no onNeedConn.
+ useEffect(() => {
+ if (onNeedConn && repEntry.groupId) {
+ Promise.resolve(onNeedConn(repEntry.groupId)).catch(() => {});
+ }
+ }, [onNeedConn, repEntry.groupId]);
+
+ const meta = useMediaMeta(
+ tRef, repEntry.id, true, repEntry.display_title || '', repEntry._connGen);
const confident = Boolean(meta && meta.confidence && meta.tmdb_id);
const metaReady = meta !== null;
@@ -339,6 +365,7 @@ function PosterCard({ title, subtitle, repEntry, transportRef, gekRef, onOpen, g
${metaReady && html`
<${MediaThumb} thumbHash=${posterHash} alt=${title}
cls="video-poster" transportRef=${tRef} gekRef=${gRef}
+ reloadKey=${repEntry._connGen}
onReady=${handleImageReady} />
`}
</div>
@@ -588,7 +615,8 @@ function VideoDetailModal({
<button class="video-episode-row" key=${ep.id} onClick=${() => onPlay(ep)}>
<${LazyTile} cls="video-episode-thumb-slot">
<${MediaThumb} thumbHash=${ep.thumb_hash} alt=${ep.display_title || ep.name}
- cls="video-episode-thumb" transportRef=${ep._tRef || transportRef} gekRef=${ep._gRef || gekRef} />
+ cls="video-episode-thumb" transportRef=${ep._tRef || transportRef} gekRef=${ep._gRef || gekRef}
+ reloadKey=${ep._connGen} />
</${LazyTile}>
<span class="video-episode-label">
S${ep.season}E${String(ep.episode).padStart(2, '0')}
@@ -615,7 +643,9 @@ function VideoDetailModal({
`;
}
-function PosterGrid({ movies, shows, transportRef, gekRef, onPreview, tmdbEnabled, isNodeAdmin }) {
+function PosterGrid({
+ movies, shows, transportRef, gekRef, onPreview, tmdbEnabled, isNodeAdmin, onNeedConn,
+}) {
const [detail, setDetail] = useState(null); // { title, repEntry, show? }
// raw (per-folder-parsed-title) show title -> its own resolved media_meta_resp.
const [metaByGroup, setMetaByGroup] = useState({});
@@ -673,7 +703,8 @@ function PosterGrid({ movies, shows, transportRef, gekRef, onPreview, tmdbEnable
const detailGRef = detail && detail.repEntry._gRef ? detail.repEntry._gRef : gekRef;
const detailMeta = useMediaMeta(
detailTRef, detail ? detail.repEntry.id : null, !!detail,
- detail ? (detail.repEntry.display_title || '') : '');
+ detail ? (detail.repEntry.display_title || '') : '',
+ detail ? detail.repEntry._connGen : 0);
return html`
<div class="video-grid">
@@ -683,6 +714,7 @@ function PosterGrid({ movies, shows, transportRef, gekRef, onPreview, tmdbEnable
subtitle=${formatDuration(e.duration)} repEntry=${e}
groupKey=${`movie:${e.id}`}
transportRef=${transportRef} gekRef=${gekRef}
+ onNeedConn=${onNeedConn}
onOpen=${() => (tmdbEnabled
// With TMDB off there is nothing the detail modal would show
// for a movie (no overview, no season list to pick from,
@@ -716,6 +748,7 @@ function PosterGrid({ movies, shows, transportRef, gekRef, onPreview, tmdbEnable
repEntry=${repEntry}
groupKey=${s.title}
onMetaResolved=${handleMetaResolved}
+ onNeedConn=${onNeedConn}
transportRef=${transportRef} gekRef=${gekRef}
onOpen=${() => openDetail(s.title, repEntry, s)} />
</${LazyTile}>
@@ -740,9 +773,16 @@ function PosterGrid({ movies, shows, transportRef, gekRef, onPreview, tmdbEnable
// is actually inside a season group (`seasonContext` set); a real,
// distinct per-episode title (a show that *does* carry one) still wins
// over the generic "Episode N" label.
-function FlatMovieRow({ entry, transportRef, gekRef, onPreview, seasonContext }) {
+function FlatMovieRow({ entry, transportRef, gekRef, onPreview, seasonContext, onNeedConn }) {
const tRef = entry._tRef || transportRef;
const gRef = entry._gRef || gekRef;
+
+ useEffect(() => {
+ if (onNeedConn && entry.groupId) {
+ Promise.resolve(onNeedConn(entry.groupId)).catch(() => {});
+ }
+ }, [onNeedConn, entry.groupId]);
+
const isEpisode = seasonContext && entry.season != null && entry.episode != null;
const hasOwnTitle = entry.display_title && entry.display_title !== seasonContext;
const label = isEpisode
@@ -754,7 +794,8 @@ function FlatMovieRow({ entry, transportRef, gekRef, onPreview, seasonContext })
<div class="video-flat-row" onClick=${() => onPreview(entry)}>
<${LazyTile} cls="video-flat-thumb-slot">
<${MediaThumb} thumbHash=${entry.thumb_hash} alt=${entry.display_title || entry.name}
- cls="video-flat-thumb" transportRef=${tRef} gekRef=${gRef} />
+ cls="video-flat-thumb" transportRef=${tRef} gekRef=${gRef}
+ reloadKey=${entry._connGen} />
</${LazyTile}>
<div class="video-flat-info">
<div class="video-flat-title">${label}</div>
@@ -771,7 +812,7 @@ function FlatMovieRow({ entry, transportRef, gekRef, onPreview, seasonContext })
`;
}
-function FlatShowFolder({ show, transportRef, gekRef, onPreview }) {
+function FlatShowFolder({ show, transportRef, gekRef, onPreview, onNeedConn }) {
const [open, setOpen] = useState(false);
return html`
<div class="video-flat-folder">
@@ -790,7 +831,8 @@ function FlatShowFolder({ show, transportRef, gekRef, onPreview }) {
</div>
${s.episodes.map((ep) => html`
<${FlatMovieRow} key=${ep.id} entry=${ep} seasonContext=${show.title}
- transportRef=${transportRef} gekRef=${gekRef} onPreview=${onPreview} />
+ transportRef=${transportRef} gekRef=${gekRef} onPreview=${onPreview}
+ onNeedConn=${onNeedConn} />
`)}
</div>
`)}
@@ -798,7 +840,7 @@ function FlatShowFolder({ show, transportRef, gekRef, onPreview }) {
`;
}
-function FlatList({ movies, shows, transportRef, gekRef, onPreview }) {
+function FlatList({ movies, shows, transportRef, gekRef, onPreview, onNeedConn }) {
const items = [
...movies.map((e) => ({ key: e.display_title || e.name, kind: 'movie', entry: e })),
...shows.map((s) => ({ key: s.title, kind: 'show', show: s })),
@@ -808,9 +850,11 @@ function FlatList({ movies, shows, transportRef, gekRef, onPreview }) {
<div class="video-flat-list">
${items.map((it) => it.kind === 'movie'
? html`<${FlatMovieRow} key=${it.entry.id} entry=${it.entry}
- transportRef=${transportRef} gekRef=${gekRef} onPreview=${onPreview} />`
+ transportRef=${transportRef} gekRef=${gekRef} onPreview=${onPreview}
+ onNeedConn=${onNeedConn} />`
: html`<${FlatShowFolder} key=${it.show.title} show=${it.show}
- transportRef=${transportRef} gekRef=${gekRef} onPreview=${onPreview} />`)}
+ transportRef=${transportRef} gekRef=${gekRef} onPreview=${onPreview}
+ onNeedConn=${onNeedConn} />`)}
</div>
`;
}
@@ -819,7 +863,7 @@ function FlatList({ movies, shows, transportRef, gekRef, onPreview }) {
function VideoApp({
groupId, transportRef, gekRef, status, entries, onPreview, videoRoot, tmdbConfig, isNodeAdmin,
- hideFilter,
+ hideFilter, onNeedConn,
}) {
const [mode, setMode] = useState(loadViewMode);
const [filter, setFilter] = useState('');
@@ -889,9 +933,10 @@ function VideoApp({
${mode === 'poster'
? html`<${PosterGrid} movies=${filteredMovies} shows=${filteredShows}
transportRef=${transportRef} gekRef=${gekRef} onPreview=${onPreview}
- tmdbEnabled=${tmdbEnabled} isNodeAdmin=${isNodeAdmin} />`
+ tmdbEnabled=${tmdbEnabled} isNodeAdmin=${isNodeAdmin} onNeedConn=${onNeedConn} />`
: html`<${FlatList} movies=${filteredMovies} shows=${filteredShows}
- transportRef=${transportRef} gekRef=${gekRef} onPreview=${onPreview} />`}
+ transportRef=${transportRef} gekRef=${gekRef} onPreview=${onPreview}
+ onNeedConn=${onNeedConn} />`}
`}
`;
}