Experiment to rebuild Diffuse using web applets.
0

Configure Feed

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

1import type { URLTrack } from "webamp"; 2import Webamp from "webamp"; 3 4import type { GroupConsult, ManagedOutput, ResolvedUri, Track } from "@applets/core/types.d.ts"; 5import { applet, inputUrl, wait } from "@scripts/applet/common"; 6 7//////////////////////////////////////////// 8// 🗂️ Applets 9//////////////////////////////////////////// 10const configurator = { 11 input: applet("/configurator/input"), 12 output: applet<ManagedOutput>("/configurator/output"), 13}; 14 15const orchestrator = { 16 primary: applet("/orchestrator/primary"), 17}; 18 19//////////////////////////////////////////// 20// ⚡ 21//////////////////////////////////////////// 22const amp = new Webamp({ 23 initialTracks: [], 24}); 25 26// Override 27const loadFromUrl = amp.media.loadFromUrl.bind(amp.media); 28 29async function loadOverride(uri: string, autoPlay: boolean) { 30 const resp = await inputUrl(await configurator.input, uri); 31 if (!resp) throw new Error("Failed to resolve URI"); 32 return await loadFromUrl(resp.url, autoPlay); 33} 34 35amp.media.loadFromUrl = loadOverride.bind(amp.media); 36 37// Render 38const ampNode = document.createElement("div"); 39ampNode.style = "height: 100vh; left: 0; position: absolute; top: 0; width: 100%; z-index: -1000;"; 40document.body.appendChild(ampNode); 41amp.renderWhenReady(ampNode); 42 43// Wait for tracks to load 44configurator.output 45 .then((output) => { 46 output.ondata = loadAndInsert; 47 return wait(output, (d) => d?.tracks.state === "loaded"); 48 }) 49 .then(async () => { 50 await loadAndInsert(); 51 (await orchestrator.primary).sendAction("process_inputs", undefined, { 52 timeoutDuration: 60000 * 60, 53 }); 54 }); 55 56// Load & insert 57let inserting = false; 58let tracksCacheId: string | undefined = undefined; 59 60async function loadAndInsert() { 61 const output = await configurator.output; 62 63 if (output.data.tracks.state !== "loaded") return; 64 if (output.data.tracks.cacheId === tracksCacheId) return; 65 if (inserting) return; 66 67 inserting = true; 68 tracksCacheId = output.data.tracks.cacheId; 69 const tracks = await loadTracks(); 70 71 // TODO: This kinda messes up the UI, 72 // but at least the active audio doesn't stop playing. 73 amp.store.dispatch({ type: "REMOVE_ALL_TRACKS" }); 74 75 // TODO: Webamp blows up if you add too much tracks 76 amp.appendTracks(tracks.slice(0, 1000)); 77 78 const status = amp.getMediaStatus(); 79 if (status !== "PLAYING") amp.nextTrack(); 80 81 inserting = false; 82} 83 84//////////////////////////////////////////// 85// 🛠️ 86//////////////////////////////////////////// 87async function loadTracks(): Promise<URLTrack[]> { 88 const input = await configurator.input; 89 const output = await configurator.output; 90 91 const groups = await input.sendAction<GroupConsult>( 92 "groupConsult", 93 output.data.tracks.collection, 94 { timeoutDuration: 60000 * 5, worker: true }, 95 ); 96 97 // Available tracks 98 const tracks = Object.values(groups).reduce((acc: Track[], value) => { 99 if (value.available === false) return acc; 100 return [...acc, ...value.tracks]; 101 }, []); 102 103 return tracks.map((track) => { 104 const urlTrack: URLTrack = { 105 url: track.uri, 106 metaData: { 107 title: track.tags?.title || "", 108 artist: track.tags?.artist || "", 109 album: track.tags?.album, 110 }, 111 duration: track.stats?.duration, 112 }; 113 114 return urlTrack; 115 }); 116}