diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-08-24 22:24:52 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-08-24 22:24:52 +0200 |
| commit | 665fb2004e55b72ea483aa50fea81f6a6fd9c322 (patch) | |
| tree | cc412f776cf993fc5c242750787c9e5c81aa3241 /packages/meshbay-node/src/meshbay_node/transport | |
| parent | 626365668508790dee70ab192a7d6c6f14725bf4 (diff) | |
| download | meshbay-665fb2004e55b72ea483aa50fea81f6a6fd9c322.tar.gz | |
feat(node): add audio_root, gate Music enrichment on it like video_root
musicbay.md's original call — Music needs no root, tag reads are cheap so
just cover the whole shared tree — didn't hold up against a real messy
library: everything under every shared folder got mixed together with no
way to scope Music down to an actual music collection. This adds an
audio_root setting, symmetric to video_root in every respect: signed
operator op (audio_root/audio_root_ack, MNP bumped to 0.10), validated
against a real directory in the group's own roots before a signature is
even asked for, gates tag/cover enrichment exactly like video_root gates
ffprobe/TMDB (nothing runs until it's set, only files under it once it
is), and a set/change fires a one-off sweep of whatever the folder already
contains.
The old trigger — sweep everything the instant "music" joins enabled_apps
— is gone along with the root-less design it belonged to; setting
audio_root is now the trigger, mirroring set_video_root's
enrich_video_root_fn exactly.
Test coverage mirrors the video_root suite: policy (refuse before a
signature round trip, accept/store correctly) and the enrichment gate
itself (nothing without a root, only files under it, sweep on set).
Diffstat (limited to 'packages/meshbay-node/src/meshbay_node/transport')
| -rw-r--r-- | packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py | 50 |
1 files changed, 50 insertions, 0 deletions
diff --git a/packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py b/packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py index 1629801..a5cccaa 100644 --- a/packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py +++ b/packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py @@ -72,6 +72,7 @@ from meshbay_common.adminop import ( OP_TMDB_OVERRIDE, OP_MUSICBRAINZ_CONFIG, OP_MUSICBRAINZ_ENABLED, + OP_AUDIO_ROOT, OP_ROOT_ADD, OP_ROOT_REMOVE, OP_GROUP_ATTACH, @@ -398,6 +399,8 @@ class WebRTCPeerSession: self._do_tmdb_enabled(msg) elif mtype == MNP.VIDEO_ROOT: self._do_video_root(msg) + elif mtype == MNP.AUDIO_ROOT: + self._do_audio_root(msg) elif mtype == MNP.MEDIA_META_REQ: self._spawn(self._do_media_meta_request(msg)) elif mtype == MNP.SEASON_META_REQ: @@ -685,6 +688,10 @@ class WebRTCPeerSession: "musicbrainz_enabled": bool(self._group_ctx().get("musicbrainz_enabled", True)), "musicbrainz_contact_configured": bool( self._ctx.get("daemon_state", {}).get("musicbrainz_contact_configured", False)), + # Which folder the Music app treats as its entry point for this + # group — same shape as video_root above, "" means unset (the + # Music tab shows nothing yet). + "audio_root": self._group_ctx().get("audio_root") or "", # So a client that connects mid-scan shows the indexing state # immediately, instead of waiting for the next periodic # INDEX_PROGRESS push. Never a path or filename — see @@ -1852,6 +1859,46 @@ class WebRTCPeerSession: except Exception: pass + def _do_audio_root(self, msg: dict) -> None: + """Same shape as _do_video_root above — the Music app's own entry point.""" + path = msg.get("path") + if not isinstance(path, str): + self._send({"type": "error", "detail": "Missing or invalid 'path'"}) + return + path = path.strip("/") + if path: + ctx = self._group_ctx() + resolved = ctx["roots"].resolve(path) if ctx.get("roots") else None + if not resolved or not resolved.is_dir(): + self._send({"type": "error", "detail": "Not a directory in this group"}) + return + if not self._has_admin_authority(): + self._send({"type": "error", "detail": "No authorized key for this"}) + return + self._issue_admin_challenge(OP_AUDIO_ROOT, path) + + async def _admin_exec_audio_root( + self, pending: dict, transcript: bytes, sig: bytes, + ) -> None: + path = pending["subject"] + if not await self._verify_admin_sig(transcript, sig): + self._send({"type": "error", "detail": "Signature verification failed"}) + self._audit("admin_auth_failed", f"audio_root:{path}") + return + try: + await self._run_op(ops.set_audio_root, self._group_id or "", path) + except ops.OpError as e: + self._send({"type": "error", "detail": e.message}) + return + self._audit("audio_root", path) + + notice = {"type": MNP.AUDIO_ROOT_ACK, "v": MNP_VERSION, "path": path} + for uid, session in list(self._peer_registry().items()): + try: + session._send(notice) + except Exception: + pass + def _do_musicbrainz_config(self, msg: dict) -> None: """ Set (or clear) the node-wide MusicBrainz User-Agent contact string @@ -3551,6 +3598,9 @@ class WebRTCPeerSession: elif pending["op"] == OP_MUSICBRAINZ_ENABLED: self._spawn( self._admin_exec_musicbrainz_enabled(pending, transcript, sig_bytes)) + elif pending["op"] == OP_AUDIO_ROOT: + self._spawn( + self._admin_exec_audio_root(pending, transcript, sig_bytes)) elif pending["op"] == OP_ROOT_ADD: self._spawn( self._admin_exec_root_add(pending, transcript, sig_bytes)) |