diff --git a/aiui/packages/app/src/composables/useArchy.ts b/aiui/packages/app/src/composables/useArchy.ts index e7a0523a..22da47dd 100644 --- a/aiui/packages/app/src/composables/useArchy.ts +++ b/aiui/packages/app/src/composables/useArchy.ts @@ -140,7 +140,7 @@ export function useArchy() { // functions already resolve to a silent, logged no-op when the user // hasn't granted Media/File access (`permitted: false`), so this never // throws into `init()`. - void requestArchyContent('all', 'own') + void requestArchyAllContent() void requestArchyLibrary('own') } @@ -239,9 +239,10 @@ export function useArchy() { console.warn('[AIUI Archy] content: not permitted — user should enable Media/File access in Archy Settings') return } - useContentPanel().setArchyContent({ + const panel = useContentPanel() + panel.setArchyContent({ films: res.films as Film[], - songs: res.songs as Song[], + songs: panel.panelSongs.value, podcasts: res.podcasts as Podcast[], }) } catch (err) { @@ -249,6 +250,63 @@ export function useArchy() { } } + /** + * Load every content scope the node can offer, merged into one grid. + * + * `own` alone was all init ever asked for, which meant a films search showed + * nothing but this node's own shared files: **IndeeHub and anything else + * purchased live in `owned` (`content.owned-list`), and other nodes' catalogs + * in `peers` — and neither scope had a single caller anywhere in the app.** + * They existed only as type-signature options. + * + * Scopes are fetched concurrently and merged once, rather than each calling + * `setArchyContent` itself: that sink REPLACES films/podcasts, so three + * separate pushes would leave only whichever resolved last. Deduped by id + * because the same title can legitimately appear in more than one scope + * (owned locally and offered by a peer). + * + * A failing scope must not cost the others — a dead or slow peer is normal, + * not exceptional — so each is caught individually and contributes nothing. + */ + async function requestArchyAllContent() { + if (!isInitialized.value) return + const scopes: Array<'own' | 'owned' | 'peers'> = ['own', 'owned', 'peers'] + + const results = await Promise.all( + scopes.map((s) => + archyBridge + .requestArchyContent('all', s) + .then((res) => (res.permitted ? res : null)) + .catch((err) => { + console.warn(`[AIUI Archy] content(${s}) failed:`, (err as Error)?.message ?? err) + return null + }), + ), + ) + + const seen = new Set() + const films: Film[] = [] + const podcasts: Podcast[] = [] + for (const res of results) { + if (!res) continue + for (const f of (res.films ?? []) as Film[]) { + const key = `film:${f.id}` + if (seen.has(key)) continue + seen.add(key) + films.push(f) + } + for (const p of (res.podcasts ?? []) as Podcast[]) { + const key = `pod:${p.id}` + if (seen.has(key)) continue + seen.add(key) + podcasts.push(p) + } + } + + const panel = useContentPanel() + panel.setArchyContent({ films, songs: panel.panelSongs.value, podcasts }) + } + /** * Request the node's real music library (13-11 — the D-13 wave) and hand * it to the same `setArchyContent` sink `requestArchyContent` uses, so @@ -381,6 +439,7 @@ export function useArchy() { readFile, tailLogs, requestArchyContent, + requestArchyAllContent, requestArchyLibrary, buildArchyContext, }