···55 "actions": {
66 "pause": {
77 "title": "Pause",
88- "description": "Pause a track",
88+ "description": "Pause audio",
99 "params_schema": {
1010 "type": "object",
1111 "properties": {
1212- "trackId": {
1212+ "audioId": {
1313 "type": "string"
1414 }
1515 },
1616- "required": ["trackId"]
1616+ "required": ["audioId"]
1717 }
1818 },
1919 "play": {
2020 "title": "Play",
2121- "description": "Play a track",
2121+ "description": "Play audio",
2222 "params_schema": {
2323 "type": "object",
2424 "properties": {
2525- "trackId": {
2525+ "audioId": {
2626 "type": "string"
2727 },
2828 "volume": {
···3030 "default": 0.5
3131 }
3232 },
3333- "required": ["trackId"]
3333+ "required": ["audioId"]
3434 }
3535 },
3636 "render": {
···3939 "params_schema": {
4040 "type": "object",
4141 "properties": {
4242- "play": {
4343- "type": "object",
4444- "description": "Pass in this object to immediately start playing one of the rendered tracks.",
4545- "properties": {
4646- "trackId": {
4747- "type": "string",
4848- "description": "The id of the rendered track we want to play."
4949- },
5050- "volume": {
5151- "type": "number",
5252- "default": 0.5,
5353- "description": "A number equal to, or between, 0 and 1, that determines how loud the track should play."
5454- }
5555- },
5656- "required": ["trackId"]
5757- },
5858- "tracks": {
4242+ "audio": {
5943 "type": "array",
6060- "description": "The tracks we want to render.",
4444+ "description": "The audio items we want to render. These represent the audio elements that are in the DOM.",
6145 "items": {
6246 "type": "object",
6347 "properties": {
···6953 },
7054 "required": ["id", "url"]
7155 }
5656+ },
5757+ "play": {
5858+ "type": "object",
5959+ "description": "Pass in this object to immediately start playing one of the rendered audio items.",
6060+ "properties": {
6161+ "audioId": {
6262+ "type": "string",
6363+ "description": "The id of the rendered audio item we want to play."
6464+ },
6565+ "volume": {
6666+ "type": "number",
6767+ "default": 0.5,
6868+ "description": "A number equal to, or between, 0 and 1, that determines how loud the audio should play."
6969+ }
7070+ },
7171+ "required": ["audioId"]
7272 }
7373 },
7474- "required": ["tracks"]
7474+ "required": ["audio"]
7575 }
7676 },
7777 "reload": {
7878 "title": "Reload",
7979- "description": "Make sure the audio node with the given track id is loading properly. This should be used when for example, the internet connection comes back and the loading of the track depended on said internet connection.",
7979+ "description": "Make sure the audio with the given id is loading properly. This should be used when for example, the internet connection comes back and the loading of the audio depended on said internet connection.",
8080 "params_schema": {
8181 "type": "object",
8282 "properties": {
8383+ "audioId": {
8484+ "type": "string"
8585+ },
8386 "play": {
8487 "type": "boolean"
8588 },
8689 "progress": {
8790 "type": "number"
8888- },
8989- "trackId": {
9090- "type": "string"
9191 }
9292 },
9393- "required": ["percentage", "trackId"]
9393+ "required": ["audioId", "percentage"]
9494 }
9595 },
9696 "seek": {
9797 "title": "Seek",
9898- "description": "Seek a track to a given position",
9898+ "description": "Seek audio to a given position",
9999 "params_schema": {
100100 "type": "object",
101101 "properties": {
102102+ "audioId": {
103103+ "type": "string"
104104+ },
102105 "percentage": {
103106 "type": "number",
104107 "description": "A number between 0 and 1 that determines the new current position in the audio"
105105- },
106106- "trackId": {
107107- "type": "string"
108108 }
109109 },
110110- "required": ["percentage", "trackId"]
110110+ "required": ["audioId", "percentage"]
111111 }
112112 },
113113 "volume": {
114114 "title": "Volume",
115115- "description": "Set the volume of all tracks, or a specific track.",
115115+ "description": "Set the volume of all audio, or a specific audio node.",
116116 "params_schema": {
117117 "type": "object",
118118 "properties": {
119119- "trackId": {
119119+ "audioId": {
120120 "type": "string"
121121 },
122122 "volume": {
···133133 abstraction, you can mix and match as you like. You can even use them on their own.
134134 </p>
135135136136+ <p>
137137+ Some themes may be constructed out of various applets that are not listed here. The reason
138138+ for that is those applets cannot be used solely on their own, they require an external
139139+ context to coordinate them.
140140+ </p>
141141+142142+ <p>
143143+ There's tradeoffs to both approaches. A particular tradeoff to keep in mind for constituents
144144+ is that they'll have nested dependencies. So when overriding applets dependencies, the
145145+ overrides need to passed down the tree.
146146+ </p>
147147+136148 <List items={constituents} />
137149 </section>
138150
+3-3
src/pages/orchestrator/input-cache/_applet.astro
···11<script>
22 import type { ManagedOutput, ResolvedUri, Track } from "@applets/core/types.d.ts";
33- import { applet, register, waitUntilAppletData } from "@scripts/applets/common";
33+ import { applet, register, wait } from "@scripts/applets/common";
4455 ////////////////////////////////////////////
66 // SETUP
···2222 metadata: applet("../../processor/metadata"),
2323 };
24242525- // Start processing when tracks are loaded
2525+ // Start processing once settled and tracks are loaded
2626 context
2727 .settled()
2828 .then(() => configurator.output)
2929- .then((output) => waitUntilAppletData(output, (d) => d?.tracks.state === "loaded"))
2929+ .then((output) => wait(output, (d) => d?.tracks.state === "loaded"))
3030 .then(() => process());
31313232 ////////////////////////////////////////////
+62-53
src/pages/orchestrator/single-queue/_applet.astro
···11<script>
22 import type { ManagedOutput, ResolvedUri, Track } from "@applets/core/types.d.ts";
33- import { applet, reactive, register, waitUntilAppletData } from "@scripts/applets/common";
33+ import { applet, inputUrl, reactive, register, wait } from "@scripts/applets/common";
4455 ////////////////////////////////////////////
66 // SETUP
77 ////////////////////////////////////////////
88 import type * as AudioEngine from "@applets/engine/audio/types.d.ts";
99 import type * as QueueEngine from "@applets/engine/queue/types.d.ts";
1010+ import type { Applet } from "@web-applets/sdk";
1111+ import { undefined } from "astro:schema";
10121113 // Register applet
1214 const context = register();
···2729 ////////////////////////////////////////////
2830 context.setActionHandler("fill", fill);
29313232+ // TODO: Shuffle, limit track amount, etc.
3033 async function fill(tracks: Track[]) {
3131- const queueItems = await tracks.reduce(
3232- async (promise: Promise<QueueEngine.QueueItem[]>, track: Track) => {
3333- const acc = await promise;
3434- const res = await configurator.input.sendAction<ResolvedUri>(
3535- "resolve",
3636- {
3737- method: "GET",
3838- uri: track.uri,
3939- },
4040- {
4141- timeoutDuration: 60000 * 5,
4242- },
4343- );
3434+ await engine.queue.sendAction("add", tracks, {
3535+ timeoutDuration: 60000,
3636+ });
3737+ }
44384545- if (!res) return acc;
4646-4747- return [
4848- ...acc,
4949- {
5050- expiresAt: res.expiresAt,
5151- id: track.id,
5252- url: res.url,
5353- },
5454- ];
5555- },
5656- Promise.resolve([]),
5757- );
3939+ ////////////////////////////////////////////
4040+ // Connections
4141+ ////////////////////////////////////////////
4242+ await context.settled();
58435959- await engine.queue.sendAction("add", queueItems);
4444+ function connect<D, T>(
4545+ applet: Applet<D>,
4646+ dataFn: (data: D) => T,
4747+ effectFn: (t: T, setter: (t: T) => void) => void,
4848+ ) {
4949+ if (!context.isMainInstance()) return;
5050+ return reactive(applet, dataFn, effectFn);
6051 }
61526253 ////////////////////////////////////////////
···6455 // 🔉 AUDIO
6556 ////////////////////////////////////////////
66575858+ // When the active audio has ended,
5959+ // shift the queue.
6060+6761 // NOTE:
6868- // These could probably be optimised, but it works.
6262+ // This could probably be optimised, but it works.
69637070- reactive(
6464+ connect(
7165 engine.audio,
7266 (data) => data.items[engine.queue.data.now?.id ?? Infinity]?.hasEnded ?? false,
7367 (hasEnded) => {
···7973 // ⚙️ [Connections → Engines]
8074 // 🚏 QUEUE
8175 ////////////////////////////////////////////
8282- reactive(
7676+7777+ // When the active queue item has changed,
7878+ // coordinate the audio engine accordingly.
7979+8080+ connect(
8381 engine.queue,
8482 (data) => data.now?.id,
8585- () => {
8383+ async () => {
8684 const playingNow = engine.queue.data.now;
8785 const volume = engine.audio.data.volume;
88868989- if (!playingNow) {
9090- // NOTE: This probably isn't correct, keep preloads?
9191- engine.audio.sendAction("render", { tracks: [] });
9292- return;
9393- }
9494-9587 // Play new active queue item
9696- engine.audio.sendAction("render", {
9797- tracks: [
9898- {
9999- id: playingNow.id,
100100- isPreload: false,
101101- url: playingNow.url,
102102- },
103103- ],
104104- play: {
105105- trackId: playingNow.id,
106106- volume,
8888+ // TODO: Take URL expiration timestamp into account
8989+ // TODO: Preload next queue item
9090+ engine.audio.sendAction(
9191+ "render",
9292+ {
9393+ audio: playingNow
9494+ ? [
9595+ {
9696+ id: playingNow.id,
9797+ isPreload: false,
9898+ url: await inputUrl(configurator.input, playingNow.uri),
9999+ },
100100+ ]
101101+ : // NOTE: This probably isn't correct, keep preloads?
102102+ [],
103103+ play: playingNow
104104+ ? {
105105+ audioId: playingNow.id,
106106+ volume,
107107+ }
108108+ : undefined,
107109 },
108108- });
110110+ {
111111+ timeoutDuration: 60000,
112112+ },
113113+ );
109114110110- fill(configurator.output.data.tracks.collection);
115115+ // Add more tracks to the queue if needed
116116+ if (playingNow) fill(configurator.output.data.tracks.collection);
111117 },
112118 );
113119···115121 // 🎻 [Connections → Configurators]
116122 // 📦 OUTPUT
117123 ////////////////////////////////////////////
118118- waitUntilAppletData(configurator.output, (d) => d?.tracks.state === "loaded").then(() => {
119119- reactive(
124124+125125+ // Add tracks to the queue once the tracks have been loaded.
126126+127127+ wait(configurator.output, (d) => d?.tracks.state === "loaded").then(() => {
128128+ connect(
120129 configurator.output,
121130 (data) => data.tracks.cacheId,
122131 () => {
+41-14
src/pages/processor/artwork/_applet.astro
···2121 };
22222323 // Applet connections
2424+ // TODO: Ideally only configurator, orchestrator and UI applets have nested applets.
2525+ // Can we find a way to remove this dependency?
2426 const processor = {
2527 metadata: applet("../../processor/metadata"),
2628 };
···4244 ////////////////////////////////////////////
4345 // ACTIONS
4446 ////////////////////////////////////////////
4747+ function artwork(request: ArtworkRequest) {
4848+ return processRequest(request);
4949+ }
5050+4551 function supply(items: ArtworkRequest[]) {
4652 const exe = !queue[0];
4753 queue = [...queue, ...items];
4854 if (exe) shiftQueue();
4955 }
50565757+ context.setActionHandler("artwork", artwork);
5158 context.setActionHandler("supply", supply);
52595360 ////////////////////////////////////////////
···111118 .catch(() => musicBrainzCover(remainingReleases.slice(1)));
112119 }
113120114114- async function shiftQueue() {
115115- const next = queue.shift();
116116- if (!next) return;
121121+ async function processRequest(req: ArtworkRequest): Promise<Artwork[]> {
122122+ // Check if already processed
123123+ // TODO: Retry if none was found?
124124+ const cache = await IDB.get(`${IDB_PREFIX}/${req.cacheId}`);
125125+ console.log("fromCache", cache);
126126+ if (cache) return cache;
117127118118- // Check if already processed
119119- const cache = await IDB.get(`${IDB_PREFIX}/${next.cacheId}`);
120120- if (cache && cache.length > 0) return;
128128+ console.log(req);
121129122130 // 🚀
123131 let art: Artwork[] = [];
124132125133 // Get metadata + possible artwork from file metadata
126134 const proc = await processor.metadata;
127127- const meta = await proc.sendAction<Extraction>("supply", { ...next, includeArtwork: true });
128128- if (!next.tags) next.tags = meta.tags;
135135+ const meta = await proc.sendAction<Extraction>(
136136+ "supply",
137137+ { ...req, includeArtwork: true },
138138+ {
139139+ timeoutDuration: 60000 * 5,
140140+ },
141141+ );
142142+143143+ if (!req.tags) req.tags = meta.tags;
144144+145145+ console.log(meta);
129146130147 // Add artwork from metadata
131148 const fromMeta =
132149 meta.artwork?.map((a: IPicture) => {
133150 return { bytes: a.data, mime: a.format };
134151 }) || [];
152152+153153+ console.log(fromMeta);
135154136155 art.push(...fromMeta);
137156138157 // If no artwork, try finding it on other sources
139158 if (art.length === 0) {
140140- const fromMusicBrainz = await musicBrainz(next);
159159+ const fromMusicBrainz = await musicBrainz(req);
141160 art.push(...fromMusicBrainz);
142161 }
143162144163 if (art.length === 0) {
145145- const fromLastFm = await lastFm(next);
164164+ const fromLastFm = await lastFm(req);
146165 art.push(...fromLastFm);
147166 }
148167149168 // Save artwork to IDB
150150- await IDB.set(`${IDB_ARTWORK_PREFIX}/${next.cacheId}`, art);
151151- context.data.artwork[next.cacheId] = art;
169169+ await IDB.set(`${IDB_ARTWORK_PREFIX}/${req.cacheId}`, art);
170170+ context.data.artwork[req.cacheId] = art;
171171+172172+ // Fin
173173+ return art;
174174+ }
175175+176176+ async function shiftQueue() {
177177+ const next = queue.shift();
178178+ if (!next) return;
152179153153- // 🏹
154154- shiftQueue();
180180+ await processRequest(next);
181181+ await shiftQueue();
155182 }
156183</script>
+51-29
src/scripts/applets/common.ts
···66import { effect, isSignal, type Signal, signal } from "spellcaster/spellcaster.js";
77import { xxh32 } from "xxh32";
8899-import type { Track } from "@applets/core/types";
99+import type { ResolvedUri, Track } from "@applets/core/types";
10101111////////////////////////////////////////////
1212-// 🪟 Applet connector
1212+// 🪟 Applet connecting
1313////////////////////////////////////////////
1414export async function applet<D>(
1515 src: string,
1616 opts: {
1717 addSlashSuffix?: boolean;
1818- applets?: Record<string, string>;
1918 container?: HTMLElement | Element;
2020- id?: string;
1919+ frameId?: string;
2020+ groupId?: string;
2121 setHeight?: boolean;
2222 } = {},
2323): Promise<Applet<D>> {
···2929 : ""
3030 }`;
31313232- if (opts.applets) {
3333- src = QS.stringifyUrl({ url: src, query: opts.applets });
3232+ let query: undefined | Record<string, string>;
3333+3434+ if (opts.groupId) {
3535+ query = { groupId: opts.groupId };
3636+ }
3737+3838+ if (query) {
3939+ src = QS.stringifyUrl({ url: src, query });
3440 }
35413642 const existingFrame: HTMLIFrameElement | null = window.document.querySelector(`[src="${src}"]`);
···4248 } else {
4349 frame = document.createElement("iframe");
4450 frame.src = src;
4545- if (opts.id) frame.id = opts.id;
5151+ if (opts.frameId) frame.id = opts.frameId;
46524753 if (opts.container) {
4854 opts.container.appendChild(frame);
···86928793 settled(): Promise<void>;
88948989- get id(): string;
9595+ get instanceId(): string;
9096 set data(data: T);
91979298 codec: {
···99105};
100106101107export function register<DataType = any>() {
102102- const channelId = `${location.host}${location.pathname}`;
108108+ const url = new URL(location.href);
103109 const scope = applets.register<DataType>();
104104- const id = crypto.randomUUID();
110110+111111+ const groupId = url.searchParams.get("groupId") || "main";
112112+ const channelId = `${location.host}${location.pathname}/${groupId}`;
113113+ const instanceId = crypto.randomUUID();
105114106115 let isMainInstance = true;
107116···119128 case "PING": {
120129 channel.postMessage({
121130 type: "PONG",
122122- id: event.data.id,
131131+ instanceId: event.data.instanceId,
123132 });
124133125134 if (isMainInstance) {
···132141 }
133142134143 case "PONG": {
135135- if (event.data.id === id) {
144144+ if (event.data.instanceId === instanceId) {
136145 isMainInstance = false;
137146 }
138147 break;
···143152 const result = await scope.actionHandlers[event.data.actionId]?.(...event.data.arguments);
144153 channel.postMessage({
145154 type: "actioncomplete",
146146- id: event.data.id,
155155+ actionInstanceId: event.data.actionInstanceId,
147156 result,
148157 });
149158 }
···159168160169 // Promise that fullfills whenever it figures out its the main instance or not.
161170 const promise = new Promise<void>((resolve) => {
162162- const id = setTimeout(() => {
171171+ const timeoutId = setTimeout(() => {
163172 channel.removeEventListener("message", handler);
164173 resolve(undefined);
165174 }, 1000);
166175167176 const handler = (event: MessageEvent) => {
168177 if (event.data === "pong" || event.data === "ping") {
169169- clearTimeout(id);
178178+ clearTimeout(timeoutId);
170179 channel.removeEventListener("message", handler);
171180 resolve(undefined);
172181 }
···178187 // Send out ping
179188 channel.postMessage({
180189 type: "PING",
181181- id,
190190+ instanceId,
182191 });
183192184193 // If the data on the main instance changes,
···202211 return promise;
203212 },
204213205205- get id() {
206206- return id;
214214+ get instanceId() {
215215+ return instanceId;
207216 },
208217209218 get data() {
···230239 }
231240232241 const actionMessage = {
233233- id: crypto.randomUUID(),
234234- type: "action",
242242+ actionInstanceId: crypto.randomUUID(),
235243 actionId,
244244+ type: "action",
236245 arguments: args,
237246 };
238247239248 return new Promise((resolve) => {
240249 const actionCallback = (event: MessageEvent) => {
241241- if (event.data?.type === "actioncomplete" && event.data?.id === actionMessage.id) {
250250+ if (
251251+ event.data?.type === "actioncomplete" &&
252252+ event.data?.actionInstanceId === actionMessage.actionInstanceId
253253+ ) {
242254 channel.removeEventListener("message", actionCallback);
243255 resolve(event.data.result);
244256 }
···277289}
278290279291////////////////////////////////////////////
292292+// ⚡️ COMMON ACTION CALLS
293293+////////////////////////////////////////////
294294+295295+export async function inputUrl(input: Applet, uri: string, method = "GET") {
296296+ return await input.sendAction<ResolvedUri>(
297297+ "resolve",
298298+ {
299299+ method,
300300+ uri,
301301+ },
302302+ {
303303+ timeoutDuration: 60000 * 5,
304304+ },
305305+ );
306306+}
307307+308308+////////////////////////////////////////////
280309// 🛠️
281310////////////////////////////////////////////
282311export function addScope<O extends object>(astroScope: string, object: O): O {
···352381 return new TextEncoder().encode(JSON.stringify(a));
353382}
354383355355-export function waitUntilAppletData<A>(
356356- applet: Applet<A>,
357357- dataFn: (a: A | undefined) => boolean,
358358-): Promise<void> {
384384+export function wait<A>(applet: Applet<A>, dataFn: (a: A | undefined) => boolean): Promise<void> {
359385 return new Promise((resolve) => {
360386 if (dataFn(applet.data) === true) {
361387 resolve();
···372398 applet.addEventListener("data", callback);
373399 });
374400}
375375-376376-export function waitUntilAppletIsReady(applet: Applet): Promise<void> {
377377- return waitUntilAppletData(applet, (data) => !!data?.ready);
378378-}