diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-08-24 10:04:46 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-08-24 10:04:46 +0200 |
| commit | 6af05abf410bbd038ce7fa6915a659defc509071 (patch) | |
| tree | 09b1c941fa446b077ff51282fa18250998528263 /docs/mediacenter.md | |
| parent | c4981454078a59f776d484f0f1828f2fc5eaad09 (diff) | |
| download | meshbay-6af05abf410bbd038ce7fa6915a659defc509071.tar.gz | |
feat(node,hub): add Videos group app (poster grid, flat list, TMDB metadata)
Implements docs/mediacenter.md: a "Videos" group application built on the
existing files index rather than a separate catalogue. On the node side,
new indexer enrichment (technical probe, filename/season parsing, thumbnail
generation) runs per-file once an operator has chosen a video_root for the
group, plus a TMDB client for on-demand poster/metadata lookups (never
client-side, thumbnails delivered over the existing chunk path). On the hub
side, a new video-app.js renders a lazily-mounted poster grid or a
thumbnail-only flat list, with TMDB entirely optional per group.
Along the way: the global apps registry now drives Settings' default-tab
picker instead of a hardcoded list, and the video_root is configured from
group Settings (like uploads) rather than from Files, with the node
refusing to run any TMDB/thumbnail work until one is set.
Fixes several bugs found via live testing against a real library, notably
a race between two effects writing the same "image ready" state that could
leave a poster grid spinning forever on a same-tab revisit — see
mediacenter.md §5.4 for the full account of each one.
Diffstat (limited to 'docs/mediacenter.md')
| -rw-r--r-- | docs/mediacenter.md | 166 |
1 files changed, 165 insertions, 1 deletions
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. |