From c5585beab3d6adefaa2ef9444946dd3816960a7c Mon Sep 17 00:00:00 2001 From: Christophe Besson Date: Mon, 24 Aug 2026 15:57:41 +0200 Subject: fix(node,hub): HEVC transcode fallback, live-add progress, per-group TMDB toggle MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three bugs found live testing the Videos app against a real HEVC/EAC3 show, plus a design change requested afterward: - Streaming always did "-c:v copy", which faithfully reports a source's real hev1 codec string but is unplayable in a browser with no HEVC decoder (most Chrome/Linux builds). The node now transcodes to H264 whenever the probed codec is browser-incompatible (media_probe.py's new BROWSER_INCOMPATIBLE_VIDEO_CODECS), with a `transcode_incompatible_video` node.toml opt-out for operators who know their viewers already decode it. - Dropping a whole season into an already-watched folder gave no scanning indicator and no progress bar: IndexProgress was only ever updated by the two bulk scan paths, never by the real-time per-file watchdog path (_schedule_update/_debounce/_update_entry). That path now accounts a "burst" the same way, without double-counting a file rewritten mid-debounce. - A stray literal "0" rendered in the video detail modal when there was no TMDB match (`meta.confidence` is 0, and `0 && x` renders "0" in JSX/htm, not nothing) — `confident` is now a real boolean. - Whether TMDB is used at all moves from a node-wide setting to per-group (OP_TMDB_ENABLED/tmdb_enabled/tmdb_enabled_ack, scoped like OP_VIDEO_ROOT): an operator running a real media-library group alongside test/demo groups on one node wants outbound TMDB traffic for the one that needs it, not all of them. The custom API token and query language stay node-wide, one shared credential/cache (tmdb_config/OP_TMDB_CONFIG, unchanged reasoning). MNP_VERSION 0.6 -> 0.7, additive. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01LAmyXtc6dAADsH23ydXQpY --- packages/meshbay-node/src/meshbay_node/roster.py | 54 ++++++++++++++++-------- 1 file changed, 37 insertions(+), 17 deletions(-) (limited to 'packages/meshbay-node/src/meshbay_node/roster.py') diff --git a/packages/meshbay-node/src/meshbay_node/roster.py b/packages/meshbay-node/src/meshbay_node/roster.py index 701462f..b4efb7c 100644 --- a/packages/meshbay-node/src/meshbay_node/roster.py +++ b/packages/meshbay-node/src/meshbay_node/roster.py @@ -599,13 +599,20 @@ class Roster: json.dumps(sorted(apps)), set_by) return apps - # TMDB is one operator's budget and credential, not a per-group concern - # (docs/mediacenter.md §5.5) — stored under the group_id="" sentinel, - # the same precedent as `roster.get_member("", user_id)` authorizing the - # operator node-wide (desktop-client-v1.md §6.3). Unset means "on, using - # the shipped default token" — the same "absent means the old behaviour" - # discipline member_upload/enabled_apps already follow. - SETTING_TMDB_ENABLED = "tmdb_enabled" + # The TMDB credential and query language are one operator's budget, not a + # per-group concern (docs/mediacenter.md §5.5) — stored under the + # group_id="" sentinel, the same precedent as `roster.get_member("", + # user_id)` authorizing the operator node-wide (desktop-client-v1.md + # §6.3). Unset means "the shipped default token, TMDB's own default + # language" — the same "absent means the old behaviour" discipline + # member_upload/enabled_apps already follow. + # + # Whether TMDB is used *at all*, though, is per-group (moved off the + # node-wide sentinel below, 2026-08-24): an operator running a real media + # library alongside test/demo groups wants outbound TMDB traffic for the + # one group that needs it, not all of them just because one node process + # serves both. See SETTING_TMDB_ENABLED's own per-group methods further + # down, next to video_root. SETTING_TMDB_TOKEN = "tmdb_api_token" # A TMDB language tag (e.g. "fr-FR") — one for the whole node, same # reasoning as the token: one shared cache, not a per-viewer request. @@ -614,18 +621,14 @@ class Roster: SETTING_TMDB_LANGUAGE = "tmdb_language" NODE_WIDE_GROUP_ID = "" - async def tmdb_config(self) -> tuple[bool, str | None, str | None]: - """Returns (enabled, custom_token_or_None, language_or_None).""" - enabled = (await self.get_setting( - self.NODE_WIDE_GROUP_ID, self.SETTING_TMDB_ENABLED, "1")) != "0" + async def tmdb_config(self) -> tuple[str | None, str | None]: + """Returns (custom_token_or_None, language_or_None).""" token = await self.get_setting(self.NODE_WIDE_GROUP_ID, self.SETTING_TMDB_TOKEN) language = await self.get_setting(self.NODE_WIDE_GROUP_ID, self.SETTING_TMDB_LANGUAGE) - return enabled, (token or None), (language or None) + return (token or None), (language or None) - async def set_tmdb_config(self, enabled: bool, token: str | None = None, + async def set_tmdb_config(self, token: str | None = None, language: str | None = None, set_by: str = "") -> None: - await self.set_setting(self.NODE_WIDE_GROUP_ID, self.SETTING_TMDB_ENABLED, - "1" if enabled else "0", set_by) if token is not None: await self.set_setting(self.NODE_WIDE_GROUP_ID, self.SETTING_TMDB_TOKEN, token, set_by) @@ -634,8 +637,9 @@ class Roster: language, set_by) # Which folder is the Videos app's entry point for this group — per-group - # (unlike tmdb_config above), since different groups share different - # trees. Empty/unset means the whole group index, exactly as today. + # (unlike the token/language above), since different groups share + # different trees. Empty/unset means the whole group index, exactly as + # today. SETTING_VIDEO_ROOT = "video_root" async def video_root(self, group_id: str) -> str: @@ -645,6 +649,22 @@ class Roster: await self.set_setting(group_id, self.SETTING_VIDEO_ROOT, path or "", set_by) return path or "" + # Whether TMDB lookups run for this group at all — per-group, unlike the + # token/language above: one node process can share a real media library + # group and several test/demo groups, and outbound TMDB traffic (and API + # quota) for the demo groups is not something turning it on for the real + # one should imply. Unset means on, same "absent means the old + # behaviour" discipline as everything else here — a node that predates + # this setting keeps working exactly as before for every group. + SETTING_TMDB_ENABLED = "tmdb_enabled" + + async def tmdb_enabled(self, group_id: str) -> bool: + return (await self.get_setting(group_id, self.SETTING_TMDB_ENABLED, "1")) != "0" + + async def set_tmdb_enabled(self, group_id: str, enabled: bool, set_by: str = "") -> None: + await self.set_setting(group_id, self.SETTING_TMDB_ENABLED, + "1" if enabled else "0", set_by) + # How often the indexer's reconciliation backstop runs, and how long it # waits after the last change on a file before hashing it. Unset means # the indexer's own defaults — an existing group's behaviour must not -- cgit v1.2.3