diff options
Diffstat (limited to 'packages/meshbay-node/src')
16 files changed, 190 insertions, 172 deletions
diff --git a/packages/meshbay-node/src/meshbay_node/bundle_store.py b/packages/meshbay-node/src/meshbay_node/bundle_store.py index c65b94d..36792ac 100644 --- a/packages/meshbay-node/src/meshbay_node/bundle_store.py +++ b/packages/meshbay-node/src/meshbay_node/bundle_store.py @@ -6,7 +6,7 @@ Keypair bundles: AES-GCM encrypted (Ed25519 + X25519) private keys, encrypted with the user's password-derived bundle_key. Opaque to the node. An optional second copy (bundle_enc_recovery) is wrapped under the account's recovery key instead, so a forgotten passphrase does not strand the identity — see -docs/auth-confirm.md §4.3. +docs/MESHBAY_DESIGN.md §3.6. Both are stored and served over the P2P DataChannel during MNP handshake. """ @@ -168,7 +168,7 @@ class BundleStore: copy wrapped under the account's recovery key. A call that omits bundle_enc_recovery — a plain re-backup, or a - passphrase-change re-wrap (docs/auth-confirm.md §3.2) — must not erase a + passphrase-change re-wrap (docs/MESHBAY_DESIGN.md §3.6) — must not erase a recovery copy already stored, so the upsert keeps the existing value when the new one is None. """ diff --git a/packages/meshbay-node/src/meshbay_node/chat/store.py b/packages/meshbay-node/src/meshbay_node/chat/store.py index 9e5ee90..17cd68e 100644 --- a/packages/meshbay-node/src/meshbay_node/chat/store.py +++ b/packages/meshbay-node/src/meshbay_node/chat/store.py @@ -4,7 +4,7 @@ MeshBay Node — SQLite-backed chat message store. One database per group. The node is a relay and an archive: it stores what it was handed, serves it back, and — once a group has chat encryption switched on — cannot read any of it. Decryption happens in the client, which is the only place -that holds the epoch key (`docs/chat-sender-keys.md` §5). +that holds the epoch key (`docs/MESHBAY_DESIGN.md` §4.5). Three things about the schema are load-bearing rather than incidental: diff --git a/packages/meshbay-node/src/meshbay_node/daemon.py b/packages/meshbay-node/src/meshbay_node/daemon.py index 26f73fc..ad4a513 100644 --- a/packages/meshbay-node/src/meshbay_node/daemon.py +++ b/packages/meshbay-node/src/meshbay_node/daemon.py @@ -192,7 +192,7 @@ class NodeDaemon: # failure — a persistently unprobeable file (corrupt, still being # written) does not get re-queued on every coalesced broadcast. A # restart retries everything, matching the "disposable, rebuildable" - # stance the rest of this cache takes (docs/mediacenter.md §1/§2). + # stance the rest of this cache takes (docs/MESHBAY_DESIGN.md §6.5). # Shared across the video and audio enrichment paths — content- # addressed ids never collide between the two. Keyed by # (group_id, entry.id), not entry.id alone: the id is a content @@ -446,7 +446,7 @@ class NodeDaemon: "tmdb_enabled": await self._roster.tmdb_enabled( group_cfg.id) if self._roster else True, # Music app equivalent of tmdb_enabled — per-group from - # the start (docs/musicbay.md §6). + # the start (docs/MESHBAY_DESIGN.md §9.8). "musicbrainz_enabled": await self._roster.musicbrainz_enabled( group_cfg.id) if self._roster else True, } @@ -474,7 +474,7 @@ class NodeDaemon: # 6b. Media cache (Videos app — TMDB metadata + thumbnails). # Node-wide like audit.db, not per-group: a thumbnail is the same # bytes regardless of which group happens to share the file - # (docs/mediacenter.md §2/§5.5). + # (docs/MESHBAY_DESIGN.md §6.5, §9.7). media_cache_db = data_dir / "media_cache.db" self._media_cache = MediaCache(db_path=media_cache_db) await self._media_cache.open() @@ -491,16 +491,18 @@ class NodeDaemon: self._state["tmdb_token_customized"] = bool(tmdb_token) self._state["tmdb_language"] = tmdb_language or "" - # 6c. Music app (docs/musicbay.md) — same media_cache.db, its own - # enricher (mutagen, not ffmpeg) and its own MusicBrainz client. + # 6c. Music app (docs/MESHBAY_DESIGN.md §9.8) — same + # media_cache.db, its own enricher (mutagen, not ffmpeg) and its + # own MusicBrainz client. # The User-Agent contact is the owner's hub email, resolved at # login — no roster setting or env var needed any more. self._audio_enricher = AudioEnricher(self._media_cache) self._musicbrainz_client = MusicBrainzClient(owner_email=session.email) self._state["musicbrainz_contact_configured"] = bool(session.email) - # 6d. Photos app (docs/photos.md) — same media_cache.db, its own - # enricher (Pillow, not ffmpeg/mutagen). No credential, no + # 6d. Photos app (docs/MESHBAY_DESIGN.md §9.9) — same + # media_cache.db, its own enricher (Pillow, not ffmpeg/mutagen). + # No credential, no # third-party client to construct: EXIF is read locally. self._photo_enricher = PhotoEnricher(self._media_cache) self._state["media_cache"] = self._media_cache @@ -1352,7 +1354,7 @@ class NodeDaemon: delta = idx.diff(previous) self._last_broadcast_snapshot[group_id] = (idx.version, idx.entries_by_id()) - # Videos app (docs/mediacenter.md §5.2): schedule async technical + # Videos app (docs/MESHBAY_DESIGN.md §6.5): schedule async technical # probe + title parse + thumbnail generation for every newly-seen # video entry under the group's configured video_root. Never blocks # this broadcast — enrichment fields arrive later as their own @@ -1377,18 +1379,19 @@ class NodeDaemon: seen = {e.id for e in new_entries} new_entries = new_entries + [e for e in rebuilt if e.id not in seen] spawn(self._enrich_new_video_entries(indexer, new_entries)) - # Music app (docs/musicbay.md §6): same shape, gated on audio_root - # exactly like video_root above (added later — musicbay.md's - # original "no root, whole shared tree" call didn't hold up). + # Music app (docs/MESHBAY_DESIGN.md §9.8): same shape, gated on + # audio_root exactly like video_root above (added later — the original + # "no root, whole shared tree" call didn't hold up). spawn(self._enrich_new_audio_entries(indexer, new_entries)) - # Photos app (docs/photos.md §5): same shape, gated on photo_roots - # (a list, not a single string — §2.1). + # Photos app (docs/MESHBAY_DESIGN.md §9.9): same shape, gated on + # photo_roots (a list, not a single string). spawn(self._enrich_new_photo_entries(indexer, new_entries)) # A rename/move changes the very filename (or season folder) that - # §3.3/§3.4's title-parse read display_title/season/episode from, - # but leaves the file's content — and so its id and everything - # ffprobe/thumbnailing already found — untouched. Only entries + # docs/MESHBAY_DESIGN.md §9.7's title-parse read + # display_title/season/episode from, but leaves the file's content — + # and so its id and everything ffprobe/thumbnailing already found — + # untouched. Only entries # whose name or path actually differ from the last broadcast get a # fresh pass; an update that is enrichment's own field-fill # (duration/thumb_hash/... landing via _on_enriched below) leaves @@ -1403,13 +1406,13 @@ class NodeDaemon: # Videos/Music/Photos apps: a file that leaves the index also loses # its thumbnail/cover and file->tmdb/file->mbid mapping — the "real - # deletion obligation" docs/mediacenter.md §2/§8 calls out - # explicitly rather than leaving implicit (docs/musicbay.md §6 + # deletion obligation" docs/MESHBAY_DESIGN.md §6.5 calls out + # explicitly rather than leaving implicit (docs/MESHBAY_DESIGN.md §9.8 # follows the same rule). tmdb_meta/mbid_meta rows are left alone - # (§2: shared across files). + # (shared across files). # - # Found live (docs/photos.md): a root removed and a new one added - # for the identical content (an operator renaming/relocating a + # Found live (docs/MESHBAY_DESIGN.md §9.9): a root removed and a new + # one added for the identical content (an operator renaming/relocating a # shared folder) pruned the thumbnail here — correctly, the content # is gone from *this* root — but left the hash in # `_enriched_attempted`, which is never otherwise cleared. The same @@ -1480,8 +1483,8 @@ class NodeDaemon: A group with no video_root set yet does not enrich anything — TMDB lookups and ffmpeg thumbnailing are real, ongoing per-file cost - (mediacenter.md §5.2/§10), and running them over an operator's whole - shared index before they have chosen which folder is actually their + (docs/MESHBAY_DESIGN.md §6.5), and running them over an operator's + whole shared index before they have chosen which folder is actually their media library would burn both TMDB's rate limit and the node's CPU on files that were never meant to be in the Videos app at all. Once a root is set, `_enrich_video_root_now` (called when it changes) @@ -1560,14 +1563,14 @@ class NodeDaemon: async def _enrich_new_audio_entries(self, indexer: DirectoryIndexer, entries: list) -> None: """ - Music app (docs/musicbay.md §2.1, §6): fire (never await further) + Music app (docs/MESHBAY_DESIGN.md §9.8): fire (never await further) tag/cover enrichment for unattempted audio entries under the group's configured audio_root — same gate as - `_enrich_new_video_entries` above (musicbay.md's original "no root, - whole shared tree" call turned out wrong against a real messy - library: everything under every shared folder got mixed together - with no way to scope it down). `_enriched_attempted` is shared with - the video path — content-addressed ids never collide across the two. + `_enrich_new_video_entries` above (the original "no root, whole + shared tree" call turned out wrong against a real messy library: + everything under every shared folder got mixed together with no way + to scope it down). `_enriched_attempted` is shared with the video + path — content-addressed ids never collide across the two. """ if not self._audio_enricher or not self._roster: return @@ -1596,9 +1599,10 @@ class NodeDaemon: # not the shared root it lives in — so the ancestor walk # (enrich_audio._artist_album_from_ancestors) treats a flat # top-level folder right under the configured Music directory as - # ambiguous (artist-or-release, musicbay.md §2.1), rather than one - # level too shallow when that directory is itself a subfolder. - # With several configured, each file is measured against its own: + # ambiguous (artist-or-release, docs/MESHBAY_DESIGN.md §9.8), + # rather than one level too shallow when that directory is + # itself a subfolder. With several configured, each file is + # measured against its own: # a single shared boundary would be wrong for all but one of them. self._audio_enricher.spawn(entry, file_path, on_done, boundaries.get(owner)) @@ -1639,8 +1643,8 @@ class NodeDaemon: async def _enrich_new_photo_entries(self, indexer: DirectoryIndexer, entries: list) -> None: """ - Photos app (docs/photos.md §5): fire (never await further) thumbnail/ - EXIF enrichment for unattempted image entries under any of the + Photos app (docs/MESHBAY_DESIGN.md §9.9): fire (never await further) + thumbnail/EXIF enrichment for unattempted image entries under any of the group's configured photo_roots. Same gate shape as `_enrich_new_video_entries`/`_enrich_new_audio_entries` — no root configured yet means no work, since thumbnailing every image in a @@ -1678,8 +1682,9 @@ class NodeDaemon: so a folder that already had photos in it before it was added to photo_roots would otherwise never get enriched at all. Also covers a root being *removed*: nothing un-enriches on removal (the cache - entry is harmless, just unused — docs/photos.md's cache is - disposable), so re-sweeping the new set is enough. + entry is harmless, just unused — the media cache is disposable and + tied to the index, docs/MESHBAY_DESIGN.md §6.5), so re-sweeping the + new set is enough. """ indexer = self._state.get("indexers", {}).get(group_id) if not indexer: diff --git a/packages/meshbay-node/src/meshbay_node/indexer/enrich.py b/packages/meshbay-node/src/meshbay_node/indexer/enrich.py index 6eeb1ef..b44a246 100644 --- a/packages/meshbay-node/src/meshbay_node/indexer/enrich.py +++ b/packages/meshbay-node/src/meshbay_node/indexer/enrich.py @@ -4,7 +4,7 @@ filename parsing (title_parse), and thumbnail generation (ffmpeg) for a newly-added video IndexEntry. Runs through its own small bounded worker pool — separate from the streaming -transcode pool (docs/mediacenter.md §5.2, mirroring webrtc_server.py's +transcode pool (docs/MESHBAY_DESIGN.md §6.5, mirroring webrtc_server.py's `_transcode_semaphore`) — so indexing a large library never blocks on this, and enrichment never competes with an active viewer for CPU. The scan itself already put the entry in the index with hash/size/type only; this fills in @@ -33,26 +33,27 @@ THUMB_WIDTH = 320 # "Show/SeasonFolder/episode.mkv" is the expected shape, with a little slack # for an extra wrapper folder — not an attempt to find the exact group root. MAX_ANCESTOR_DEPTH = 4 -# Bounds the "borrow a title from a sibling episode filename" scan (§3.4) so -# a folder with thousands of files costs a fixed, small amount of work. +# Bounds the "borrow a title from a sibling episode filename" scan +# (docs/MESHBAY_DESIGN.md §9.7) so a folder with thousands of files costs a +# fixed, small amount of work. MAX_SIBLINGS_CHECKED = 20 # Bounds the whole-show scan _synthetic_episode_number uses to rank a -# season's files across more than one folder (§3.4c) — a show's total file -# count, not just one folder's, so this needs more headroom than -# MAX_SIBLINGS_CHECKED. +# season's files across more than one folder (docs/MESHBAY_DESIGN.md §9.7) — +# a show's total file count, not just one folder's, so this needs more +# headroom than MAX_SIBLINGS_CHECKED. MAX_SEASON_FILES_CHECKED = 500 def _season_and_show_from_ancestors(file_path: Path) -> tuple[int, Path] | None: """ - §3.4/§3.4c: walk up ancestor folders for a season-like one (a numbered - season, or Specials/Bonus/Extras -> season 0) — season from the first - (innermost) match, but the show's own name from *above every - consecutive season-like ancestor*, not just the first one. A - per-season Bonus folder (`Show/Season N/Bonus/file.ext`) is nested two - levels inside the show, both of them season-like on their own - ("Bonus" and "Season N") — stopping at the first would hand back - "Season N" as the show's name instead of "Show". + docs/MESHBAY_DESIGN.md §9.7: walk up ancestor folders for a season-like one + (a numbered season, or Specials/Bonus/Extras -> season 0) — season from the + first (innermost) match, but the show's own name from *above every + consecutive season-like ancestor*, not just the first one. A per-season + Bonus folder (`Show/Season N/Bonus/file.ext`) is nested two levels inside + the show, both of them season-like on their own ("Bonus" and "Season N") — + stopping at the first would hand back "Season N" as the show's name instead + of "Show". Trusted over any per-file guessit title once found: a bare episode numbering convention with no show name embedded at all @@ -87,15 +88,17 @@ def _season_and_show_from_ancestors(file_path: Path) -> tuple[int, Path] | None: def _title_from_siblings(file_path: Path) -> str | None: """ - §3.4: an episode filename with no show name in it borrows the title from - a representative sibling in the same folder, never from the folder name - alone (an acronym-named show folder is a real, observed case). + docs/MESHBAY_DESIGN.md §9.7: an episode filename with no show name in it + borrows the title from a representative sibling in the same folder, never + from the folder name alone (an acronym-named show folder is a real, + observed case). Requires the sibling to carry its own episode number too, not just a - title — a folder where every file is a one-off-named Special (§3.4b) - has plenty of `display_title`s (guessit reads *a* title off nearly - anything) but none of them name the show; requiring a real episode - number alongside is what tells apart a genuinely representative sibling + title — a folder where every file is a one-off-named Special + (docs/MESHBAY_DESIGN.md §9.7) has plenty of `display_title`s (guessit + reads *a* title off nearly anything) but none of them name the show; + requiring a real episode number alongside is what tells apart a + genuinely representative sibling from another Special just like this one. """ try: @@ -119,11 +122,11 @@ def _title_from_siblings(file_path: Path) -> str | None: def _synthetic_episode_number(file_path: Path, show_root: Path, season: int) -> int: """ - §3.4b/§3.4c: a Specials/Bonus folder's files often carry no episode - number at all — each is just named after its own one-off title. The - frontend (video-app.js's buildSeasons) sorts within a season by this - number but only needs it to provide a stable order, not to mean - anything beyond that. + docs/MESHBAY_DESIGN.md §9.7: a Specials/Bonus folder's files often carry no + episode number at all — each is just named after its own one-off title. The + frontend (video-app.js's buildSeasons) sorts within a season by this number + but only needs it to provide a stable order, not to mean anything beyond + that. Ranked across the *whole show*, not just this file's own folder: season 0 routinely spans more than one folder under the show's root — a @@ -289,10 +292,11 @@ class Enricher: and (title_parse.has_episode_marker(entry.name) or title_parse.year_in(entry.name) is None)): # No season-like ancestor at all (a flat library) but the - # filename itself carries season+episode (§3.4) — *and* it - # is a real marker, not guessit reading a bare number as - # SxxExx. A movie whose "1080p" tag was truncated to "108", - # or "1280" left in the name, otherwise parses to S01E08 / + # filename itself carries season+episode + # (docs/MESHBAY_DESIGN.md §9.7) — *and* it is a real marker, + # not guessit reading a bare number as SxxExx. A movie whose + # "1080p" tag was truncated to "108", or "1280" left in the + # name, otherwise parses to S01E08 / # S12E80 and gets shelved as a nonexistent series # (found live 2026-08-29). A genuine flat-dumped episode # has an explicit SxxExx/1x08/"Episode N" marker; a movie diff --git a/packages/meshbay-node/src/meshbay_node/indexer/enrich_audio.py b/packages/meshbay-node/src/meshbay_node/indexer/enrich_audio.py index 4fa08ef..d484e35 100644 --- a/packages/meshbay-node/src/meshbay_node/indexer/enrich_audio.py +++ b/packages/meshbay-node/src/meshbay_node/indexer/enrich_audio.py @@ -1,7 +1,7 @@ """ Index-time enrichment for the Music group app: embedded tag/cover extraction (mutagen) and filename-parse fallback for a newly-added audio -IndexEntry (docs/musicbay.md §2.1, §6). +IndexEntry (docs/MESHBAY_DESIGN.md §9.8). Runs through its own small bounded worker pool, the same discipline as the Videos app's `enrich.py` — separate from any other pool, never blocking a @@ -15,14 +15,13 @@ block the event loop. MusicBrainz lookups are **not** done here. Tag/cover extraction is free and local, so it runs for every audio file the Music app is enabled for, regardless of whether MusicBrainz itself is turned on for the group — the -flat view (docs/musicbay.md §5.2) needs nothing more than this. MusicBrainz +flat view (docs/MESHBAY_DESIGN.md §9.8) needs nothing more than this. MusicBrainz is a separate, lazy, per-request enrichment (`music_meta_req`, handled in webrtc_server.py), the same "fetched on demand, cached once" shape TMDB already uses. **Revised 2026-08-24** against a real ~5700-file library (folder-per-artist -mostly, but not uniformly — see musicbay.md's own "what got measured" note -if one gets added). Two findings drove this revision, both confirmed with +mostly, but not uniformly). Two findings drove this revision, both confirmed with real data before writing the fix: 1. The original ancestor walk always went up two levels (parent = album, diff --git a/packages/meshbay-node/src/meshbay_node/indexer/enrich_photo.py b/packages/meshbay-node/src/meshbay_node/indexer/enrich_photo.py index 93a68cc..6613f85 100644 --- a/packages/meshbay-node/src/meshbay_node/indexer/enrich_photo.py +++ b/packages/meshbay-node/src/meshbay_node/indexer/enrich_photo.py @@ -3,7 +3,7 @@ Index-time enrichment for the Photos group app: a resized thumbnail and a minimal, best-effort info set (`taken_at`, `camera`) read from the image's own EXIF block, for a newly-added image IndexEntry. -Deliberately small — docs/photos.md §2.4 is explicit that this app does not +Deliberately small — docs/MESHBAY_DESIGN.md §9.9 is explicit that this app does not build a full EXIF-viewer panel. Two fields only, both best-effort (missing EXIF is the ordinary case for a screenshot or a re-saved/edited image, not an error). GPS is never read here, on purpose: it is a location disclosure @@ -12,7 +12,8 @@ module extracts, caches, or hands it to a caller. Runs through its own small bounded worker pool, separate from the video (ffmpeg) and audio (mutagen) enrichment pools — mirrors enrich.py exactly, -per docs/photos.md §5's "never shared with either" rule, even though +per docs/MESHBAY_DESIGN.md §6.5's "its own small bounded pool, never the +streaming pool" rule, even though Pillow's own work is comparatively cheap: a burst of hundreds of newly shared photos should not peg every CPU core at once. """ @@ -100,7 +101,7 @@ def _read_image(file_path: Path) -> tuple[bytes, int, int, int | None, str | Non # width/height and resizing — otherwise a phone photo stored # "sideways" reports its raw, pre-rotation dimensions (swapped from # what it actually displays as) and produces a sideways thumbnail - # (docs/photos.md §2.4). Never reads Orientation itself as a + # (docs/MESHBAY_DESIGN.md §9.9). Never reads Orientation itself as a # client-visible field; this is display correction only, and # width/height must describe the *displayed* image, matching what # the lightbox and the info panel show. diff --git a/packages/meshbay-node/src/meshbay_node/indexer/indexer.py b/packages/meshbay-node/src/meshbay_node/indexer/indexer.py index 887d435..3fe4f3e 100644 --- a/packages/meshbay-node/src/meshbay_node/indexer/indexer.py +++ b/packages/meshbay-node/src/meshbay_node/indexer/indexer.py @@ -82,7 +82,8 @@ def _is_indexable(path: Path) -> bool: # Found live: a 1256-byte ".mp3" with no audio stream at all, just an ID3 # tag — a truncated/corrupted rip, sitting between two good tracks of the -# same album (docs/musicbay.md). A source this small claiming to be audio +# same album (docs/MESHBAY_DESIGN.md §9.8). A source this small claiming to +# be audio # is far more likely broken than real, so it is skipped before ever being # hashed rather than indexed and left to fail at playback time. Scoped to # audio only — a tiny real file of any other type is still worth indexing. @@ -186,9 +187,8 @@ def _size_files(files: list[Path]) -> list[tuple[Path, int]]: executor for the same reason `_walk_root` is (its own docstring above). Previously a plain loop straight on the asyncio event loop thread: for a root with many thousands of files (a real personal library, not a - hypothetical — docs/musicbay.md's own "several thousand files" example) - that blocked the entire daemon, every WebRTC session and the admin UI - included, for as long as the stat() calls took — and did so *before* + hypothetical) that blocked the entire daemon, every WebRTC session and + the admin UI included, for as long as the stat() calls took — and did so *before* `_scan_root` had even set `progress.scanning`, so a consumer polling it saw "not scanning" the whole time real, blocking work was happening. """ @@ -673,8 +673,9 @@ class DirectoryIndexer: Entries under a root that is gone from the config are dropped — the operator removed it deliberately, which is not the same event as a - volume disappearing, and conflating the two is what §6.9 exists to - prevent. Roots that survive keep their entries; new ones are scanned. + volume disappearing, and conflating the two is what + docs/MESHBAY_DESIGN.md §6.2 exists to prevent. Roots that survive + keep their entries; new ones are scanned. The set takes effect before anything is scanned: the roots table, the watcher and `self.roots` all move at once. With ``wait=False`` the scan diff --git a/packages/meshbay-node/src/meshbay_node/indexer/title_parse.py b/packages/meshbay-node/src/meshbay_node/indexer/title_parse.py index beb0d3f..14728ff 100644 --- a/packages/meshbay-node/src/meshbay_node/indexer/title_parse.py +++ b/packages/meshbay-node/src/meshbay_node/indexer/title_parse.py @@ -1,18 +1,18 @@ """ Filename -> title/year/season/episode parsing for the Videos group app. -Wraps `guessit` and layers the fixes from docs/mediacenter.md §3.3/§3.4 on +Wraps `guessit` and layers the fixes from docs/MESHBAY_DESIGN.md §9.7 on top of it: none of them are per-title hacks, each is a generic rule found by validating guessit's raw output against real TMDB search results over a ~1950-file library (movies, TV shows, and a small franchise set). -Scope is deliberately narrow (§3.5): title, year, season, episode. Technical +Scope is deliberately narrow: title, year, season, episode. Technical facts (resolution, codec, duration) come from ffprobe, never the filename — a mislabeled `1080p` tag is a real, observed failure mode. This module never touches the filesystem or the network. The orchestration that decides *which* file supplies a show's title (a representative episode -filename, not the folder name — §3.4) lives in the indexer, which has the +filename, not the folder name) lives in the indexer, which has the directory listing; this module only parses strings it's handed. """ @@ -91,7 +91,7 @@ _YEAR_RE = re.compile(r"(?<!\d)(?:19|20)\d{2}(?!\d)") def year_in(text: str) -> int | None: """First 19xx/20xx in `text`, or None — used to lift a year off a show - folder name ("Some.Show.2022.S01") for the search fallback (§10.1/V8).""" + folder name ("Some.Show.2022.S01") for the search fallback (V8).""" m = _YEAR_RE.search(text or "") return int(m.group(0)) if m else None @@ -102,7 +102,7 @@ def clean_query(s: str) -> str: parenthesized-year stripping `naive_title` does. `naive_title` assumes a real filename; a show's `display_title` is a folder basename ("Some.Show.Name" — `rsplit('.', 1)` would eat ".Name"), so it needs a - gentler normaliser (§10.1/V8). + gentler normaliser (V8). """ s = re.sub(r"[._-]+", " ", s or "") s = _strip_editions(s) @@ -115,7 +115,7 @@ def _strip_editions(title: str) -> str: def naive_title(filename: str) -> str: """ - The mandated fallback (§3.6, §4.1): strip the extension, replace every + The mandated fallback: strip the extension, replace every `.`/`_`/`-` with a space, drop a trailing parenthesized year, collapse whitespace. Always computable, never fails, used both as the flat-mode display name of last resort and as a second TMDB query candidate. @@ -150,14 +150,14 @@ def sequel_variants(title: str) -> list[str]: A trailing sequel index often has no exact match in the real TMDB title: the file has a digit where TMDB uses a Roman numeral (or the reverse), spells the number out, or wraps it as "Part N" / "Chapitre N" - (§3.3 row 4, §10.1/V10). Returns extra candidate titles to try — the + (V10). Returns extra candidate titles to try — the index re-rendered as digit and as Roman numeral, plus (only when there is no "Part"/"Episode"/… keyword) the bare base. The bare base is withheld for a keyword'd index — "<Saga> Chapter III" → "<Saga>" — because a franchise's bare name is very often a real, *different* film (the series' first entry), and that variant matched - every later entry to it (§10.1/V14). Without the keyword ("<Franchise> + every later entry to it (V14). Without the keyword ("<Franchise> 3") the number is decoration and the bare base is the right thing to try. """ @@ -190,7 +190,7 @@ def sequel_variants(title: str) -> list[str]: def season_from_folder_name(name: str) -> int | None: """ - §3.4: a season-like ancestor folder, vocabulary-driven rather than + A season-like ancestor folder, vocabulary-driven rather than assuming a numeric convention everywhere. A specials/bonus/extras folder maps to season 0 (matching TMDB's own `season_number: 0`). Returns None if `name` doesn't look like a season folder at all. @@ -212,7 +212,8 @@ def season_from_folder_name(name: str) -> int | None: @dataclass class ParsedName: display_title: str | None # None => caller must supply from elsewhere (e.g. a sibling file) - alt_title: str | None = None # guessit's alternative_title, a second query candidate (§3.3 row 1) + alt_title: str | None = None # guessit's alternative_title, a second + # query candidate naive_title: str = "" # always available, fully punctuation-normalized fallback year: int | None = None season: int | None = None @@ -249,7 +250,7 @@ def parse_movie_filename(filename: str) -> ParsedName: -# ── Music app (docs/musicbay.md §2.1) ──────────────────────────────────────── +# ── Music app (docs/MESHBAY_DESIGN.md §9.8) ──────────────────────────────────────── # # Filename parsing is the *fallback* here, not the primary source (unlike # Videos, where guessit does all the work): embedded ID3/Vorbis tags are read @@ -307,7 +308,7 @@ def strip_track_prefix(text: str) -> str: # "Season 1"/"Saison 1". guessit will also invent a season+episode from a # bare 3-4 digit run ("1080p" truncated to "108" -> S01E08; "1280" -> # S12E80), which is how a plain movie ends up shelved as a series -# (§10.1/V14). The indexer uses this to tell a real flat-library episode +# (V14). The indexer uses this to tell a real flat-library episode # from that hallucination. _EPISODE_MARKER_RE = re.compile( r"s\d{1,2}[\s._-]*e\d{1,3}" @@ -326,9 +327,9 @@ def has_episode_marker(filename: str) -> bool: def parse_episode_filename(filename: str) -> ParsedName: """ Parse an episode filename. `display_title` may come back None (e.g. - `S08E02.SUBFRENCH.720p.mkv` carries no show name at all, §3.2) — the + `S08E02.SUBFRENCH.720p.mkv` carries no show name at all) — the indexer then supplies the show title from a representative sibling - filename in the same folder rather than the folder name itself (§3.4). + filename in the same folder rather than the folder name itself. """ g = guessit(filename) title = g.get("title") @@ -351,7 +352,7 @@ def parse_episode_filename(filename: str) -> ParsedName: ) -# A bare leading episode number, no show name attached (§3.4c) — the same +# A bare leading episode number, no show name attached — the same # shape as music's _TRACK_PREFIX_RE, capped at 3 digits for the same reason: # a leading year ("2010 - Episode.mkv") is 4 digits and must not match. # guessit's own `episode` is not a substitute here: given exactly 3 digits it diff --git a/packages/meshbay-node/src/meshbay_node/media_cache.py b/packages/meshbay-node/src/meshbay_node/media_cache.py index 2898dea..9dfdf73 100644 --- a/packages/meshbay-node/src/meshbay_node/media_cache.py +++ b/packages/meshbay-node/src/meshbay_node/media_cache.py @@ -4,8 +4,8 @@ Videos and Music group apps. Node-wide (not per-group, `data_dir/media_cache.db`), same rationale as `tmdb_enabled`/`tmdb_api_token` (and `musicbrainz_enabled`, -docs/musicbay.md §6) living in `group_settings` under the `group_id=""` -sentinel (docs/mediacenter.md §5.5): the credential/budget is one +docs/MESHBAY_DESIGN.md §9.8) living in `group_settings` under the `group_id=""` +sentinel (docs/MESHBAY_DESIGN.md §9.7): the credential/budget is one operator's, and a thumbnail or cover image is the same bytes regardless of which group happens to share the file. The `file_mbid`/ `mbid_meta` tables below are the Music app's equivalent of `file_tmdb`/ @@ -13,7 +13,8 @@ regardless of which group happens to share the file. The `file_mbid`/ release's cover is cached under a synthetic `musicbrainz:{mbid}` file_id, the same trick `_fetch_and_cache_poster` uses for a TMDB poster_path). -Disposable and rebuildable, like the rest of the file index (§1, §2) — never +Disposable and rebuildable, like the rest of the file index +(docs/MESHBAY_DESIGN.md §6.5) — never a second identity for a file. Every row here is keyed off a value the node can already derive (a file's own blake3 id, or a TMDB id), so losing this database costs re-probing/re-fetching, not data. @@ -98,14 +99,14 @@ CREATE TABLE IF NOT EXISTS mbid_meta ( json TEXT NOT NULL, fetched_at REAL NOT NULL ); --- Photos app (docs/photos.md): the technical/EXIF fields enrich_photo.py --- reads alongside the thumbnail. Durable for the same reason `thumbs` is — --- without this, only the thumbnail bytes survived a restart, and every --- image was still fully re-decoded through Pillow just to re-derive --- width/height/taken_at/camera, which get_thumb_hash_by_file_id's own --- cache hit had already proven unnecessary. thumb_hash is not duplicated --- here — get_thumb_hash_by_file_id(file_id) already answers that, and a --- second copy would just be one more place for the two to drift. +-- Photos app (docs/MESHBAY_DESIGN.md §9.9): the technical/EXIF fields +-- enrich_photo.py reads alongside the thumbnail. Durable for the same reason +-- `thumbs` is — without this, only the thumbnail bytes survived a restart, and +-- every image was still fully re-decoded through Pillow just to re-derive +-- width/height/taken_at/camera, which get_thumb_hash_by_file_id's own cache +-- hit had already proven unnecessary. thumb_hash is not duplicated here — +-- get_thumb_hash_by_file_id(file_id) already answers that, and a second copy +-- would just be one more place for the two to drift. CREATE TABLE IF NOT EXISTS photo_meta ( file_id TEXT PRIMARY KEY, width INTEGER, @@ -116,10 +117,10 @@ CREATE TABLE IF NOT EXISTS photo_meta ( """ # TMDB overviews/ratings do drift; a file's own resolved tmdb_id does not -# need re-checking on this schedule, only the metadata blob (§5.4, V3). +# need re-checking on this schedule, only the metadata blob (V3). TMDB_META_TTL_SECS = 30 * 86400 -# Same default as TMDB (docs/musicbay.md §6) — MusicBrainz release data is +# Same default as TMDB (docs/MESHBAY_DESIGN.md §9.8) — MusicBrainz release data is # not expected to drift faster; revisit if that proves wrong in practice. MUSICBRAINZ_META_TTL_SECS = 30 * 86400 @@ -231,7 +232,7 @@ class MediaCache: Full per-file reset: forget the match *and* any manual override marker, so the next `media_meta_req` re-resolves from scratch with the current matcher. This is the explicit operator "re-match this - one" action (§10.1/V13) — deliberately stronger than + one" action (V13) — deliberately stronger than `clear_file_tmdb`, which spares an override. """ await self._db.execute("DELETE FROM file_tmdb WHERE file_id = ?", (file_id,)) @@ -282,9 +283,10 @@ class MediaCache: # ── tmdb id + season number -> season-level metadata json ──────────────── # # A show's own overview (tmdb_meta above) is one static field an operator - # found does not necessarily describe every season alike (mediacenter.md - # §5.4) — this is TMDB's per-season `overview`/`air_date`/`poster_path`, - # fetched and cached independently, on the same staleness schedule. + # found does not necessarily describe every season alike + # (docs/MESHBAY_DESIGN.md §9.7) — this is TMDB's per-season + # `overview`/`air_date`/`poster_path`, fetched and cached independently, + # on the same staleness schedule. async def get_season_meta(self, tmdb_id: str, season: int) -> dict | None: async with self._db.execute( diff --git a/packages/meshbay-node/src/meshbay_node/media_probe.py b/packages/meshbay-node/src/meshbay_node/media_probe.py index 81174c8..a6267d9 100644 --- a/packages/meshbay-node/src/meshbay_node/media_probe.py +++ b/packages/meshbay-node/src/meshbay_node/media_probe.py @@ -145,7 +145,7 @@ async def probe_video(path: str) -> VideoProbe: width/height come from the same ffprobe call (one extra `-show_entries` field, no second process spawn) — resolution is deliberately never - guessed from the filename (docs/mediacenter.md §3.5). + guessed from the filename (docs/MESHBAY_DESIGN.md §9.7). """ from meshbay_node.platform import ffprobe_cmd proc = await asyncio.create_subprocess_exec( diff --git a/packages/meshbay-node/src/meshbay_node/musicbrainz.py b/packages/meshbay-node/src/meshbay_node/musicbrainz.py index d5ca1e9..ce2d233 100644 --- a/packages/meshbay-node/src/meshbay_node/musicbrainz.py +++ b/packages/meshbay-node/src/meshbay_node/musicbrainz.py @@ -2,7 +2,7 @@ MusicBrainz (musicbrainz.org) + Cover Art Archive (coverartarchive.org) client for the Music group app. -Called only by the node, never by a client (docs/musicbay.md §3): the node +Called only by the node, never by a client (docs/MESHBAY_DESIGN.md §9.8): the node makes the one lookup per unique release, shared by every member, and self- paces against MusicBrainz's shared rate limit rather than letting several members' tile requests multiply it. @@ -70,8 +70,8 @@ def _similarity(query: str, val: str | None) -> float: def _best_match_release(artist: str, album: str, results: list[dict]) -> tuple[dict | None, float]: """ Same "trust the search's own ranking" shape as tmdb.py's `_best_match` - (§3.3 of mediacenter.md found a locally-recomputed re-rank pick a - coincidentally closer-looking wrong result once — no reason to expect + (docs/MESHBAY_DESIGN.md §9.7: a locally-recomputed re-rank was found to + pick a coincidentally closer-looking wrong result once — no reason to expect MusicBrainz's own scored search to fare differently under the same treatment). MusicBrainz already returns results ordered by its own `score`; only the top one is considered. diff --git a/packages/meshbay-node/src/meshbay_node/ops.py b/packages/meshbay-node/src/meshbay_node/ops.py index 2221bea..98f67c5 100644 --- a/packages/meshbay-node/src/meshbay_node/ops.py +++ b/packages/meshbay-node/src/meshbay_node/ops.py @@ -283,7 +283,7 @@ async def unpin_member(state: dict, user_id: str) -> dict: # the key, as `set_gek` does — would make every message anyone ever sent # permanently unreadable to everybody, which is what a plain GEK-derived # archive key would have done on the very first `member unpin` -# (docs/chat-sender-keys.md F4). +# (finding F4, docs/MESHBAY_DESIGN.md §13.6). async def _wrap_for_node(state: dict, key: bytes) -> dict: @@ -1369,7 +1369,8 @@ async def set_node_settings(state: dict, settings: dict) -> dict: # `webrtc._stream_sem` was assigned here for months. That attribute # has never existed -- the pool is `ctx["_transcode_sem"]` -- so the # `hasattr` guard was always False and the setting only ever took - # effect on a restart, which draft-v6 §2.11 says it does not need. + # effect on a restart, which docs/MESHBAY_DESIGN.md §6.8 says it + # does not need. if webrtc is not None: webrtc.set_capacity( max_concurrent_streams=updated["max_concurrent_streams"]) @@ -1524,7 +1525,7 @@ async def set_tmdb_config(state: dict, token: str | None = None, language: str | None = None) -> dict: """ Whether the node uses a custom API token instead of the shipped default, - and in what language it queries TMDB (docs/mediacenter.md §5.5). + and in what language it queries TMDB (docs/MESHBAY_DESIGN.md §9.7). Node-wide (roster.py group_settings, group_id="") rather than per-group like set_enabled_apps: the token and the shared-cache @@ -1554,8 +1555,8 @@ async def set_tmdb_config(state: dict, token: str | None = None, async def set_tmdb_enabled(state: dict, group_id: str, enabled: bool) -> dict: """ - Whether TMDB lookups run for this group at all (docs/mediacenter.md - §5.5) — per-group, unlike set_tmdb_config above: an operator running a + Whether TMDB lookups run for this group at all (docs/MESHBAY_DESIGN.md + §9.7) — per-group, unlike set_tmdb_config above: an operator running a real media library alongside test/demo groups on one node wants outbound TMDB traffic (and API quota) spent for the one that needs it, not all of them just because one process serves both. @@ -1576,7 +1577,7 @@ async def set_tmdb_enabled(state: dict, group_id: str, enabled: bool) -> dict: async def set_musicbrainz_enabled(state: dict, group_id: str, enabled: bool) -> dict: """ Whether MusicBrainz lookups run for this group at all - (docs/musicbay.md §6) — per-group from the start, same reasoning as + (docs/MESHBAY_DESIGN.md §9.8) — per-group from the start, same reasoning as set_tmdb_enabled: a real media-library group and a test/demo group on one node need not share the decision to make outbound requests. """ diff --git a/packages/meshbay-node/src/meshbay_node/roster.py b/packages/meshbay-node/src/meshbay_node/roster.py index b0ca78b..064ab93 100644 --- a/packages/meshbay-node/src/meshbay_node/roster.py +++ b/packages/meshbay-node/src/meshbay_node/roster.py @@ -16,7 +16,7 @@ Three tables: only in the operator's hands and the invitee's. The code is what binds a public key to an account without asking the hub -(finding H3). See `docs/invite-pairing-v1.md`. +(finding H3). See `docs/MESHBAY_DESIGN.md` §3.4. """ from __future__ import annotations @@ -63,7 +63,7 @@ DEFAULT_DEVICE_REQUEST_TTL = 3600 _SCHEMA = """\ -- One row per DEVICE, not per person. A browser and a desktop client are two -- keys belonging to one account, and `user_id` alone as the key made the second --- silently overwrite the first (INSERT OR REPLACE). See docs/desktop-client-v1.md §4. +-- silently overwrite the first (INSERT OR REPLACE). See docs/MESHBAY_DESIGN.md §3.3. CREATE TABLE IF NOT EXISTS identities ( user_id TEXT NOT NULL, username TEXT NOT NULL, @@ -81,7 +81,7 @@ CREATE TABLE IF NOT EXISTS identities ( -- transcript binds `nonce_node` — the approving connection's handshake -- nonce — so even a stored signature is unverifiable without it. -- - -- This is what Tier 2 needs (docs/desktop-client-v1.md §4.8): relayed with + -- This is what Tier 2 needs (docs/MESHBAY_DESIGN.md §3.3): relayed with -- the roster, it lets a member verify for themselves that a second device -- belongs to an account whose first device they have already pinned, -- instead of taking the node's word. Verified and discarded until @@ -407,7 +407,7 @@ class Roster: Every live device of every active member of one group, with the evidence that admitted it. - For Tier 2 (`docs/desktop-client-v1.md` §4.8), and therefore + For Tier 2 (`docs/MESHBAY_DESIGN.md` §3.3), and therefore **member-visible** — unlike `list_identities`, which answers the operator. Two consequences of that, and both are the price of the feature rather than oversights: @@ -750,10 +750,10 @@ class Roster: return apps # The TMDB credential and query language are one operator's budget, not a - # per-group concern (docs/mediacenter.md §5.5) — stored under the + # per-group concern (docs/MESHBAY_DESIGN.md §9.7) — 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 + # user_id)` authorizing the operator node-wide (docs/MESHBAY_DESIGN.md + # §6.1). Unset means "the shipped default token, TMDB's own default # language" — the same "absent means the old behaviour" discipline # enabled_apps already follows. # @@ -791,7 +791,7 @@ class Roster: # Which folder(s) inside the group's shared roots each application uses as # its entry point. One storage shape for every app, keyed by the app's own # name, so adding an application needs no change here at all — that is the - # whole point of the plugin architecture (docs/refactor-groups.md §1.6). + # whole point of the plugin architecture (docs/MESHBAY_DESIGN.md §9.3). # # Always a JSON list, even for an app that only ever wants one directory. # Two shapes for one idea is how `video_root` (scalar) and `photo_roots` diff --git a/packages/meshbay-node/src/meshbay_node/tmdb.py b/packages/meshbay-node/src/meshbay_node/tmdb.py index c9623df..9a0db12 100644 --- a/packages/meshbay-node/src/meshbay_node/tmdb.py +++ b/packages/meshbay-node/src/meshbay_node/tmdb.py @@ -1,7 +1,7 @@ """ TMDB (themoviedb.org) client for the Videos group app. -Called only by the node, never by a client (docs/mediacenter.md §2): the +Called only by the node, never by a client (docs/MESHBAY_DESIGN.md §6.5): the node holds the one credential and makes the one request per unique title, shared by every member. Token resolution order (§5.5): @@ -11,9 +11,9 @@ shared by every member. Token resolution order (§5.5): exception, so a node with no token configured just serves thumbnails) The real secret (whichever token resolves) never appears in source control: -there is no literal fallback value in this file. See mediacenter.md's -implementation notes on why a shipped default is a deployment concern, not -a code concern. +there is no literal fallback value in this file. Videos is the one +credentialed application (docs/MESHBAY_DESIGN.md §9.7, §9.8), and which +token ships is a deployment concern, not a code concern. Results also come back in whatever language the operator configured (roster.py's `tmdb_language`, e.g. "fr-FR") — one language for the whole @@ -75,7 +75,7 @@ def _best_match( top result is what's returned. The similarity ratio rides along purely as a confidence signal for the caller's fallback decision. - One narrow exception (§10.1/V9): when the top result is *not* a + One narrow exception (V9): when the top result is *not* a confident textual hit (ratio < 0.6) and a `year` was requested, a different result of that **exact** release year is preferred. TMDB already year-filtered the search, so an entry landing on the requested 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 3bb0df7..4540f2f 100644 --- a/packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py +++ b/packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py @@ -174,7 +174,8 @@ USER_BLOB_ACCOUNT_MAX = 8 * 1024 * 1024 _USER_BLOB_KIND_RE = re.compile( r"^(playlists|playlist:[A-Za-z0-9_-]{1,64})$") -# Chat link-preview results, kept in memory only (draft-v6 §2.7: the node +# Chat link-preview results, kept in memory only (docs/MESHBAY_DESIGN.md §6.5: +# the node # produces enrichment on demand and keeps nothing durable — the asking device # caches). Bounded and time-limited so a busy group cannot grow it without end # and a page that changed its card is picked up within the hour. @@ -998,7 +999,7 @@ class WebRTCPeerSession: self._ctx.get("daemon_state", {}).get("tmdb_token_customized", False)), "tmdb_language": str( self._ctx.get("daemon_state", {}).get("tmdb_language") or ""), - # Music app (docs/musicbay.md §6) — same shape as the TMDB + # Music app (docs/MESHBAY_DESIGN.md §9.8) — same shape as the TMDB # fields above. No language field: MusicBrainz search doesn't # take one the way TMDB does. "musicbrainz_enabled": bool(self._group_ctx().get("musicbrainz_enabled", True)), @@ -1174,7 +1175,7 @@ class WebRTCPeerSession: } # The recovery-wrapped copy (MNP 0.14) rides along when present, so a # client holding the recovery key can re-wrap it under a new - # passphrase — docs/auth-confirm.md §4.5. + # passphrase — docs/MESHBAY_DESIGN.md §3.6. if kp.get("bundle_enc_recovery"): resp["bundle_enc_recovery"] = kp["bundle_enc_recovery"] self._send(resp) @@ -1558,7 +1559,7 @@ class WebRTCPeerSession: # A person may hold several devices on one node. The authority admitting a # new one is a key the node already pinned — never the hub, which has stored # no user keys since 2026-08-14 and therefore cannot countersign anything. - # See docs/desktop-client-v1.md §4. + # See docs/MESHBAY_DESIGN.md §3.3. async def _do_device_request(self, msg: dict) -> None: """ @@ -1719,7 +1720,7 @@ class WebRTCPeerSession: # # This is what lets another member check for themselves that this device # belongs to an account whose earlier device they have already pinned, - # instead of taking the node's word (Tier 2, desktop-client-v1.md §4.8). + # instead of taking the node's word (Tier 2, docs/MESHBAY_DESIGN.md §3.3). await roster.pin_identity( user_id=self._user_id, username=self._username or "", pk_ed25519=pk_ed_b64, pk_x25519=pk_x_b64, via="device", @@ -1746,7 +1747,7 @@ class WebRTCPeerSession: What is checked, in order: the key is a live device *of this account* in the node's own roster (never a token claim — that is - `per-node-identity-v1.md`'s rule), the timestamp is fresh, and the + `docs/MESHBAY_DESIGN.md` §3.2's rule), the timestamp is fresh, and the signature verifies over a transcript naming this node, this group and this connection's nonce. A key that is merely well-formed proves nothing. @@ -1964,7 +1965,7 @@ class WebRTCPeerSession: Admission policy for a group, read from the node's own configuration. Never from the hub: a hub that could declare a group open would be handed - the key to it (§3.4 of docs/invite-pairing-v1.md). + the key to it (docs/MESHBAY_DESIGN.md §3.4). """ gctx = (self._ctx.get("groups") or {}).get(group_id) or {} return gctx.get("join_policy", "invite") @@ -2290,9 +2291,9 @@ class WebRTCPeerSession: # include "video" or "music" — both can make outbound third-party # network calls (TMDB, MusicBrainz) once enabled, so an operator opts a # group in explicitly rather than getting it for free - # (docs/mediacenter.md §5.6, docs/musicbay.md §4.4). - # `helloworld` is the reference implementation (docs/refactor-groups.md - # §4.1), hidden client-side behind `?dev=1`. It is here because the + # (docs/MESHBAY_DESIGN.md §9.7, §9.8). + # `helloworld` is the reference implementation (docs/MESHBAY_DESIGN.md + # §9.4), hidden client-side behind `?dev=1`. It is here because the # allow-list is server-side enforcement — a client that names an app this # node does not know is refused — and an app the node refused could not # demonstrate anything. This entry and the client's registry line are the @@ -2363,7 +2364,7 @@ class WebRTCPeerSession: per-group concern (see _do_tmdb_enabled for the per-group on/off switch). Signed like the rest: this changes outbound third-party network traffic the node did not have before the Videos app - (docs/mediacenter.md §5.5, §8) — an unsigned change would let any + (docs/MESHBAY_DESIGN.md §9.7, §6.5) — an unsigned change would let any member alter egress the operator never agreed to. """ token = msg.get("token") @@ -2746,7 +2747,7 @@ class WebRTCPeerSession: def _do_musicbrainz_enabled(self, msg: dict) -> None: """ Whether MusicBrainz lookups run for this group at all. Per-group - from the start (docs/musicbay.md §3.2/§6) — signed like + from the start (docs/MESHBAY_DESIGN.md §9.8) — signed like tmdb_enabled: it decides whether this group's members' Music tab ever makes outbound MusicBrainz traffic. """ @@ -3813,13 +3814,13 @@ class WebRTCPeerSession: self, thumb_hash: str, chunk_index: int, gek: bytes | None, ) -> dict | None: """ - docs/mediacenter.md §5.3: a thumbnail is served through the same + docs/MESHBAY_DESIGN.md §6.5: a thumbnail is served through the same chunked file_req path as a real file, resolved against the media cache instead of the index when the id doesn't match a file. Sliced by `chunk_index` like a real file's chunks, not just handed back whole: a thumbnail/poster/cover never approached CHUNK_SIZE so this used to be equivalent to "only chunk 0 exists", but an audio - transcode result (docs/musicbay.md, the WMA/Musepack exception) is + transcode result (docs/MESHBAY_DESIGN.md §9.8, the WMA/Musepack exception) is cached in the same media_cache blob store and can be several MB — genuinely multi-chunk, same as a file read straight off disk. """ @@ -3939,7 +3940,7 @@ class WebRTCPeerSession: async def _fetch_and_cache_poster(media_cache, tmdb_client, poster_path: str | None) -> str | None: """ Downloads a TMDB poster/backdrop once, caches it under its own - blake3 like a video thumbnail (docs/mediacenter.md §5.4), and + blake3 like a video thumbnail (docs/MESHBAY_DESIGN.md §9.7), and returns the hash a client then fetches via the normal file_req/ chunk path (§5.3) — no client ever contacts image.tmdb.org directly. @@ -3988,7 +3989,7 @@ class WebRTCPeerSession: async def _do_audio_transcode_request(self, msg: dict) -> None: """ - docs/musicbay.md's one exception to "no node-side transcode pool": + docs/MESHBAY_DESIGN.md §9.8's one exception to "no node-side transcode pool": WMA and Musepack tag/cover fine (enrich_audio.py) but decode in no mainstream browser's <audio> element at all. Transcoded to AAC/M4A once and cached under its own content hash — same "computed once, @@ -4195,7 +4196,7 @@ class WebRTCPeerSession: async def _do_music_meta_request(self, msg: dict) -> None: """ - docs/musicbay.md §4.3: MusicBrainz metadata for one track, resolved + docs/MESHBAY_DESIGN.md §9.8: MusicBrainz metadata for one track, resolved from the group's index by its content id. Album-level (release), the direct analogue of Videos' show-level TMDB caching: one search per (artist, album) pair serves cover art and canonical naming to every @@ -4275,7 +4276,7 @@ class WebRTCPeerSession: async def _do_media_meta_request(self, msg: dict) -> None: """ - docs/mediacenter.md §5.4: TMDB metadata for one file, resolved from + docs/MESHBAY_DESIGN.md §9.7: TMDB metadata for one file, resolved from the group's index by its content id (root+relpath the client already knows from index_sync/index_delta identify the entry; its own `id` is what actually names one file — never a raw filesystem path off @@ -4300,7 +4301,7 @@ class WebRTCPeerSession: media_cache = self._ctx.get("media_cache") tmdb_client = self._ctx.get("tmdb_client") - # Per-group, not node-wide (docs/mediacenter.md §5.5, 2026-08-24): + # Per-group, not node-wide (docs/MESHBAY_DESIGN.md §9.7, 2026-08-24): # treated exactly like "no client configured" — same silent, no-error # degradation, since a member's Videos tab already has to handle "no # TMDB match" as the ordinary case. @@ -4409,7 +4410,7 @@ class WebRTCPeerSession: return media_cache = self._ctx.get("media_cache") tmdb_client = self._ctx.get("tmdb_client") - # Per-group, not node-wide (docs/mediacenter.md §5.5, 2026-08-24) — + # Per-group, not node-wide (docs/MESHBAY_DESIGN.md §9.7, 2026-08-24) — # same silent zero-confidence degradation as "no client configured". if (media_cache is None or tmdb_client is None or not self._group_ctx().get("tmdb_enabled", True)): @@ -4447,7 +4448,7 @@ class WebRTCPeerSession: async def _do_tmdb_search_request(self, msg: dict) -> None: """ Candidate TMDB matches for an operator correcting a wrong automatic - match (docs/mediacenter.md, §V-whatever this becomes) — a plain + match (docs/MESHBAY_DESIGN.md §9.7, §V-whatever this becomes) — a plain lookup, not a mutation, so unlike `tmdb_override` this needs no admin authority: any member can see what TMDB itself would offer, the same as the automatic search already silently does on their @@ -4460,7 +4461,7 @@ class WebRTCPeerSession: return media_cache = self._ctx.get("media_cache") tmdb_client = self._ctx.get("tmdb_client") - # Per-group, not node-wide (docs/mediacenter.md §5.5, 2026-08-24) — + # Per-group, not node-wide (docs/MESHBAY_DESIGN.md §9.7, 2026-08-24) — # same silent empty-results degradation as "no client configured": # a member with TMDB off for this group sees the same "type it in # yourself" affordance either way, never an error. @@ -4593,7 +4594,7 @@ class WebRTCPeerSession: def _do_tmdb_rematch(self, msg: dict) -> None: """ An operator dropping one file's cached TMDB match so it re-resolves - with the current matcher (§10.1/V13) — the one-click alternative to + with the current matcher (V13) — the one-click alternative to the full search-and-pick "Fix match" flow, and reachable without SSH (`meshbay-node video rematch` clears a whole group). Signed like `tmdb_override`: `media_cache` is shared node-wide. @@ -4635,7 +4636,8 @@ class WebRTCPeerSession: async def _tmdb_search(self, tmdb_client, entry, is_show: bool): """ - §3.3's retry ladder — same shape for movies and shows (§10.1/V8). + docs/MESHBAY_DESIGN.md §9.7's scored ladder — same shape for movies + and shows (V8). TMDB's own top result is still trusted per query (§3.3's last row — no local re-ranking of *its* list); what the ladder adds is that it *scores every candidate query* and keeps the best, instead of @@ -4681,7 +4683,7 @@ class WebRTCPeerSession: specific than a punctuation-normalised restatement of `primary` (an alternative_title, a sequel variant); when it does not and the primary hit is already decent, the remaining calls are skipped - (§10.1/V11 — they almost never win and cost a round trip each). + (V11 — they almost never win and cost a round trip each). """ def _year_of(res: dict) -> int | None: d = str(res.get("release_date") or res.get("first_air_date") or "") @@ -5124,7 +5126,8 @@ class WebRTCPeerSession: async def _do_link_preview_request(self, msg: dict) -> None: """ - Unfurl a URL a member pasted into chat (draft-v6 §2.7 enrichment rule: + Unfurl a URL a member pasted into chat (docs/MESHBAY_DESIGN.md §6.5's + enrichment rule: the client asks, the node produces on demand, the asking device caches — nothing durable here). @@ -5895,13 +5898,14 @@ class WebRTCPeerSession: # anything failing loudly: a file uploaded from a phone could not be # deleted from the same person's laptop, and the only symptom was # "Signature verification failed" on their own file - # (docs/desktop-client-v1.md §4.8 A). + # (docs/MESHBAY_DESIGN.md §3.3). # # `uploader_pk` is kept, and stops being the authorization key: it is # now the audit record of *which device* did it. Authorization is by # account, through the roster — never through a token claim, which is - # the protection `per-node-identity-v1.md` added and which a lookup by - # `uploader_id` in the hub's world would give straight back. + # the protection per-node identity keys give (docs/MESHBAY_DESIGN.md + # §3.2) and which a lookup by `uploader_id` in the hub's world would + # give straight back. if not (await self._verify_admin_sig(transcript, sig) or await self._verify_uploader_sig(entry, transcript, sig)): self._send({"type": "error", "detail": "Signature verification failed"}) @@ -5915,7 +5919,7 @@ class WebRTCPeerSession: ) -> None: # Node operator only. A group admin who does not run the node has no # authority over who this node admits (deny by default). Delegation is - # designed but deferred — see §6.2 of docs/invite-pairing-v1.md. + # designed but deferred — see docs/MESHBAY_DESIGN.md §3.4. if not await self._verify_admin_sig(transcript, sig): self._send({"type": "error", "detail": "Signature verification failed"}) self._audit("admin_auth_failed", f"invite_create:{pending['subject'][:16]}") @@ -6220,7 +6224,7 @@ class WebRTCPeerSession: # node's answer to it, since ffmpeg re-encodes these in real time on # any machine that can run this daemon. Reported live against an # Xvid/MP3 .avi. `transcode_incompatible_video`'s own documentation - # (draft-v6 §2.11) already said "HEVC *and other browser- + # (docs/MESHBAY_DESIGN.md §6.8) already said "HEVC *and other browser- # incompatible video codecs*"; only HEVC was ever wired up. can_copy = (bool(codec_str) and raw_video_codec not in BROWSER_INCOMPATIBLE_VIDEO_CODECS) @@ -6906,9 +6910,9 @@ class WebRTCTransport: `ctx["_transcode_sem"]`, and `hasattr(webrtc, "_stream_sem")` is always False. So the hot-swap was a no-op and **`max_concurrent_streams` has never taken effect from the Node page without a restart**, contrary to - draft-v6 §2.11. This is the one implementation, on the object that owns - the state, so the next two caps do not each grow their own copy of the - mistake. + docs/MESHBAY_DESIGN.md §6.8. This is the one implementation, on the + object that owns the state, so the next two caps do not each grow their + own copy of the mistake. What resizing means, stated because it is a decision and not a detail: **the new cap governs new streams; the ones already running are diff --git a/packages/meshbay-node/src/meshbay_node/ui/app.py b/packages/meshbay-node/src/meshbay_node/ui/app.py index 96fa137..77491a0 100644 --- a/packages/meshbay-node/src/meshbay_node/ui/app.py +++ b/packages/meshbay-node/src/meshbay_node/ui/app.py @@ -9,7 +9,7 @@ are both clients of it. (Chat is served to browsers over MNP/WebRTC, not here.) Served only on 127.0.0.1 — never network-exposed — and every request is gated by a per-run session token (11.5.3) written to `<data_dir>/ui-token`. There is no server-rendered UI: the Node page ships in the desktop client (see -`docs/refactor-node-ui.md`). +`docs/MESHBAY_DESIGN.md` §6.7). """ import logging |