diff options
Diffstat (limited to 'docs')
| -rw-r--r-- | docs/apps.md | 32 | ||||
| -rw-r--r-- | docs/desktop-client-v1.md | 10 | ||||
| -rw-r--r-- | docs/mediacenter.md | 166 |
3 files changed, 193 insertions, 15 deletions
diff --git a/docs/apps.md b/docs/apps.md index 94116c6..931114d 100644 --- a/docs/apps.md +++ b/docs/apps.md @@ -6,7 +6,7 @@ > document is the how-to. A group has "applications" — Chat and Files today, Videos/Music/Photos planned -(Netflix-style browsing, Spotify-style playback, an album viewer). None of the +(a poster-grid browser, a music player, an album viewer). None of the planned ones need an MNP protocol change: video/audio/image files are already classified by the node's indexer (`meshbay_node/indexer/indexer.py`, `type: video|audio|image`) and flow through the same `index_sync`/`file_req`/ @@ -172,18 +172,24 @@ the only node-side touches, and both are allow-lists, not new wire messages. ## 5. What does not exist yet -- **Thumbnails/posters.** No generation mechanism, client or node side. A - Netflix-style grid or a photo album needs *something* here; the pragmatic - v1 (decided but not built) is lazy, client-side: decrypt the image, or a - video's first segment, only for tiles actually visible, with an in-memory - cache — no new MNP message, no node-side store. This is real per-tile cost - (a full chunk-pipelined decrypt per thumbnail), acceptable for a browsing - grid, not for hundreds of tiles rendered at once — a virtualized grid is - part of building Videos/Photos, not optional. -- **Videos, Music, Photos themselves.** Deliberately out of scope for the - refactor that built this — see `meshbay-draft-v6.md` §2.7. The - infrastructure above is proven end-to-end with the two apps that already - existed (Chat, Files); a new one is additive. +- **Thumbnails/posters — built for Videos, 2026-08-23, see `docs/mediacenter.md`.** + The plan below (lazy, client-side, no node-side store) turned out to be + wrong once a real design pass ran the numbers: `docs/mediacenter.md` §2 + revises `desktop-client-v1.md`'s O12 and has the node generate thumbnails + (an `ffmpeg` frame grab, its own bounded worker pool) and cache them + durably in its own `data_dir`, delivered over the existing `file_req`/ + chunk path addressed by their own blake3 hash. TMDB posters/metadata are + fetched and cached by the node the same way — no client ever talks to + TMDB directly. A virtualized grid (`IntersectionObserver`-based lazy + mount) is built in `video-app.js`, per the note below. A future Photos + app can reuse the same node-side machinery (thumbnail cache, chunk-path + delivery) without re-deciding any of this. +- **Videos, Music, Photos themselves.** Videos is now built (`video-app.js`, + `docs/mediacenter.md`). Music and Photos remain deliberately out of scope + — see `meshbay-draft-v6.md` §2.7. The infrastructure in this document was + proven end-to-end first with Chat/Files, then with Videos; Music/Photos + are additive from here, and can reuse Videos' thumbnail/chunk-delivery + machinery rather than re-deciding it. - **The offline/loopback settings path.** `member_upload` can be toggled two ways: over a live MNP connection, or (Electron only) via the node's local HTTP API when MNP isn't connected (`platform.node.call('PUT', .../member- diff --git a/docs/desktop-client-v1.md b/docs/desktop-client-v1.md index 08dd548..c11837c 100644 --- a/docs/desktop-client-v1.md +++ b/docs/desktop-client-v1.md @@ -733,6 +733,14 @@ Detecting it differs by platform, and neither method is exotic: ### 6.10 The planned libraries are views, not a catalogue +> **Revised 2026-08-23 for the Videos group app** — see `docs/mediacenter.md` §2. +> The "no thumbnail store, ever" conclusion below (O12) was an extension by +> analogy to draft-v5 §5.2, not a direct consequence of it. The Videos app +> now caches thumbnails and TMDB metadata durably in the node's own +> `data_dir` — still never inside a shared root, and still no per-file +> identity beyond the file's own content hash. Everything else in this +> section stands: views over the index, not a catalogue. + **Scope settled 2026-08-17, and it is deliberately small.** The planned "video library" and "audio library" are **alternative views over the existing file index** — a variant of the explorer. Presentation rests on the filename and the directory structure, plus a @@ -1124,7 +1132,7 @@ Two rules came out of building it and both are easy to undo by accident: | O10 | Canonical file identity across filesystems | Case folding and NFC normalization decide when two names are "the same file". It affects the index, the no-overwrite rule and cross-platform replication, so it must be defined once and shared (§6.8) | | O11 | Root alias when the basename cannot be used | Duplicate basenames across volumes will be common, and a basename can fail the portability rules. Refusing is correct; "rename the folder on your disk" is a poor answer, so an explicit alias is the escape hatch (§6.7) | | O13 | Hub identity pinning | The client points at a hub by URL and nothing pins that hub's identity. `GET /v1/hub/pubkey` exists; pinning it on first use — as `pk_node` already is (11.5.8) — would stop an account's hub being silently swapped. Bounded today, because a substituted hub cannot read content and no longer ships the code; worth doing all the same | -| ~~O12~~ | Derived thumbnails vs draft-v5 §5.2 | **Resolved 2026-08-17** — frame grab produced on demand by the node, cached on the requesting device. The node keeps no thumbnail store, so §5.2 stands unchanged (§6.10) | +| ~~O12~~ | Derived thumbnails vs draft-v5 §5.2 | **Resolved 2026-08-17, revised 2026-08-23** — frame grab produced on demand by the node, cached on the requesting device. The node keeps no thumbnail store, so §5.2 stands unchanged (§6.10). **Revision:** for the Videos group app, the node now also caches thumbnails and TMDB metadata durably in its own `data_dir` (not a shared root) — see `docs/mediacenter.md` §2 for the reasoning (TMDB quota is per-node not per-device; thin clients benefit from a node that does more) | --- diff --git a/docs/mediacenter.md b/docs/mediacenter.md index e3054f7..a165d64 100644 --- a/docs/mediacenter.md +++ b/docs/mediacenter.md @@ -283,6 +283,45 @@ preference with no security or authority implication, so it does not need the `roster.db` + signed-op treatment `member_upload`/`apps_enabled` get. Default: Mode A, per the operator's own spec. +### 4.4 Scoping to a folder — added 2026-08-24, this was in §0 and got missed + +The operator's original ask was explicit: pick an already-shared directory, +possibly a subdirectory of one already exposed — not necessarily the whole +group. This did not make it into the plan when the rest of this document was +written; caught only once the feature was in real use against an existing +group. + +**Unlike the mode toggle above, this is not a per-device display +preference** — it decides what *every* member's Videos tab shows, the same +class of decision `apps_enabled`/`member_upload` already are. So it gets the +same treatment: a per-group setting in `roster.db` (`video_root`, empty +string meaning "the whole group index" — today's behaviour, unchanged), +changed by a new signed op (`OP_VIDEO_ROOT`, `video_root`/`video_root_ack` +on the wire) that any already-connected member is told about without +reconnecting, exactly like `apps_enabled_ack`. + +**Where the operator sets it: Files, not Settings.** The folder being +scoped is something the operator is already looking at while browsing — +Files gets a toolbar action that toggles between "Set as Videos root" (any +other folder) and "Clear Videos root" (the folder that already is one, or +the top level when one is set deeper in the tree). No separate path-picker +UI was built; browsing *to* the folder in the browser used for browsing +everything else in the group is the picker. + +**Validation happens before a signature is ever asked for**, the same +principle as apps_enabled's "empty set refused up front": a candidate path +is resolved against the group's actual `RootSet` (the same traversal-safe +resolution `entry_abs_path`/directory creation already use) and must name a +real, currently-readable directory, or the request is refused immediately +— a stale or mistyped path never reaches the operator's browser as a +signing prompt. + +The client-side filter is a plain path-prefix test against `IndexEntry.path` +(already root-relative, e.g. `"Séries/OVNI/S1/ep01.mkv"`) — no new index +field, no server-side filtering; the node still sends the whole index, and +narrowing to the configured folder is exactly the same kind of view-over- +the-index work Files' own current-folder filter already does. + --- ## 5. Protocol and index changes @@ -356,6 +395,80 @@ own blake3 in `data_dir`, and clients pull it exactly like a video thumbnail. No client ever contacts `image.tmdb.org` or `api.themoviedb.org` directly (§2). +**Bug found live, 2026-08-24**: `transport.js`'s generic reply routing +resolves an untyped response against whichever request is *oldest pending* +— correct for every request the client only ever has one of in flight, but +`media_meta_req` is fired once per visible poster-grid tile, several at a +time (apps.md §5's virtualization), exactly the shape `file_req` already +special-cases with a `chunk:{file_id}:{chunk_index}` key. Without the same +treatment, two concurrent lookups could resolve to each other's answer +whenever their responses arrived out of order — reproduced live as two +shows' posters/season lists swapping across reloads. Fixed by keying +`media_meta_req`/`media_meta_resp` by `path`, the same way `file_req` is +keyed by id+chunk. + +**Bug found live, 2026-08-24**: a poster-grid tile used to render its raw +per-file frame immediately (already known from the index, no network round +trip needed), then swap it for the TMDB poster once `media_meta_resp` +arrived — and, for a §V6 merge candidate, briefly render as its own +standalone card before collapsing into its sibling once both resolved to +the same `tmdb_id`. Individually harmless, but visibly distracting in +sequence (reported as "hurts the eyes"). `video-app.js`'s `PosterCard` now +withholds a tile's content entirely — title, subtitle, and image alike — +until both the metadata lookup and the chosen image (poster or fallback +frame) have actually settled, showing a spinner in the meantime; a slow +lookup (a very large, freshly-scanned library) just means the spinner +stays a little longer. This does not (and structurally cannot, without +giving up per-tile virtualization, apps.md §5) suppress every trace of a +§V6 merge: a rare split-release show can still show two fully-resolved +cards for an instant before they collapse into one, since the merge is +only known once both siblings' independent lookups return. + +**Bug found live, 2026-08-24**: an operator reported the poster grid +hanging on spinners forever on a *second* visit to the same group's Videos +tab within one page session (revisit the tab, or a full reload — either +way, reproducibly). Two real, separate causes: + +1. `_fetch_and_cache_poster` (`webrtc_server.py`) downloaded a TMDB poster/ + backdrop from `image.tmdb.org` on *every* `media_meta_req`, even for a + file whose TMDB match was already cached — nothing had ever checked "is + this poster_path already fetched" before making the network call, since + the content-addressed `thumb_hash` isn't known until the bytes already + are. Fixed by keying the `thumbs` cache on a synthetic `tmdb:{poster_path}` + id, checked before the request (`media_cache.get_thumb_hash_by_file_id`). +2. §V6's merge used to mint a brand-new key (every constituent title, + joined) the instant two raw groups turned out to be the same show — + destroying an already-visible, already-resolved card and rebuilding one + from scratch. Fixed by keying the merged entry as `groups[0].title` + instead: the overwhelmingly common (unmerged) case is unaffected, and a + merge now updates an existing card's props rather than replacing it. + +Neither of those was the actual hang, though both were real, worth fixing, +and made it worse. The hang itself was a race in `PosterCard`'s own +"nothing shown until ready" gate (§ above): `imageReady` was a `useState` +reset to `false` by its *own* `useEffect` keyed on `posterHash`, and set to +`true` by `MediaThumb`'s `onReady` callback. The instant `metaReady` flips +true, `posterHash` jumps from the raw fallback frame to the resolved +poster **in the same commit** that mounts `MediaThumb` for it. If that +poster's bytes are already in `MediaThumb`'s session-lifetime blob cache — +exactly true on a same-tab revisit, since that cache is never cleared — +its `onReady` fires *synchronously* from that mount effect. Effects run +children-first, so `PosterCard`'s own "reset on posterHash change" effect +fired *after* it in that same commit, unconditionally overwriting the flag +back to `false` — with nothing left to ever set it `true` again, since +`posterHash` doesn't change a second time. The card was stuck as a +spinner forever despite the image already being fully loaded and cached. +Fixed by deriving `imageReady` from a direct comparison — +`readyHash === posterHash`, where `onReady` sets `readyHash` — rather than +a second, separately-timed writer of the same boolean: whichever of +"meta resolves" and "image confirms ready" happens first, in whichever +order effects run, the next render sees the same, correct answer. A cold +fetch (no reload, or a genuinely new poster) never hit this at all, +because `onReady` there fires asynchronously, in a later commit, well +after the reset had already settled — which is exactly why it only ever +showed up on a revisit, and only after the two fixes above had already +made a revisit's fetch fast enough to *be* synchronous. + ### 5.5 Node-wide config, not per-group TMDB is one operator's budget and one credential, not a per-group concern, @@ -373,6 +486,41 @@ means the shipped default token, present with a token means "use this one instead" — the same "absent means the old behaviour" discipline `member_upload`/`apps_enabled` already follow. +**Added 2026-08-24**: a third field, `tmdb_language` (e.g. `"fr-FR"`), +travels the same way — one node-wide setting, not a per-viewer request. +Live against a real French-language show, the node's TMDB matches came back +in English by default (`"UFOs"` for a show whose real title and audience +are French) purely because no language was ever requested; TMDB honors a +`language` parameter on every search/details call, so `tmdb.py`'s `_get()` +now attaches whichever language is configured (or none, TMDB's own English +default, if unset) to every request transparently — no change needed at +any call site. The Settings UI pre-fills this field from the operator's own +current browser language (a sensible default, not an attempt to read their +hub-account profile, which the node has no way to learn), and the operator +can still change it. This does not by itself fix an outright wrong match +(a real, separate accuracy question, §3) — only which language a *correct* +match's title/overview/poster come back in. + +**Correction, same day**: it turned out to fix more than that. Live, the +French query didn't just translate the operator's example show — it +resolved season 1's match correctly for the first time (previously a wrong, +unrelated film in the same fallback slot, §3's kind of miss). A likely +explanation, not a guaranteed one: TMDB's own search ranking can differ by +language, and the French title happened to disambiguate what the English +query could not. Not a substitute for §3's accuracy work — a French-only +title with no ambiguity would not be helped this way — but a real, +observed improvement worth having independent of translation. + +**Per-field English fallback, added same day**: TMDB does not fall back to +English server-side for a field with no translation in the requested +language — it returns `""` (or `[]`) for that one field, confirmed live +(a French query left `overview` empty for a title TMDB has no French copy +of, despite a good poster and cast). The TMDB *website* papers over exactly +this gap client-side; `_tmdb_build_meta` (webrtc_server.py) now does the +same — a second, English-forced details fetch only when a checked field +(`overview`, `poster_path`, `genres`) comes back empty, merged field by +field so a good localized field is never overwritten by the fallback. + ### 5.6 `ALLOWED_APPS` `webrtc_server.py:1629` — add `"video"` to the frozenset. `DEFAULT_APPS` @@ -471,6 +619,7 @@ centralization was for (one computation, reused by every member). | V3 | TMDB metadata cache TTL (30 days proposed) | Needs no more than a config constant; not worth deciding without seeing real staleness complaints | | V4 | Multi-audio-track / subtitle-track surfacing in the detail view | Out of scope for this pass — `video-player.js`'s existing track handling is unchanged; Videos only adds discovery and metadata | | V5 | `Music`/`Photos` apps | Explicitly out of scope, per `apps.md` — this document only builds `video-app.js` and the shared node-side machinery (TMDB client, thumbnail cache, title parser) that a future audio/photo app could also reuse | +| ~~V6~~ | ~~Two folders of the same show, named by different release groups, can produce two separate poster-grid cards~~ | **Closed, 2026-08-24.** Rather than fuzzy title matching (real design decision, still deferred), `PosterGrid` now merges raw show-groups client-side once each group's TMDB lookup resolves to the same confident `tmdb_id` (`onMetaResolved` reports each `PosterCard`'s resolved meta upward; a `useMemo` groups by id, combining episodes/seasons into one card). This only merges what TMDB already agrees is one show — a genuinely unmatched show still gets its own card, which is correct. Confirmed live: `OVNIs.S01...`/`Ovnis-S01...` now render as a single "OVNI(s)" card with both seasons | ## 11. Acceptance before shipping @@ -484,4 +633,19 @@ centralization was for (one computation, reused by every member). 3. Verify cache pruning on file deletion actually fires (§2, §8) before calling the durable-cache reversal complete — an untested claim here would repeat the exact "documented but not built" pattern this project's - own history (CLAUDE.md) warns about repeatedly. + own history (CLAUDE.md) warns about repeatedly. Covered by an automated + test (`test_media_cache.py`), not just argued. + +**Done, 2026-08-24**: the full pipeline ran live against the operator's real +node and library (a real show, real TMDB token) — probe/parse/thumbnail +enrichment, TMDB search/match/credits, poster fetch-and-cache, chunk-path +delivery, and the poster-grid/flat-list/detail-modal UI, end to end, no +errors. One real bug was found and fixed this way that no unit test would +have caught: `DirectoryIndexer.initial_scan()` (run once at startup) never +itself triggers the `on_change` callback enrichment hangs off — daemon.py's +`_bg_scan` now explicitly calls `_on_index_change` after the startup scan +completes, or every file already on disk at boot (the common case) would +never get enriched, only files added later while the node runs. Covered by +`test_startup_scan_enrichment.py`. The exhaustive §3 corpus re-run (item 1 +above, item 2's >95% bar) is still open — this was a live smoke test of the +mechanism, not the accuracy study. |