summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--docs/mediacenter.md45
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/video-app.js37
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); }} />
`}