summaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src/meshbay_hub/static
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/static')
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/group-page.js13
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/group-settings.js71
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/transport.js62
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/video-app.js4
4 files changed, 108 insertions, 42 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/group-page.js b/packages/meshbay-hub/src/meshbay_hub/static/group-page.js
index 55f8e36..aa6b29c 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/group-page.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/group-page.js
@@ -207,7 +207,9 @@ function GroupPage({ groupId, group, token, username, userId, userPrefs,
setEnabledApps(ack.enabled_apps || null);
setScanSettings(ack.scan_settings || null);
setTmdbConfig({
+ // Per-group (2026-08-24, used to be node-wide).
enabled: ack.tmdb_enabled !== false,
+ // Node-wide — one shared credential/cache.
tokenCustomized: !!ack.tmdb_token_customized,
language: ack.tmdb_language || '',
});
@@ -217,7 +219,13 @@ function GroupPage({ groupId, group, token, username, userId, userPrefs,
// and a button that is still there is a button people press.
transport.onUploadPolicy = (allowed) => setMemberUpload(allowed);
transport.onAppsEnabled = (apps) => setEnabledApps(apps);
- transport.onTmdbConfig = (cfg) => setTmdbConfig(cfg);
+ // Two independent acks now (tmdb_config_ack: token/language,
+ // node-wide; tmdb_enabled_ack: the per-group switch) — each merges
+ // its own slice into the one tmdbConfig object rather than
+ // replacing it, so one changing does not clobber the other's most
+ // recent value.
+ transport.onTmdbConfig = (cfg) => setTmdbConfig((prev) => ({ ...(prev || {}), ...cfg }));
+ transport.onTmdbEnabled = (enabled) => setTmdbConfig((prev) => ({ ...(prev || {}), enabled }));
transport.onVideoRoot = (path) => setVideoRoot(path);
// The node's own scan (a root added while we were already connected,
// or reconcile catching one back up) — never the entries, just
@@ -524,7 +532,8 @@ function GroupPage({ groupId, group, token, username, userId, userPrefs,
scanSettings=${scanSettings}
onScanSettings=${(s) => setScanSettings(s)}
tmdbConfig=${tmdbConfig}
- onTmdbConfig=${(cfg) => setTmdbConfig(cfg)}
+ onTmdbConfig=${(cfg) => setTmdbConfig((prev) => ({ ...(prev || {}), ...cfg }))}
+ onTmdbEnabled=${(enabled) => setTmdbConfig((prev) => ({ ...(prev || {}), enabled }))}
entries=${entries} nodeDirs=${nodeDirs}
videoRoot=${videoRoot}
onVideoRoot=${(path) => setVideoRoot(path)}
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/group-settings.js b/packages/meshbay-hub/src/meshbay_hub/static/group-settings.js
index 22532fa..88ab68e 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/group-settings.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/group-settings.js
@@ -33,7 +33,7 @@ function GroupSettingsPanel({ groupId, group, token, transportRef, gekRef,
memberUpload, onMemberUpload,
enabledApps, onEnabledApps,
scanSettings, onScanSettings,
- tmdbConfig, onTmdbConfig,
+ tmdbConfig, onTmdbConfig, onTmdbEnabled,
entries, nodeDirs, videoRoot, onVideoRoot,
onPaired, onLeft }) {
const [members, setMembers] = useState([]);
@@ -259,6 +259,7 @@ function GroupSettingsPanel({ groupId, group, token, transportRef, gekRef,
const [tmdbBusy, setTmdbBusy] = useState(false);
const [tmdbMsg, setTmdbMsg] = useState('');
const [tmdbTokenDraft, setTmdbTokenDraft] = useState('');
+ const [tmdbEnabledBusy, setTmdbEnabledBusy] = useState(false);
const tmdbEnabled = tmdbConfig ? tmdbConfig.enabled : true;
// Pre-filled from the operator's own current UI language the first time
// this renders with nothing configured yet — a sensible default, not a
@@ -271,15 +272,44 @@ function GroupSettingsPanel({ groupId, group, token, transportRef, gekRef,
}, [tmdbConfig && tmdbConfig.language]);
/**
- * TMDB on/off, an optional custom API token, and the language TMDB is
- * queried in — node-wide, not per-group (docs/mediacenter.md §5.5). Same
- * shape as saveScanSettings: signed, and the toggle does not claim
- * success until the node confirms it. The token field is cleared after a
- * save either way: it is never echoed back by the node (tmdb_config_ack
- * carries only whether one is set, never the value), so there is
- * nothing to keep showing.
+ * Whether TMDB is used at all — per-group (2026-08-24, used to be bundled
+ * into the same signed op as the token/language below): a real
+ * media-library group and a test/demo group on the same node need not
+ * share this decision. Saves immediately on toggle, same as an ordinary
+ * checkbox-style setting elsewhere — there is nothing else on the form to
+ * batch it with any more.
*/
- const saveTmdbConfig = useCallback(async (nextEnabled) => {
+ const saveTmdbEnabled = useCallback(async (nextEnabled) => {
+ const transport = transportRef && transportRef.current;
+ setTmdbMsg('');
+ setTmdbEnabledBusy(true);
+ try {
+ if (!transport || !transport.connected) {
+ throw new Error('Not connected to the node');
+ }
+ const sk = transport.sessionKeys && transport.sessionKeys.skEdB64;
+ const signFn = (sk && window.MeshBayKeys)
+ ? (transcript) => window.MeshBayKeys.signBytes(sk, transcript)
+ : null;
+ await transport.setTmdbEnabled(nextEnabled, signFn);
+ if (onTmdbEnabled) onTmdbEnabled(nextEnabled);
+ } catch (err) {
+ setTmdbMsg(err.message);
+ } finally {
+ setTmdbEnabledBusy(false);
+ }
+ }, [transportRef, onTmdbEnabled]);
+
+ /**
+ * An optional custom API token, and the language TMDB is queried in —
+ * node-wide, not per-group (docs/mediacenter.md §5.5): one shared
+ * credential and cache. Same shape as saveScanSettings: signed, and the
+ * button does not claim success until the node confirms it. The token
+ * field is cleared after a save either way: it is never echoed back by
+ * the node (tmdb_config_ack carries only whether one is set, never the
+ * value), so there is nothing to keep showing.
+ */
+ const saveTmdbConfig = useCallback(async () => {
const transport = transportRef && transportRef.current;
setTmdbMsg('');
setTmdbBusy(true);
@@ -292,11 +322,10 @@ function GroupSettingsPanel({ groupId, group, token, transportRef, gekRef,
? (transcript) => window.MeshBayKeys.signBytes(sk, transcript)
: null;
const token = tmdbTokenDraft.trim();
- await transport.setTmdbConfig(nextEnabled, token || undefined, tmdbLanguage, signFn);
+ await transport.setTmdbConfig(token || undefined, tmdbLanguage, signFn);
setTmdbTokenDraft('');
if (onTmdbConfig) {
onTmdbConfig({
- enabled: nextEnabled,
tokenCustomized: token
? true
: (tmdbConfig ? tmdbConfig.tokenCustomized : false),
@@ -326,8 +355,8 @@ function GroupSettingsPanel({ groupId, group, token, transportRef, gekRef,
if (!isNodeAdmin || !connected || !tmdbConfig || tmdbConfig.language) return;
if (autoLanguageSetRef.current) return;
autoLanguageSetRef.current = true;
- saveTmdbConfig(tmdbEnabled);
- }, [isNodeAdmin, connected, tmdbConfig, tmdbEnabled, saveTmdbConfig]);
+ saveTmdbConfig();
+ }, [isNodeAdmin, connected, tmdbConfig, saveTmdbConfig]);
// Every folder anywhere in the group's shared index, deepest included —
// `entries[].path` is each file's containing directory (files-app.js's own
@@ -607,18 +636,20 @@ function GroupSettingsPanel({ groupId, group, token, transportRef, gekRef,
</div>
`}
- ${/* TMDB on/off + custom token, node-wide (docs/mediacenter.md §5.5) —
- new outbound third-party traffic the node did not have before
- the Videos app, so it is a signed operator setting like the
- rest, not a display preference. */
+ ${/* The on/off switch is per-group (2026-08-24); the custom token and
+ query language stay node-wide, one shared credential/cache
+ (docs/mediacenter.md §5.5). Both are new outbound third-party
+ traffic the node did not have before the Videos app, so both are
+ signed operator settings, not display preferences — but two
+ independent ones now, saved separately. */
isNodeAdmin && connected && html`
<div class="settings-section">
<h3 class="settings-heading">${t('settings_node.tmdb_title')}</h3>
<p class="settings-hint">${t('settings_node.tmdb_hint')}</p>
<div class="settings-row">
<label class="settings-label">
- <input type="checkbox" checked=${tmdbEnabled} disabled=${tmdbBusy}
- onChange=${(e) => saveTmdbConfig(e.target.checked)} />
+ <input type="checkbox" checked=${tmdbEnabled} disabled=${tmdbEnabledBusy}
+ onChange=${(e) => saveTmdbEnabled(e.target.checked)} />
${' '}${tmdbEnabled ? t('settings_node.tmdb_enabled') : t('settings_node.tmdb_disabled')}
</label>
</div>
@@ -650,7 +681,7 @@ function GroupSettingsPanel({ groupId, group, token, transportRef, gekRef,
<p class="settings-hint">${t('settings_node.tmdb_language_hint')}</p>
</div>
<button class="btn btn-small btn-secondary" style="margin-top:8px"
- disabled=${tmdbBusy} onClick=${() => saveTmdbConfig(tmdbEnabled)}>
+ disabled=${tmdbBusy} onClick=${() => saveTmdbConfig()}>
${tmdbBusy ? t('settings_node.scan_saving') : t('settings_node.tmdb_save')}
</button>
${tmdbMsg && html`<p class="settings-hint">${tmdbMsg}</p>`}
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/transport.js b/packages/meshbay-hub/src/meshbay_hub/static/transport.js
index b4bfe87..763e279 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/transport.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/transport.js
@@ -107,6 +107,7 @@ class MeshBayTransport {
set onUploadPolicy(fn) { this._onUploadPolicy = fn; }
set onAppsEnabled(fn) { this._onAppsEnabled = fn; }
set onTmdbConfig(fn) { this._onTmdbConfig = fn; }
+ set onTmdbEnabled(fn) { this._onTmdbEnabled = fn; }
set onVideoRoot(fn) { this._onVideoRoot = fn; }
set onIndexProgress(fn) { this._onIndexProgress = fn; }
@@ -536,37 +537,57 @@ class MeshBayTransport {
}
/**
- * Turn TMDB lookups on/off node-wide, optionally set/clear a custom API
- * token, and optionally set the language TMDB is queried in (e.g.
- * "fr-FR") — one for the whole node, same reasoning as the token: one
- * shared cache, not a per-viewer request. Signed like setAppsEnabled/
- * setMemberUpload — an unsigned toggle would let any member turn on
- * outbound third-party network traffic the operator never agreed to
- * (docs/mediacenter.md §5.5, §8). `token: ''` explicitly clears a
- * previously-set custom token; omit it (undefined/null), like
+ * Set/clear a custom TMDB API token, and/or set the language TMDB is
+ * queried in (e.g. "fr-FR") — one for the whole node, since both are one
+ * operator's shared credential/cache, not a per-group concern (see
+ * setTmdbEnabled below for the per-group on/off switch). Signed like
+ * setAppsEnabled/setMemberUpload — an unsigned change would let any
+ * member alter outbound third-party network traffic the operator never
+ * agreed to (docs/mediacenter.md §5.5, §8). `token: ''` explicitly clears
+ * a previously-set custom token; omit it (undefined/null), like
* `language`, to leave whatever is stored unchanged.
*/
- async setTmdbConfig(enabled, token, language, signFn) {
+ async setTmdbConfig(token, language, signFn) {
const msg = await this._sendAndWait({
- type: 'tmdb_config', v: '0.5', enabled: Boolean(enabled),
+ type: 'tmdb_config', v: '0.7',
token: token === undefined ? null : token,
language: language === undefined ? null : language,
});
if (msg.type === 'error') throw new Error(msg.detail);
if (msg.type === 'admin_challenge') {
// Must match the node's subject byte-for-byte (webrtc_server.py
- // _do_tmdb_config): Python's f"{bool}" is "True"/"False", not JS's
- // lowercase — and the token itself is never part of the subject
+ // _do_tmdb_config) — the token itself is never part of the subject
// (it would end up in the audit log in plaintext), only whether one
// was supplied. The language is not a secret, so it appears as-is.
- const subject = `enabled=${enabled ? 'True' : 'False'},` +
- `custom_token=${token ? 'yes' : 'no'},language=${language || 'default'}`;
+ const subject = `custom_token=${token ? 'yes' : 'no'},language=${language || 'default'}`;
return this._authorizeAdminOp(msg, 'tmdb_config', subject, signFn);
}
return msg;
}
/**
+ * Whether TMDB lookups run for this group at all — per-group (2026-08-24,
+ * used to be node-wide): a real media-library group and a test/demo group
+ * on the same node need not share the decision to spend TMDB quota and
+ * make outbound requests. Signed like setVideoRoot — it decides whether
+ * this group's members' Videos tab ever makes outbound TMDB traffic.
+ */
+ async setTmdbEnabled(enabled, signFn) {
+ const msg = await this._sendAndWait({
+ type: 'tmdb_enabled', v: '0.7', enabled: Boolean(enabled),
+ });
+ if (msg.type === 'error') throw new Error(msg.detail);
+ if (msg.type === 'admin_challenge') {
+ // Must match the node's subject byte-for-byte (webrtc_server.py
+ // _do_tmdb_enabled): Python's f"{bool}" is "True"/"False", not JS's
+ // lowercase.
+ const subject = enabled ? 'True' : 'False';
+ return this._authorizeAdminOp(msg, 'tmdb_enabled', subject, signFn);
+ }
+ return msg;
+ }
+
+ /**
* Which folder (possibly a subfolder of a shared root) the Videos app
* treats as its entry point for this group. `path: ''` means the whole
* group index. Signed like setAppsEnabled — it decides what every
@@ -1404,17 +1425,22 @@ class MeshBayTransport {
this._onAppsEnabled(msg.apps || []);
}
- // Node-wide (not per-group) — the operator changed whether TMDB is
- // called at all, or supplied/cleared a custom token. `token_customized`
- // only says whether one is set, never the token itself.
+ // Node-wide (not per-group) — the operator supplied/cleared a custom
+ // token, or changed the query language. `token_customized` only says
+ // whether one is set, never the token itself.
if (msg.type === 'tmdb_config_ack' && this._onTmdbConfig) {
this._onTmdbConfig({
- enabled: Boolean(msg.enabled),
tokenCustomized: Boolean(msg.token_customized),
language: msg.language || '',
});
}
+ // Per-group (2026-08-24, used to be folded into tmdb_config_ack above) —
+ // the operator turned TMDB on/off for this group specifically.
+ if (msg.type === 'tmdb_enabled_ack' && this._onTmdbEnabled) {
+ this._onTmdbEnabled(Boolean(msg.enabled));
+ }
+
// Same shape: an operator corrected a wrong automatic TMDB match, and
// everyone connected needs to know their poster grid/detail modal for
// this show is now stale — falls through so the operator's own
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 00643f8..ece95ea 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/video-app.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/video-app.js
@@ -267,7 +267,7 @@ function useSeasonMeta(transportRef, tmdbId, season, active) {
function PosterCard({ title, subtitle, repEntry, transportRef, gekRef, onOpen, groupKey, onMetaResolved }) {
const meta = useMediaMeta(transportRef, repEntry.path, true);
- const confident = meta && meta.confidence && meta.tmdb_id;
+ const confident = Boolean(meta && meta.confidence && meta.tmdb_id);
const metaReady = meta !== null;
// Reports this tile's own resolution upward so PosterGrid can notice two
@@ -458,7 +458,7 @@ function TmdbSearchOverlay({
function VideoDetailModal({
title, meta, repEntry, show, transportRef, gekRef, onClose, onPlay, isNodeAdmin,
}) {
- const confident = meta && meta.confidence && meta.tmdb_id;
+ const confident = Boolean(meta && meta.confidence && meta.tmdb_id);
const [searching, setSearching] = useState(false);
const mediaType = show ? 'tv' : 'movie';