diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-08-24 10:04:46 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-08-24 10:04:46 +0200 |
| commit | 6af05abf410bbd038ce7fa6915a659defc509071 (patch) | |
| tree | 09b1c941fa446b077ff51282fa18250998528263 /packages/meshbay-node/src/meshbay_node/ops.py | |
| parent | c4981454078a59f776d484f0f1828f2fc5eaad09 (diff) | |
| download | meshbay-6af05abf410bbd038ce7fa6915a659defc509071.tar.gz | |
feat(node,hub): add Videos group app (poster grid, flat list, TMDB metadata)
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.
Diffstat (limited to 'packages/meshbay-node/src/meshbay_node/ops.py')
| -rw-r--r-- | packages/meshbay-node/src/meshbay_node/ops.py | 62 |
1 files changed, 62 insertions, 0 deletions
diff --git a/packages/meshbay-node/src/meshbay_node/ops.py b/packages/meshbay-node/src/meshbay_node/ops.py index 154813d..6848e3a 100644 --- a/packages/meshbay-node/src/meshbay_node/ops.py +++ b/packages/meshbay-node/src/meshbay_node/ops.py @@ -731,6 +731,68 @@ async def set_enabled_apps(state: dict, group_id: str, apps: list[str]) -> dict: return {"apps": apps, "group_id": group_id} +# ── TMDB config (Videos app) ───────────────────────────────────────────────── + +async def set_tmdb_config(state: dict, enabled: bool, token: str | None = None, + language: str | None = None) -> dict: + """ + Whether the node calls TMDB at all, whether it uses a custom API token + instead of the shipped default, and in what language it queries TMDB + (docs/mediacenter.md §5.5). + + Node-wide (roster.py group_settings, group_id="") rather than per-group + like set_member_upload/set_enabled_apps: TMDB is one operator's budget, + one credential and one shared cache, not a per-group or per-viewer + concern. `token=""` explicitly clears a previously-set custom token + (reverts to the shipped default); `token=None` leaves whatever was + there unchanged. Same discipline for `language`. + """ + roster = _roster(state) + await roster.set_tmdb_config(enabled, token, language, set_by=state.get("node_user_id", "")) + state["tmdb_enabled"] = enabled + # `token=None` means "leave whatever was there" (§ set_tmdb_config's own + # docstring) — so the customized flag only changes when a value (a real + # token, or "" to clear one) was actually given. + if token is not None: + state["tmdb_token_customized"] = bool(token) + if language is not None: + state["tmdb_language"] = language + log.info("TMDB config: enabled=%s custom_token=%s language=%s", + enabled, bool(token), language or state.get("tmdb_language", "")) + return { + "enabled": enabled, + "token_customized": state.get("tmdb_token_customized", False), + "language": state.get("tmdb_language", ""), + } + + +async def set_video_root(state: dict, group_id: str, path: str) -> dict: + """ + Which folder (possibly a subfolder of a shared root) is the Videos app's + entry point for this group. Same shape as set_enabled_apps: lives on the + node (roster.db), takes effect without a restart, signed by the operator. + `path=""` clears it — the Videos tab then asks for one to be chosen + before anything (including TMDB enrichment, docs/mediacenter.md §5.2) + runs, rather than defaulting to the whole shared index. + + A non-empty path fires (never awaits) a sweep of whatever that folder + already contains: the ordinary per-change enrichment path only ever + looks at files new since the last broadcast, so anything already sitting + in a folder before it became the video_root would otherwise never be + picked up. + """ + roster = _roster(state) + ctx = _group_ctx(state, group_id) + await roster.set_video_root(group_id, path, set_by=state.get("node_user_id", "")) + ctx["video_root"] = path + log.info("Videos root for group %s: %r", group_id[:8], path) + if path: + enrich_fn = state.get("enrich_video_root_fn") + if enrich_fn: + asyncio.ensure_future(enrich_fn(group_id)) + return {"path": path, "group_id": group_id} + + # ── Scan settings ──────────────────────────────────────────────────────────── async def set_scan_settings(state: dict, group_id: str, reconcile_interval_secs: float, |