// 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