Experiment to rebuild Diffuse using web applets.
0
fork

Configure Feed

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

feat: hook up orchestrators to webamp

+126 -97
+19 -5
src/pages/configurator/input/_applet.astro
··· 33 33 import { applets } from "@web-applets/sdk"; 34 34 35 35 import type { Track } from "@applets/core/types.d.ts"; 36 - import { applet } from "@scripts/theme"; 36 + import { applet, waitUntilAppletIsReady } from "@scripts/theme"; 37 37 38 38 //////////////////////////////////////////// 39 39 // SETUP 40 40 //////////////////////////////////////////// 41 41 const container = document.querySelector("#iframes") || undefined; 42 42 43 + // Register applet 44 + const context = applets.register<{ ready: boolean }>(); 45 + 46 + // Initial state 47 + context.data = { 48 + ready: false, 49 + }; 50 + 43 51 // Applet connections 44 52 const input = { 45 53 nativeFs: await applet("../../input/native-fs", { container }), 46 54 }; 47 55 48 - // Register applet 49 - const context = applets.register<Track[]>(); 50 - 51 56 //////////////////////////////////////////// 52 57 // ACTIONS 53 58 //////////////////////////////////////////// 54 59 55 60 const list = async (cachedTracks: Track[] = []) => { 61 + await waitUntilAppletIsReady(input.nativeFs); 62 + 56 63 const groups = cachedTracks.reduce( 57 64 (acc: Record<string, Track[]>, track: Track) => { 58 65 const scheme = track.uri.split(":", 1)[0]; ··· 67 74 async ([scheme, cachedTracksGroup]: [string, Track[]]) => { 68 75 switch (scheme) { 69 76 case input.nativeFs.manifest.input_properties.scheme: 70 - return await input.nativeFs.sendAction("list", cachedTracksGroup); 77 + return await input.nativeFs.sendAction("list", cachedTracksGroup, { 78 + timeoutDuration: 60000 * 60 * 24, 79 + }); 71 80 72 81 default: 73 82 return cachedTracks; ··· 95 104 96 105 context.setActionHandler("list", list); 97 106 context.setActionHandler("resolve", resolve); 107 + 108 + //////////////////////////////////////////// 109 + // 🚦 110 + //////////////////////////////////////////// 111 + context.data = { ready: true }; 98 112 </script>
+1 -4
src/pages/configurator/output/_applet.astro
··· 65 65 //////////////////////////////////////////// 66 66 const context = applets.register<{ ready: boolean }>(); 67 67 68 + // Applets container 68 69 const container = document.createElement("div"); 69 70 container.id = "iframes"; 70 71 document.body.appendChild(container); ··· 338 339 const get: OutputGetter = async (args) => { 339 340 let data: Uint8Array | undefined; 340 341 341 - console.log("------>", active()); 342 - 343 342 switch (active()) { 344 343 case "browser": { 345 344 return await storage.output.indexedDB.sendAction<Uint8Array | undefined>("get", args); ··· 370 369 break; 371 370 } 372 371 }; 373 - 374 - console.log("BIND GET"); 375 372 376 373 context.setActionHandler("get", get); 377 374 context.setActionHandler("put", put);
+7 -1
src/pages/input/native-fs/_applet.astro
··· 41 41 const IDB_HANDLES = `${IDB_PREFIX}/handles`; 42 42 const SCHEME = manifest.input_properties.scheme; 43 43 44 - const context = applets.register(); 44 + // Register applet 45 + const context = applets.register<{ ready: boolean }>(); 45 46 46 47 //////////////////////////////////////////// 47 48 // UI ··· 286 287 287 288 return tracks; 288 289 } 290 + 291 + //////////////////////////////////////////// 292 + // 🚦 293 + //////////////////////////////////////////// 294 + context.data = { ready: true }; 289 295 </script>
+53 -37
src/pages/orchestrator/input-cache/_applet.astro
··· 2 2 import { applets } from "@web-applets/sdk"; 3 3 4 4 import type { Output, Track, TrackStats, TrackTags } from "@applets/core/types.d.ts"; 5 - import { applet } from "@scripts/theme"; 5 + import { applet, waitUntilAppletIsReady } from "@scripts/theme"; 6 6 7 7 //////////////////////////////////////////// 8 8 // SETUP 9 9 //////////////////////////////////////////// 10 - // Register applet 11 - const context = applets.register<Output>(); 10 + const context = applets.register<{ ready: boolean }>(); 11 + 12 + // Initial data 13 + context.data = { 14 + ready: false, 15 + }; 12 16 13 17 // Applet connections 14 18 const configurator = { ··· 16 20 }; 17 21 18 22 const orchestrator = { 19 - output: await applet<Output>("../../orchestrator/output-management", { context: self.parent }), 23 + output: await applet<Output>("../../orchestrator/output-management", { 24 + context: self.parent, 25 + }), 20 26 }; 21 27 22 28 const processor = { 23 29 metadataFetcher: await applet("../../processor/metadata-fetcher", { context: self.parent }), 24 30 }; 25 31 26 - console.log("🔮", orchestrator.output.data); 27 - 28 - orchestrator.output.ondata = () => console.log("🚀", orchestrator.output.data); 29 - 30 - orchestrator.output.addEventListener( 31 - "data", 32 - () => { 33 - console.log("BEFORE", orchestrator.output.data.tracks); 34 - processInputs(); 35 - }, 36 - { once: true }, 37 - ); 32 + // 🚀 33 + process(); 38 34 39 35 //////////////////////////////////////////// 40 36 // ACTIONS 41 37 //////////////////////////////////////////// 42 - async function processInputs() { 38 + context.setActionHandler("process", process); 39 + 40 + async function process() { 41 + await waitUntilAppletIsReady(configurator.input); 42 + 43 43 const cachedTracks = orchestrator.output.data.tracks; 44 44 const tracks = await configurator.input.sendAction<Track[]>("list", cachedTracks, { 45 - timeoutDuration: Infinity, 45 + timeoutDuration: 60000 * 60 * 24, 46 46 }); 47 47 48 48 // Process 49 - tracks.reduce(async (promise: Promise<Track[]>, track: Track) => { 50 - const acc = await promise; 51 - if (track.tags) return [...acc, track]; 49 + const tracksWithMetadata = await tracks.reduce( 50 + async (promise: Promise<Track[]>, track: Track) => { 51 + const acc = await promise; 52 52 53 - await configurator.input.sendAction("resolve", track.uri); 54 - const url = configurator.input.data as string | undefined; 53 + if (track.tags) return [...acc, track]; 55 54 56 - await processor.metadataFetcher.sendAction("extract", url); 57 - const meta: any = processor.metadataFetcher.data; 55 + const url = await configurator.input.sendAction<string | undefined>("resolve", track.uri, { 56 + timeoutDuration: 60000, 57 + }); 58 58 59 - const stats: TrackStats = { 60 - duration: meta.format.duration, 61 - }; 59 + if (!url) return acc; 60 + 61 + const meta = await processor.metadataFetcher.sendAction("extract", url, { 62 + timeoutDuration: 60000, 63 + }); 62 64 63 - const tags: TrackTags = { 64 - album: meta.common.album, 65 - artist: meta.common.artist, 66 - title: meta.common.title, 67 - }; 65 + const stats: TrackStats = { 66 + duration: meta.format.duration, 67 + }; 68 68 69 - return [...acc, { ...track, stats, tags }]; 70 - }, Promise.resolve([])); 69 + const tags: TrackTags = { 70 + album: meta.common.album, 71 + artist: meta.common.artist, 72 + title: meta.common.title, 73 + }; 71 74 72 - console.log("AFTER", tracks); 75 + return [...acc, { ...track, stats, tags }]; 76 + }, 77 + Promise.resolve([]), 78 + ); 73 79 74 80 // Save 75 - await orchestrator.output.sendAction("saveTracks", tracks); 81 + await orchestrator.output.sendAction("tracks", tracksWithMetadata, { 82 + timeoutDuration: 60000 * 2, 83 + }); 84 + 85 + // Log 86 + console.log("🪵 PROCESSING COMPLETED"); 76 87 } 88 + 89 + //////////////////////////////////////////// 90 + // 🚦 91 + //////////////////////////////////////////// 92 + context.data = { ready: true }; 77 93 </script>
+24 -34
src/pages/orchestrator/output-management/_applet.astro
··· 3 3 import { applets } from "@web-applets/sdk"; 4 4 import { debounce } from "throttle-debounce"; 5 5 6 - import type { Output, Track } from "@applets/core/types.d.ts"; 6 + import type { Track } from "@applets/core/types.d.ts"; 7 + import type { State } from "./types.d.ts"; 7 8 import { applet, waitUntilAppletIsReady } from "@scripts/theme"; 8 9 9 10 //////////////////////////////////////////// 10 11 // SETUP 11 12 //////////////////////////////////////////// 12 - // Register applet 13 - const context = applets.register<Output>(); 13 + const context = applets.register<State>(); 14 + 15 + // Initial data 16 + context.data = { 17 + ready: false, 18 + tracks: [], 19 + }; 14 20 15 21 // Applet connections 16 22 const configurator = { 17 23 output: await applet("../../configurator/output", { context: self.parent }), 18 24 }; 19 25 20 - // Sample content 21 - const SAMPLE_TRACKS: Track[] = [ 22 - { 23 - id: crypto.randomUUID(), 24 - uri: "https://archive.org/download/SUSPENSE_Radio_Digitally_Restored_Collection/%2040-07-22%20The%20Lodger%20%28audition%29%20%28Herbert%20Marshall%2C%20Alfred%20Hitchcock%2C%20Edmund%20Gwenn%29.mp3", 25 - tags: { 26 - title: "Yours Truly, Johnny Dollar", 27 - }, 28 - }, 29 - { 30 - id: crypto.randomUUID(), 31 - uri: "https://archive.org/download/OTRR_Dimension_X_Singles/Dimension_X_1950-04-08__01_OuterLimit.mp3", 32 - tags: { 33 - title: "Dimension X", 34 - }, 35 - }, 36 - ]; 37 - 38 - // Initial state 26 + // Load tracks 39 27 context.data = { 28 + ready: false, 40 29 tracks: await loadTracks(), 41 30 }; 42 31 43 32 // State helpers 44 - function update(partial: Partial<Output>): void { 33 + function update(partial: Partial<State>): void { 45 34 context.data = { ...context.data, ...partial }; 46 35 } 47 36 ··· 49 38 // LOADERS 50 39 //////////////////////////////////////////// 51 40 async function loadTracks(): Promise<Track[]> { 52 - console.log("🚧🚧🚧 LOADING TRACKS"); 53 - 54 41 await waitUntilAppletIsReady(configurator.output); 55 42 56 - // TODO: This is not concurrency safe! 57 - await configurator.output.sendAction( 43 + const data = await configurator.output.sendAction( 58 44 "get", 59 45 { 60 46 name: "tracks.json", 61 47 }, 62 48 { 63 - timeoutDuration: 60000, 49 + timeoutDuration: 120000, 64 50 }, 65 51 ); 66 52 67 - const data = configurator.output.data; 68 - 69 - console.log("🥝🥝🥝 TRACKS LOADED", data); 70 - 71 53 if (!data) { 72 54 return []; 73 55 } ··· 78 60 //////////////////////////////////////////// 79 61 // ACTIONS 80 62 //////////////////////////////////////////// 81 - const saveTracks = (tracks: Track[]) => { 63 + const tracksHandler = (tracks: Track[]) => { 82 64 update({ tracks }); 65 + 66 + // Save tracks to output, but only the ones that need to be saved. 67 + // TODO: For each track.uri scheme ask the output configurator if it needs to be cached. 83 68 saveTracksToOutput(tracks); 84 69 }; 85 70 ··· 92 77 }); 93 78 }); 94 79 95 - context.setActionHandler("saveTracks", saveTracks); 80 + context.setActionHandler("tracks", tracksHandler); 96 81 97 82 //////////////////////////////////////////// 98 83 // 🛠️ ··· 104 89 function encode(data: Object) { 105 90 return new TextEncoder().encode(JSON.stringify(data)); 106 91 } 92 + 93 + //////////////////////////////////////////// 94 + // 🚦 95 + //////////////////////////////////////////// 96 + update({ ready: true }); 107 97 </script>
+2 -2
src/pages/orchestrator/output-management/_manifest.json
··· 3 3 "title": "Diffuse Orchestrator | Output management", 4 4 "entrypoint": "index.html", 5 5 "actions": { 6 - "saveTracks": { 7 - "description": "Save tracks to output." 6 + "tracks": { 7 + "description": "Manage tracks." 8 8 } 9 9 } 10 10 }
+3
src/pages/orchestrator/output-management/types.d.ts
··· 1 + import { Output } from "@applets/core/types"; 2 + 3 + export type State = Output & { ready: boolean };
+17 -14
src/scripts/themes/webamp/index.ts
··· 2 2 import { URLTrack } from "webamp"; 3 3 4 4 import type { Output, Track } from "@applets/core/types.d.ts"; 5 - import { applet } from "../../theme.ts"; 5 + import { applet, waitUntilAppletIsReady } from "../../theme.ts"; 6 6 7 7 //////////////////////////////////////////// 8 8 // 🎨 Styles ··· 19 19 20 20 const orchestrator = { 21 21 output: await applet<Output>("../../orchestrator/output-management"), 22 - }; 23 22 24 - // setTimeout(async () => { 25 - // await applet("../../orchestrator/input-cache"); 26 - // }, 15000); 27 - 28 - // await applet("../../orchestrator/input-cache"); 23 + // TODO: Should this be explicitely be ran after the output orchestrator is loaded? 24 + input: await applet("../../orchestrator/input-cache"), 25 + }; 29 26 30 27 //////////////////////////////////////////// 31 28 // ⚡ ··· 39 36 document.body.appendChild(ampNode); 40 37 amp.renderWhenReady(ampNode); 41 38 42 - // orchestrator.output.ondata = async () => { 43 - // console.log("DATA CHANGED"); 44 - // const tracks = await loadTracks(); 45 - // amp.setTracksToPlay(tracks); 46 - // }; 39 + orchestrator.output.ondata = async () => { 40 + const tracks = await loadTracks(); 41 + amp.setTracksToPlay([]); 42 + amp.appendTracks(tracks); 43 + amp.nextTrack(); 44 + }; 47 45 48 46 //////////////////////////////////////////// 49 47 // 🛠️ ··· 53 51 async (promise: Promise<URLTrack[]>, track: Track) => { 54 52 const acc = await promise; 55 53 56 - await configurator.input.sendAction("resolve", track.uri); 57 - const url = configurator.input.data as string | undefined; 54 + // TODO: Ideally the URL should only be resolved when needed, 55 + // but webamp doesn't allow for that. 56 + // Maybe you could work around it with a service worker. 57 + const url = await configurator.input.sendAction<string | undefined>("resolve", track.uri, { 58 + timeoutDuration: 60000, 59 + }); 60 + 58 61 if (!url) return acc; 59 62 60 63 const urlTrack: URLTrack = {