diff options
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/static')
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'; |