aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-node/src/meshbay_node
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-09-26 14:28:13 +0200
committerChristophe Besson <cbesson@gmail.com>2026-09-26 14:28:13 +0200
commiteccd465f8954bfd49c3f7d8f02446bc2aa5c4338 (patch)
tree0033f9232c56ca79c4838795c1cf5bdab13bc55f /packages/meshbay-node/src/meshbay_node
parentfc761e7df40eac828d4e9858fab56958078c928b (diff)
downloadmeshbay-eccd465f8954bfd49c3f7d8f02446bc2aa5c4338.tar.gz
fix(node,client): query TMDB only once a language is chosen, in that languageHEADmain
TMDB fiches are fetched lazily, on browse, and the fetch used to run whatever the moment it was first triggered — routinely before the operator had opened settings and picked a language, so it queried in TMDB's English default. Then the fiche was cached by tmdb_id alone, with no note of language and a 30-day TTL, so switching to the intended language afterwards changed nothing: the English fiche was served until it expired. The operator's only recourse was to find and wipe the cache by hand (found live 2026-09-26: a whole library indexed in English although "Français" had been chosen). Two rules now, both there to make the first fetch the right language rather than English-then-corrected, and to stop the doubled requests that eventually get a node rate-limited: - No language configured, no query. media_meta_req/season_meta_req answer confidence 0 and make no TMDB call while tmdb_language is unset; the fetch waits for the operator's choice, so the first (and only) query is in it. English is now a first-class choice (en-US), not the default of skipping the setting. - Changing the language wipes the metadata cache (ops.set_tmdb_config), so the new language takes effect on an already-browsed library. The file->tmdb matches are language-independent and kept. The client refetches on the tmdb_config_ack that carries the new language, so the grid updates without a page reload. docs/MESHBAY_DESIGN.md §9.7 states both rules; tests cover the gate and the cache wipe, and two existing handler harnesses now declare a language. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Diffstat (limited to 'packages/meshbay-node/src/meshbay_node')
-rw-r--r--packages/meshbay-node/src/meshbay_node/media_cache.py27
-rw-r--r--packages/meshbay-node/src/meshbay_node/ops/apps.py20
-rw-r--r--packages/meshbay-node/src/meshbay_node/transport/webrtc/apps/video_meta.py31
3 files changed, 78 insertions, 0 deletions
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/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,