Experiment to rebuild Diffuse using web applets.
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}