+3
-2
deno.lock
+3
-2
deno.lock
···
21
21
],
22
22
"packageJson": {
23
23
"dependencies": [
24
-
"npm:98.css@~0.1.21",
25
-
"npm:@atcute/cid@^2.2.2",
24
+
"npm:@jsr/bradenmacdonald__s3-lite-client@0.9",
25
+
"npm:@jsr/std__media-types@^1.1.0",
26
26
"npm:@picocss/pico@^2.1.1",
27
27
"npm:@types/throttle-debounce@^5.0.2",
28
28
"npm:astro-purgecss@^5.2.2",
···
32
32
"npm:idb-keyval@^6.2.1",
33
33
"npm:music-metadata@^11.2.3",
34
34
"npm:native-file-system-adapter@^3.0.1",
35
+
"npm:node-s3-url-encode@^0.0.4",
35
36
"npm:purgecss@^7.0.2",
36
37
"npm:query-string@^9.1.2",
37
38
"npm:sass@^1.87.0",
+14
-30
package-lock.json
+14
-30
package-lock.json
···
5
5
"packages": {
6
6
"": {
7
7
"dependencies": {
8
-
"@atcute/cid": "^2.2.2",
9
8
"@bradenmacdonald/s3-lite-client": "npm:@jsr/bradenmacdonald__s3-lite-client@^0.9.0",
10
9
"@picocss/pico": "^2.1.1",
10
+
"@std/media-types": "npm:@jsr/std__media-types@^1.1.0",
11
11
"@web-applets/sdk": "https://gitpkg.vercel.app/unternet-co/web-applets/sdk?tokono.ma/experiment&scripts.postinstall=npm%20i%20%40types%2Fnode%20%26%26%20npx%20tsc",
12
-
"98.css": "^0.1.21",
13
12
"iconoir": "^7.11.0",
14
13
"idb-keyval": "^6.2.1",
15
14
"music-metadata": "^11.2.3",
16
15
"native-file-system-adapter": "^3.0.1",
16
+
"node-s3-url-encode": "^0.0.4",
17
17
"query-string": "^9.1.2",
18
18
"spellcaster": "^6.0.0",
19
19
"throttle-debounce": "^5.0.2",
···
107
107
"node": "^18.17.1 || ^20.3.0 || >=22.0.0"
108
108
}
109
109
},
110
-
"node_modules/@atcute/cid": {
111
-
"version": "2.2.2",
112
-
"resolved": "https://registry.npmjs.org/@atcute/cid/-/cid-2.2.2.tgz",
113
-
"integrity": "sha512-deAGMqLAyplt7eIukhkjlsGubvrcMrtXkDKlUYZDo4WUdL7hSjBywtPXf6SbMK+Mjvst7l2+83OqTcY5AuuxtA==",
114
-
"dependencies": {
115
-
"@atcute/multibase": "^1.1.3",
116
-
"@atcute/uint8array": "^1.0.1"
117
-
}
118
-
},
119
-
"node_modules/@atcute/multibase": {
120
-
"version": "1.1.3",
121
-
"resolved": "https://registry.npmjs.org/@atcute/multibase/-/multibase-1.1.3.tgz",
122
-
"integrity": "sha512-vQQO0tDuQPguBvHdgV3ryn7R8U6beQ50KA/juYm+dCeT/3hOK2stMbX+IaW8JEuwkT5lJsU8wDIOicQT4mB7Ag==",
123
-
"dependencies": {
124
-
"@atcute/uint8array": "^1.0.1"
125
-
}
126
-
},
127
-
"node_modules/@atcute/uint8array": {
128
-
"version": "1.0.1",
129
-
"resolved": "https://registry.npmjs.org/@atcute/uint8array/-/uint8array-1.0.1.tgz",
130
-
"integrity": "sha512-AAnlFKyfDRgb9GNZJbhQ6OuMhbmNPirQyapb8KnmcEhxQZ3+tt+4NcwqekEegY4MpNqSTYeeTdyxq0wGZv1JHg=="
131
-
},
132
110
"node_modules/@babel/helper-string-parser": {
133
111
"version": "7.25.9",
134
112
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz",
···
1710
1688
"integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==",
1711
1689
"dev": true
1712
1690
},
1691
+
"node_modules/@std/media-types": {
1692
+
"name": "@jsr/std__media-types",
1693
+
"version": "1.1.0",
1694
+
"resolved": "https://npm.jsr.io/~/11/@jsr/std__media-types/1.1.0.tgz",
1695
+
"integrity": "sha512-dHvaxHL7ENWnltgL653uo3KnKFse3ZbopZop2gqsT7yrscx7irZEClu5Cba7gMPPRk4Lg1FbriNcaBViM2RSBw=="
1696
+
},
1713
1697
"node_modules/@swc/helpers": {
1714
1698
"version": "0.5.17",
1715
1699
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.17.tgz",
···
1848
1832
"hasInstallScript": true,
1849
1833
"license": "MIT"
1850
1834
},
1851
-
"node_modules/98.css": {
1852
-
"version": "0.1.21",
1853
-
"resolved": "https://registry.npmjs.org/98.css/-/98.css-0.1.21.tgz",
1854
-
"integrity": "sha512-ddk5qtUWyapM0Bzd5jwGExoE5fdSEGrP+F5VbYjyZLf2c9UVmn6w2NPTvCsoD4BWdGsjdLjlkQGhWwWTJcYQJQ==",
1855
-
"license": "MIT"
1856
-
},
1857
1835
"node_modules/acorn": {
1858
1836
"version": "8.14.1",
1859
1837
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz",
···
5039
5017
"integrity": "sha512-0uGYQ1WQL1M5kKvGRXWQ3uZCHtLTO8hln3oBjIusM75WoesZ909uQJs/Hb946i2SS+Gsrhkaa6iAO17jRIv6DQ==",
5040
5018
"dev": true
5041
5019
},
5020
+
"node_modules/node-s3-url-encode": {
5021
+
"version": "0.0.4",
5022
+
"resolved": "https://registry.npmjs.org/node-s3-url-encode/-/node-s3-url-encode-0.0.4.tgz",
5023
+
"integrity": "sha512-l0IizfnxE1hb9dadzYBpA27syfL9LFkPzCKH6YWrssv2sPLjVuCent67A8GPe4isdj4bEsbgdPWLTcV4gxEg9w==",
5024
+
"license": "MIT"
5025
+
},
5042
5026
"node_modules/normalize-path": {
5043
5027
"version": "3.0.0",
5044
5028
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
+1
package.json
+1
package.json
···
2
2
"dependencies": {
3
3
"@bradenmacdonald/s3-lite-client": "npm:@jsr/bradenmacdonald__s3-lite-client@^0.9.0",
4
4
"@picocss/pico": "^2.1.1",
5
+
"@std/media-types": "npm:@jsr/std__media-types@^1.1.0",
5
6
"@web-applets/sdk": "https://gitpkg.vercel.app/unternet-co/web-applets/sdk?tokono.ma/experiment&scripts.postinstall=npm%20i%20%40types%2Fnode%20%26%26%20npx%20tsc",
6
7
"iconoir": "^7.11.0",
7
8
"idb-keyval": "^6.2.1",
+14
-4
src/pages/configurator/input/_applet.astro
+14
-4
src/pages/configurator/input/_applet.astro
···
33
33
import { applets } from "@web-applets/sdk";
34
34
35
35
import type { Track } from "@applets/core/types.d.ts";
36
-
import { applet, waitUntilAppletIsReady } from "@scripts/theme";
36
+
import { applet } from "@scripts/theme";
37
37
38
38
////////////////////////////////////////////
39
39
// SETUP
···
51
51
// Applet connections
52
52
const input = {
53
53
nativeFs: await applet("../../input/native-fs", { container }),
54
+
s3: await applet("../../input/s3", { container }),
54
55
};
55
56
56
57
////////////////////////////////////////////
···
65
66
},
66
67
{
67
68
[input.nativeFs.manifest.input_properties.scheme]: [],
69
+
[input.s3.manifest.input_properties.scheme]: [],
68
70
},
69
71
);
70
72
···
76
78
timeoutDuration: 60000 * 60 * 24,
77
79
});
78
80
81
+
case input.s3.manifest.input_properties.scheme:
82
+
return await input.s3.sendAction("list", cachedTracksGroup, {
83
+
timeoutDuration: 60000 * 60 * 24,
84
+
});
85
+
79
86
default:
80
87
return cachedTracks;
81
88
}
···
88
95
return tracks;
89
96
};
90
97
91
-
const resolve = async (fileUri: string) => {
92
-
const scheme = fileUri.split(":", 1)[0];
98
+
const resolve = async (args: { method: string; uri: string }) => {
99
+
const scheme = args.uri.split(":", 1)[0];
93
100
94
101
switch (scheme) {
95
102
case input.nativeFs.manifest.input_properties.scheme:
96
-
return await input.nativeFs.sendAction("resolve", fileUri);
103
+
return await input.nativeFs.sendAction("resolve", args);
104
+
105
+
case input.s3.manifest.input_properties.scheme:
106
+
return await input.s3.sendAction("resolve", args);
97
107
98
108
default:
99
109
return undefined;
+9
-2
src/pages/configurator/input/_manifest.json
+9
-2
src/pages/configurator/input/_manifest.json
···
18
18
"title": "Resolve",
19
19
"description": "Potentially translates a track uri with a matching scheme into a URL pointing at the audio bytes. If it can be resolved that is, otherwise you'll get `undefined`.",
20
20
"params_schema": {
21
-
"type": "string",
22
-
"description": "The uri to resolve"
21
+
"type": "object",
22
+
"properties": {
23
+
"method": {
24
+
"type": "string",
25
+
"description": "The HTTP method that is going to be used on the resolved URI."
26
+
},
27
+
"uri": { "type": "string", "description": "The URI to resolve." }
28
+
},
29
+
"required": ["method", "uri"]
23
30
}
24
31
}
25
32
}
+2
src/pages/index.astro
+2
src/pages/index.astro
···
48
48
const output = [
49
49
{ url: "output/indexed-db/", title: "IndexedDB" },
50
50
{ url: "output/native-fs/", title: "Native File System" },
51
+
{ url: "output/todo/", title: "(TODO) Keyhive/Beelay" },
52
+
{ url: "output/todo/", title: "(TODO) Some local-first sync engine" },
51
53
];
52
54
53
55
const processors = [
+4
-3
src/pages/input/native-fs/_applet.astro
+4
-3
src/pages/input/native-fs/_applet.astro
···
16
16
</main>
17
17
18
18
<script>
19
-
import * as IDB from "idb-keyval";
20
-
21
19
import { applets } from "@web-applets/sdk";
22
20
import { computed, effect, Signal, signal } from "spellcaster";
23
21
import { repeat, tags, text } from "spellcaster/hyperscript.js";
24
22
import { type FileSystemDirectoryHandle, showDirectoryPicker } from "native-file-system-adapter";
23
+
import * as IDB from "idb-keyval";
25
24
import * as URI from "uri-js";
26
25
import QS from "query-string";
27
26
···
163
162
return data;
164
163
};
165
164
166
-
const resolve = async (fileUri: string) => {
165
+
const resolve = async (args: { uri: string }) => {
166
+
const fileUri = args.uri;
167
+
167
168
if (!isSupported()) {
168
169
return undefined;
169
170
}
+9
-2
src/pages/input/native-fs/_manifest.json
+9
-2
src/pages/input/native-fs/_manifest.json
···
29
29
"title": "Resolve",
30
30
"description": "Potentially translates a track uri with a matching scheme into a URL pointing at the audio bytes. If it can be resolved that is, otherwise you'll get `undefined`. Use the `consult` action to get a more detailed answer.",
31
31
"params_schema": {
32
-
"type": "string",
33
-
"description": "The uri to resolve"
32
+
"type": "object",
33
+
"properties": {
34
+
"method": {
35
+
"type": "string",
36
+
"description": "The HTTP method that is going to be used on the resolved URI."
37
+
},
38
+
"uri": { "type": "string", "description": "The URI to resolve." }
39
+
},
40
+
"required": ["method", "uri"]
34
41
}
35
42
},
36
43
"mount": {
+362
src/pages/input/s3/_applet.astro
+362
src/pages/input/s3/_applet.astro
···
1
+
<main class="container">
2
+
<h1>S3-compatible input</h1>
3
+
4
+
<h4>Mounted buckets</h4>
5
+
6
+
<div id="buckets">
7
+
<p>
8
+
<span class="with-icon">
9
+
<i class="iconoir-bonfire"></i>
10
+
<small>Just a moment, loading mounted buckets.</small>
11
+
</span>
12
+
</p>
13
+
</div>
14
+
15
+
<h4>Add a new S3 bucket</h4>
16
+
17
+
<form id="form"></form>
18
+
19
+
<!-- Warning about hostnames/regions/buckets -->
20
+
<!-- <p>
21
+
<small>
22
+
<span class="with-icon">
23
+
<i class="iconoir-warning-triangle"></i>
24
+
<span
25
+
>The bucket name and region are not automatically prefixed/inserted into the hostname, you
26
+
must add them yourself to either the host or the path <strong>if needed</strong>.</span
27
+
>
28
+
</span>
29
+
</small>
30
+
</p> -->
31
+
</main>
32
+
33
+
<style is:global>
34
+
iframe {
35
+
display: none;
36
+
}
37
+
</style>
38
+
39
+
<script>
40
+
import { S3Client } from "@bradenmacdonald/s3-lite-client";
41
+
import { type AppletEvent, applets } from "@web-applets/sdk";
42
+
import { computed, effect, Signal, signal } from "spellcaster";
43
+
import { type Props, repeat, tags, text } from "spellcaster/hyperscript.js";
44
+
import * as IDB from "idb-keyval";
45
+
import * as URI from "uri-js";
46
+
import QS from "query-string";
47
+
48
+
// @ts-ignore
49
+
import { encodeS3URI } from "node-s3-url-encode";
50
+
51
+
import type { Output, Track } from "@applets/core/types.d.ts";
52
+
import { applet } from "@scripts/theme";
53
+
54
+
import manifest from "./_manifest.json";
55
+
import { isAudioFile } from "@scripts/inputs/common";
56
+
57
+
type Bucket = {
58
+
accessKey: string;
59
+
bucketName: string;
60
+
host: string;
61
+
path: string;
62
+
region: string;
63
+
secretKey: string;
64
+
};
65
+
66
+
const ENCODINGS = {
67
+
"\+": "%2B",
68
+
"\!": "%21",
69
+
'\"': "%22",
70
+
"\#": "%23",
71
+
"\$": "%24",
72
+
"\&": "%26",
73
+
"'": "%27",
74
+
"\(": "%28",
75
+
"\)": "%29",
76
+
"\*": "%2A",
77
+
"\,": "%2C",
78
+
"\:": "%3A",
79
+
"\;": "%3B",
80
+
"\=": "%3D",
81
+
"\?": "%3F",
82
+
"\@": "%40",
83
+
};
84
+
85
+
////////////////////////////////////////////
86
+
// SETUP
87
+
////////////////////////////////////////////
88
+
const IDB_PREFIX = "@applets/input/s3";
89
+
const IDB_BUCKETS = `${IDB_PREFIX}/buckets`;
90
+
const SCHEME = manifest.input_properties.scheme;
91
+
92
+
// Register applet
93
+
const context = applets.register();
94
+
95
+
// Applet connections
96
+
const orchestrator = {
97
+
output: await applet<Output>("../../orchestrator/output-management", {
98
+
context: self.top || self.parent,
99
+
}),
100
+
};
101
+
102
+
// Watch for data changes
103
+
orchestrator.output.addEventListener("data", async (event: AppletEvent) => {
104
+
await loadBuckets();
105
+
});
106
+
107
+
////////////////////////////////////////////
108
+
// UI
109
+
////////////////////////////////////////////
110
+
const [buckets, setBuckets] = signal<Record<string, Bucket>>(await loadBuckets());
111
+
const [form, setForm] = signal<{
112
+
access_key?: string;
113
+
bucket_name?: string;
114
+
host?: string;
115
+
path?: string;
116
+
region?: string;
117
+
secret_key?: string;
118
+
}>({});
119
+
120
+
const bucketsMap = computed(() => {
121
+
return new Map(Object.entries(buckets()));
122
+
});
123
+
124
+
effect(() => {
125
+
saveBuckets(buckets());
126
+
});
127
+
128
+
////////////////////////////////////////////
129
+
// UI ~ BUCKETS
130
+
////////////////////////////////////////////
131
+
const Bucket = (bucket: Signal<Bucket>) => {
132
+
const onclick = () => {
133
+
const b = bucket();
134
+
const id = bucketId(b);
135
+
136
+
const col = { ...buckets() };
137
+
delete col[id];
138
+
139
+
setBuckets(col);
140
+
};
141
+
142
+
return tags.li({ onclick, style: "cursor: pointer" }, text(bucket().host));
143
+
};
144
+
145
+
const BucketList = computed(() => {
146
+
if (bucketsMap().size === 0) {
147
+
return tags.p({ id: "buckets" }, [tags.small({}, text("Nothing added so far."))]);
148
+
}
149
+
150
+
return tags.ul({ id: "buckets" }, repeat(bucketsMap, Bucket));
151
+
});
152
+
153
+
effect(() => {
154
+
document.querySelector("#buckets")?.replaceWith(BucketList());
155
+
});
156
+
157
+
////////////////////////////////////////////
158
+
// UI ~ FORM
159
+
////////////////////////////////////////////
160
+
function addBucket(event: Event) {
161
+
event.preventDefault();
162
+
163
+
const f = form();
164
+
165
+
const bucket: Bucket = {
166
+
accessKey: f.access_key || "",
167
+
bucketName: f.bucket_name || "",
168
+
host: f.host || "s3.amazonaws.com",
169
+
path: f.path || "/",
170
+
region: f.region || "us-east-1",
171
+
secretKey: f.secret_key || "",
172
+
};
173
+
174
+
setBuckets({
175
+
...buckets(),
176
+
[bucketId(bucket)]: bucket,
177
+
});
178
+
}
179
+
180
+
function Form() {
181
+
return tags.form({ onsubmit: addBucket }, [
182
+
tags.fieldset({ className: "grid" }, [
183
+
Input("access_key", "Access key", "r31w7m9c", { required: true }),
184
+
Input("secret_key", "Secret key", "v02g2l29", { required: true }),
185
+
]),
186
+
tags.fieldset({ className: "grid" }, [
187
+
Input("bucket_name", "Bucket name", "bucket", { required: true }),
188
+
Input("region", "Region", "us-east-1", { required: true }),
189
+
]),
190
+
tags.fieldset({ className: "grid" }, [
191
+
Input("host", "Host", "s3.amazonaws.com", { required: true }),
192
+
Input("path", "Path", "/"),
193
+
]),
194
+
tags.fieldset({ className: "grid" }, [tags.input({ type: "submit", value: "Connect" }, [])]),
195
+
]);
196
+
}
197
+
198
+
function Input(name: string, label: string, placeholder: string, opts: Props = {}) {
199
+
return tags.label({}, [
200
+
tags.span({}, [
201
+
tags.span({}, text(label)),
202
+
tags.small({}, text("required" in opts ? "" : " (optional)")),
203
+
]),
204
+
tags.input({
205
+
...opts,
206
+
name,
207
+
placeholder,
208
+
oninput: (event: InputEvent) => formInput(name, (event.target as HTMLInputElement).value),
209
+
}),
210
+
]);
211
+
}
212
+
213
+
function formInput(name: string, value: string) {
214
+
setForm({ ...form(), [name]: value });
215
+
}
216
+
217
+
// ๐
218
+
document.querySelector("#form")?.replaceWith(Form());
219
+
220
+
////////////////////////////////////////////
221
+
// ACTIONS
222
+
////////////////////////////////////////////
223
+
const consult = async (fileUriOrScheme: string) => {
224
+
if (!navigator.onLine) return false;
225
+
226
+
// TODO: Check if bucket is avail*able + CORS works?
227
+
return true;
228
+
};
229
+
230
+
const list = async (_cachedTracks: Track[] = []) => {
231
+
// TODO: Do we need to do something with the old tracks here?
232
+
233
+
const promises = Object.values(buckets()).map(async (bucket) => {
234
+
const client = createClient(bucket);
235
+
236
+
const list = await Array.fromAsync(
237
+
client.listObjects({
238
+
prefix: bucket.path.replace(/^\//, ""),
239
+
}),
240
+
);
241
+
242
+
return list
243
+
.filter((l) => isAudioFile(l.key))
244
+
.map((l) => {
245
+
const track: Track = {
246
+
id: crypto.randomUUID(),
247
+
uri: buildURI(bucket, l.key),
248
+
};
249
+
250
+
return track;
251
+
});
252
+
});
253
+
254
+
return (await Promise.all(promises)).flat(1);
255
+
};
256
+
257
+
const resolve = async ({ method, uri }: { method: string; uri: string }) => {
258
+
const bucket = parseURI(uri);
259
+
if (!bucket) return undefined;
260
+
261
+
const client = createClient(bucket);
262
+
const parsedURI = URI.parse(uri);
263
+
const path = (
264
+
bucket.path.replace(/\/$/, "") + URI.unescapeComponent(parsedURI.path || "")
265
+
).replace(/^\//, "");
266
+
267
+
const url = await client.getPresignedUrl(method.toUpperCase() as any, path);
268
+
return url;
269
+
};
270
+
271
+
const mount = async () => {};
272
+
273
+
const unmount = async () => {};
274
+
275
+
context.setActionHandler("consult", consult);
276
+
context.setActionHandler("list", list);
277
+
context.setActionHandler("resolve", resolve);
278
+
context.setActionHandler("mount", mount);
279
+
context.setActionHandler("unmount", unmount);
280
+
281
+
////////////////////////////////////////////
282
+
// ๐ ๏ธ
283
+
////////////////////////////////////////////
284
+
function bucketsFromTracks(tracks: Track[]) {
285
+
return tracks.reduce((acc: Record<string, Bucket>, track: Track) => {
286
+
const bucket = parseURI(track.uri);
287
+
if (!bucket) return acc;
288
+
289
+
const id = bucketId(bucket);
290
+
if (acc[id]) return acc;
291
+
292
+
return { ...acc, [id]: bucket };
293
+
}, {});
294
+
}
295
+
296
+
function bucketId(bucket: Bucket) {
297
+
return `${bucket.accessKey}:${bucket.secretKey}@${bucket.host}`;
298
+
}
299
+
300
+
function buildURI(bucket: Bucket, path: string) {
301
+
return URI.serialize({
302
+
scheme: SCHEME,
303
+
userinfo: `${bucket.accessKey}:${bucket.secretKey}`,
304
+
host: bucket.host,
305
+
path: path,
306
+
query: QS.stringify({
307
+
bucketName: bucket.bucketName,
308
+
bucketPath: bucket.path,
309
+
region: bucket.region,
310
+
}),
311
+
});
312
+
}
313
+
314
+
function createClient(bucket: Bucket) {
315
+
return new S3Client({
316
+
bucket: bucket.bucketName,
317
+
endPoint: bucket.host.includes("://") ? bucket.host : `https://${bucket.host}`,
318
+
region: bucket.region,
319
+
pathStyle: false,
320
+
accessKey: bucket.accessKey,
321
+
secretKey: bucket.secretKey,
322
+
});
323
+
}
324
+
325
+
function encodeAwsUriComponent(a: string) {
326
+
return encodeURIComponent(a).replace(
327
+
/(\+|!|"|#|\$|&|'|\(|\)|\*|\+|,|:|;|=|\?|@)/gim,
328
+
(match) => (ENCODINGS as any)[match] ?? match,
329
+
);
330
+
}
331
+
332
+
async function loadBuckets() {
333
+
const i = await IDB.get(IDB_BUCKETS);
334
+
const t = bucketsFromTracks(orchestrator.output.data.tracks);
335
+
336
+
return { ...i, ...t };
337
+
}
338
+
339
+
function parseURI(uriString: string): Bucket | undefined {
340
+
const uri = URI.parse(uriString);
341
+
if (uri.scheme !== SCHEME) return undefined;
342
+
if (!uri.host) return undefined;
343
+
344
+
const [accessKey, secretKey] = uri.userinfo?.split(":") ?? [];
345
+
if (!accessKey || !secretKey) return undefined;
346
+
347
+
const qs = QS.parse(uri.query || "");
348
+
349
+
return {
350
+
accessKey,
351
+
bucketName: typeof qs.bucketName === "string" ? qs.bucketName : "",
352
+
host: uri.host,
353
+
path: qs.bucketPath === "string" ? qs.bucketPath : "/",
354
+
region: typeof qs.region === "string" ? qs.region : "",
355
+
secretKey,
356
+
};
357
+
}
358
+
359
+
async function saveBuckets(items: Record<string, Bucket>) {
360
+
await IDB.set(IDB_BUCKETS, items);
361
+
}
362
+
</script>
+55
src/pages/input/s3/_manifest.json
+55
src/pages/input/s3/_manifest.json
···
1
+
{
2
+
"name": "diffuse/input/s3",
3
+
"title": "Diffuse Input | S3",
4
+
"entrypoint": "index.html",
5
+
"input_properties": {
6
+
"scheme": "s3"
7
+
},
8
+
"actions": {
9
+
"consult": {
10
+
"title": "Consult",
11
+
"params_schema": {
12
+
"type": "string",
13
+
"description": "The uri to check the availability of."
14
+
}
15
+
},
16
+
"list": {
17
+
"title": "List",
18
+
"description": "List tracks.",
19
+
"params_schema": {
20
+
"type": "array",
21
+
"description": "A list of (cached) tracks with an uri matching the scheme",
22
+
"items": {
23
+
"type": "object"
24
+
}
25
+
}
26
+
},
27
+
"resolve": {
28
+
"title": "Resolve",
29
+
"description": "Potentially translates a track uri with a matching scheme into a URL pointing at the audio bytes. If it can be resolved that is, otherwise you'll get `undefined`. Use the `consult` action to get a more detailed answer.",
30
+
"params_schema": {
31
+
"type": "object",
32
+
"properties": {
33
+
"method": {
34
+
"type": "string",
35
+
"description": "The HTTP method that is going to be used on the resolved URI."
36
+
},
37
+
"uri": { "type": "string", "description": "The URI to resolve." }
38
+
},
39
+
"required": ["method", "uri"]
40
+
}
41
+
},
42
+
"mount": {
43
+
"title": "Mount",
44
+
"description": "Prepare for usage."
45
+
},
46
+
"unmount": {
47
+
"title": "Unmount",
48
+
"description": "Callback after usage.",
49
+
"params_schema": {
50
+
"type": "string",
51
+
"description": "The handle id to unmount"
52
+
}
53
+
}
54
+
}
55
+
}
+9
src/pages/input/s3/index.astro
+9
src/pages/input/s3/index.astro
+11
-6
src/pages/orchestrator/input-cache/_applet.astro
+11
-6
src/pages/orchestrator/input-cache/_applet.astro
···
16
16
17
17
// Applet connections
18
18
const configurator = {
19
-
input: await applet("../../configurator/input", { context: self.parent }),
19
+
input: await applet("../../configurator/input", { context: self.top || self.parent }),
20
20
};
21
21
22
22
const orchestrator = {
···
52
52
53
53
if (track.tags) return [...acc, track];
54
54
55
-
const url = await configurator.input.sendAction<string | undefined>("resolve", track.uri, {
56
-
timeoutDuration: 60000,
57
-
});
55
+
const getURL = await configurator.input.sendAction<string | undefined>(
56
+
"resolve",
57
+
{ method: "GET", uri: track.uri },
58
+
{
59
+
timeoutDuration: 60000,
60
+
},
61
+
);
58
62
59
-
if (!url) return acc;
63
+
if (!getURL) return acc;
60
64
61
-
const meta = await processor.metadataFetcher.sendAction("extract", url, {
65
+
// TODO: Do we need to pass the HEAD URL too?
66
+
const meta = await processor.metadataFetcher.sendAction("extract", getURL, {
62
67
timeoutDuration: 60000,
63
68
});
64
69
+1
-1
src/pages/orchestrator/output-management/_applet.astro
+1
-1
src/pages/orchestrator/output-management/_applet.astro
+6
-2
src/pages/orchestrator/single-queue/_applet.astro
+6
-2
src/pages/orchestrator/single-queue/_applet.astro
···
15
15
16
16
// Applet connections
17
17
const engine = {
18
-
audio: await applet<AudioEngine.State>("../../engine/audio", { context: self.parent }),
19
-
queue: await applet<QueueEngine.State>("../../engine/queue", { context: self.parent }),
18
+
audio: await applet<AudioEngine.State>("../../engine/audio", {
19
+
context: self.top || self.parent,
20
+
}),
21
+
queue: await applet<QueueEngine.State>("../../engine/queue", {
22
+
context: self.top || self.parent,
23
+
}),
20
24
};
21
25
22
26
const orchestrator = {
+8
-1
src/pages/processor/metadata-fetcher/_applet.astro
+8
-1
src/pages/processor/metadata-fetcher/_applet.astro
···
1
1
<script>
2
2
import { applets } from "@web-applets/sdk";
3
3
import { parseWebStream } from "music-metadata";
4
+
import { contentType } from "@std/media-types";
5
+
import * as URI from "uri-js";
4
6
5
7
////////////////////////////////////////////
6
8
// SETUP
···
13
15
context.setActionHandler("extract", extract);
14
16
15
17
async function extract(url: string) {
18
+
const uri = URI.parse(url);
19
+
const pathParts = uri.path?.split("/");
20
+
const mimeType = pathParts?.[pathParts.length - 1]?.includes(".")
21
+
? contentType(pathParts[pathParts.length - 1].split(".").reverse()[0])
22
+
: undefined;
16
23
const resp = await fetch(url);
17
24
const stream = resp.body;
18
-
const metadata = await parseWebStream(stream);
25
+
const metadata = await parseWebStream(stream, { mimeType });
19
26
20
27
return metadata;
21
28
}
+8
-3
src/scripts/theme.ts
+8
-3
src/scripts/theme.ts
···
50
50
throw new Error("iframe does not have a contentWindow");
51
51
}
52
52
53
-
const applet = await applets.connect<D>(frame.contentWindow, {
54
-
context: opts.context,
55
-
});
53
+
const applet = await applets
54
+
.connect<D>(frame.contentWindow, {
55
+
context: opts.context,
56
+
})
57
+
.catch((err) => {
58
+
console.error("Error connecting to " + src, err);
59
+
throw err;
60
+
});
56
61
57
62
if (opts.setHeight) {
58
63
applet.onresize = () => {
+7
-3
src/scripts/themes/webamp/index.ts
+7
-3
src/scripts/themes/webamp/index.ts
···
54
54
// TODO: Ideally the URL should only be resolved when needed,
55
55
// but webamp doesn't allow for that.
56
56
// Maybe you could work around it with a service worker.
57
-
const url = await configurator.input.sendAction<string | undefined>("resolve", track.uri, {
58
-
timeoutDuration: 60000,
59
-
});
57
+
const url = await configurator.input.sendAction<string | undefined>(
58
+
"resolve",
59
+
{ method: "GET", uri: track.uri },
60
+
{
61
+
timeoutDuration: 60000,
62
+
},
63
+
);
60
64
61
65
if (!url) return acc;
62
66