diff options
Diffstat (limited to 'packages/meshbay-node/src')
7 files changed, 108 insertions, 41 deletions
diff --git a/packages/meshbay-node/src/meshbay_node/cli/setup.py b/packages/meshbay-node/src/meshbay_node/cli/setup.py index b2a8e83..48a55ef 100644 --- a/packages/meshbay-node/src/meshbay_node/cli/setup.py +++ b/packages/meshbay-node/src/meshbay_node/cli/setup.py @@ -33,11 +33,6 @@ def init(args) -> None: cfg_dir = cfg_path.parent cfg_dir.mkdir(parents=True, exist_ok=True) - from meshbay_node.platform import install_node_env - env_written = install_node_env(cfg_dir) - if env_written: - print(f"Wrote {env_written} (packaged defaults).") - hub_url = args.hub_url username = args.username diff --git a/packages/meshbay-node/src/meshbay_node/media_cache.py b/packages/meshbay-node/src/meshbay_node/media_cache.py index 9c692ca..17262bc 100644 --- a/packages/meshbay-node/src/meshbay_node/media_cache.py +++ b/packages/meshbay-node/src/meshbay_node/media_cache.py @@ -290,6 +290,33 @@ class MediaCache: await self._db.commit() return cur.rowcount + async def clear_tmdb_metadata(self) -> int: + """ + Drop every cached TMDB fiche — show/movie details (`tmdb_meta`) and + per-season metadata (`season_meta`) — so the next `media_meta_req` + refetches each from TMDB. The file->tmdb *matches* (`file_tmdb`) are + language-independent and deliberately kept: the match is the same + title whatever language its blurb is in. + + Called when the node's TMDB *language* changes (`ops.set_tmdb_config`). + A fiche is cached under `tmdb_id` alone, on purpose — there is only + ever one node-wide language, so a per-language key would be dead + weight — which is exactly why the language it was fetched in is not + recorded, and a fiche cached under the old language would otherwise be + served unchanged for its whole 30-day TTL after the operator switched. + Wiping them on the switch is what makes the new language actually take + effect on a library that has already been browsed. Returns the number + of rows removed. + """ + if not self._db: + return 0 + cur = await self._db.execute("DELETE FROM tmdb_meta") + removed = cur.rowcount + cur = await self._db.execute("DELETE FROM season_meta") + removed += cur.rowcount + await self._db.commit() + return removed + # ── tmdb id -> metadata json ───────────────────────────────────────────── async def get_tmdb_meta(self, tmdb_id: str, media_type: str) -> dict | None: diff --git a/packages/meshbay-node/src/meshbay_node/ops/apps.py b/packages/meshbay-node/src/meshbay_node/ops/apps.py index 0e4f6dd..fbd984d 100644 --- a/packages/meshbay-node/src/meshbay_node/ops/apps.py +++ b/packages/meshbay-node/src/meshbay_node/ops/apps.py @@ -53,6 +53,13 @@ async def set_tmdb_config(state: dict, token: str | None = None, `language`. """ roster = _roster(state) + # Whether the *language* actually changes decides whether the cached + # fiches must go (below) — read the old value before overwriting it. + # "" (default/English) and None (unset) are the same language here. + language_changed = False + if language is not None: + _, old_language = await roster.tmdb_config() + language_changed = (language or None) != (old_language or None) await roster.set_tmdb_config(token, language, set_by=state.get("node_user_id", "")) # `token=None` means "leave whatever was there" (§ set_tmdb_config's own # docstring) — so the customized flag only changes when a value (a real @@ -61,6 +68,19 @@ async def set_tmdb_config(state: dict, token: str | None = None, state["tmdb_token_customized"] = bool(token) if language is not None: state["tmdb_language"] = language + # A cached TMDB fiche is stored under its tmdb_id alone and carries no note + # of the language it was fetched in (there is only one node-wide language), + # so changing the language leaves every fiche stale for its 30-day TTL. + # Drop the metadata cache here so the next media_meta_req refetches in the + # new language — this is what makes the setting take on a library that was + # already browsed, instead of the operator having to find a cache to clear. + # The matches (file_tmdb) are language-independent and kept. + if language_changed: + media_cache = state.get("media_cache") + if media_cache is not None: + removed = await media_cache.clear_tmdb_metadata() + log.info("TMDB language changed to %s: cleared %d cached fiche(s)", + language or "(default)", removed) log.info("TMDB config: custom_token=%s language=%s", bool(token), language or state.get("tmdb_language", "")) return { diff --git a/packages/meshbay-node/src/meshbay_node/platform.py b/packages/meshbay-node/src/meshbay_node/platform.py index 7e840ad..7db251b 100644 --- a/packages/meshbay-node/src/meshbay_node/platform.py +++ b/packages/meshbay-node/src/meshbay_node/platform.py @@ -80,9 +80,10 @@ def state_dir() -> Path: def packaged_default_env() -> Path | None: """ The `default.env` shipped with the package: build-time defaults, currently - the shared read-only TMDB token. `init` copies it to config_dir()/node.env - and nothing reads it in place, so an operator's edits to their own copy - survive an upgrade. + the shared read-only TMDB token. The daemon reads it in place, beneath + <config>/node.env, so it reaches every node however that node was set up -- + `meshbay-node init` and the desktop client's onboarding alike -- and an + operator's own node.env still wins. Frozen (PyInstaller/Windows): beside the executable, where build-node-runtime.ps1 puts it -- the same placement it uses for ffmpeg. @@ -102,39 +103,7 @@ def packaged_default_env() -> Path | None: return None -def install_node_env(target_dir: Path) -> Path | None: - """ - Copy the packaged default.env to <target_dir>/node.env, once, at init. - - Never overwrites: an existing node.env holds the operator's own values, and - silently replacing a configured token with the packaged one would be worse - than doing nothing. Returns the path when written, None when there was - nothing to copy or a file was already there. - """ - src = packaged_default_env() - if src is None: - return None - dest = target_dir / "node.env" - if dest.exists(): - return None - dest.write_bytes(src.read_bytes()) - chmod_private(dest) - return dest - - -def load_node_env(source_dir: Path) -> int: - """ - Read <source_dir>/node.env into os.environ, returning how many names were - set. - - systemd does this on Linux through `EnvironmentFile=`, but the Windows - autostart is a Startup-folder .vbs with no equivalent, so the daemon reads - the file itself and both platforms behave the same. An existing environment - variable always wins -- an operator exporting a value, or systemd having - already loaded the same file, overrides the packaged default rather than - being overridden by it. - """ - path = source_dir / "node.env" +def _load_env_file(path: Path) -> int: try: text = path.read_text(encoding="utf-8") except (OSError, UnicodeDecodeError): @@ -154,6 +123,29 @@ def load_node_env(source_dir: Path) -> int: return count +def load_node_env(source_dir: Path) -> int: + """ + Read <source_dir>/node.env, then the packaged default.env, into + os.environ, returning how many names were set. + + systemd does the first through `EnvironmentFile=`, but the Windows + autostart is a Startup-folder .vbs with no equivalent, so the daemon reads + the file itself and both platforms behave the same. An existing environment + variable always wins, and node.env wins over the packaged default -- an + operator exporting a value, or systemd having already loaded the same file, + overrides the packaged default rather than being overridden by it. + + The packaged default used to reach a node only as a copy made by + `meshbay-node init`; a node onboarded by the desktop client never ran it and + ran with no TMDB token at all. + """ + count = _load_env_file(source_dir / "node.env") + packaged = packaged_default_env() + if packaged is not None: + count += _load_env_file(packaged) + return count + + # ── File permissions ───────────────────────────────────────────────────────── diff --git a/packages/meshbay-node/src/meshbay_node/transport/quic_server.py b/packages/meshbay-node/src/meshbay_node/transport/quic_server.py index 96cd752..715448f 100644 --- a/packages/meshbay-node/src/meshbay_node/transport/quic_server.py +++ b/packages/meshbay-node/src/meshbay_node/transport/quic_server.py @@ -286,6 +286,7 @@ class _MNPServerProtocol(QuicConnectionProtocol): group_id=msg.get("group_id", ""), hosted_groups=self._ctx.get("groups"), denylist=self._ctx.get("denylist"), + node_pk_b64=pk_to_b64(self._ctx["sk_node"].public_key()), ) except HandshakeError as refusal: self._send(stream_id, {"type": "error", "detail": str(refusal), diff --git a/packages/meshbay-node/src/meshbay_node/transport/webrtc/apps/video_meta.py b/packages/meshbay-node/src/meshbay_node/transport/webrtc/apps/video_meta.py index 9222ad8..834bac4 100644 --- a/packages/meshbay-node/src/meshbay_node/transport/webrtc/apps/video_meta.py +++ b/packages/meshbay-node/src/meshbay_node/transport/webrtc/apps/video_meta.py @@ -169,6 +169,15 @@ class VideoMetaMixin: await media_cache.put_thumb(thumb_hash, synthetic_id, content) return thumb_hash + def _tmdb_language(self) -> str: + """ + The node-wide TMDB query language, or "" when the operator has not + chosen one yet. Read from the live daemon state (kept current by + tmdb_config_ack, same source the handshake ack reads), not the DB, so + it is a cheap in-memory lookup on the hot metadata path. + """ + return (self._ctx.get("daemon_state") or {}).get("tmdb_language") or "" + async def _do_media_meta_request(self, msg: dict) -> None: """ docs/MESHBAY_DESIGN.md §9.7: TMDB metadata for one file, resolved from @@ -250,6 +259,19 @@ class VideoMetaMixin: self._send({"type": MNP.MEDIA_META_RESP, "v": MNP_VERSION, "file_id": file_id, "confidence": 0}) return + if not self._tmdb_language(): + # No query language chosen yet: hold off entirely rather than + # search now. TMDB would answer in its English default, which + # is both the wrong language and a wasted call — the whole + # library fetched now would be thrown away and refetched the + # moment a language is set, doubling the request count against + # TMDB's rate limit. Waiting until the operator has chosen one + # is what makes the first (and only) fetch the chosen language + # (docs/MESHBAY_DESIGN.md §9.7). The client refetches on the + # tmdb_config_ack that carries the new language. + self._send({"type": MNP.MEDIA_META_RESP, "v": MNP_VERSION, + "file_id": file_id, "confidence": 0}) + return result, ratio = await self._tmdb_search(tmdb_client, entry, is_show) if result is None or ratio < 0.6: self._send({"type": MNP.MEDIA_META_RESP, "v": MNP_VERSION, @@ -315,6 +337,15 @@ class VideoMetaMixin: details = await media_cache.get_season_meta(tmdb_id, season) if details is None: + if not self._tmdb_language(): + # Same gate as media_meta_req above: no query language yet + # means no TMDB call (docs/MESHBAY_DESIGN.md §9.7). A show is + # only matched once a language is set, so this is normally + # unreachable, but a client holding a tmdb_id from an earlier + # session must not reopen an English fetch either. + self._send({"type": MNP.SEASON_META_RESP, "v": MNP_VERSION, + "tmdb_id": tmdb_id, "season": season, "confidence": 0}) + return fetched = await tmdb_client.tv_season(tmdb_id, season) if fetched is None: self._send({"type": MNP.SEASON_META_RESP, "v": MNP_VERSION, diff --git a/packages/meshbay-node/src/meshbay_node/transport/webrtc/handshake.py b/packages/meshbay-node/src/meshbay_node/transport/webrtc/handshake.py index 87394d1..eceb2b4 100644 --- a/packages/meshbay-node/src/meshbay_node/transport/webrtc/handshake.py +++ b/packages/meshbay-node/src/meshbay_node/transport/webrtc/handshake.py @@ -65,6 +65,7 @@ class HandshakeMixin: group_id=group_id, hosted_groups=self._ctx.get("groups"), denylist=self._ctx.get("denylist"), + node_pk_b64=self._node_pk_b64(), ) except HandshakeError as refusal: # HandshakeError messages are authored to be peer-safe, unlike arbitrary |