diff options
| -rw-r--r-- | docs/mediacenter.md | 487 |
1 files changed, 487 insertions, 0 deletions
diff --git a/docs/mediacenter.md b/docs/mediacenter.md new file mode 100644 index 0000000..e3054f7 --- /dev/null +++ b/docs/mediacenter.md @@ -0,0 +1,487 @@ +# MeshBay — Videos application (design) + +> Status: **proposal**, prepared 2026-08-23, not implemented. Read `docs/apps.md` +> first — Videos is a new group application built on the plug-in mechanism +> described there, and this document assumes that mechanism rather than +> re-explaining it. Read `docs/desktop-client-v1.md` §6.10 before touching +> thumbnails or metadata — this document **revises decision O12** from that +> file, with the reasoning recorded in §2 below. +> +> Follows the project convention: every claim names the adversary it holds +> against (§8). + +--- + +## 0. What was asked, in one paragraph + +A streaming-service-style poster browser for a shared directory (or a subtree +of one), with two operator-selectable modes: (a) **poster-grid**, metadata +and artwork from TMDB, falling back to a thumbnail when TMDB match fails; (b) +**flat**, alphabetically sorted, thumbnail-based, folders as +categories/seasons, no TMDB. Both need a filename parser, evaluated in §3 +against a real local media library (~1950 files across three collections — +standalone movies, TV shows with nested season folders, and a small +single-franchise set) against a >95% bar. A default TMDB read token is +provided; an operator may supply their own. + +--- + +## 1. What this design does not reopen + +Everything already settled stands, and this plan is written to fit inside it, +not around it: + +- **Views over the index, never a catalogue** (`desktop-client-v1.md` §6.10, + draft-v6 §2.7). A file stays tied to its filesystem representation; nothing + here adds a second identity for a file, and nothing reaches the hub. +- **The apps plug-in mechanism** (`apps.md`): a new `video-app.js`, one + registry entry, one node-side `ALLOWED_APPS` entry, i18n keys, the asset + list, the two file-set tests. Enablement is a per-group, + operator-signed setting, same shape as `member_upload`. +- **Group-related server state lives on the node** (E9). Nothing in this + design puts a row on the hub. TMDB and thumbnail caches are node-local, + disposable, and rebuildable from the shared files plus TMDB — the same + status the file index's own hash/size already have, not the kind of + authority state E9 protects (membership, roster, uploads). +- **Filesystem portability** (§6.8): nothing here writes into a shared root. + See §2. + +## 2. Revision to decision O12 — where the cache lives, and why + +`desktop-client-v1.md` §6.10 (2026-08-17) decided thumbnails are computed +**on demand by the node** and cached **on the requesting device**, never +durably on the node. The stated reason was an analogy to draft-v5 §5.2's +chat-thumbnail rule ("the node stores no second copy of an image it already +holds"). Re-reading §5.2's actual text: it is about the **upload directory**, +and the chat case it names is one the **browser computes itself for free** +(resizing an image it already decrypted) — the node has no work to do at all. +A video thumbnail is not that: it needs `ffmpeg` to decode a frame, work only +the node can do, and §6.10 already conceded this ("a frame grab is strictly +less than the decoding the node already does for streaming"). So the +per-device-cache-only conclusion was an **extension by analogy** to a general +project preference for keeping the node free of derived state — a real, +deliberate stance (E9, "no metadata store, ever"), but a policy call, not a +consequence of §5.2 itself. + +**Revised here, on two concrete grounds raised by the operator:** + +1. **TMDB quota is per API token, not per device.** A token shipped inside + every install and called directly from every browser/Electron client + scales with the number of *devices* across every MeshBay deployment in + existence, hitting TMDB's rate limits far sooner than if each **node** + makes the calls on behalf of all its own members — the number of nodes is + the relevant denominator, and each node's own cache means one lookup per + unique title serves every member, indefinitely. +2. **Thin/future clients benefit from a node that does more, not less.** + `desktop-client-v1.md` §7.6 already flags Android as hostile to *hosting* + a node but fine as a *client*; a phone with limited storage and battery + should receive a small ready-made JPEG and a JSON blob, not decode video + or hold its own multi-gigabyte thumbnail cache. + +**What is decided:** + +- **TMDB metadata and posters/backdrops are fetched, cached and served by the + node.** Clients never call TMDB directly, from any platform. This also + removes a question this design would otherwise raise: the native client's + tested invariant is *"issues no request outside `/v1/` and the signaling + socket"* (`desktop-client-v1.md` §8.1) — if the client never talks to TMDB, + that invariant is untouched, no CSP change, no new egress from the + renderer. +- **Video thumbnails are generated by the node (unchanged) and now cached by + the node too**, not only handed over and forgotten. +- **The cache lives in the node's own `data_dir`** — a SQLite store or blob + files beside `chat.db`/`audit.db`/`bundle_store.db` — **never inside a + shared root**. This is not a compromise; it is strictly better than the + operator's original `.thumbs`-in-the-media-folder idea, and for reasons + independent of the sovereignty argument (§9): the node's own `data_dir` is + always writable by the process regardless of platform, whereas a shared + root is routinely a read-only backup mount, a NAS share the node has no + write access to, or an external exFAT/NTFS drive where a stray `.thumbs` + folder is not hidden by default on Windows and would appear as an ordinary + visible folder full of preview images of a private group's content to + anyone who plugs that drive into a different machine. Keeping the cache + entirely off the shared volume avoids that whole class of problem for + free, with no loss of the benefit the operator asked for (one computation, + reused by every member, every platform). +- **A visible, node-wide toggle exists to turn TMDB calls off entirely** + (`tmdb_enabled`, default on) for an operator who wants zero third-party + network traffic. This is new "outbound to a third party" behaviour the + node did not have before (§8), and it is disabled independently of the + Videos app itself being enabled. +- **Cache lifecycle is tied to the index.** A file's thumbnail cache entry + and its file→TMDB-id mapping are pruned when the indexer removes that + file's `IndexEntry` (the same event that already prunes the index itself). + TMDB metadata JSON is cached per TMDB id with its own refresh window (30 + days is a reasonable default — TMDB ratings/overviews do change) rather + than tied to any one file, since several files (episodes of a show) share + one show-level fetch. + +**Security read of this reversal (per adversary, full table in §8):** against +the node's own operator, nothing changes — they already hold every file in +plaintext and could compute all of this themselves; against another member or +an outside attacker, the same GEK-proofed MNP channel that already serves +files and streams now also serves metadata and thumbnails, no new +authorization surface. Keeping the TMDB token server-side is a **strict +improvement** over shipping it to every renderer: one place holds the +credential, not every device. + +`desktop-client-v1.md` should get a one-line pointer added at O12 when this is +implemented, noting the revision and pointing here — not done in this +document, which only adds the new design. + +--- + +## 3. Filename parsing — empirical study + +### 3.1 Method + +The operator's own library was used as the test corpus, listed on +2026-08-23: + +| Set | Files | Notes | +|---|---|---| +| movies collection | 261 | flat directory, mixed naming conventions, ~15 years of scene/tracker releases | +| TV shows collection | 1677 | 21 shows, nested up to `Show/SxxSeasonFolder/episode.mkv`, several non-standard layouts | +| single-franchise collection | 9 | small, single-franchise sanity check | + +`guessit` 4.4.0 (pure Python, MIT, pip-installable, actively maintained — +the de facto reference implementation for this exact problem, used by +Sonarr/Radarr-adjacent tooling) was installed in a scratch venv and run +against every bare filename. Its output was then validated for real against +the live TMDB API using the token the operator provided, with a small +Python harness: search by parsed title (+ year for movies), score the result +against the query with `difflib.SequenceMatcher`, and — this matters, see +§3.3 — retry with a more naively cleaned title before giving up. + +The harness and raw output are not committed; the corpus itself is a local +media library external to the repo. Re-running this validation against +the production parser, on the same corpus, is listed as an acceptance step +in §6. + +### 3.2 Results + +| Metric | Movies (261) | TV shows, show-level (21 shows / 1677 episodes) | +|---|---|---| +| `guessit` extracts a non-empty title from the bare filename | **100%** | 98.7% raw; the 1.3% gap is entirely episode files with no show name in the filename itself (e.g. `S08E02.SUBFRENCH.720p.mkv`) — recovered to 100% once the parent folder supplies the title (§3.4) | +| `guessit` extracts season+episode | — | 98.6% raw (same gap, same fix) | +| **Confident TMDB match, real network call** | **92.3%** (241/261), first pass, before the fixes in §3.3 | **100%** (21/21 shows, 1677/1677 episodes weighted) | + +Series clears the 95% bar outright. Films does not on the raw first pass — +every miss has an identified, generic (not per-title) cause, listed next. + +### 3.3 What the film misses were, and the fix for each + +| Cause | Example pattern | Fix | +|---|---|---| +| Real title lands in `guessit`'s `alternative_title` field, not `title` | A "Franchise Name + numeric code" filename (e.g. `Franchise.007.-.1999.-.The.Real.Subtitle...`) parses to `title="Franchise 007"`, `alternative_title="The Real Subtitle"` | **Search both fields**; prefer whichever gets a confident TMDB result | +| Hyphenated proper noun split at the hyphen | A hyphenated title immediately followed by a parenthesized year (e.g. `Hero-Name.(2002)...`) parses to `release_group="Hero"`, `title="Name"` (a known `guessit` tokenizer quirk before a parenthesized year) | Fallback tier: retry with **all** of `.`, `_`, `-` normalized to spaces (the naive fallback used in this test only normalized `.`/`_`, which is why this specific case still missed even with a fallback in place — noted as a bug in the test harness itself, not the parser design) | +| French edition vocabulary not in `guessit`'s (English-centric) edition list | Titles carrying `Version.Longue`, `remasterise`, `Original.Version` as a release tag | Extend the edition/tag stripping list with `version longue`, `version intégrale`, `remasterisé`/`remasterise`, `non censuré`, `original version` | +| Trailing sequel digit kept, real TMDB title has none or uses Roman numerals | A sequel filename ending in a bare `1`/`2` where the real TMDB title has no trailing digit, or uses a Roman numeral instead | Additional retry: same title **without** the trailing digit, and with it converted to a Roman numeral | +| The test's own scoring picked the wrong result | A show referenced by its acronym in the folder name: TMDB's own search genuinely returns the right show first for that acronym — confirmed by a direct re-query — but the harness re-ranked TMDB's top-5 by local string similarity and preferred a coincidentally closer-looking unrelated show | **Trust TMDB's own top-ranked result** rather than re-ranking locally; use local similarity only as a confidence signal for a "not the right one? search again" affordance, never to override the choice | +| No TMDB entry exists under any reasonable query | An obscure abbreviated title, unresolved even manually | Inherent, expected. Falls to the mandated fallback: thumbnail + minimally cleaned filename (§4.3) | + +None of these are per-title hacks; each is a generic rule. Applying them was +not re-run against the full corpus for this planning pass (that means writing +the production-quality parser, which is implementation, not planning) but +every miss in the 261-file set is accounted for by one of the six rows above, +which gives good confidence the >95% bar holds once built — to be confirmed +empirically in §6's acceptance step, on the same corpus, before shipping. + +### 3.4 Directory context — the one thing bare-filename parsing cannot give you + +Two real findings from the series corpus, both because a bare filename does +not carry the folder it lives in: + +- **Abbreviated folder names need corroboration from a file inside them.** + A show folder named after its acronym rather than its full name (with + episode files inside following the usual `Show.Name.SxxExx...` pattern) — + a design that only trusts a strict "does this look like the real title" + gate on the folder name alone is fragile the moment the query is + ambiguous, even when TMDB's own search actually does resolve the acronym + correctly (verified directly). **Rule:** derive the show title primarily + from `guessit` run on a representative **episode filename** inside the + folder (which reliably contains the full show name, per the table in + §3.2), and use the folder name only as a grouping key and a + manual-override anchor — never as the sole source of the TMDB query. +- **Non-standard season folders are real, and TMDB already models them.** + One show in the corpus numbers its episodes with a bare leading integer + and no `SxxExx` token anywhere, uses a French word (not "season") plus a + Roman numeral as its season-folder name, and keeps a sibling folder for + specials. Querying TMDB directly confirmed this is not an obstacle to + work around — **TMDB's own season list for that show uses the identical + non-English season-naming scheme**, with a `season_number: 0` entry for + specials that the sibling folder maps onto exactly. **Rule, kept generic:** + when no `SxxExx` token is found, look for a season-like ancestor folder + matching `season|saison|<local-language season word>\s*(\d+|[ivx]+)` + (Roman numerals included, vocabulary configurable per locale), and treat a + folder matching `bonus|extras|specials?` as season 0. Match that folder's + name against the show's own TMDB season list rather than assuming a + numeric convention everywhere. + +### 3.5 What the parser does **not** need to guess + +The node already probes real technical facts from the file itself via +`ffprobe` for streaming (`_probe_video()`, +`meshbay_node/transport/webrtc_server.py`) — resolution, codec, duration. +**None of that should come from the filename.** A file mislabeled `1080p` +that is actually 720p is a real, observed failure mode of filename-derived +metadata; ffprobe is authoritative and already runs on the node. The parser's +job is narrowed to exactly what TMDB search and the flat view need: title, +year, season, episode — a materially smaller surface than `guessit`'s full +scope (it also parses codecs, languages, release groups — useful for +debugging, not needed by either view). + +### 3.6 Where the parser runs + +**On the node, in Python, using `guessit` as a real dependency** (pure +Python, MIT license, no native extensions — trivially addable to +`packages/meshbay-node/pyproject.toml`), wrapped by a small +`meshbay_node.indexer.title_parse` module implementing the rules in §3.3–3.4. +This follows directly from §2: since the node already owns the TMDB call, it +is the natural place to own the parse step too — no filename-parsing code is +needed in the browser/Electron client at all. The parsed `(title, year, +season, episode)` and a best-effort `display_title` (the flat-mode fallback: +extension stripped, `_`/`.` replaced by spaces, used when parsing confidence +is low) travel to clients as ordinary index fields (§5), computed once by the +indexer and cached the same way size/hash already are. + +--- + +## 4. The two view modes + +### 4.1 Mode A — poster grid (TMDB), default + +Browse by poster, click through to a detail view (poster, +year, resolution + duration from the real probe, overview, cast, director, +a **Play** button that hands off to the existing `video-player.js`/ +`stream_req` path unchanged). Falls back to a thumbnail-only card, with just +the cleaned filename, when the node's TMDB lookup returns nothing or a +low-confidence match (§3.3's last row) — exactly the operator's own fallback +rule. + +Grouping for the poster grid: one card per **movie**, and one card per +**show** (not per episode) that expands to a season/episode picker on click, +using TMDB's own season list (§3.4) — the standard model for this kind of +browser, and requiring no new grouping logic beyond what the folder structure +and the node's per-show TMDB fetch already give. + +### 4.2 Mode B — flat, thumbnail-based, no TMDB + +Alphabetical, folder-driven, exactly as specified: a folder of movies is a +flat list of thumbnail cards; a folder that turns out to be a show is a list +of season subfolders (or files directly, if there's no season layer), each +expandable, never TMDB-enriched. Per-card info comes entirely from the +(now-populated, §5) index entry: `display_title`, `duration`, resolution +(from the technical probe fields), file `size` — no network call, no TMDB +dependency, and it keeps working with TMDB disabled node-wide. + +### 4.3 The toggle between modes + +**Per-device, `localStorage`**, same mechanism already used for the video +player's resume position and the i18n language choice — a display +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. + +--- + +## 5. Protocol and index changes + +### 5.1 `IndexEntry` already reserves two of the three fields this needs + +`meshbay_common/protocol.py:133` — checked directly, and both fields are +declared but **never populated by the indexer nor consumed by any client +today**: + +```python +duration: int | None = None # seconds, for media +thumb_hash: str | None = None # blake3 of thumbnail +``` + +This is smaller than a fresh addition: `duration` needs the indexer to run +(the existing) `ffprobe`-based probe at index time and store the result; +`thumb_hash` needs a thumbnail to have been generated and cached, addressed +by its own blake3 the same way a file is addressed by its `id` — which +opens a clean reuse (§5.3). **New**, not yet present: `width`/`height` +(resolution) and the parsed `display_title`/`season`/`episode` fields from +§3. All are additive fields on the same dataclass, so this is a MNP +**MINOR** bump (0.4 → 0.5, same class of change as the multi-root 0.2→0.3 +bump), not a breaking one — an older client simply doesn't render the new +fields. + +### 5.2 Index-time cost, bounded the same way thumbnails already are + +Probing every video file with `ffprobe` at index time is real cost on a +large library. `desktop-client-v1.md` §6.10 already established the pattern +for exactly this shape of problem (thumbnails): **a small, bounded worker +pool, its own short timeout, never sharing the streaming pool** +(`max_concurrent_streams`), draining stdout and releasing the slot +regardless — the same lesson `CLAUDE.md` records repeatedly for anything +that shells out to `ffmpeg`/`ffprobe`. The probe (and the `guessit` parse, +which is cheap by comparison) runs asynchronously after a file is first +seen; the file appears in the index immediately with size/hash only, and an +`index_sync` delta fills in the technical/parsed fields once ready — no +scan is blocked waiting for it. + +### 5.3 Thumbnail delivery — reuse the chunk path + +Since `thumb_hash` is a blake3 exactly like a file's own `id`, the cleanest +delivery is to **serve a thumbnail through the existing chunked +`file_req`/`ChunkRequest` path**, with the node's chunk handler resolving a +requested id against either a real file or the thumbnail cache. This reuses +transport, GEK-scoped encryption, and backpressure handling that already +exist and are already tested, rather than inventing a parallel mechanism. If +that reuse proves awkward against the concrete dispatch code once someone +is in it, a dedicated `thumb_req`/`thumb_resp` pair is a perfectly fine +fallback — noted as an implementation choice, not a decision this document +needs to force. + +### 5.4 New request/response: TMDB metadata + +A JSON blob, not chunked file content, so a plain request/response pair +fits better than reusing the chunk path: + +``` +media_meta_req { path } # client → node, root+relpath already known from the index +media_meta_resp { tmdb_id, title, original_title, overview, poster_thumb_hash, + backdrop_thumb_hash, release_date | first_air_date, + genres[], vote_average, runtime, + cast[] (name, character), director, + season?, episode? (for a TV request), confidence } +``` + +`poster_thumb_hash`/`backdrop_thumb_hash` point back at §5.3's mechanism — +the node fetches the image from `image.tmdb.org` once, caches it under its +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). + +### 5.5 Node-wide config, not per-group + +TMDB is one operator's budget and one credential, not a per-group concern, +so it does not belong in `group_settings` keyed by a real `group_id`. The +existing precedent for node-wide operator state stored in that same table is +already established (`desktop-client-v1.md` §6.3: *"the precedent exists: +`roster.get_member("", user_id)` already authorizes the operator +node-wide"*) — so `tmdb_enabled` and an optional `tmdb_api_token` land in +`group_settings` under the sentinel `group_id=""`, changed by a signed +operator op (`OP_TMDB_CONFIG`, same shape as `OP_MEMBER_UPLOAD`/ +`OP_APPS_ENABLED`), and broadcast in `node_status` (the node-wide channel, +as opposed to the per-group `handshake_ack`) so every connected client, +across every group, sees whether TMDB is on without reconnecting. Absent +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. + +### 5.6 `ALLOWED_APPS` + +`webrtc_server.py:1629` — add `"video"` to the frozenset. `DEFAULT_APPS` +(`roster.py`) is deliberately **left unchanged** (`("chat", "files")`): a +brand-new group does not get Videos for free, since it is the first app that +makes outbound third-party network calls (once TMDB is on) — the operator +opts a group into it explicitly via the existing Settings checklist, same +gesture as any other app. + +--- + +## 6. Node-side implementation, concretely + +| Piece | Where | What | +|---|---|---| +| Title/season/episode parse | new `meshbay_node/indexer/title_parse.py` | Wraps `guessit`, applies §3.3–3.4's rules, returns `(display_title, year, season, episode, confidence)` | +| Technical probe at index time | `meshbay_node/indexer/indexer.py`, reusing `_probe_video`'s ffprobe call from `webrtc_server.py` (extend `-show_entries` with `width,height`) | Populates `IndexEntry.duration`, new `width`/`height` | +| Bounded probe/thumbnail pool | `meshbay_node/transport/webrtc_server.py`, sibling to the streaming pool | Small worker count, short timeout, drains stdout, never touches `max_concurrent_streams` | +| TMDB client | new `meshbay_node/tmdb.py` | `httpx` (already a dependency) against `api.themoviedb.org`; resolves token via §5.5, falls back to the shipped default | +| Caches | `data_dir/media_cache.db` (SQLite, alongside `chat.db`/`audit.db`/`bundle_store.db`) | `(content_hash) → tmdb_id` mapping; `(tmdb_id) → metadata json`, TTL 30 days; `(thumb_hash) → jpeg bytes`, pruned when the owning file leaves the index | +| Operator config | `roster.py` `group_settings`, `group_id=""` | `tmdb_enabled`, `tmdb_api_token`; `ops.py` gains `set_tmdb_config(state, enabled, token=None)`, one `_op(...)` line, same adapters (loopback/CLI/MNP) as everything else in `ops.py` | +| `pyproject.toml` | `packages/meshbay-node/pyproject.toml` | add `guessit>=4.4` | + +## 7. Client-side, per `apps.md`'s checklist + +1. `video-app.js` — receives the standard props (§2 of `apps.md`); internally + two presentational sub-views (poster grid / flat), switched by the + `localStorage` toggle (§4.3). Requests `media_meta_req` lazily, only for + currently-visible tiles in the poster grid (same virtualization discipline + `apps.md` §5 already calls out as mandatory for a grid of many tiles), and + thumbnails the same way. +2. Register `{ key: "video", icon, labelKey: "group.tab_video", Component: VideoApp }` in `apps.js`. +3. `ALLOWED_APPS` (§5.6). +4. `group.tab_video` in all ten `static/locales/*.js`. +5. `webapp.py`'s `_ASSETS` tuple — add `video-app.js`. +6. `test_hook_ordering.py` (`STATIC_FILES`) and `test_transport_contracts.py` + (`SPLIT_FILES`) — add the new file to both. +7. `npm run sync-ui` in `meshbay-client`. + +No hub change, no protocol change beyond §5's additive fields and two new +message pairs — consistent with every other app added under this mechanism. + +--- + +## 8. Security — per adversary + +| Claim | Passive hub | Active hub | Malicious node operator | Another member | +|---|---|---|---|---| +| TMDB token confidentiality | ✅ unreachable | ✅ unreachable | held by the operator, who could always query TMDB manually anyway — no new exposure | ✅ never transmitted to members | +| Metadata/thumbnail delivery | ✅ unchanged transport | ✅ unchanged transport | sees it already (holds the plaintext file) | same GEK-proofed MNP channel as files/streaming — no new authorization surface | +| New outbound traffic (node → TMDB) | — | — | the operator already controls their own node's egress | — | +| Stale cache after file deletion | — | — | pruned on the index's own deletion event (§2); until built, a deleted file's thumbnail/metadata could persist on the node's disk after removal from the share — a real, addressable gap, not a design flaw, and called out here so it is not forgotten | — | + +**The claim this design supports:** moving TMDB/thumbnail work to the node +adds no new authorization boundary — everything travels over the channel +that already proves group membership and GEK possession — and *removes* a +credential (the TMDB token) from every client that would otherwise hold it. + +**The claim it must not make:** that this is free. It is new node behaviour +(egress to a third party, a new disk-resident cache with a real deletion +obligation) that did not exist before, and both are called out explicitly +rather than left implicit. + +## 9. Filesystem/Windows — why §2's choice avoids the trap + +`desktop-client-v1.md` §6.8/§7.5 already established exFAT/NTFS/Windows as +the **common** case, not an edge case. Had the cache lived inside a shared +root (the operator's original `.thumbs` idea), it would have inherited every +one of those constraints for no benefit: + +- A read-only backup mount or a NAS share the node has no write access to + would refuse the write outright — not a Windows quirk, a real failure on + any platform. +- Windows does not hide a leading-dot folder the way Unix does; `.thumbs` + would need an explicit "hidden" attribute set (and kept set across + recreation), or it shows up as an ordinary visible folder in Explorer — + previews of a private group's content, visible to anyone who plugs that + drive into a different machine. +- It would need to be filtered out of every directory listing path + consistently (indexer, root-name collision checks, watcher + reconciliation) — one more place a future change could forget it. + +Keeping the cache in the node's own `data_dir` (§2) — the same location +`chat.db`/`audit.db`/`bundle_store.db` already use on every supported +platform — sidesteps all three for free, with no loss of the benefit the +centralization was for (one computation, reused by every member). + +--- + +## 10. Open items + +| # | Item | Why it is not decided here | +|---|---|---| +| V1 | Exact wording/UX of the "TMDB disabled" state in Mode A (Mode B unaffected) | Product/UI choice, not architectural | +| V2 | Whether `poster_thumb_hash`/`backdrop_thumb_hash` reuse the chunk path (§5.3) or get a dedicated pair | Depends on how the concrete `file_req` dispatch code reads once someone is implementing against it | +| 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 | + +## 11. Acceptance before shipping + +1. Re-run the §3 validation (real TMDB calls, same corpus, same script + methodology) against the actual production parser and `tmdb.py`, not the + scratch harness used for this planning pass. Keep the script in `QE/` + (not versioned, per repo convention) as a regression check for future + parser changes. +2. Confirm the >95% bar on `Films/` with the §3.3 fixes applied for real, + not just argued. +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. |