alpha
Login
or
Join now
tokono.ma
/
diffuse-applets
Star
0
Fork
0
Atom
Configure Feed
Issues
Pull Requests
Commits
Tags
Feed URL
Select the types of activity you want to include in your feed.
Experiment to rebuild Diffuse using web applets.
Star
0
Fork
0
Atom
Configure Feed
Issues
Pull Requests
Commits
Tags
Feed URL
Select the types of activity you want to include in your feed.
Overview
Issues
Pulls
Pipelines
feat: primary orchestrator
author
Steven Vandevelde
date
11 months ago
(Jul 15, 2025, 5:01 PM +0200)
commit
1d0ba7de
1d0ba7de884a7b9a91393f1477f2ece27499759b
parent
8ccdabc2
8ccdabc231c08bef03379a3ddd0f0145c18ad6fc
+398
-398
29 changed files
Expand all
Collapse all
Unified
Split
src
pages
configurator
input
_applet.astro
constituent
blur
artwork-controller
_applet.astro
engine
audio
_applet.astro
queue
_applet.astro
index.astro
input
native-fs
_applet.astro
opensubsonic
_applet.astro
s3
_applet.astro
orchestrator
input-cache
_applet.astro
_manifest.json
primary
_applet.astro
_manifest.json
index.astro
queue-audio
_applet.astro
_manifest.json
index.astro
queue-tracks
_applet.astro
_manifest.json
index.astro
scripts
applet
common.ts
common.ts
engine
queue
worker.ts
input
native-fs
worker.ts
opensubsonic
worker.ts
s3
worker.ts
orchestrator
primary
worker.ts
theme
blur
index.ts
pilot
index.ts
webamp
index.ts
+5
-5
src/pages/configurator/input/_applet.astro
Reviewed
···
42
42
// SETUP
43
43
////////////////////////////////////////////
44
44
const worker = endpoint<Tasks>(
45
45
-
new SharedWorker(new URL("../../../scripts/configurator/input/worker", import.meta.url), {
45
45
+
new Worker(new URL("../../../scripts/configurator/input/worker", import.meta.url), {
46
46
type: "module",
47
47
name: manifest.name,
48
48
-
}).port,
48
48
+
}),
49
49
);
50
50
51
51
// Register applet + worker
···
53
53
54
54
// Applet connections
55
55
const input = {
56
56
-
"file+local": applet("/input/native-fs"),
57
57
-
opensubsonic: applet("/input/opensubsonic"),
58
58
-
s3: applet("/input/s3"),
56
56
+
"file+local": applet("/input/native-fs", { context: self }),
57
57
+
opensubsonic: applet("/input/opensubsonic", { context: self }),
58
58
+
s3: applet("/input/s3", { context: self }),
59
59
};
60
60
61
61
// Provide tunnel to worker
+41
-15
src/pages/constituent/blur/artwork-controller/_applet.astro
Reviewed
···
87
87
transition:
88
88
background-color var(--transition-durition),
89
89
color var(--transition-durition);
90
90
-
z-index: 1;
90
90
+
z-index: 10;
91
91
}
92
92
93
93
/* Progress bars */
···
339
339
import { computed, effect, type Signal, signal } from "spellcaster";
340
340
import { tags, text, type ElementConfigurator } from "spellcaster/hyperscript.js";
341
341
342
342
-
import type { ManagedOutput, Track } from "@applets/core/types";
342
342
+
import type { Track } from "@applets/core/types";
343
343
import { applet, hs, inputUrl, reactive, register } from "@scripts/applet/common";
344
344
-
import { comparable, trackArtworkCacheId } from "@scripts/common";
344
344
+
import { trackArtworkCacheId } from "@scripts/common";
345
345
346
346
////////////////////////////////////////////
347
347
// SETUP
···
382
382
queue: await applet<QueueEngine.State>("/engine/queue", { groupId: context.groupId }),
383
383
};
384
384
385
385
-
const _orchestrator = {
386
386
-
queueAudio: await applet("/orchestrator/queue-audio", {
385
385
+
const orchestrator = {
386
386
+
primary: await applet("/orchestrator/primary", {
387
387
groupId: context.groupId,
388
388
}),
389
389
-
390
390
-
// When using the `main` group, load additional orchestrators:
391
391
-
inputCache:
392
392
-
context.groupId === undefined || context.groupId === "main"
393
393
-
? await applet("/orchestrator/input-cache")
394
394
-
: undefined,
395
395
-
queueTracks:
396
396
-
context.groupId === undefined || context.groupId === "main"
397
397
-
? applet("/orchestrator/queue-tracks")
398
398
-
: undefined,
399
389
};
400
390
401
391
const processor = {
···
472
462
(volume) => setVolume(volume),
473
463
);
474
464
465
465
+
// ORCHESTRATED
466
466
+
467
467
+
context.settled().then(() => {
468
468
+
if (context.isMainInstance())
469
469
+
orchestrator.primary.sendAction("monitor_audio_end", undefined, { timeoutDuration: 60000 });
470
470
+
});
471
471
+
475
472
////////////////////////////////////////////
476
473
// 🎢 QUEUE
477
474
////////////////////////////////////////////
···
518
515
const currCacheId = currTrack ? await trackArtworkCacheId(currTrack) : undefined;
519
516
if (cacheId === currCacheId) setArtwork(art);
520
517
}
518
518
+
519
519
+
// ORCHESTRATED
520
520
+
521
521
+
context.settled().then(() => {
522
522
+
if (context.isMainInstance())
523
523
+
orchestrator.primary.sendAction("monitor_active_queue_item", undefined, {
524
524
+
timeoutDuration: 60000,
525
525
+
});
526
526
+
});
527
527
+
528
528
+
////////////////////////////////////////////
529
529
+
// TRACKS
530
530
+
////////////////////////////////////////////
531
531
+
532
532
+
// ORCHESTRATED
533
533
+
534
534
+
context.settled().then(() => {
535
535
+
if (isMainGroup() && context.isMainInstance()) {
536
536
+
orchestrator.primary
537
537
+
.sendAction("insert_tracks_into_queue", undefined, {
538
538
+
timeoutDuration: 60000 * 5,
539
539
+
})
540
540
+
.then(() => {
541
541
+
orchestrator.primary.sendAction("process_inputs", undefined, {
542
542
+
timeoutDuration: 60000 * 60,
543
543
+
});
544
544
+
});
545
545
+
}
546
546
+
});
521
547
522
548
////////////////////////////////////////////
523
549
// UI
+8
-7
src/pages/engine/audio/_applet.astro
Reviewed
···
25
25
const vol = localStorage.getItem(VOLUME_KEY);
26
26
27
27
// Initial state
28
28
-
context.data = {
29
29
-
isPlaying: false,
30
30
-
items: {},
31
31
-
volume: {
32
32
-
default: vol ? parseFloat(vol) : 0.5,
33
33
-
},
34
34
-
};
28
28
+
if (context.isMainInstance())
29
29
+
context.data = {
30
30
+
isPlaying: false,
31
31
+
items: {},
32
32
+
volume: {
33
33
+
default: vol ? parseFloat(vol) : 0.5,
34
34
+
},
35
35
+
};
35
36
36
37
// State helpers
37
38
function update(partial: Partial<State>): void {
+3
-7
src/pages/engine/queue/_applet.astro
Reviewed
···
20
20
// Register applet
21
21
const context = register<State>({ mode: "shared-worker", worker });
22
22
23
23
+
// Initial state
24
24
+
context.data = await worker.data();
25
25
+
23
26
// Keep applet data with worker data in sync
24
27
sync(context, port);
25
25
-
26
26
-
// Initial state
27
27
-
context.data = {
28
28
-
past: [],
29
29
-
now: null,
30
30
-
future: [],
31
31
-
};
32
28
33
29
////////////////////////////////////////////
34
30
// ACTIONS
+1
-5
src/pages/index.astro
Reviewed
···
46
46
{ url: "input/s3/", title: "S3-Compatible API" },
47
47
];
48
48
49
49
-
const orchestrators = [
50
50
-
{ url: "orchestrator/input-cache/", title: "Input caching" },
51
51
-
{ url: "orchestrator/queue-audio/", title: "Queue ⭤ Audio" },
52
52
-
{ url: "orchestrator/queue-tracks/", title: "Queue ⭤ Tracks" },
53
53
-
];
49
49
+
const orchestrators = [{ url: "orchestrator/primary/", title: "Primary (Queue, audio, tracks)" }];
54
50
55
51
const output = [
56
52
{ url: "output/indexed-db/", title: "IndexedDB" },
+2
-2
src/pages/input/native-fs/_applet.astro
Reviewed
···
27
27
// SETUP
28
28
////////////////////////////////////////////
29
29
const worker = endpoint<Tasks>(
30
30
-
new SharedWorker(new URL("../../../scripts/input/native-fs/worker", import.meta.url), {
30
30
+
new Worker(new URL("../../../scripts/input/native-fs/worker", import.meta.url), {
31
31
type: "module",
32
32
name: manifest.name,
33
33
-
}).port,
33
33
+
}),
34
34
);
35
35
36
36
// Register applet
+2
-2
src/pages/input/opensubsonic/_applet.astro
Reviewed
···
28
28
// SETUP
29
29
////////////////////////////////////////////
30
30
const worker = endpoint<Tasks>(
31
31
-
new SharedWorker(new URL("../../../scripts/input/opensubsonic/worker", import.meta.url), {
31
31
+
new Worker(new URL("../../../scripts/input/opensubsonic/worker", import.meta.url), {
32
32
type: "module",
33
33
name: manifest.name,
34
34
-
}).port,
34
34
+
}),
35
35
);
36
36
37
37
// Register applet
+2
-2
src/pages/input/s3/_applet.astro
Reviewed
···
47
47
// SETUP
48
48
////////////////////////////////////////////
49
49
const worker = endpoint<Tasks>(
50
50
-
new SharedWorker(new URL("../../../scripts/input/s3/worker", import.meta.url), {
50
50
+
new Worker(new URL("../../../scripts/input/s3/worker", import.meta.url), {
51
51
type: "module",
52
52
name: manifest.name,
53
53
-
}).port,
53
53
+
}),
54
54
);
55
55
56
56
// Register applet
-112
src/pages/orchestrator/input-cache/_applet.astro
Reviewed
···
1
1
-
<script>
2
2
-
import type { ManagedOutput, ResolvedUri, Track } from "@applets/core/types.d.ts";
3
3
-
import { applet, register, wait } from "@scripts/applet/common";
4
4
-
import { tracksCacheId } from "@scripts/output/common";
5
5
-
6
6
-
////////////////////////////////////////////
7
7
-
// SETUP
8
8
-
////////////////////////////////////////////
9
9
-
const context = register<{ isProcessing: boolean }>();
10
10
-
11
11
-
// Initial data
12
12
-
context.data = {
13
13
-
isProcessing: false,
14
14
-
};
15
15
-
16
16
-
// Applet connections
17
17
-
const configurator = {
18
18
-
input: applet("/configurator/input"),
19
19
-
output: applet<ManagedOutput>("/configurator/output"),
20
20
-
};
21
21
-
22
22
-
const processor = {
23
23
-
metadata: applet("/processor/metadata"),
24
24
-
};
25
25
-
26
26
-
// Start processing once settled and tracks are loaded
27
27
-
context
28
28
-
.settled()
29
29
-
.then(() => configurator.output)
30
30
-
.then((output) => wait(output, (d) => d?.tracks.state === "loaded"))
31
31
-
.then(() => (context.isMainInstance() ? process() : undefined));
32
32
-
33
33
-
////////////////////////////////////////////
34
34
-
// ACTIONS
35
35
-
////////////////////////////////////////////
36
36
-
context.setActionHandler("process", process);
37
37
-
38
38
-
async function process() {
39
39
-
if (context.data.isProcessing) return;
40
40
-
context.data = { ...context.data, isProcessing: true };
41
41
-
console.log("🪵 Processing initiated");
42
42
-
43
43
-
const input = await configurator.input;
44
44
-
const output = await configurator.output;
45
45
-
46
46
-
const cachedTracks = output.data.tracks.collection;
47
47
-
48
48
-
await input.sendAction("contextualize", cachedTracks, {
49
49
-
timeoutDuration: 60000 * 5,
50
50
-
worker: true,
51
51
-
});
52
52
-
53
53
-
const tracks = await input.sendAction<Track[]>("list", cachedTracks, {
54
54
-
timeoutDuration: 60000 * 60 * 24,
55
55
-
worker: true,
56
56
-
});
57
57
-
58
58
-
// Process
59
59
-
const tracksWithMetadata = await tracks.reduce(
60
60
-
async (promise: Promise<Track[]>, track: Track) => {
61
61
-
const acc = await promise;
62
62
-
63
63
-
if (track.tags && track.stats) return [...acc, track];
64
64
-
65
65
-
const resGet = await input.sendAction<ResolvedUri>(
66
66
-
"resolve",
67
67
-
{ method: "GET", uri: track.uri },
68
68
-
{
69
69
-
timeoutDuration: 60000 * 5,
70
70
-
worker: true,
71
71
-
},
72
72
-
);
73
73
-
74
74
-
const resHead = await input.sendAction<ResolvedUri>(
75
75
-
"resolve",
76
76
-
{ method: "HEAD", uri: track.uri },
77
77
-
{
78
78
-
timeoutDuration: 60000 * 5,
79
79
-
worker: true,
80
80
-
},
81
81
-
);
82
82
-
83
83
-
if (!resGet) return [...acc, track];
84
84
-
85
85
-
const metadataProcessor = await processor.metadata;
86
86
-
const { stats, tags } = await metadataProcessor.sendAction(
87
87
-
"supply",
88
88
-
{ urls: { get: resGet.url, head: resHead?.url || resGet.url } },
89
89
-
{
90
90
-
timeoutDuration: 60000 * 15,
91
91
-
worker: true,
92
92
-
},
93
93
-
);
94
94
-
95
95
-
return [...acc, { ...track, stats, tags }];
96
96
-
},
97
97
-
Promise.resolve([]),
98
98
-
);
99
99
-
100
100
-
// Save
101
101
-
const changed = tracksCacheId(tracksWithMetadata) !== output.data.tracks.cacheId;
102
102
-
103
103
-
if (changed)
104
104
-
await output.sendAction("tracks", tracksWithMetadata, {
105
105
-
timeoutDuration: 60000 * 5,
106
106
-
});
107
107
-
108
108
-
// Fin
109
109
-
console.log("🪵 Processing completed");
110
110
-
context.data = { ...context.data, isProcessing: false };
111
111
-
}
112
112
-
</script>
-11
src/pages/orchestrator/input-cache/_manifest.json
Reviewed
···
1
1
-
{
2
2
-
"name": "diffuse/orchestrator/input-cache",
3
3
-
"title": "Diffuse Orchestrator | Input cache",
4
4
-
"entrypoint": "index.html",
5
5
-
"actions": {
6
6
-
"process": {
7
7
-
"title": "Process",
8
8
-
"description": "Process inputs; listing all tracks, fetching metadata where needed and passing the result to the output manager."
9
9
-
}
10
10
-
}
11
11
-
}
src/pages/orchestrator/input-cache/index.astro
src/pages/orchestrator/primary/index.astro
Reviewed
+231
src/pages/orchestrator/primary/_applet.astro
Reviewed
···
1
1
+
<script>
2
2
+
import type { Applet } from "@web-applets/sdk";
3
3
+
import type { GroupConsult, ManagedOutput, ResolvedUri, Track } from "@applets/core/types.d.ts";
4
4
+
import { applet, inputUrl, reactive, register, wait } from "@scripts/applet/common";
5
5
+
import { tracksCacheId } from "@scripts/output/common";
6
6
+
7
7
+
////////////////////////////////////////////
8
8
+
// SETUP
9
9
+
////////////////////////////////////////////
10
10
+
import type * as AudioEngine from "@applets/engine/audio/types.d.ts";
11
11
+
import type * as QueueEngine from "@applets/engine/queue/types.d.ts";
12
12
+
13
13
+
const context = register<{ isProcessing: boolean }>();
14
14
+
15
15
+
// Initial data
16
16
+
context.data = {
17
17
+
isProcessing: false,
18
18
+
};
19
19
+
20
20
+
// Applet connections
21
21
+
const configurator = {
22
22
+
input: applet("/configurator/input", { context: self }),
23
23
+
input_2: undefined as Applet | undefined,
24
24
+
output: applet<ManagedOutput>("/configurator/output"),
25
25
+
};
26
26
+
27
27
+
const engine = {
28
28
+
audio: applet<AudioEngine.State>("/engine/audio", { groupId: context.groupId }),
29
29
+
queue: applet<QueueEngine.State>("/engine/queue", { groupId: context.groupId }),
30
30
+
};
31
31
+
32
32
+
const processor = {
33
33
+
metadata: applet("/processor/metadata"),
34
34
+
};
35
35
+
36
36
+
////////////////////////////////////////////
37
37
+
// [ACTIONS] AUDIO ⭤ QUEUE
38
38
+
////////////////////////////////////////////
39
39
+
context.setActionHandler("monitor_active_queue_item", monitorActiveQueueItem);
40
40
+
context.setActionHandler("monitor_audio_end", monitorAudioEnd);
41
41
+
42
42
+
async function monitorActiveQueueItem() {
43
43
+
await context.settled();
44
44
+
45
45
+
const audio = await engine.audio;
46
46
+
const queue = await engine.queue;
47
47
+
48
48
+
// When the active queue item has changed,
49
49
+
// coordinate the audio engine accordingly.
50
50
+
reactive(
51
51
+
queue,
52
52
+
(data) => data.now?.id,
53
53
+
async () => {
54
54
+
const activeTrack = queue.data.now;
55
55
+
const isPlaying = audio.data.isPlaying;
56
56
+
57
57
+
// Resolve URIs
58
58
+
const url = activeTrack
59
59
+
? await inputUrl(await configurator.input, activeTrack.uri).then((a) => a?.url)
60
60
+
: undefined;
61
61
+
62
62
+
// Check if we still need to render
63
63
+
if (queue.data.now?.id !== activeTrack?.id) return;
64
64
+
65
65
+
// Play new active queue item
66
66
+
// TODO: Take URL expiration timestamp into account
67
67
+
// TODO: Preload next queue item
68
68
+
audio.sendAction(
69
69
+
"render",
70
70
+
{
71
71
+
audio: activeTrack
72
72
+
? [
73
73
+
{
74
74
+
id: activeTrack.id,
75
75
+
isPreload: false,
76
76
+
url,
77
77
+
},
78
78
+
]
79
79
+
: // TODO: Keep preloads
80
80
+
[],
81
81
+
play: activeTrack && isPlaying ? { audioId: activeTrack.id } : undefined,
82
82
+
},
83
83
+
{
84
84
+
timeoutDuration: 60000,
85
85
+
},
86
86
+
);
87
87
+
},
88
88
+
);
89
89
+
}
90
90
+
91
91
+
async function monitorAudioEnd() {
92
92
+
const audio = await engine.audio;
93
93
+
const queue = await engine.queue;
94
94
+
95
95
+
// When the active audio has ended,
96
96
+
// shift the queue.
97
97
+
reactive(
98
98
+
audio,
99
99
+
(data) => data.items[queue.data.now?.id ?? Infinity]?.hasEnded ?? false,
100
100
+
(hasEnded) => {
101
101
+
if (hasEnded) queue.sendAction("shift", undefined, { worker: true });
102
102
+
},
103
103
+
);
104
104
+
}
105
105
+
106
106
+
////////////////////////////////////////////
107
107
+
// [ACTIONS] PROCESS
108
108
+
////////////////////////////////////////////
109
109
+
context.setActionHandler("process_inputs", processInputs);
110
110
+
111
111
+
async function processInputs() {
112
112
+
if (context.data.isProcessing) return;
113
113
+
context.data = { ...context.data, isProcessing: true };
114
114
+
console.log("🪵 Processing initiated");
115
115
+
116
116
+
const input = configurator.input_2
117
117
+
? configurator.input_2
118
118
+
: await applet("/configurator/input", { context: self, newInstance: true });
119
119
+
120
120
+
if (!configurator.input_2) configurator.input_2 = input;
121
121
+
122
122
+
const output = await configurator.output;
123
123
+
const cachedTracks = output.data.tracks.collection;
124
124
+
125
125
+
await input.sendAction("contextualize", cachedTracks, {
126
126
+
timeoutDuration: 60000 * 5,
127
127
+
worker: true,
128
128
+
});
129
129
+
130
130
+
const tracks = await input.sendAction<Track[]>("list", cachedTracks, {
131
131
+
timeoutDuration: 60000 * 60 * 24,
132
132
+
worker: true,
133
133
+
});
134
134
+
135
135
+
// Process
136
136
+
const tracksWithMetadata = await tracks.reduce(
137
137
+
async (promise: Promise<Track[]>, track: Track) => {
138
138
+
const acc = await promise;
139
139
+
140
140
+
if (track.tags && track.stats) return [...acc, track];
141
141
+
142
142
+
const resGet = await input.sendAction<ResolvedUri>(
143
143
+
"resolve",
144
144
+
{ method: "GET", uri: track.uri },
145
145
+
{
146
146
+
timeoutDuration: 60000 * 5,
147
147
+
worker: true,
148
148
+
},
149
149
+
);
150
150
+
151
151
+
const resHead = await input.sendAction<ResolvedUri>(
152
152
+
"resolve",
153
153
+
{ method: "HEAD", uri: track.uri },
154
154
+
{
155
155
+
timeoutDuration: 60000 * 5,
156
156
+
worker: true,
157
157
+
},
158
158
+
);
159
159
+
160
160
+
if (!resGet) return [...acc, track];
161
161
+
162
162
+
const metadataProcessor = await processor.metadata;
163
163
+
const { stats, tags } = await metadataProcessor.sendAction(
164
164
+
"supply",
165
165
+
{ urls: { get: resGet.url, head: resHead?.url || resGet.url } },
166
166
+
{
167
167
+
timeoutDuration: 60000 * 15,
168
168
+
worker: true,
169
169
+
},
170
170
+
);
171
171
+
172
172
+
return [...acc, { ...track, stats, tags }];
173
173
+
},
174
174
+
Promise.resolve([]),
175
175
+
);
176
176
+
177
177
+
// Save
178
178
+
const changed = tracksCacheId(tracksWithMetadata) !== output.data.tracks.cacheId;
179
179
+
180
180
+
if (changed)
181
181
+
await output.sendAction("tracks", tracksWithMetadata, {
182
182
+
timeoutDuration: 60000 * 5,
183
183
+
});
184
184
+
185
185
+
// Fin
186
186
+
console.log("🪵 Processing completed");
187
187
+
context.data = { ...context.data, isProcessing: false };
188
188
+
}
189
189
+
190
190
+
////////////////////////////////////////////
191
191
+
// [ACTIONS] QUEUE ⭤ TRACKS
192
192
+
////////////////////////////////////////////
193
193
+
context.setActionHandler("insert_tracks_into_queue", insertTracksIntoQueue);
194
194
+
195
195
+
async function insertTracksIntoQueue() {
196
196
+
await context.settled();
197
197
+
198
198
+
// Add tracks to the queue once the tracks have been loaded;
199
199
+
// and every time the collection changes.
200
200
+
201
201
+
const input = await configurator.input;
202
202
+
const output = await configurator.output;
203
203
+
const queue = await engine.queue;
204
204
+
205
205
+
await wait(output, (d) => d?.tracks.state === "loaded");
206
206
+
207
207
+
reactive(
208
208
+
output,
209
209
+
(data) => data.tracks.cacheId,
210
210
+
async () => {
211
211
+
const groups = await input.sendAction<GroupConsult>(
212
212
+
"groupConsult",
213
213
+
output.data.tracks.collection,
214
214
+
{ timeoutDuration: 60000 * 5, worker: true },
215
215
+
);
216
216
+
217
217
+
// Available tracks
218
218
+
const tracks = Object.values(groups).reduce((acc: Track[], value) => {
219
219
+
if (value.available === false) return acc;
220
220
+
return [...acc, ...value.tracks];
221
221
+
}, []);
222
222
+
223
223
+
// Set pool
224
224
+
await queue.sendAction("pool", tracks, {
225
225
+
timeoutDuration: 60000,
226
226
+
worker: true,
227
227
+
});
228
228
+
},
229
229
+
);
230
230
+
}
231
231
+
</script>
+6
src/pages/orchestrator/primary/_manifest.json
Reviewed
···
1
1
+
{
2
2
+
"name": "diffuse/orchestrator/primary",
3
3
+
"title": "Diffuse Orchestrator | Primary",
4
4
+
"entrypoint": "index.html",
5
5
+
"actions": {}
6
6
+
}
-95
src/pages/orchestrator/queue-audio/_applet.astro
Reviewed
···
1
1
-
<script>
2
2
-
import { applet, inputUrl, makeConnect, register } from "@scripts/applet/common";
3
3
-
4
4
-
////////////////////////////////////////////
5
5
-
// SETUP
6
6
-
////////////////////////////////////////////
7
7
-
import type * as AudioEngine from "@applets/engine/audio/types.d.ts";
8
8
-
import type * as QueueEngine from "@applets/engine/queue/types.d.ts";
9
9
-
10
10
-
// Register applet
11
11
-
const context = register();
12
12
-
const connect = makeConnect(context);
13
13
-
14
14
-
// Applet connections
15
15
-
const configurator = {
16
16
-
input: await applet("/configurator/input"),
17
17
-
};
18
18
-
19
19
-
const engine = {
20
20
-
audio: await applet<AudioEngine.State>("/engine/audio", { groupId: context.groupId }),
21
21
-
queue: await applet<QueueEngine.State>("/engine/queue", { groupId: context.groupId }),
22
22
-
};
23
23
-
24
24
-
////////////////////////////////////////////
25
25
-
// Connections
26
26
-
////////////////////////////////////////////
27
27
-
await context.settled();
28
28
-
29
29
-
////////////////////////////////////////////
30
30
-
// ⚙️ [Connections → Engines]
31
31
-
// 🔉 AUDIO
32
32
-
////////////////////////////////////////////
33
33
-
34
34
-
// When the active audio has ended,
35
35
-
// shift the queue.
36
36
-
37
37
-
// NOTE:
38
38
-
// This could probably be optimised, but it works.
39
39
-
40
40
-
connect(
41
41
-
engine.audio,
42
42
-
(data) => data.items[engine.queue.data.now?.id ?? Infinity]?.hasEnded ?? false,
43
43
-
(hasEnded) => {
44
44
-
if (hasEnded) engine.queue.sendAction("shift", undefined, { worker: true });
45
45
-
},
46
46
-
);
47
47
-
48
48
-
////////////////////////////////////////////
49
49
-
// ⚙️ [Connections → Engines]
50
50
-
// 🚏 QUEUE
51
51
-
////////////////////////////////////////////
52
52
-
53
53
-
// When the active queue item has changed,
54
54
-
// coordinate the audio engine accordingly.
55
55
-
56
56
-
connect(
57
57
-
engine.queue,
58
58
-
(data) => data.now?.id,
59
59
-
async () => {
60
60
-
const activeTrack = engine.queue.data.now;
61
61
-
const isPlaying = engine.audio.data.isPlaying;
62
62
-
63
63
-
// Resolve URIs
64
64
-
const url = activeTrack
65
65
-
? await inputUrl(configurator.input, activeTrack.uri).then((a) => a?.url)
66
66
-
: undefined;
67
67
-
68
68
-
// Check if we still need to render
69
69
-
if (engine.queue.data.now?.id !== activeTrack?.id) return;
70
70
-
71
71
-
// Play new active queue item
72
72
-
// TODO: Take URL expiration timestamp into account
73
73
-
// TODO: Preload next queue item
74
74
-
engine.audio.sendAction(
75
75
-
"render",
76
76
-
{
77
77
-
audio: activeTrack
78
78
-
? [
79
79
-
{
80
80
-
id: activeTrack.id,
81
81
-
isPreload: false,
82
82
-
url,
83
83
-
},
84
84
-
]
85
85
-
: // TODO: Keep preloads
86
86
-
[],
87
87
-
play: activeTrack && isPlaying ? { audioId: activeTrack.id } : undefined,
88
88
-
},
89
89
-
{
90
90
-
timeoutDuration: 60000,
91
91
-
},
92
92
-
);
93
93
-
},
94
94
-
);
95
95
-
</script>
-6
src/pages/orchestrator/queue-audio/_manifest.json
Reviewed
···
1
1
-
{
2
2
-
"name": "diffuse/orchestrator/queue-audio",
3
3
-
"title": "Diffuse Orchestrator | Queue Audio",
4
4
-
"entrypoint": "index.html",
5
5
-
"actions": {}
6
6
-
}
-9
src/pages/orchestrator/queue-audio/index.astro
Reviewed
···
1
1
-
---
2
2
-
import Layout from "@layouts/applet.astro";
3
3
-
import Applet from "./_applet.astro";
4
4
-
import { title } from "./_manifest.json";
5
5
-
---
6
6
-
7
7
-
<Layout title={title}>
8
8
-
<Applet />
9
9
-
</Layout>
-57
src/pages/orchestrator/queue-tracks/_applet.astro
Reviewed
···
1
1
-
<script>
2
2
-
import type { GroupConsult, ManagedOutput, Track } from "@applets/core/types.d.ts";
3
3
-
import { applet, makeConnect, register, wait } from "@scripts/applet/common";
4
4
-
5
5
-
////////////////////////////////////////////
6
6
-
// SETUP
7
7
-
////////////////////////////////////////////
8
8
-
import type * as QueueEngine from "@applets/engine/queue/types.d.ts";
9
9
-
10
10
-
// Register applet
11
11
-
const context = register();
12
12
-
const connect = makeConnect(context);
13
13
-
14
14
-
// Applet connections
15
15
-
const configurator = {
16
16
-
input: await applet("/configurator/input"),
17
17
-
output: await applet<ManagedOutput>("/configurator/output"),
18
18
-
};
19
19
-
20
20
-
const engine = {
21
21
-
queue: await applet<QueueEngine.State>("/engine/queue", { groupId: context.groupId }),
22
22
-
};
23
23
-
24
24
-
////////////////////////////////////////////
25
25
-
// Connections
26
26
-
////////////////////////////////////////////
27
27
-
await context.settled();
28
28
-
29
29
-
// Add tracks to the queue once the tracks have been loaded;
30
30
-
// and every time the collection changes.
31
31
-
32
32
-
wait(configurator.output, (d) => d?.tracks.state === "loaded").then(async () => {
33
33
-
connect(
34
34
-
configurator.output,
35
35
-
(data) => data.tracks.cacheId,
36
36
-
async () => {
37
37
-
const groups = await configurator.input.sendAction<GroupConsult>(
38
38
-
"groupConsult",
39
39
-
configurator.output.data.tracks.collection,
40
40
-
{ timeoutDuration: 60000 * 5, worker: true },
41
41
-
);
42
42
-
43
43
-
// Available tracks
44
44
-
const tracks = Object.values(groups).reduce((acc: Track[], value) => {
45
45
-
if (value.available === false) return acc;
46
46
-
return [...acc, ...value.tracks];
47
47
-
}, []);
48
48
-
49
49
-
// Clear
50
50
-
engine.queue.sendAction("pool", tracks, {
51
51
-
timeoutDuration: 60000,
52
52
-
worker: true,
53
53
-
});
54
54
-
},
55
55
-
);
56
56
-
});
57
57
-
</script>
-6
src/pages/orchestrator/queue-tracks/_manifest.json
Reviewed
···
1
1
-
{
2
2
-
"name": "diffuse/orchestrator/queue-tracks",
3
3
-
"title": "Diffuse Orchestrator | Queue Tracks",
4
4
-
"entrypoint": "index.html",
5
5
-
"actions": {}
6
6
-
}
-9
src/pages/orchestrator/queue-tracks/index.astro
Reviewed
···
1
1
-
---
2
2
-
import Layout from "@layouts/applet.astro";
3
3
-
import Applet from "./_applet.astro";
4
4
-
import { title } from "./_manifest.json";
5
5
-
---
6
6
-
7
7
-
<Layout title={title}>
8
8
-
<Applet />
9
9
-
</Layout>
+17
-20
src/scripts/applet/common.ts
Reviewed
···
20
20
context?: Window;
21
21
frameId?: string;
22
22
groupId?: string;
23
23
+
newInstance?: boolean;
23
24
setHeight?: boolean;
24
25
} = {},
25
26
): Promise<Applet<D>> {
···
41
42
src = QS.stringifyUrl({ url: src, query });
42
43
}
43
44
44
44
-
const context = opts.context || self.top || self.parent;
45
45
-
const existingFrame: HTMLIFrameElement | null = context.document.querySelector(`[src="${src}"]`);
45
45
+
const context = opts.newInstance ? self : opts.context || self.top || self.parent;
46
46
+
const existingFrame: HTMLIFrameElement | null = opts.newInstance
47
47
+
? null
48
48
+
: context.document.querySelector(`[src="${src}"]`);
46
49
47
50
let frame;
48
51
···
122
125
encode(data: T): any;
123
126
};
124
127
128
128
+
export function lookupGroupId() {
129
129
+
const url = new URL(location.href);
130
130
+
return url.searchParams.get("groupId") || "main";
131
131
+
}
132
132
+
125
133
export function register<DataType = any>(
126
134
options: { mode?: "broadcast" | "shared-worker"; worker?: Comlink.Remote<WorkerTasks> } = {},
127
135
): DiffuseApplet<DataType> {
128
136
const mode = options.mode ?? "broadcast";
129
129
-
const url = new URL(location.href);
130
137
const scope = applets.register<DataType>();
131
138
132
132
-
const groupId = url.searchParams.get("groupId") || "main";
139
139
+
const groupId = lookupGroupId();
133
140
const channelId = `${location.host}${location.pathname}/${groupId}`;
134
141
const instanceId = crypto.randomUUID();
135
142
···
270
277
const timeoutId = setTimeout(() => {
271
278
channel.removeEventListener("message", handler);
272
279
resolve({ isMain: true });
273
273
-
}, 1000);
280
280
+
}, 5000);
274
281
275
282
const handler = (event: MessageEvent) => {
276
276
-
if (event.data === "pong" || event.data === "ping") {
283
283
+
if (event.data?.type === "PONG" || event.data?.type === "PING") {
277
284
clearTimeout(timeoutId);
278
285
channel.removeEventListener("message", handler);
279
286
resolve({ isMain: false });
···
281
288
};
282
289
283
290
channel.addEventListener("message", handler);
291
291
+
channel.postMessage({
292
292
+
type: "PING",
293
293
+
instanceId,
294
294
+
});
284
295
});
285
296
}
286
297
···
297
308
data: codec.encode(event.data),
298
309
});
299
310
}
300
300
-
});
301
301
-
302
302
-
// Send out ping
303
303
-
channel.postMessage({
304
304
-
type: "PING",
305
305
-
instanceId,
306
311
});
307
312
308
313
// Action handler
···
374
379
effectFn(value);
375
380
}
376
381
});
377
377
-
}
378
378
-
379
379
-
export function makeConnect<X>(context: DiffuseApplet<X>) {
380
380
-
return <D, T>(applet: Applet<D>, dataFn: (data: D) => T, effectFn: (t: T) => void) => {
381
381
-
return reactive(applet, dataFn, (t: T) => {
382
382
-
if (context.isMainInstance()) effectFn(t);
383
383
-
});
384
384
-
};
385
382
}
386
383
387
384
////////////////////////////////////////////
+4
-1
src/scripts/common.ts
Reviewed
···
178
178
};
179
179
}
180
180
181
181
-
export function sync<DataType = unknown>(context: DiffuseApplet<DataType>, port: MessagePort) {
181
181
+
export function sync<DataType = unknown>(
182
182
+
context: DiffuseApplet<DataType>,
183
183
+
port: MessagePort | Worker,
184
184
+
) {
182
185
port.onmessage = (event) => {
183
186
if (event.data?.type === "data") {
184
187
context.data = event.data.data;
+14
-8
src/scripts/engine/queue/worker.ts
Reviewed
···
18
18
19
19
const { ports, tasks } = provide({
20
20
actions,
21
21
-
tasks: actions,
21
21
+
tasks: { ...actions, data },
22
22
});
23
23
24
24
export type Actions = typeof actions;
···
39
39
const [now, setNow] = signal<Item | null>(null);
40
40
41
41
effect(() => {
42
42
-
const state: State = {
43
43
-
future: future(),
44
44
-
past: past(),
45
45
-
now: now(),
46
46
-
};
47
47
-
48
42
postMessages({
49
43
data: {
50
44
type: "data",
51
51
-
data: state,
45
45
+
data: state(),
52
46
},
53
47
ports: ports.applets,
54
48
transfer: getTransferables(state),
55
49
});
56
50
});
51
51
+
52
52
+
function data() {
53
53
+
return state();
54
54
+
}
55
55
+
56
56
+
function state(): State {
57
57
+
return {
58
58
+
future: future(),
59
59
+
past: past(),
60
60
+
now: now(),
61
61
+
};
62
62
+
}
57
63
58
64
////////////////////////////////////////////
59
65
// ACTIONS
+1
-1
src/scripts/input/native-fs/worker.ts
Reviewed
···
22
22
resolve,
23
23
};
24
24
25
25
-
const tasks = provide({ actions, tasks: actions });
25
25
+
const { tasks } = provide({ actions, tasks: actions });
26
26
27
27
export type Actions = typeof actions;
28
28
export type Tasks = typeof tasks;
+1
-1
src/scripts/input/opensubsonic/worker.ts
Reviewed
···
27
27
resolve,
28
28
};
29
29
30
30
-
const tasks = provide({ actions, tasks: actions });
30
30
+
const { tasks } = provide({ actions, tasks: actions });
31
31
32
32
export type Actions = typeof actions;
33
33
export type Tasks = typeof tasks;
+1
-1
src/scripts/input/s3/worker.ts
Reviewed
···
24
24
resolve,
25
25
};
26
26
27
27
-
const tasks = provide({ actions, tasks: actions });
27
27
+
const { tasks } = provide({ actions, tasks: actions });
28
28
29
29
export type Actions = typeof actions;
30
30
export type Tasks = typeof tasks;
+16
src/scripts/orchestrator/primary/worker.ts
Reviewed
···
1
1
+
import { provide } from "@scripts/common";
2
2
+
3
3
+
////////////////////////////////////////////
4
4
+
// SETUP
5
5
+
////////////////////////////////////////////
6
6
+
7
7
+
const actions = {};
8
8
+
9
9
+
const { tasks } = provide({ actions, tasks: actions });
10
10
+
11
11
+
export type Actions = typeof actions;
12
12
+
export type Tasks = typeof tasks;
13
13
+
14
14
+
////////////////////////////////////////////
15
15
+
// ACTIONS
16
16
+
////////////////////////////////////////////
+3
-3
src/scripts/theme/blur/index.ts
Reviewed
···
21
21
b: applet("/constituent/blur/artwork-controller", { container, groupId: labelB }),
22
22
};
23
23
24
24
-
const _orchestrator = {
25
25
-
queueTracks: applet("/orchestrator/queue-tracks", { groupId: labelA }),
26
26
-
};
24
24
+
// const _orchestrator = {
25
25
+
// primary: applet("/orchestrator/primary", { groupId: labelA }),
26
26
+
// };
27
27
28
28
// const engine = {
29
29
// queue: {
+2
-4
src/scripts/theme/pilot/index.ts
Reviewed
···
13
13
queue: await applet<QueueEngine.State>("/engine/queue"),
14
14
};
15
15
16
16
-
const _orchestrator = {
17
17
-
input: await applet("/orchestrator/input-cache"),
18
18
-
queueAudio: await applet("/orchestrator/queue-audio"),
19
19
-
queueTracks: await applet("/orchestrator/queue-tracks"),
16
16
+
const orchestrator = {
17
17
+
primary: await applet("/orchestrator/primary"),
20
18
};
21
19
22
20
const ui = {
+38
-9
src/scripts/theme/webamp/index.ts
Reviewed
···
1
1
import type { URLTrack } from "webamp";
2
2
import Webamp from "webamp";
3
3
4
4
-
import type { ManagedOutput, ResolvedUri, Track } from "@applets/core/types.d.ts";
4
4
+
import type { GroupConsult, ManagedOutput, ResolvedUri, Track } from "@applets/core/types.d.ts";
5
5
import { applet, inputUrl, wait } from "@scripts/applet/common";
6
6
7
7
////////////////////////////////////////////
8
8
// 🗂️ Applets
9
9
////////////////////////////////////////////
10
10
const configurator = {
11
11
-
input: await applet("/configurator/input"),
12
12
-
output: await applet<ManagedOutput>("/configurator/output"),
11
11
+
input: applet("/configurator/input"),
12
12
+
output: applet<ManagedOutput>("/configurator/output"),
13
13
};
14
14
15
15
const orchestrator = {
16
16
-
input: await applet("/orchestrator/input-cache"),
16
16
+
primary: applet("/orchestrator/primary"),
17
17
};
18
18
19
19
////////////////////////////////////////////
···
27
27
const loadFromUrl = amp.media.loadFromUrl.bind(amp.media);
28
28
29
29
async function loadOverride(uri: string, autoPlay: boolean) {
30
30
-
const resp = await inputUrl(configurator.input, uri);
30
30
+
const resp = await inputUrl(await configurator.input, uri);
31
31
if (!resp) throw new Error("Failed to resolve URI");
32
32
return await loadFromUrl(resp.url, autoPlay);
33
33
}
···
41
41
amp.renderWhenReady(ampNode);
42
42
43
43
// Wait for tracks to load
44
44
-
wait(configurator.output, (d) => d?.tracks.state === "loaded").then(loadAndInsert);
45
45
-
configurator.output.ondata = loadAndInsert;
44
44
+
configurator.output
45
45
+
.then((output) => {
46
46
+
output.ondata = loadAndInsert;
47
47
+
return wait(output, (d) => d?.tracks.state === "loaded");
48
48
+
})
49
49
+
.then(async () => {
50
50
+
await loadAndInsert();
51
51
+
(await orchestrator.primary).sendAction("process_inputs", undefined, {
52
52
+
timeoutDuration: 60000 * 60,
53
53
+
});
54
54
+
});
46
55
47
56
// Load & insert
48
57
let inserting = false;
58
58
+
let tracksCacheId: string | undefined = undefined;
49
59
50
60
async function loadAndInsert() {
51
51
-
if (configurator.output.data.tracks.state !== "loaded") return;
61
61
+
const output = await configurator.output;
62
62
+
63
63
+
if (output.data.tracks.state !== "loaded") return;
64
64
+
if (output.data.tracks.cacheId === tracksCacheId) return;
52
65
if (inserting) return;
66
66
+
53
67
inserting = true;
68
68
+
tracksCacheId = output.data.tracks.cacheId;
54
69
const tracks = await loadTracks();
55
70
56
71
// TODO: This kinda messes up the UI,
···
70
85
// 🛠️
71
86
////////////////////////////////////////////
72
87
async function loadTracks(): Promise<URLTrack[]> {
73
73
-
const tracks = configurator.output.data.tracks.collection;
88
88
+
const input = await configurator.input;
89
89
+
const output = await configurator.output;
90
90
+
91
91
+
const groups = await input.sendAction<GroupConsult>(
92
92
+
"groupConsult",
93
93
+
output.data.tracks.collection,
94
94
+
{ timeoutDuration: 60000 * 5, worker: true },
95
95
+
);
96
96
+
97
97
+
// Available tracks
98
98
+
const tracks = Object.values(groups).reduce((acc: Track[], value) => {
99
99
+
if (value.available === false) return acc;
100
100
+
return [...acc, ...value.tracks];
101
101
+
}, []);
102
102
+
74
103
return tracks.map((track) => {
75
104
const urlTrack: URLTrack = {
76
105
url: track.uri,