From b7bf11c077bdd165400cf8d80c5cd1ad4248d854 Mon Sep 17 00:00:00 2001 From: Christophe Besson Date: Fri, 25 Sep 2026 11:28:31 +0200 Subject: refactor(client): split transport.js into classic scripts transport.js keeps the core (connection, reconnect, leases, dispatch). Chat, media, admin, upload and device methods move, cut as text, into transport-*.js scripts that hand a class of their own to extendTransport, which copies each method onto MeshBayTransport.prototype; the codec, roster checks, node pins and the rewrap fan-out move as they were. Both shells load them after transport.js. Every prototype member, class property and top-level function has the same source text as before. Co-Authored-By: Claude Opus 5.5 --- .../src/meshbay_hub/static/transport-media.js | 284 +++++++++++++++++++++ 1 file changed, 284 insertions(+) create mode 100644 packages/meshbay-hub/src/meshbay_hub/static/transport-media.js (limited to 'packages/meshbay-hub/src/meshbay_hub/static/transport-media.js') diff --git a/packages/meshbay-hub/src/meshbay_hub/static/transport-media.js b/packages/meshbay-hub/src/meshbay_hub/static/transport-media.js new file mode 100644 index 0000000..f94eb40 --- /dev/null +++ b/packages/meshbay-hub/src/meshbay_hub/static/transport-media.js @@ -0,0 +1,284 @@ +// What the media apps ask the node for: TMDB and MusicBrainz metadata, audio +// transcoding, subtitles, and the video stream. +// +// Methods of MeshBayTransport, copied onto its prototype by extendTransport +// (transport.js, which the shell loads first). + +extendTransport(class { + /** + * TMDB metadata for one file (Videos app, docs/MESHBAY_DESIGN.md §9.7). + * Keyed by the entry's own `id` (its content hash) — never a path: a + * path names the *folder* a file is in (indexer.py's `_virtual_dir`), so + * two files sharing a folder (any multi-episode season) would resolve to + * whichever entry the node's index happened to return first (found live + * via the Music app's identical bug, 2026-08-25 — see apps/video_meta.py's + * `_do_media_meta_request`). + * `confidence: 0` (no tmdb_id, no fields) means no confident match — + * the caller falls back to a thumbnail-only card (§4.1), not an error. + */ + async fetchMediaMeta(fileId) { + const msg = await this._sendAndWait({ type: 'media_meta_req', v: '0.6', file_id: fileId }); + if (msg.type === 'error') throw new Error(msg.detail); + return msg; + } + + /** + * One season's own overview/air_date/poster (docs/MESHBAY_DESIGN.md §9.7's + * per-season view) — a show's own tmdb_meta is one static field that does + * not necessarily describe every season alike, found live: a 3-season + * show whose overview read as season-3-specific for every season. + * Keyed like media_meta_req: a season-tab bar can fire a request per tab + * before the previous one lands, and matching by arrival order would hand + * one season's data to a different season's tab whenever two responses + * reordered. + */ + async fetchSeasonMeta(tmdbId, season) { + const msg = await this._sendAndWait({ + type: 'season_meta_req', v: '0.6', tmdb_id: tmdbId, season, + }); + if (msg.type === 'error') throw new Error(msg.detail); + return msg; + } + + /** + * Raw TMDB search candidates for an operator correcting a wrong automatic + * match — unlike fetchMediaMeta, this never collapses to one best guess: + * a human picks from several, so several is the point. Read-only, not an + * admin op: it looks nothing up in this node's own state and changes + * nothing, so it needs no signature (mirrors why media_meta_req isn't + * signed either). + */ + async searchTmdb(mediaType, query) { + const msg = await this._sendAndWait({ + type: 'tmdb_search_req', v: '0.6', media_type: mediaType, query, + }); + if (msg.type === 'error') throw new Error(msg.detail); + return msg; + } + + /** + * Correct a wrong automatic TMDB match. Signed like + * setTmdbConfig: it replaces what every member sees for a show/movie, + * node-wide (media_cache is shared, not per-viewer) — an unsigned + * override would let any member vandalize another show's metadata. + * Applies to every file sharing the representative one's display_title, + * not just the file the operator happened to be looking at (apps/ + * video_meta.py's _admin_exec_tmdb_override). Keyed by `fileId`, not a path + * — same reasoning as fetchMediaMeta above. + */ + async overrideTmdbMatch(fileId, tmdbId, mediaType, signFn) { + const msg = await this._sendAndWait({ + type: 'tmdb_override', v: '0.7', file_id: fileId, tmdb_id: tmdbId, media_type: mediaType, + }); + if (msg.type === 'error') throw new Error(msg.detail); + if (msg.type === 'admin_challenge') { + const subject = `file_id=${fileId},tmdb_id=${tmdbId},media_type=${mediaType}`; + return this._authorizeAdminOp(msg, 'tmdb_override', subject, signFn); + } + return msg; + } + + /** + * Drop one file's cached TMDB match so it re-resolves with the node's + * current matcher (V13) — the one-click alternative to the full + * search-and-pick flow. Signed for the same reason as overrideTmdbMatch. + */ + async rematchTmdbMatch(fileId, signFn) { + const msg = await this._sendAndWait({ + type: 'tmdb_rematch', v: '0.7', file_id: fileId, + }); + if (msg.type === 'error') throw new Error(msg.detail); + if (msg.type === 'admin_challenge') { + return this._authorizeAdminOp(msg, 'tmdb_rematch', `file_id=${fileId}`, signFn); + } + return msg; + } + + /** + * Set/clear a custom TMDB API token, and/or set the language TMDB is + * queried in (e.g. "fr-FR") — one for the whole node, since both are one + * operator's shared credential/cache, not a per-group concern (see + * setTmdbEnabled below for the per-group on/off switch). Signed like + * setAppsEnabled/updateRoot — an unsigned change would let any + * member alter outbound third-party network traffic the operator never + * agreed to (docs/MESHBAY_DESIGN.md §9.7, §6.5). `token: ''` explicitly clears + * a previously-set custom token; omit it (undefined/null), like + * `language`, to leave whatever is stored unchanged. + */ + async setTmdbConfig(token, language, signFn) { + const msg = await this._sendAndWait({ + type: 'tmdb_config', v: '0.7', + token: token === undefined ? null : token, + language: language === undefined ? null : language, + }); + if (msg.type === 'error') throw new Error(msg.detail); + if (msg.type === 'admin_challenge') { + // Must match the node's subject byte-for-byte (apps/video_meta.py + // _do_tmdb_config) — the token itself is never part of the subject + // (it would end up in the audit log in plaintext), only whether one + // was supplied. The language is not a secret, so it appears as-is. + const subject = `custom_token=${token ? 'yes' : 'no'},language=${language || 'default'}`; + return this._authorizeAdminOp(msg, 'tmdb_config', subject, signFn); + } + return msg; + } + + /** + * Whether TMDB lookups run for this group at all — per-group (2026-08-24, + * used to be node-wide): a real media-library group and a test/demo group + * on the same node need not share the decision to spend TMDB quota and + * make outbound requests. Signed like the rest — it decides whether + * this group's members' Videos tab ever makes outbound TMDB traffic. + */ + async setTmdbEnabled(enabled, signFn) { + const msg = await this._sendAndWait({ + type: 'tmdb_enabled', v: '0.7', enabled: Boolean(enabled), + }); + if (msg.type === 'error') throw new Error(msg.detail); + if (msg.type === 'admin_challenge') { + // Must match the node's subject byte-for-byte (apps/video_meta.py + // _do_tmdb_enabled): Python's f"{bool}" is "True"/"False", not JS's + // lowercase. + const subject = enabled ? 'True' : 'False'; + return this._authorizeAdminOp(msg, 'tmdb_enabled', subject, signFn); + } + return msg; + } + + /** + * MusicBrainz metadata for one track (Music app, docs/MESHBAY_DESIGN.md §9.8) + * — same shape as fetchMediaMeta, minus a season/episode concept: + * album-level (release), resolved from the track's own artist/album + * fields already in the index. Keyed by the track's own `id` (content + * hash), not a path — a path names the *folder* a track is in, and an + * album is one folder with many tracks in it; three unrelated albums + * shared one folder's track's cover before this fix (found live, + * 2026-08-25). `confidence: 0` means no confident match (or MusicBrainz + * off for this group, or nothing configured) — the caller falls back to + * the embedded/no cover it already had, not an error. + */ + async fetchMusicMeta(fileId) { + const msg = await this._sendAndWait({ type: 'music_meta_req', v: '0.9', file_id: fileId }); + if (msg.type === 'error') throw new Error(msg.detail); + return msg; + } + + /** + * Server-side transcode of a Music-app file the browser's own