From 313b72f15e8788ba3abcd3e44b5f7785fbc779fe Mon Sep 17 00:00:00 2001 From: Christophe Besson Date: Wed, 2 Sep 2026 15:23:30 +0200 Subject: fix(hub): one entry per file in the Search view's Videos grid MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A library shared by two groups arrived in the cross-group Search view as two entries per file: every film was two poster cards, every episode was listed twice in the season list under the synopsis. Inside one group this cannot happen — GroupIndex is keyed by blake3 — so the duplication was the Search page's own, from concatenating N independently keyed indexes. source-merge.js folds entries on the content hash and resolves one source per *unit* (a film, a whole show), so a season does not scatter across two nodes. A group hosted by the reader's own node wins; failing that the pick is a hash of the unit key and the reader's id, stable across renders and reloads — a source that changed mid-stream would tear down the connection under a film that is playing — and spread across readers and units. The units come from video-app.js's own groupVideoEntries rather than a second copy of its keys here. Only the Videos view is wired up so far; Music, Photos, failover and the "N sources" badge are phases 5-8 of docs/refactoring-search.md. Every test was checked against the fix removed. That is how the first version of "a unit's files share its source" turned out to prove nothing: with every episode in every group, per-file and per-unit picking give the same answer, so it passed against a per-file implementation. It now uses a unit whose files have unequal sources. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01AbwJDbNTkiRUh7HTWEoyss --- docs/refactoring-search.md | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) (limited to 'docs') diff --git a/docs/refactoring-search.md b/docs/refactoring-search.md index 08774d1..f23e1ef 100644 --- a/docs/refactoring-search.md +++ b/docs/refactoring-search.md @@ -1,6 +1,9 @@ # Refactor: one file, one entry — merging duplicate sources in the Search view -> Status: **planned, not built** (2026-09-02). Branch `feat/search-source-merge`. +> Status: **phases 1–4 built** (2026-09-02), 5–9 planned. Branch +> `feat/search-source-merge`. The Videos view is merged and the reported bug is +> gone under test; Music, Photos, failover and the badge are still to come, so +> a merged film still shows its chosen group's name rather than `N sources`. > Scope: the cross-group Search page (`static/search-page.js`) and the three > media applications it reuses (Videos, Music, Photos). The Files **explorer** > inside Search is explicitly out of scope and must not change. @@ -328,10 +331,10 @@ Each phase is independently testable and leaves the tree working. | # | Phase | Files | |---|---|---| -| 1 | Export `groupPhotoAlbums`; record `is_node_admin` in `fetchGroupIndex`'s result | `photos-app.js`, `search-page.js` | -| 2 | `source-merge.js` — `pickSource`, `mergeUnitEntries`, `sourceLabel`. No caller yet | new file | -| 3 | Tests for phase 2, read out of the source | `test_search_source_merge.py` | -| 4 | Wire the Videos list through the merge | `search-page.js` | +| 1 ✅ | Export `groupPhotoAlbums`; record `is_node_admin` in `fetchGroupIndex`'s result | `photos-app.js`, `search-page.js` | +| 2 ✅ | `source-merge.js` — `pickSource`, `mergeUnitEntries`, `sourceLabel`. No caller yet | new file | +| 3 ✅ | Tests for phase 2, read out of the source | `test_search_source_merge.py`, `test_search_video_merge.py` | +| 4 ✅ | Wire the Videos list through the merge | `search-page.js` | | 5 | Same for Music, including the `onPreview` queue (§6.4) | `search-page.js` | | 6 | Same for Photos | `search-page.js` | | 7 | `downGroups` and failover | `search-page.js` | @@ -352,11 +355,22 @@ value from the real source rather than restating a constant. | Test | Holds | |---|---| -| `test_search_source_merge.py` | `pickSource` / `mergeUnitEntries` lifted out of `source-merge.js` and executed: a file in two groups yields one entry with two sources; a local source always wins; the pick is stable across calls and varies with the salt; a unit's files share the unit's source; a file the unit's source lacks falls back to its own; a down group is skipped; all-down falls back to the full list | +| `test_search_source_merge.py` ✅ | The whole of `source-merge.js` executed standalone — it has no imports precisely so that it can be, and the test refuses a build where it gains one. A file in two groups yields one entry with two sources; a local source always wins, over every salt; the pick is stable across calls, varies with the salt, and spreads one reader across units; source order does not decide it; a unit's files share the unit's source; a file the unit's source lacks falls back with its siblings; a down group is skipped, a down *local* group yields to a live remote, all-down still returns an entry; no field is back-filled from another source | +| `test_search_video_merge.py` ✅ | The reported symptom end to end. `groupVideoEntries` + `buildSeasons` (video-app.js) and `videoUnits` (search-page.js) are lifted from their real sources, the pipeline is assembled as the page assembles it, and the result is re-grouped the way `VideoApp` re-groups it — so what is counted is what the grid renders. Two groups sharing one library give one film card and one show whose seasons hold three episodes, not six; one group is unchanged; an episode only one group has survives | | extend `test_locales.py` | already fails on a key present in `en.js` and missing elsewhere — no change needed, listed so the ten-catalogue edit is not forgotten | | `test_search_files_unmerged.py` | reads `search-page.js` and refuses a build where `fileEntries` is fed through the merge — the one guarantee §6.1 makes, and the one a later refactor is most likely to break by tidying the four lists into one | | extend `test_transport_contracts.py` | the existing "declared vs. called setters" check covers the new state in `search-page.js` for free | +**Every one of these was checked against the fix removed**, which is where the +first version of "a unit's files share its source" turned out to prove nothing: +with every episode in every group, picking per file and picking per unit give +the same answer — the same key over the same set — so the test passed against a +per-file implementation. It now uses a unit whose files have *unequal* sources, +which is the only shape where the two rules come apart. Five mutations are +caught: dropping the local preference, picking per file, not de-duplicating a +group announcing a file twice, dropping the group-id sort, and hashing the salt +without the unit key. + Beyond the suite, this needs a person: two groups sharing one directory, one film and one multi-season show, checked in Posters, Flat list, the detail modal, Music and Photos, plus one playback and one download from a merged entry. The -- cgit v1.2.3