Experiment to rebuild Diffuse using web applets.
0
fork

Configure Feed

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

feat: improve native-fs input

+122 -42
+1
deno.lock
··· 33 33 "npm:purgecss@^7.0.2", 34 34 "npm:query-string@^9.1.2", 35 35 "npm:sass@^1.87.0", 36 + "npm:spellcaster@6", 36 37 "npm:throttle-debounce@^5.0.2", 37 38 "npm:xxh32@^2.0.5" 38 39 ]
+4 -4
package-lock.json
··· 13 13 "idb-keyval": "^6.2.1", 14 14 "native-file-system-adapter": "^3.0.1", 15 15 "query-string": "^9.1.2", 16 - "spellcaster": "gordonbrander/spellcaster#1613e5e3b7f202cfe57f37ea7c637ec83588a297", 16 + "spellcaster": "^6.0.0", 17 17 "throttle-debounce": "^5.0.2", 18 18 "xxh32": "^2.0.5" 19 19 }, ··· 5018 5018 } 5019 5019 }, 5020 5020 "node_modules/spellcaster": { 5021 - "version": "5.0.2", 5022 - "resolved": "git+ssh://git@github.com/gordonbrander/spellcaster.git#1613e5e3b7f202cfe57f37ea7c637ec83588a297", 5023 - "integrity": "sha512-BR1ND3XDjrl9kWrBbGTVD/z2YJ+929ksqzYDMXDiNfaSTVIC3E8qdfXQ3wS4ZPo5CVSKlbctpJmp7vEjo5rwdQ==", 5021 + "version": "6.0.0", 5022 + "resolved": "https://registry.npmjs.org/spellcaster/-/spellcaster-6.0.0.tgz", 5023 + "integrity": "sha512-BLHYZFnvf5XtVwVr2x/esn7gJjUCevywkJoVmlN33MrneSR7AVTTYkeu6Nt9NUguGaOv11yb4zjLo5hV0PYj0w==", 5024 5024 "dependencies": { 5025 5025 "signal-polyfill": "^0.2.0" 5026 5026 }
+1 -1
package.json
··· 8 8 "idb-keyval": "^6.2.1", 9 9 "native-file-system-adapter": "^3.0.1", 10 10 "query-string": "^9.1.2", 11 - "spellcaster": "gordonbrander/spellcaster#1613e5e3b7f202cfe57f37ea7c637ec83588a297", 11 + "spellcaster": "^6.0.0", 12 12 "throttle-debounce": "^5.0.2", 13 13 "xxh32": "^2.0.5" 14 14 },
+6 -6
src/pages/index.astro
··· 29 29 ]; 30 30 31 31 const input = [ 32 - { url: "input/native-fs", title: "(TODO) Native File System" }, 33 - { url: "input/s3-compatible", title: "(TODO) S3-Compatible API" }, 32 + { url: "input/native-fs/", title: "Native File System" }, 33 + { url: "input/s3-compatible/", title: "(TODO) S3-Compatible API" }, 34 34 ]; 35 35 36 36 const orchestrators = [ ··· 39 39 ]; 40 40 41 41 const output = [ 42 - { url: "output/indexed-db", title: "IndexedDB" }, 43 - { url: "output/native-fs", title: "Native File System" }, 42 + { url: "output/indexed-db/", title: "IndexedDB" }, 43 + { url: "output/native-fs/", title: "Native File System" }, 44 44 ]; 45 45 46 46 const processors = [ 47 - { url: "processor/artwork", title: "(TODO) Artwork fetcher" }, 48 - { url: "processor/http-metadata", title: "(TODO) HTTP(S) metadata fetcher" }, 47 + { url: "processor/artwork/", title: "(TODO) Artwork fetcher" }, 48 + { url: "processor/http-metadata/", title: "(TODO) HTTP(S) metadata fetcher" }, 49 49 ]; 50 50 --- 51 51
+67 -28
src/pages/input/native-fs/_applet.astro
··· 1 1 <script> 2 - import * as CID from "@atcute/cid"; 3 2 import * as IDB from "idb-keyval"; 4 3 5 4 import { applets } from "@web-applets/sdk"; ··· 10 9 import type { Track } from "@applets/core/types.d.ts"; 11 10 import { isAudioFile } from "@scripts/inputs/common"; 12 11 13 - type Handles = Array<{ id: string; handle: FileSystemDirectoryHandle }>; 12 + type Handles = Record<string, FileSystemDirectoryHandle>; 14 13 15 14 // TODO: Add ability to list cached tracks from other devices (ie. unknown handle ids) 16 15 ··· 25 24 //////////////////////////////////////////// 26 25 // ACTIONS 27 26 //////////////////////////////////////////// 27 + const isAvailable = async (fileUri: string) => { 28 + const handles = await fetchHandles(); 29 + const uri = URI.parse(fileUri); 30 + return uri.host && !!handles[uri.host]; 31 + }; 32 + 28 33 const list = async (cachedTracks: Track[] = []) => { 29 - const handles: Handles = (await IDB.get(IDB_HANDLES)) ?? []; 34 + const handles = Object.entries(await fetchHandles()).map(([id, handle]) => { 35 + return { id, handle }; 36 + }); 30 37 31 38 const processed: Track[][] = await Promise.all( 32 - handles.map(({ id, handle }) => recursiveList(handle, id, [])), 39 + handles.map(({ id, handle }) => { 40 + return recursiveList(handle, id, []); 41 + }), 33 42 ); 34 43 35 44 const cache = cachedTracks.reduce( 36 45 (acc: Record<string, Record<string, Track>>, track: Track) => { 37 46 const handleId = trackHandleId(track); 38 - const cid = trackCid(track); 47 + if (!handleId) return acc; 39 48 40 - if (!handleId || !cid) return acc; 41 - 42 - return { ...acc, [handleId]: { ...(acc[handleId] || {}), [cid]: track } }; 49 + return { ...acc, [handleId]: { ...(acc[handleId] || {}), [track.uri]: track } }; 43 50 }, 44 51 {}, 45 52 ); ··· 47 54 const groups = processed.flat(1).reduce( 48 55 (acc, track) => { 49 56 const handleId = trackHandleId(track); 50 - const cid = trackCid(track); 51 - 52 57 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 58 55 - return { ...acc, [handleId]: { ...acc[handleId], [cid]: track } }; 59 + return { ...acc, [handleId]: { ...acc[handleId], [track.uri]: track } }; 56 60 }, 57 61 handles.reduce((acc: Record<string, Record<string, Track>>, handle) => { 58 62 return { ...acc, [handle.id]: {} }; ··· 73 77 return data; 74 78 }; 75 79 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()` 80 + const resolve = async (fileUri: string) => { 81 + const uri = URI.parse(fileUri); 82 + if (uri.scheme !== "file+local") return undefined; 83 + if (!uri.host || !uri.path) return undefined; 84 + 85 + const handles = await fetchHandles(); 86 + const handle = handles[uri.host]; 87 + if (!handle) return undefined; 88 + 89 + const parts = (uri.path.startsWith("/") ? uri.path.slice(1) : uri.path) 90 + .split("/") 91 + .map((a) => decodeURIComponent(a)); 92 + const filename = parts[parts.length - 1]; 93 + 94 + console.log(parts); 95 + 96 + const dirHandle = await parts 97 + .slice(0, -1) 98 + .reduce( 99 + async ( 100 + acc: Promise<FileSystemDirectoryHandle>, 101 + part: string, 102 + ): Promise<FileSystemDirectoryHandle> => { 103 + const h = await acc; 104 + console.log("→", part); 105 + return await h.getDirectoryHandle(part); 106 + }, 107 + Promise.resolve(handle), 108 + ); 109 + 110 + const fileHandle = await dirHandle.getFileHandle(filename); 111 + const file = await fileHandle.getFile(); 112 + const url = URL.createObjectURL(file); 113 + 114 + // TODO: Should be able to just return the data in the handler 115 + context.data = url; 116 + return url; 82 117 }; 83 118 84 119 const mount = async () => { 85 120 await showDirectoryPicker() 86 121 .then(async (handle) => { 87 - const existingHandles: Handles = (await IDB.get(IDB_HANDLES)) ?? []; 122 + const existingHandles = await fetchHandles(); 88 123 const id = crypto.randomUUID(); 89 124 90 125 await handle.requestPermission({ mode: "read" }); 91 - await IDB.set(IDB_HANDLES, [...existingHandles, { id, handle }]); 126 + await IDB.set(IDB_HANDLES, { ...existingHandles, [id]: handle }); 92 127 }) 93 128 .catch(() => {}); 94 129 }; 95 130 131 + const unmount = async (handleId: string) => { 132 + const handles = await fetchHandles(); 133 + delete handles[handleId]; 134 + await IDB.set(IDB_HANDLES, { ...handles }); 135 + }; 136 + 137 + context.setActionHandler("isAvailable", isAvailable); 96 138 context.setActionHandler("list", list); 97 139 context.setActionHandler("resolve", resolve); 98 140 context.setActionHandler("mount", mount); 141 + context.setActionHandler("unmount", unmount); 99 142 100 143 //////////////////////////////////////////// 101 144 // 🛠️ 102 145 //////////////////////////////////////////// 146 + async function fetchHandles(): Promise<Handles> { 147 + return (await IDB.get(IDB_HANDLES)) ?? {}; 148 + } 149 + 103 150 function trackCid(track: Track): string | undefined { 104 151 const a = URI.parse(track.uri); 105 152 const cid = a.query ? QS.parse(a.query).cid || undefined : undefined; ··· 120 167 121 168 for await (const item of dir.values()) { 122 169 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 170 const uri = URI.serialize({ 129 171 scheme: "file+local", 130 172 host: rootHandleId, 131 173 path: `${path.length ? "/" + path.join("/") : ""}/${item.name}`, 132 - query: QS.stringify({ cid: CID.toString(cid) }), 133 174 }); 134 - 135 - console.log(uri); 136 175 137 176 const track: Track = { 138 177 id: crypto.randomUUID(),
+21 -1
src/pages/input/native-fs/_manifest.json
··· 6 6 "scheme": "file+local" 7 7 }, 8 8 "actions": { 9 + "consult": { 10 + "title": "Consult", 11 + "description": "Check if a handle is available to be used by passing in a file uri that uses that handle as the host.", 12 + "params_schema": { 13 + "type": "string", 14 + "description": "The uri with the handle to check the availability of." 15 + } 16 + }, 9 17 "list": { 10 18 "title": "List", 11 19 "description": "List tracks.", ··· 22 30 } 23 31 } 24 32 }, 33 + "resolve": { 34 + "title": "Resolve", 35 + "description": "Potentially translates a track uri with a matching scheme into a URL pointing at the audio bytes. If it can be resolved that is, otherwise you'll get `undefined`.", 36 + "params_schema": { 37 + "type": "string", 38 + "description": "The uri to resolve" 39 + } 40 + }, 25 41 "mount": { 26 42 "title": "Mount", 27 43 "description": "Prepare for usage." 28 44 }, 29 45 "unmount": { 30 46 "title": "Unmount", 31 - "description": "Callback after usage." 47 + "description": "Callback after usage.", 48 + "params_schema": { 49 + "type": "string", 50 + "description": "The handle id to unmount" 51 + } 32 52 } 33 53 } 34 54 }
+22 -2
src/scripts/themes/pilot/index.ts
··· 1 - import type { Output } from "@applets/core/types.d.ts"; 1 + import type { Output, Track } from "@applets/core/types.d.ts"; 2 2 import { applet, reactive } from "../../theme.ts"; 3 3 4 4 //////////////////////////////////////////// ··· 100 100 document.onclick = async () => { 101 101 await input.nativeFs.sendAction("mount"); 102 102 await input.nativeFs.sendAction("list"); 103 - console.log(input.nativeFs.data); 103 + const list = input.nativeFs.data as Track[]; 104 + console.log(list); 105 + await input.nativeFs.sendAction("resolve", list[0].uri); 106 + const url = input.nativeFs.data; 107 + console.log(url); 108 + 109 + // const id = crypto.randomUUID(); 110 + // 111 + // await engine.audio.sendAction("render", { 112 + // tracks: [ 113 + // { 114 + // id, 115 + // isPreload: false, 116 + // url, 117 + // }, 118 + // ], 119 + // play: { 120 + // trackId: id, 121 + // volume: 0.5, 122 + // }, 123 + // }); 104 124 };