diff options
| -rw-r--r-- | docs/mediacenter.md | 45 | ||||
| -rw-r--r-- | packages/meshbay-hub/src/meshbay_hub/static/video-app.js | 37 |
2 files changed, 75 insertions, 7 deletions
diff --git a/docs/mediacenter.md b/docs/mediacenter.md index d714366..2b25011 100644 --- a/docs/mediacenter.md +++ b/docs/mediacenter.md @@ -947,6 +947,51 @@ Full design, the adversary this names, and what must not change: `docs/refactoring-search.md`. `test_search_source_merge.py` holds the rules, `test_search_media_merge.py` holds this symptom end to end. +### 10.7 Every card badged "unmatched" when TMDB is off (2026-09-08) + +§10.1 gave an unmatched poster a badge: a dashed warning outline on the card +plus a `?` next to the title, with `video.no_match` ("no confident TMDB match +— showing the filename") as its tooltip, and the same sentence again at the +top of the detail modal. That is a useful signal — it is how an operator finds +the titles worth a "Fix match". + +It says the wrong thing when the group's operator has turned TMDB **off** +(§5.5's per-group switch). No lookup was made, so nothing failed to match; +`media_meta_req` answers `confidence: 0` for the whole library by design +(`webrtc_server._do_media_meta_request`, the same silent degradation as "no +client configured"). The poster grid therefore drew *every* card as a failure +of the very thing the operator chose, and no poster was ever going to arrive +to clear it. + +Poster mode now presents the file's own thumbnail plainly in that case — +what Mode B (§4.2) does — and keeps the badge for what it was built for, a +lookup that ran and came back with nothing: + +- `PosterCard` takes `tmdbEnabled` and derives `flagUnmatched = tmdbEnabled && + !confident`, which gates both `video-card-unmatched` and the `?` span. The + title still falls back to the parsed filename exactly as before. +- `VideoDetailModal` takes it too and gates its `video-detail-nomatch` + paragraph. A **show** still opens that modal with TMDB off — it is where the + season list lives, unlike a movie, which §V12's `onOpen` already sends + straight to the player — so suppressing it on the card alone would have left + the same claim one click deeper. +- Both default the prop to `true`, so a call site that forgets it keeps the + badge rather than silently losing it. +- The modal's operator-only **"Fix match" / "Rematch"** buttons are gated on + `isNodeAdmin && tmdbEnabled` for the same reason one step further on: both + act on a TMDB match that, with the switch off, does not exist. "Fix match" + opened a search the node answers with an empty result list (§5.7's silent + degradation) and "Rematch" dropped a cached match that was never made — two + dead buttons offered to the one person who already knows why. Nothing else + reaches `TmdbSearchOverlay` or `doRematch`; those buttons are their only + entry points, so gating them here is the whole change. + +Deliberately unchanged: `search-page.js` passes `tmdbConfig={{ enabled: true }}` +because a cross-group view has no single group's switch to read, so a merged +card there still badges — and, being `readOnly`/`isNodeAdmin={false}`, it never +showed the operator buttons in the first place. + + ## 11. Acceptance before shipping 1. Re-run the §3 validation (real TMDB calls, same corpus, same script 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 57da9ff..f590d20 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/video-app.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/video-app.js @@ -321,7 +321,7 @@ function useSeasonMeta(transportRef, tmdbId, season, active) { function PosterCard({ title, subtitle, repEntry, transportRef, gekRef, onOpen, groupKey, onMetaResolved, onNeedConn, - sourceEntries, + sourceEntries, tmdbEnabled = true, }) { const tRef = repEntry._tRef || transportRef; const gRef = repEntry._gRef || gekRef; @@ -341,6 +341,16 @@ function PosterCard({ const confident = Boolean(meta && meta.confidence && meta.tmdb_id); const metaReady = meta !== null; + // "Unmatched" is a statement about a lookup that ran and came back with + // nothing — it tells an operator which titles are worth a "Fix match". + // With TMDB switched off for this group no lookup was ever made, so every + // card would carry the dashed outline and the "?" at once, flagging as a + // failure exactly the thing the operator chose. The poster grid then just + // shows what it does have — the file's own frame, presented plainly, the + // same as Flat does. Defaults to `true` so a caller that forgets the prop + // keeps the badge rather than silently losing it. + const flagUnmatched = tmdbEnabled && !confident; + // Reports this tile's own resolution upward so PosterGrid can notice two // differently-parsed folders (a show split across release groups that // named its seasons inconsistently, §3.4/V6) resolving to the same TMDB @@ -382,7 +392,7 @@ function PosterCard({ const ready = metaReady && imageReady; return html` - <div class="video-card ${ready && !confident ? 'video-card-unmatched' : ''}" onClick=${onOpen}> + <div class="video-card ${ready && flagUnmatched ? 'video-card-unmatched' : ''}" onClick=${onOpen}> ${!ready && html` <div class="video-poster video-poster-loading"><span class="spinner"></span></div> `} @@ -398,7 +408,7 @@ function PosterCard({ <div class="video-card-info"> <div class="video-card-title"> ${(confident && meta.title) || title} - ${!confident && html`<span class="video-card-flag" title=${t('video.no_match')}>?</span>`} + ${flagUnmatched && html`<span class="video-card-flag" title=${t('video.no_match')}>?</span>`} </div> <div class="video-card-sub"> ${confident && meta.release_date ? yearOf(meta.release_date) : ''} @@ -690,6 +700,7 @@ function TmdbSearchOverlay({ function VideoDetailModal({ title, meta, repEntry, show, transportRef, gekRef, onClose, onPlay, isNodeAdmin, + tmdbEnabled = true, }) { const confident = Boolean(meta && meta.confidence && meta.tmdb_id); const [searching, setSearching] = useState(false); @@ -743,7 +754,12 @@ function VideoDetailModal({ <${Icon} name="close" /></button> </div> <div class="video-detail-body"> - ${meta !== null && !confident && html` + ${/* Same reason as PosterCard's flagUnmatched: with TMDB off for + this group nothing was looked up, so "no confident match" is + not a fact about this title. A show still opens this modal + with TMDB off — it is where its season list lives — so the + notice has to be suppressed here too, not only on the card. */''} + ${tmdbEnabled && meta !== null && !confident && html` <p class="video-detail-nomatch">${t('video.no_match')}</p> `} <p class="video-detail-source"> @@ -760,7 +776,13 @@ function VideoDetailModal({ </p> `} `} - ${isNodeAdmin && html` + ${/* Both of these act on a TMDB match, and with TMDB off for this + group there is none to act on: "Fix match" opens a search the + node answers with an empty result list (§5.7's silent + degradation), and "Rematch" drops a cached match that was + never made. Two buttons that cannot do anything, offered to + the one person who already knows why. */''} + ${isNodeAdmin && tmdbEnabled && html` <div class="video-admin-actions"> <button class="admin-btn video-fix-match" onClick=${() => setSearching(true)}> ${t('video.fix_match')} @@ -895,7 +917,7 @@ function PosterGrid({ groupKey=${`movie:${e.id}`} sourceEntries=${e} transportRef=${transportRef} gekRef=${gekRef} - onNeedConn=${onNeedConn} + onNeedConn=${onNeedConn} tmdbEnabled=${tmdbEnabled} onOpen=${() => (tmdbEnabled // With TMDB off there is nothing the detail modal would show // for a movie (no overview, no season list to pick from, @@ -930,7 +952,7 @@ function PosterGrid({ sourceEntries=${s.episodes} groupKey=${s.title} onMetaResolved=${handleMetaResolved} - onNeedConn=${onNeedConn} + onNeedConn=${onNeedConn} tmdbEnabled=${tmdbEnabled} transportRef=${transportRef} gekRef=${gekRef} onOpen=${() => openDetail(s.title, repEntry, s)} /> </${LazyTile}> @@ -940,6 +962,7 @@ function PosterGrid({ <${VideoDetailModal} title=${detail.title} meta=${detailMeta} repEntry=${detail.repEntry} show=${detail.show} transportRef=${detailTRef} gekRef=${detailGRef} isNodeAdmin=${isNodeAdmin} + tmdbEnabled=${tmdbEnabled} onClose=${() => setDetail(null)} onPlay=${(entry) => { setDetail(null); onPreview(entry); }} /> `} |