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