From 6af05abf410bbd038ce7fa6915a659defc509071 Mon Sep 17 00:00:00 2001 From: Christophe Besson Date: Mon, 24 Aug 2026 10:04:46 +0200 Subject: feat(node,hub): add Videos group app (poster grid, flat list, TMDB metadata) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements docs/mediacenter.md: a "Videos" group application built on the existing files index rather than a separate catalogue. On the node side, new indexer enrichment (technical probe, filename/season parsing, thumbnail generation) runs per-file once an operator has chosen a video_root for the group, plus a TMDB client for on-demand poster/metadata lookups (never client-side, thumbnails delivered over the existing chunk path). On the hub side, a new video-app.js renders a lazily-mounted poster grid or a thumbnail-only flat list, with TMDB entirely optional per group. Along the way: the global apps registry now drives Settings' default-tab picker instead of a hardcoded list, and the video_root is configured from group Settings (like uploads) rather than from Files, with the node refusing to run any TMDB/thumbnail work until one is set. Fixes several bugs found via live testing against a real library, notably a race between two effects writing the same "image ready" state that could leave a poster grid spinning forever on a same-tab revisit — see mediacenter.md §5.4 for the full account of each one. --- .../src/meshbay_hub/static/group-settings.js | 211 ++++++++++++++++++++- 1 file changed, 209 insertions(+), 2 deletions(-) (limited to 'packages/meshbay-hub/src/meshbay_hub/static/group-settings.js') 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 0b64a03..4ae832f 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/group-settings.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/group-settings.js @@ -1,12 +1,22 @@ import { - html, useState, useEffect, useCallback, + html, useState, useEffect, useCallback, useMemo, } from './vendor/htm-preact.js'; -import { t } from './i18n.js'; +import { t, getLocale, LOCALES } from './i18n.js'; import { Icon } from './icon.js'; import { hubFetch, navigate } from './hub-client.js'; import { APPS } from './apps.js'; import * as platform from './platform.js'; +// MeshBay's own locale codes (i18n.js LOCALES) to the language tag TMDB +// expects — the two don't share a format (MeshBay's "en" vs TMDB's +// required region, "en-US"). Used only to pre-fill the TMDB language field +// with the operator's own current UI language, a reasonable default they +// can still change; the node never guesses this on its own. +const TMDB_LANGUAGE_BY_LOCALE = { + en: 'en-US', fr: 'fr-FR', es: 'es-ES', 'pt-BR': 'pt-BR', 'zh-CN': 'zh-CN', + ja: 'ja-JP', de: 'de-DE', it: 'it-IT', nl: 'nl-NL', pl: 'pl-PL', +}; + // ── Members Panel ──────────────────────────────────────────────────────── /** @@ -23,6 +33,8 @@ function GroupSettingsPanel({ groupId, group, token, transportRef, gekRef, memberUpload, onMemberUpload, enabledApps, onEnabledApps, scanSettings, onScanSettings, + tmdbConfig, onTmdbConfig, + entries, nodeDirs, videoRoot, onVideoRoot, onPaired, onLeft }) { const [members, setMembers] = useState([]); const [adminId, setAdminId] = useState(''); @@ -244,6 +256,120 @@ function GroupSettingsPanel({ groupId, group, token, transportRef, gekRef, } }, [transportRef, onScanSettings, reconcileMinutes, debounceSeconds]); + const [tmdbBusy, setTmdbBusy] = useState(false); + const [tmdbMsg, setTmdbMsg] = useState(''); + const [tmdbTokenDraft, setTmdbTokenDraft] = useState(''); + 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 + // claim about what the node is actually using until they hit Save. + const [tmdbLanguage, setTmdbLanguage] = useState( + () => (tmdbConfig && tmdbConfig.language) + || TMDB_LANGUAGE_BY_LOCALE[getLocale()] || 'en-US'); + useEffect(() => { + if (tmdbConfig && tmdbConfig.language) setTmdbLanguage(tmdbConfig.language); + }, [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. + */ + const saveTmdbConfig = useCallback(async (nextEnabled) => { + const transport = transportRef && transportRef.current; + setTmdbMsg(''); + setTmdbBusy(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; + const token = tmdbTokenDraft.trim(); + await transport.setTmdbConfig(nextEnabled, token || undefined, tmdbLanguage, signFn); + setTmdbTokenDraft(''); + if (onTmdbConfig) { + onTmdbConfig({ + enabled: nextEnabled, + tokenCustomized: token + ? true + : (tmdbConfig ? tmdbConfig.tokenCustomized : false), + language: tmdbLanguage, + }); + } + setTmdbMsg(t('settings_node.scan_saved')); + } catch (err) { + setTmdbMsg(err.message); + } finally { + setTmdbBusy(false); + } + }, [transportRef, onTmdbConfig, tmdbTokenDraft, tmdbConfig, tmdbLanguage]); + + // Every folder anywhere in the group's shared index, deepest included — + // `entries[].path` is each file's containing directory (files-app.js's own + // convention), so every ancestor prefix of it is a real folder, and + // `nodeDirs` covers ones with nothing in them yet. A flat, depth-indented + // saveTmdbConfig(e.target.checked)} /> + ${' '}${tmdbEnabled ? t('settings_node.tmdb_enabled') : t('settings_node.tmdb_disabled')} + + +
+ +

+ ${tmdbConfig && tmdbConfig.tokenCustomized + ? t('settings_node.tmdb_token_customized') + : t('settings_node.tmdb_token_default')} +

+
+
+ +

${t('settings_node.tmdb_language_hint')}

+
+ + ${tmdbMsg && html`

${tmdbMsg}

`} + + `} + + ${/* Which folder is the Videos app's entry point for this group — + per-group like uploads, not node-wide like TMDB (mediacenter.md + §5.6). Until one is chosen, the Videos tab says so instead of + listing anything, and the node runs no TMDB/thumbnail work for + this group at all (daemon.py's _enrich_new_video_entries). */ + isNodeAdmin && connected && html` +
+

${t('settings_node.video_root_title')}

+

${t('settings_node.video_root_hint')}

+
+ +
+ + ${videoRootMsg && html`

${videoRootMsg}

`} +
+ `} + ${/* Roots management (Electron-only, when node is local) */ nodeDetected && nodeRoots.length > 0 && html`
-- cgit v1.2.3