summaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src/meshbay_hub/static/transport.js
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/static/transport.js')
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/transport.js198
1 files changed, 192 insertions, 6 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/transport.js b/packages/meshbay-hub/src/meshbay_hub/static/transport.js
index 4f6b656..b4bfe87 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/transport.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/transport.js
@@ -106,6 +106,8 @@ class MeshBayTransport {
set onIndexDelta(fn) { this._onIndexDelta = fn; }
set onUploadPolicy(fn) { this._onUploadPolicy = fn; }
set onAppsEnabled(fn) { this._onAppsEnabled = fn; }
+ set onTmdbConfig(fn) { this._onTmdbConfig = fn; }
+ set onVideoRoot(fn) { this._onVideoRoot = fn; }
set onIndexProgress(fn) { this._onIndexProgress = fn; }
get sessionKeys() { return this._sessionKeys; }
@@ -465,6 +467,121 @@ class MeshBayTransport {
return msg;
}
+ /**
+ * TMDB metadata for one file (Videos app, docs/mediacenter.md §5.4).
+ * `path` is root+relpath, exactly what index_sync/index_delta already
+ * gave this browser — never a raw filesystem path constructed here.
+ * `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(path) {
+ const msg = await this._sendAndWait({ type: 'media_meta_req', v: '0.5', path });
+ if (msg.type === 'error') throw new Error(msg.detail);
+ return msg;
+ }
+
+ /**
+ * One season's own overview/air_date/poster (docs/mediacenter.md §5.4'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 setVideoRoot/
+ * 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 (webrtc_
+ * server.py's _admin_exec_tmdb_override).
+ */
+ async overrideTmdbMatch(path, tmdbId, mediaType, signFn) {
+ const msg = await this._sendAndWait({
+ type: 'tmdb_override', v: '0.6', path, tmdb_id: tmdbId, media_type: mediaType,
+ });
+ if (msg.type === 'error') throw new Error(msg.detail);
+ if (msg.type === 'admin_challenge') {
+ const subject = `path=${path},tmdb_id=${tmdbId},media_type=${mediaType}`;
+ return this._authorizeAdminOp(msg, 'tmdb_override', subject, signFn);
+ }
+ return msg;
+ }
+
+ /**
+ * Turn TMDB lookups on/off node-wide, optionally set/clear a custom API
+ * token, and optionally set the language TMDB is queried in (e.g.
+ * "fr-FR") — one for the whole node, same reasoning as the token: one
+ * shared cache, not a per-viewer request. Signed like setAppsEnabled/
+ * setMemberUpload — an unsigned toggle would let any member turn on
+ * outbound third-party network traffic the operator never agreed to
+ * (docs/mediacenter.md §5.5, §8). `token: ''` explicitly clears a
+ * previously-set custom token; omit it (undefined/null), like
+ * `language`, to leave whatever is stored unchanged.
+ */
+ async setTmdbConfig(enabled, token, language, signFn) {
+ const msg = await this._sendAndWait({
+ type: 'tmdb_config', v: '0.5', enabled: Boolean(enabled),
+ 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 (webrtc_server.py
+ // _do_tmdb_config): Python's f"{bool}" is "True"/"False", not JS's
+ // lowercase — and 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 = `enabled=${enabled ? 'True' : 'False'},` +
+ `custom_token=${token ? 'yes' : 'no'},language=${language || 'default'}`;
+ return this._authorizeAdminOp(msg, 'tmdb_config', subject, signFn);
+ }
+ return msg;
+ }
+
+ /**
+ * Which folder (possibly a subfolder of a shared root) the Videos app
+ * treats as its entry point for this group. `path: ''` means the whole
+ * group index. Signed like setAppsEnabled — it decides what every
+ * member's Videos tab shows.
+ */
+ async setVideoRoot(path, signFn) {
+ const clean = (path || '').replace(/^\/+|\/+$/g, '');
+ const msg = await this._sendAndWait({ type: 'video_root', v: '0.5', path: clean });
+ if (msg.type === 'error') throw new Error(msg.detail);
+ if (msg.type === 'admin_challenge') {
+ return this._authorizeAdminOp(msg, 'video_root', clean, signFn);
+ }
+ return msg;
+ }
+
async fetchStreamSegment(fileId, segmentIndex, segmentDuration) {
const msg = await this._sendAndWait({
type: 'stream_seg',
@@ -1173,9 +1290,20 @@ class MeshBayTransport {
// *while* other traffic is in flight, so the fallback below would hand
// a pong to whatever was waiting — resolving a history request with a
// message that has no messages in it, and emptying the conversation.
+ // media_meta_req is the same shape as file_req: video-app.js fires
+ // one per visible poster-grid tile, several at a time — matching by
+ // arrival order handed one tile's TMDB result to a different tile
+ // whenever two responses reordered (reproduced live: which of two
+ // shows got the confident match flipped across reloads).
_key: obj.type === 'file_req'
? `chunk:${obj.file_id}:${obj.chunk_index}`
- : obj.type === 'ping' ? `ping:${obj.token}` : null,
+ : obj.type === 'ping' ? `ping:${obj.token}`
+ : obj.type === 'media_meta_req' ? `media_meta:${obj.path}`
+ // Same reordering hazard as media_meta_req: a season-tab bar or a
+ // search box can have more than one of these in flight at once.
+ : obj.type === 'season_meta_req' ? `season_meta:${obj.tmdb_id}:${obj.season}`
+ : obj.type === 'tmdb_search_req' ? `tmdb_search:${obj.media_type}:${obj.query}`
+ : null,
resolve: (msg) => { clearTimeout(timeout); this._pending.delete(id); resolve(msg); },
reject: (err) => { clearTimeout(timeout); this._pending.delete(id); reject(err); },
});
@@ -1276,6 +1404,34 @@ class MeshBayTransport {
this._onAppsEnabled(msg.apps || []);
}
+ // Node-wide (not per-group) — the operator changed whether TMDB is
+ // called at all, or supplied/cleared a custom token. `token_customized`
+ // only says whether one is set, never the token itself.
+ if (msg.type === 'tmdb_config_ack' && this._onTmdbConfig) {
+ this._onTmdbConfig({
+ enabled: Boolean(msg.enabled),
+ tokenCustomized: Boolean(msg.token_customized),
+ language: msg.language || '',
+ });
+ }
+
+ // Same shape: an operator corrected a wrong automatic TMDB match, and
+ // everyone connected needs to know their poster grid/detail modal for
+ // this show is now stale — falls through so the operator's own
+ // admin_response promise resolves on this same message, exactly like
+ // member_upload_ack/apps_enabled_ack above.
+ if (msg.type === 'tmdb_override_ack' && this._onTmdbOverride) {
+ this._onTmdbOverride({
+ path: msg.path || '', tmdbId: msg.tmdb_id || '', mediaType: msg.media_type || '',
+ });
+ }
+
+ // Same shape: the operator changed which folder is the Videos app's
+ // entry point for this group.
+ if (msg.type === 'video_root_ack' && this._onVideoRoot) {
+ this._onVideoRoot(msg.path || '');
+ }
+
// The operator's node is scanning — never the entries themselves, just
// enough to animate a presence dot. Pushed periodically while it runs,
// plus once more on the transition back to idle (daemon.py
@@ -1316,11 +1472,11 @@ class MeshBayTransport {
return;
}
- // Incremental update — additions/deletions only, never the whole index.
- // Only ever arrives after the full index this browser already has (the
- // node's first push to a newly connected peer is always index_sync, see
- // daemon.py _broadcast_index_change), so there is always a base to
- // apply it to.
+ // Incremental update — additions/deletions/updates, never the whole
+ // index. Only ever arrives after the full index this browser already
+ // has (the node's first push to a newly connected peer is always
+ // index_sync, see daemon.py _broadcast_index_change), so there is
+ // always a base to apply it to.
if (msg.type === 'index_delta') {
if (this._onIndexDelta) this._onIndexDelta(msg);
return;
@@ -1364,6 +1520,36 @@ class MeshBayTransport {
return;
}
+ if (msg.type === 'media_meta_resp') {
+ const key = `media_meta:${msg.path}`;
+ for (const [, handler] of this._pending) {
+ if (handler._key === key) { handler.resolve(msg); return; }
+ }
+ // Nobody asked for this path any more (tile scrolled out and a fresh
+ // request superseded it, most likely) — must not fall through to the
+ // oldest pending request, which would hand a different tile's promise
+ // a TMDB result for a path it never asked about.
+ return;
+ }
+
+ // Same reasoning as media_meta_resp: keyed, not arrival-order, and
+ // "nobody's waiting any more" must not fall through either.
+ if (msg.type === 'season_meta_resp') {
+ const key = `season_meta:${msg.tmdb_id}:${msg.season}`;
+ for (const [, handler] of this._pending) {
+ if (handler._key === key) { handler.resolve(msg); return; }
+ }
+ return;
+ }
+
+ if (msg.type === 'tmdb_search_resp') {
+ const key = `tmdb_search:${msg.media_type}:${msg.query}`;
+ for (const [, handler] of this._pending) {
+ if (handler._key === key) { handler.resolve(msg); return; }
+ }
+ return;
+ }
+
// chat_hist_resp answers a `chat_hist` request, but under a different
// type string — unlike index_sync, which is asked for and answered under
// the same name, so the generic fallback below happens to work for it by