Experiment to rebuild Diffuse using web applets.
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: initial work for native-fs input

+319 -35
+3
deno.lock
··· 21 21 ], 22 22 "packageJson": { 23 23 "dependencies": [ 24 + "npm:@atcute/cid@^2.2.2", 24 25 "npm:@picocss/pico@^2.1.1", 25 26 "npm:astro-purgecss@^5.2.2", 26 27 "npm:astro-scope@^3.0.1", 27 28 "npm:astro@^5.7.4", 29 + "npm:fast-uri@^3.0.6", 28 30 "npm:iconoir@^7.11.0", 29 31 "npm:idb-keyval@^6.2.1", 30 32 "npm:native-file-system-adapter@^3.0.1", 31 33 "npm:purgecss@^7.0.2", 34 + "npm:query-string@^9.1.2", 32 35 "npm:sass@^1.87.0", 33 36 "npm:throttle-debounce@^5.0.2", 34 37 "npm:xxh32@^2.0.5"
+86
package-lock.json
··· 5 5 "packages": { 6 6 "": { 7 7 "dependencies": { 8 + "@atcute/cid": "^2.2.2", 8 9 "@picocss/pico": "^2.1.1", 9 10 "@web-applets/sdk": "file:../../unternet-co/web-applets/sdk/", 11 + "fast-uri": "^3.0.6", 10 12 "iconoir": "^7.11.0", 11 13 "idb-keyval": "^6.2.1", 12 14 "native-file-system-adapter": "^3.0.1", 15 + "query-string": "^9.1.2", 13 16 "spellcaster": "gordonbrander/spellcaster#1613e5e3b7f202cfe57f37ea7c637ec83588a297", 14 17 "throttle-debounce": "^5.0.2", 15 18 "xxh32": "^2.0.5" ··· 103 106 "engines": { 104 107 "node": "^18.17.1 || ^20.3.0 || >=22.0.0" 105 108 } 109 + }, 110 + "node_modules/@atcute/cid": { 111 + "version": "2.2.2", 112 + "resolved": "https://registry.npmjs.org/@atcute/cid/-/cid-2.2.2.tgz", 113 + "integrity": "sha512-deAGMqLAyplt7eIukhkjlsGubvrcMrtXkDKlUYZDo4WUdL7hSjBywtPXf6SbMK+Mjvst7l2+83OqTcY5AuuxtA==", 114 + "dependencies": { 115 + "@atcute/multibase": "^1.1.3", 116 + "@atcute/uint8array": "^1.0.1" 117 + } 118 + }, 119 + "node_modules/@atcute/multibase": { 120 + "version": "1.1.3", 121 + "resolved": "https://registry.npmjs.org/@atcute/multibase/-/multibase-1.1.3.tgz", 122 + "integrity": "sha512-vQQO0tDuQPguBvHdgV3ryn7R8U6beQ50KA/juYm+dCeT/3hOK2stMbX+IaW8JEuwkT5lJsU8wDIOicQT4mB7Ag==", 123 + "dependencies": { 124 + "@atcute/uint8array": "^1.0.1" 125 + } 126 + }, 127 + "node_modules/@atcute/uint8array": { 128 + "version": "1.0.1", 129 + "resolved": "https://registry.npmjs.org/@atcute/uint8array/-/uint8array-1.0.1.tgz", 130 + "integrity": "sha512-AAnlFKyfDRgb9GNZJbhQ6OuMhbmNPirQyapb8KnmcEhxQZ3+tt+4NcwqekEegY4MpNqSTYeeTdyxq0wGZv1JHg==" 106 131 }, 107 132 "node_modules/@babel/helper-string-parser": { 108 133 "version": "7.25.9", ··· 2398 2423 "url": "https://github.com/sponsors/wooorm" 2399 2424 } 2400 2425 }, 2426 + "node_modules/decode-uri-component": { 2427 + "version": "0.4.1", 2428 + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.4.1.tgz", 2429 + "integrity": "sha512-+8VxcR21HhTy8nOt6jf20w0c9CADrw1O8d+VZ/YzzCt4bJ3uBjw+D1q2osAB8RnpwwaeYBxy0HyKQxD5JBMuuQ==", 2430 + "engines": { 2431 + "node": ">=14.16" 2432 + } 2433 + }, 2401 2434 "node_modules/defu": { 2402 2435 "version": "6.1.4", 2403 2436 "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz", ··· 2605 2638 "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", 2606 2639 "dev": true 2607 2640 }, 2641 + "node_modules/fast-uri": { 2642 + "version": "3.0.6", 2643 + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz", 2644 + "integrity": "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==", 2645 + "funding": [ 2646 + { 2647 + "type": "github", 2648 + "url": "https://github.com/sponsors/fastify" 2649 + }, 2650 + { 2651 + "type": "opencollective", 2652 + "url": "https://opencollective.com/fastify" 2653 + } 2654 + ] 2655 + }, 2608 2656 "node_modules/fdir": { 2609 2657 "version": "6.4.4", 2610 2658 "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz", ··· 2653 2701 }, 2654 2702 "engines": { 2655 2703 "node": ">=8" 2704 + } 2705 + }, 2706 + "node_modules/filter-obj": { 2707 + "version": "5.1.0", 2708 + "resolved": "https://registry.npmjs.org/filter-obj/-/filter-obj-5.1.0.tgz", 2709 + "integrity": "sha512-qWeTREPoT7I0bifpPUXtxkZJ1XJzxWtfoWWkdVGqa+eCr3SHW/Ocp89o8vLvbUuQnadybJpjOKu4V+RwO6sGng==", 2710 + "engines": { 2711 + "node": ">=14.16" 2712 + }, 2713 + "funding": { 2714 + "url": "https://github.com/sponsors/sindresorhus" 2656 2715 } 2657 2716 }, 2658 2717 "node_modules/flattie": { ··· 4478 4537 "purgecss": "bin/purgecss.js" 4479 4538 } 4480 4539 }, 4540 + "node_modules/query-string": { 4541 + "version": "9.1.2", 4542 + "resolved": "https://registry.npmjs.org/query-string/-/query-string-9.1.2.tgz", 4543 + "integrity": "sha512-s3UlTyjxRux4KjwWaJsjh1Mp8zoCkSGKirbD9H89pEM9UOZsfpRZpdfzvsy2/mGlLfC3NnYVpy2gk7jXITHEtA==", 4544 + "dependencies": { 4545 + "decode-uri-component": "^0.4.1", 4546 + "filter-obj": "^5.1.0", 4547 + "split-on-first": "^3.0.0" 4548 + }, 4549 + "engines": { 4550 + "node": ">=18" 4551 + }, 4552 + "funding": { 4553 + "url": "https://github.com/sponsors/sindresorhus" 4554 + } 4555 + }, 4481 4556 "node_modules/radix3": { 4482 4557 "version": "1.1.2", 4483 4558 "resolved": "https://registry.npmjs.org/radix3/-/radix3-1.1.2.tgz", ··· 4948 5023 "integrity": "sha512-BR1ND3XDjrl9kWrBbGTVD/z2YJ+929ksqzYDMXDiNfaSTVIC3E8qdfXQ3wS4ZPo5CVSKlbctpJmp7vEjo5rwdQ==", 4949 5024 "dependencies": { 4950 5025 "signal-polyfill": "^0.2.0" 5026 + } 5027 + }, 5028 + "node_modules/split-on-first": { 5029 + "version": "3.0.0", 5030 + "resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-3.0.0.tgz", 5031 + "integrity": "sha512-qxQJTx2ryR0Dw0ITYyekNQWpz6f8dGd7vffGNflQQ3Iqj9NJ6qiZ7ELpZsJ/QBhIVAiDfXdag3+Gp8RvWa62AA==", 5032 + "engines": { 5033 + "node": ">=12" 5034 + }, 5035 + "funding": { 5036 + "url": "https://github.com/sponsors/sindresorhus" 4951 5037 } 4952 5038 }, 4953 5039 "node_modules/string-width": {
+3
package.json
··· 1 1 { 2 2 "dependencies": { 3 + "@atcute/cid": "^2.2.2", 3 4 "@picocss/pico": "^2.1.1", 4 5 "@web-applets/sdk": "file:../../unternet-co/web-applets/sdk/", 6 + "fast-uri": "^3.0.6", 5 7 "iconoir": "^7.11.0", 6 8 "idb-keyval": "^6.2.1", 7 9 "native-file-system-adapter": "^3.0.1", 10 + "query-string": "^9.1.2", 8 11 "spellcaster": "gordonbrander/spellcaster#1613e5e3b7f202cfe57f37ea7c637ec83588a297", 9 12 "throttle-debounce": "^5.0.2", 10 13 "xxh32": "^2.0.5"
+1 -19
src/pages/core/types.d.ts
··· 1 1 /* OUTPUT */ 2 2 3 3 export interface Output<T = TrackTags> { 4 - sources: Source[]; 5 4 tracks: Track<T>[]; 6 5 } 7 6 8 7 export type OutputGetter = ({ name }: { name: string }) => Promise<Uint8Array | undefined>; 9 8 export type OutputSetter = ({ data, name }: { data: Uint8Array; name: string }) => Promise<void>; 10 9 11 - /* SOURCES */ 12 - // TODO: Rename to input? 13 - 14 - export interface Source<Meta = Record<string, string>> { 15 - id: string; 16 - meta: Meta; 17 - 18 - // NOTE: This is associated with a data input applet. 19 - // For example, `diffuse.sh/storage/amazon-s3/` 20 - // 21 - // This association is needed to, for example, 22 - // optionally translate permanent URIs into usable 23 - // URLs in order actually play the audio track. 24 - appletURI: string; 25 - } 26 - 27 10 /* TRACKS */ 28 11 29 12 export interface Track<Tags = TrackTags> { 30 13 id: string; 31 - sourceId: string; 32 - tags: Tags; 14 + tags?: Tags; 33 15 34 16 // NOTE: This is a "semi-permanent" URI. 35 17 //
+9 -13
src/pages/engine/audio/_manifest.json
··· 59 59 "type": "array", 60 60 "description": "The tracks we want to render.", 61 61 "items": { 62 - "anyOf": [ 63 - { 64 - "type": "object", 65 - "properties": { 66 - "id": { "type": "string" }, 67 - "isPreload": { "type": "boolean" }, 68 - "mimeType": { "type": "string" }, 69 - "progress": { "type": "number" }, 70 - "url": { "type": "string" } 71 - }, 72 - "required": ["id", "url"] 73 - } 74 - ] 62 + "type": "object", 63 + "properties": { 64 + "id": { "type": "string" }, 65 + "isPreload": { "type": "boolean" }, 66 + "mimeType": { "type": "string" }, 67 + "progress": { "type": "number" }, 68 + "url": { "type": "string" } 69 + }, 70 + "required": ["id", "url"] 75 71 } 76 72 } 77 73 },
+155
src/pages/input/native-fs/_applet.astro
··· 1 + <script> 2 + import * as CID from "@atcute/cid"; 3 + import * as IDB from "idb-keyval"; 4 + 5 + import { applets } from "@web-applets/sdk"; 6 + import { type FileSystemDirectoryHandle, showDirectoryPicker } from "native-file-system-adapter"; 7 + import QS from "query-string"; 8 + import URI from "fast-uri"; 9 + 10 + import type { Track } from "@applets/core/types.d.ts"; 11 + import { isAudioFile } from "@scripts/inputs/common"; 12 + 13 + type Handles = Array<{ id: string; handle: FileSystemDirectoryHandle }>; 14 + 15 + // TODO: Add ability to list cached tracks from other devices (ie. unknown handle ids) 16 + 17 + //////////////////////////////////////////// 18 + // SETUP 19 + //////////////////////////////////////////// 20 + const IDB_PREFIX = "@applets/input/native-fs"; 21 + const IDB_HANDLES = `${IDB_PREFIX}/handles`; 22 + 23 + const context = applets.register(); 24 + 25 + //////////////////////////////////////////// 26 + // ACTIONS 27 + //////////////////////////////////////////// 28 + const list = async (cachedTracks: Track[] = []) => { 29 + const handles: Handles = (await IDB.get(IDB_HANDLES)) ?? []; 30 + 31 + const processed: Track[][] = await Promise.all( 32 + handles.map(({ id, handle }) => recursiveList(handle, id, [])), 33 + ); 34 + 35 + const cache = cachedTracks.reduce( 36 + (acc: Record<string, Record<string, Track>>, track: Track) => { 37 + const handleId = trackHandleId(track); 38 + const cid = trackCid(track); 39 + 40 + if (!handleId || !cid) return acc; 41 + 42 + return { ...acc, [handleId]: { ...(acc[handleId] || {}), [cid]: track } }; 43 + }, 44 + {}, 45 + ); 46 + 47 + const groups = processed.flat(1).reduce( 48 + (acc, track) => { 49 + const handleId = trackHandleId(track); 50 + const cid = trackCid(track); 51 + 52 + if (!handleId) throw new Error("New tracks are missing a handle id!"); 53 + if (!cid) throw new Error("New tracks are missing a cid!"); 54 + 55 + return { ...acc, [handleId]: { ...acc[handleId], [cid]: track } }; 56 + }, 57 + handles.reduce((acc: Record<string, Record<string, Track>>, handle) => { 58 + return { ...acc, [handle.id]: {} }; 59 + }, cache), 60 + ); 61 + 62 + const data = Object.values(groups) 63 + .map((tracks) => Object.values(tracks)) 64 + .flat(1) 65 + .sort((a: any, b: any) => { 66 + if (a.uri < b.uri) return -1; 67 + if (a.uri > b.uri) return 1; 68 + return 0; 69 + }); 70 + 71 + // TODO: Should be able to just return the data in the handler 72 + context.data = data; 73 + return data; 74 + }; 75 + 76 + const resolve = async () => { 77 + // TODO: Resolve the track URI to an URL. 78 + // This can utilise `createObjectURL()` 79 + // 80 + // Get a file handle, then the `File` ref, 81 + // and pass it to `createObjectURL()` 82 + }; 83 + 84 + const mount = async () => { 85 + await showDirectoryPicker() 86 + .then(async (handle) => { 87 + const existingHandles: Handles = (await IDB.get(IDB_HANDLES)) ?? []; 88 + const id = crypto.randomUUID(); 89 + 90 + await handle.requestPermission({ mode: "read" }); 91 + await IDB.set(IDB_HANDLES, [...existingHandles, { id, handle }]); 92 + }) 93 + .catch(() => {}); 94 + }; 95 + 96 + context.setActionHandler("list", list); 97 + context.setActionHandler("resolve", resolve); 98 + context.setActionHandler("mount", mount); 99 + 100 + //////////////////////////////////////////// 101 + // 🛠️ 102 + //////////////////////////////////////////// 103 + function trackCid(track: Track): string | undefined { 104 + const a = URI.parse(track.uri); 105 + const cid = a.query ? QS.parse(a.query).cid || undefined : undefined; 106 + return Array.isArray(cid) && cid[0] ? cid[0] : typeof cid === "string" ? cid : undefined; 107 + } 108 + 109 + function trackHandleId(track: Track): string | undefined { 110 + const a = URI.parse(track.uri); 111 + return a.host; 112 + } 113 + 114 + async function recursiveList( 115 + dir: FileSystemDirectoryHandle, 116 + rootHandleId: string, 117 + path: string[], 118 + ): Promise<Track[]> { 119 + const tracks: Track[] = []; 120 + 121 + for await (const item of dir.values()) { 122 + if (item.kind === "file" && isAudioFile(item.name)) { 123 + const fileHandle = await dir.getFileHandle(item.name); 124 + const file = await fileHandle.getFile(); 125 + const bytes = new Uint8Array(await file.arrayBuffer()); 126 + const cid = await CID.create(0x55, bytes); 127 + 128 + const uri = URI.serialize({ 129 + scheme: "file+local", 130 + host: rootHandleId, 131 + path: `${path.length ? "/" + path.join("/") : ""}/${item.name}`, 132 + query: QS.stringify({ cid: CID.toString(cid) }), 133 + }); 134 + 135 + console.log(uri); 136 + 137 + const track: Track = { 138 + id: crypto.randomUUID(), 139 + uri, 140 + }; 141 + 142 + tracks.push(track); 143 + } else if (item.kind === "directory") { 144 + const nestedItems = await recursiveList(item as FileSystemDirectoryHandle, rootHandleId, [ 145 + ...path, 146 + item.name, 147 + ]); 148 + 149 + tracks.push(...nestedItems); 150 + } 151 + } 152 + 153 + return tracks; 154 + } 155 + </script>
+34
src/pages/input/native-fs/_manifest.json
··· 1 + { 2 + "name": "diffuse/input/native-fs", 3 + "title": "Diffuse Input | Native File System", 4 + "entrypoint": "index.html", 5 + "input_properties": { 6 + "scheme": "file+local" 7 + }, 8 + "actions": { 9 + "list": { 10 + "title": "List", 11 + "description": "List tracks.", 12 + "params_schema": { 13 + "type": "object", 14 + "properties": { 15 + "tracks": { 16 + "type": "array", 17 + "description": "A list of (cached) tracks with an uri matching the scheme", 18 + "items": { 19 + "type": "object" 20 + } 21 + } 22 + } 23 + } 24 + }, 25 + "mount": { 26 + "title": "Mount", 27 + "description": "Prepare for usage." 28 + }, 29 + "unmount": { 30 + "title": "Unmount", 31 + "description": "Callback after usage." 32 + } 33 + } 34 + }
+9
src/pages/input/native-fs/index.astro
··· 1 + --- 2 + import Layout from "@layouts/applet.astro"; 3 + import Applet from "./_applet.astro"; 4 + import { title } from "./_manifest.json"; 5 + --- 6 + 7 + <Layout title={title}> 8 + <Applet /> 9 + </Layout>
+2 -2
src/pages/output/native-fs/_applet.astro
··· 17 17 // ACTIONS 18 18 //////////////////////////////////////////// 19 19 const get: OutputGetter = async ({ name }) => { 20 - const handle = await IDB.get(IDB_DEVICE_KEY); 20 + const handle: FileSystemDirectoryHandle | null = (await IDB.get(IDB_DEVICE_KEY)) ?? null; 21 21 if (!handle) throw new Error("Storage not configured properly, handle not found."); 22 22 23 23 try { ··· 34 34 }; 35 35 36 36 const put: OutputSetter = async ({ data, name }) => { 37 - const handle = await IDB.get(IDB_DEVICE_KEY); 37 + const handle: FileSystemDirectoryHandle | null = (await IDB.get(IDB_DEVICE_KEY)) ?? null; 38 38 if (!handle) throw new Error("Storage not configured properly, handle not found."); 39 39 const fileHandle = await handle.getFileHandle(name, { create: true }); 40 40 const stream = await fileHandle.createWritable();
+3
src/scripts/inputs/common.ts
··· 1 + export function isAudioFile(filename: string) { 2 + return filename.match(/\.(flac|m4a|mp3|mp4|ogg|opus|wav|webm)$/); 3 + }
+11
src/scripts/themes/pilot/index.ts
··· 23 23 queue: await applet<QueueEngine.State>("../../engine/queue"), 24 24 }; 25 25 26 + const input = { 27 + nativeFs: await applet("../../input/native-fs"), 28 + }; 29 + 26 30 const _orchestrator = { 27 31 queue: await applet("../../orchestrator/single-queue"), 28 32 output: await applet<Output>("../../orchestrator/output-management"), ··· 91 95 } 92 96 }, 93 97 ); 98 + 99 + // TODO: 100 + document.onclick = async () => { 101 + await input.nativeFs.sendAction("mount"); 102 + await input.nativeFs.sendAction("list"); 103 + console.log(input.nativeFs.data); 104 + };
+3 -1
src/styles/themes/pilot/index.css
··· 52 52 ***********************************/ 53 53 iframe[src*="/configurator/"], 54 54 iframe[src*="/engine/"], 55 - iframe[src*="/orchestrator/"] { 55 + iframe[src*="/input/"], 56 + iframe[src*="/orchestrator/"], 57 + iframe[src*="/output/"] { 56 58 height: 0; 57 59 left: 110vw; 58 60 opacity: 0;