summaryrefslogtreecommitdiffstats
path: root/docs
diff options
context:
space:
mode:
Diffstat (limited to 'docs')
-rw-r--r--docs/apps.md32
-rw-r--r--docs/desktop-client-v1.md10
-rw-r--r--docs/mediacenter.md166
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.