diff options
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/static/search-page.js')
| -rw-r--r-- | packages/meshbay-hub/src/meshbay_hub/static/search-page.js | 56 |
1 files changed, 51 insertions, 5 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/search-page.js b/packages/meshbay-hub/src/meshbay_hub/static/search-page.js index ec92e76..5897acb 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/search-page.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/search-page.js @@ -8,11 +8,12 @@ import { HUB, session, cacheGroupIndex, hubFetch, ensureFreshToken, _loadBundleKey, } from './hub-client.js'; import { FilesPanel, FilePreview } from './files-app.js'; -import { VideoApp } from './video-app.js'; +import { VideoApp, groupVideoEntries } from './video-app.js'; import { MusicApp } from './music-app.js'; import { PhotosApp } from './photos-app.js'; import { VideoPlayer } from './video-player.js'; import { transfers } from './transfers.js'; +import { mergeUnitEntries } from './source-merge.js'; const BATCH_SIZE = 3; // One WebRTC peer connection per group the search view touches. The cap bounds @@ -151,7 +152,13 @@ async function fetchGroupIndex(groupId, token, bundleKey, username, userId) { audioRoot: ack.audio_root || '', photoRoots: ack.photo_roots || [], }; - return { entries: indexMsg.entries || [], roots }; + // Which of the reader's groups sit on their own node — the tie-breaker + // when the same file is announced by several of them + // (docs/refactoring-search.md §5.3). Computed by the node from its own + // record of who it belongs to (webrtc_server.py's _is_node_admin), never + // from a hub claim, and deliberately not written to the index cache: it + // describes this connection, not the group's content. + return { entries: indexMsg.entries || [], roots, isNodeAdmin: !!ack.is_node_admin }; } finally { try { transport.close(); } catch {} } @@ -200,6 +207,32 @@ function underRoot(entry, root) { return p === root || p.startsWith(root + '/'); } +// -- Merging the same file announced by several groups ------------------------ +// +// A directory shared by two groups — the reason two groups exist at all: +// different people invited to different libraries — arrived here as two +// entries per file, so a film showed as two poster cards and every episode +// twice inside a show. `source-merge.js` folds them on the content hash and +// resolves one source per *unit*. See docs/refactoring-search.md. +// +// The units come from video-app.js's own `groupVideoEntries`, never from a +// second copy of its keys here: a copy would keep agreeing with the original +// right up until one of them changed, and the symptom would be a show whose +// episodes stream from two different nodes. Running it twice per recompute (it +// runs again inside VideoApp) is a linear pass over an index already in memory +// and already re-walked on every keystroke of the filter. +// +// One naive unit per copy of a film rather than a pre-grouped one: +// `mergeUnitEntries` folds lists that share a key, so the two copies become +// one unit without this having to group them first. +function videoUnits(entries) { + const { movies, shows } = groupVideoEntries(entries, SEARCH_VIDEO_ROOT); + return [ + ...movies.map((e) => ({ key: `movie:${e.id}`, entries: [e] })), + ...shows.map((s) => ({ key: `show:${s.title}`, entries: s.episodes })), + ]; +} + function SearchPage({ token, username, userId, groups, onPlayQueue, userPrefs }) { const [indexedGroups, setIndexedGroups] = useState(new Map()); const [progress, setProgress] = useState({ done: 0, total: 0, unreachable: [] }); @@ -388,7 +421,20 @@ function SearchPage({ token, username, userId, groups, onPlayQueue, userPrefs }) return dirs; }, [indexedGroups]); - // Videos view: pre-filtered by videoRoot, path-prefixed + // How a unit's source is chosen, shared by every merged view + // (docs/refactoring-search.md §5.3). `isLocal` reads the flag the node itself + // put in the handshake ack — computed from its own record of who it belongs + // to (webrtc_server.py's `_is_node_admin`), never from a hub claim. + const mergeOpts = useMemo(() => ({ + salt: userId || '', + isLocal: (gid) => { + const data = indexedGroups.get(gid); + return !!(data && data.isNodeAdmin); + }, + }), [indexedGroups, userId]); + + // Videos view: pre-filtered by videoRoot, path-prefixed, then merged so a + // file several groups share is one card and one list entry. const videoEntries = useMemo(() => { const result = []; for (const [groupId, data] of indexedGroups) { @@ -411,8 +457,8 @@ function SearchPage({ token, username, userId, groups, onPlayQueue, userPrefs }) }); } } - return result; - }, [indexedGroups, q, matchesQuery, connectionGen]); + return mergeUnitEntries(videoUnits(result), mergeOpts); + }, [indexedGroups, q, matchesQuery, connectionGen, mergeOpts]); // Music view: pre-filtered by audioRoot, path-prefixed const musicEntries = useMemo(() => { |