+7
astro.config.js
+7
astro.config.js
···
1
1
import { defineConfig } from "astro/config";
2
2
import scope from "astro-scope";
3
+
import wasm from "vite-plugin-wasm";
3
4
4
5
import purgecss from "astro-purgecss";
5
6
···
7
8
integrations: [scope(), purgecss()],
8
9
build: {
9
10
inlineStylesheets: "never",
11
+
},
12
+
vite: {
13
+
plugins: [wasm()],
14
+
server: {
15
+
hmr: false,
16
+
},
10
17
},
11
18
});
+1
deno.json
+1
deno.json
+6
-1
deno.lock
+6
-1
deno.lock
···
21
21
],
22
22
"packageJson": {
23
23
"dependencies": [
24
+
"npm:98.css@~0.1.21",
25
+
"npm:@automerge/automerge@^3.0.0-beta.0",
24
26
"npm:@jsr/bradenmacdonald__s3-lite-client@0.9",
25
27
"npm:@jsr/std__media-types@^1.1.0",
26
28
"npm:@picocss/pico@^2.1.1",
29
+
"npm:@tokenizer/http@~0.9.2",
30
+
"npm:@tokenizer/range@0.13",
27
31
"npm:@types/throttle-debounce@^5.0.2",
28
32
"npm:astro-purgecss@^5.2.2",
29
33
"npm:astro-scope@^3.0.1",
···
32
36
"npm:idb-keyval@^6.2.1",
33
37
"npm:music-metadata@^11.2.3",
34
38
"npm:native-file-system-adapter@^3.0.1",
35
-
"npm:node-s3-url-encode@^0.0.4",
36
39
"npm:purgecss@^7.0.2",
37
40
"npm:query-string@^9.1.2",
38
41
"npm:sass@^1.87.0",
39
42
"npm:spellcaster@6",
40
43
"npm:throttle-debounce@^5.0.2",
44
+
"npm:uint8arrays@^5.1.0",
41
45
"npm:uri-js@^4.4.1",
46
+
"npm:vite-plugin-wasm@^3.4.1",
42
47
"npm:webamp@^1.5.0",
43
48
"npm:xxh32@^2.0.5"
44
49
]
+139
-8
package-lock.json
+139
-8
package-lock.json
···
5
5
"packages": {
6
6
"": {
7
7
"dependencies": {
8
+
"@automerge/automerge": "^3.0.0-beta.0",
8
9
"@bradenmacdonald/s3-lite-client": "npm:@jsr/bradenmacdonald__s3-lite-client@^0.9.0",
9
10
"@picocss/pico": "^2.1.1",
10
11
"@std/media-types": "npm:@jsr/std__media-types@^1.1.0",
12
+
"@tokenizer/http": "^0.9.2",
13
+
"@tokenizer/range": "^0.13.0",
11
14
"@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",
15
+
"98.css": "^0.1.21",
12
16
"iconoir": "^7.11.0",
13
17
"idb-keyval": "^6.2.1",
14
18
"music-metadata": "^11.2.3",
15
19
"native-file-system-adapter": "^3.0.1",
16
-
"node-s3-url-encode": "^0.0.4",
17
20
"query-string": "^9.1.2",
18
21
"spellcaster": "^6.0.0",
19
22
"throttle-debounce": "^5.0.2",
23
+
"uint8arrays": "^5.1.0",
20
24
"uri-js": "^4.4.1",
21
25
"webamp": "^1.5.0",
22
26
"xxh32": "^2.0.5"
···
27
31
"astro-purgecss": "^5.2.2",
28
32
"astro-scope": "^3.0.1",
29
33
"purgecss": "^7.0.2",
30
-
"sass": "^1.87.0"
34
+
"sass": "^1.87.0",
35
+
"vite-plugin-wasm": "^3.4.1"
31
36
}
32
37
},
33
38
"node_modules/@assemblyscript/loader": {
···
105
110
},
106
111
"engines": {
107
112
"node": "^18.17.1 || ^20.3.0 || >=22.0.0"
113
+
}
114
+
},
115
+
"node_modules/@automerge/automerge": {
116
+
"version": "3.0.0-preview.13",
117
+
"resolved": "https://registry.npmjs.org/@automerge/automerge/-/automerge-3.0.0-preview.13.tgz",
118
+
"integrity": "sha512-1r7ggaTqsQ4PHGv45QjVOxPOvJIKjSrHY+HTiFxCU04Qlx3kvXxDLVyBbZeN1jg2I+Y8tpuG0eVtC4QxL9wGIg==",
119
+
"license": "MIT",
120
+
"dependencies": {
121
+
"uuid": "^9.0.0"
108
122
}
109
123
},
110
124
"node_modules/@babel/helper-string-parser": {
···
1703
1717
"tslib": "^2.8.0"
1704
1718
}
1705
1719
},
1720
+
"node_modules/@tokenizer/http": {
1721
+
"version": "0.9.2",
1722
+
"resolved": "https://registry.npmjs.org/@tokenizer/http/-/http-0.9.2.tgz",
1723
+
"integrity": "sha512-rzJwHcqDjO3FdBPr+FK2R6dYE6Qbg6QZP7S47rhCEtG+/YqEFLqZ+gFCLcL8y5D39aYQB9vDssiwbsJlRLePPg==",
1724
+
"license": "MIT",
1725
+
"dependencies": {
1726
+
"@tokenizer/range": "^0.12.0",
1727
+
"debug": "^4.3.7",
1728
+
"strtok3": "^10.0.0"
1729
+
},
1730
+
"funding": {
1731
+
"type": "github",
1732
+
"url": "https://github.com/sponsors/Borewit"
1733
+
}
1734
+
},
1735
+
"node_modules/@tokenizer/http/node_modules/@tokenizer/range": {
1736
+
"version": "0.12.0",
1737
+
"resolved": "https://registry.npmjs.org/@tokenizer/range/-/range-0.12.0.tgz",
1738
+
"integrity": "sha512-xvJ1OflWjopkC5EgLge+9HrwsWStgVewQkmusoF2BxgCuGdm1KuhZAMVMNzC7h1WNei9JA6xKQlkbPNJtjZ6aw==",
1739
+
"license": "MIT",
1740
+
"dependencies": {
1741
+
"debug": "^4.3.7",
1742
+
"strtok3": "^9.1.1"
1743
+
},
1744
+
"engines": {
1745
+
"node": ">=16"
1746
+
},
1747
+
"funding": {
1748
+
"type": "github",
1749
+
"url": "https://github.com/sponsors/Borewit"
1750
+
}
1751
+
},
1752
+
"node_modules/@tokenizer/http/node_modules/@tokenizer/range/node_modules/strtok3": {
1753
+
"version": "9.1.1",
1754
+
"resolved": "https://registry.npmjs.org/strtok3/-/strtok3-9.1.1.tgz",
1755
+
"integrity": "sha512-FhwotcEqjr241ZbjFzjlIYg6c5/L/s4yBGWSMvJ9UoExiSqL+FnFA/CaeZx17WGaZMS/4SOZp8wH18jSS4R4lw==",
1756
+
"license": "MIT",
1757
+
"dependencies": {
1758
+
"@tokenizer/token": "^0.3.0",
1759
+
"peek-readable": "^5.3.1"
1760
+
},
1761
+
"engines": {
1762
+
"node": ">=16"
1763
+
},
1764
+
"funding": {
1765
+
"type": "github",
1766
+
"url": "https://github.com/sponsors/Borewit"
1767
+
}
1768
+
},
1769
+
"node_modules/@tokenizer/http/node_modules/peek-readable": {
1770
+
"version": "5.4.2",
1771
+
"resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-5.4.2.tgz",
1772
+
"integrity": "sha512-peBp3qZyuS6cNIJ2akRNG1uo1WJ1d0wTxg/fxMdZ0BqCVhx242bSFHM9eNqflfJVS9SsgkzgT/1UgnsurBOTMg==",
1773
+
"license": "MIT",
1774
+
"engines": {
1775
+
"node": ">=14.16"
1776
+
},
1777
+
"funding": {
1778
+
"type": "github",
1779
+
"url": "https://github.com/sponsors/Borewit"
1780
+
}
1781
+
},
1706
1782
"node_modules/@tokenizer/inflate": {
1707
1783
"version": "0.2.7",
1708
1784
"resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.2.7.tgz",
···
1715
1791
},
1716
1792
"engines": {
1717
1793
"node": ">=18"
1794
+
},
1795
+
"funding": {
1796
+
"type": "github",
1797
+
"url": "https://github.com/sponsors/Borewit"
1798
+
}
1799
+
},
1800
+
"node_modules/@tokenizer/range": {
1801
+
"version": "0.13.0",
1802
+
"resolved": "https://registry.npmjs.org/@tokenizer/range/-/range-0.13.0.tgz",
1803
+
"integrity": "sha512-ibLGQRU8an1g/y952+OxeZDGIj+W1HW8AQPtk26VIFWzy3tvQImmGBwYbpHJXMMAz1nhCPAAepCRptGKB8YrKg==",
1804
+
"license": "MIT",
1805
+
"dependencies": {
1806
+
"debug": "^4.4.0",
1807
+
"strtok3": "^10.2.0"
1808
+
},
1809
+
"engines": {
1810
+
"node": ">=16"
1718
1811
},
1719
1812
"funding": {
1720
1813
"type": "github",
···
1830
1923
"resolved": "https://gitpkg.vercel.app/unternet-co/web-applets/sdk?tokono.ma/experiment&scripts.postinstall=npm%20i%20%40types%2Fnode%20%26%26%20npx%20tsc",
1831
1924
"integrity": "sha512-AL1T69Yr2yA0MV+JaCWj+SufF83aSBfwLe3iPVh5WB7qH1nH4vu2cC7JJK1FYNBs8wEYmyh2SNHGQjKQyoFy4w==",
1832
1925
"hasInstallScript": true,
1926
+
"license": "MIT"
1927
+
},
1928
+
"node_modules/98.css": {
1929
+
"version": "0.1.21",
1930
+
"resolved": "https://registry.npmjs.org/98.css/-/98.css-0.1.21.tgz",
1931
+
"integrity": "sha512-ddk5qtUWyapM0Bzd5jwGExoE5fdSEGrP+F5VbYjyZLf2c9UVmn6w2NPTvCsoD4BWdGsjdLjlkQGhWwWTJcYQJQ==",
1833
1932
"license": "MIT"
1834
1933
},
1835
1934
"node_modules/acorn": {
···
4778
4877
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
4779
4878
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
4780
4879
},
4880
+
"node_modules/multiformats": {
4881
+
"version": "13.3.6",
4882
+
"resolved": "https://registry.npmjs.org/multiformats/-/multiformats-13.3.6.tgz",
4883
+
"integrity": "sha512-yakbt9cPYj8d3vi/8o/XWm61MrOILo7fsTL0qxNx6zS0Nso6K5JqqS2WV7vK/KSuDBvrW3KfCwAdAgarAgOmww==",
4884
+
"license": "Apache-2.0 OR MIT"
4885
+
},
4781
4886
"node_modules/music-metadata": {
4782
4887
"version": "11.2.3",
4783
4888
"resolved": "https://registry.npmjs.org/music-metadata/-/music-metadata-11.2.3.tgz",
···
5016
5121
"resolved": "https://registry.npmjs.org/node-mock-http/-/node-mock-http-1.0.0.tgz",
5017
5122
"integrity": "sha512-0uGYQ1WQL1M5kKvGRXWQ3uZCHtLTO8hln3oBjIusM75WoesZ909uQJs/Hb946i2SS+Gsrhkaa6iAO17jRIv6DQ==",
5018
5123
"dev": true
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
5124
},
5026
5125
"node_modules/normalize-path": {
5027
5126
"version": "3.0.0",
···
6388
6487
"url": "https://github.com/sponsors/sindresorhus"
6389
6488
}
6390
6489
},
6490
+
"node_modules/uint8arrays": {
6491
+
"version": "5.1.0",
6492
+
"resolved": "https://registry.npmjs.org/uint8arrays/-/uint8arrays-5.1.0.tgz",
6493
+
"integrity": "sha512-vA6nFepEmlSKkMBnLBaUMVvAC4G3CTmO58C12y4sq6WPDOR7mOFYOi7GlrQ4djeSbP6JG9Pv9tJDM97PedRSww==",
6494
+
"license": "Apache-2.0 OR MIT",
6495
+
"dependencies": {
6496
+
"multiformats": "^13.0.0"
6497
+
}
6498
+
},
6391
6499
"node_modules/ultrahtml": {
6392
6500
"version": "1.6.0",
6393
6501
"resolved": "https://registry.npmjs.org/ultrahtml/-/ultrahtml-1.6.0.tgz",
···
6702
6810
"integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==",
6703
6811
"license": "ISC"
6704
6812
},
6813
+
"node_modules/uuid": {
6814
+
"version": "9.0.1",
6815
+
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
6816
+
"integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
6817
+
"funding": [
6818
+
"https://github.com/sponsors/broofa",
6819
+
"https://github.com/sponsors/ctavan"
6820
+
],
6821
+
"license": "MIT",
6822
+
"bin": {
6823
+
"uuid": "dist/bin/uuid"
6824
+
}
6825
+
},
6705
6826
"node_modules/vfile": {
6706
6827
"version": "6.0.3",
6707
6828
"resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz",
···
6816
6937
"yaml": {
6817
6938
"optional": true
6818
6939
}
6940
+
}
6941
+
},
6942
+
"node_modules/vite-plugin-wasm": {
6943
+
"version": "3.4.1",
6944
+
"resolved": "https://registry.npmjs.org/vite-plugin-wasm/-/vite-plugin-wasm-3.4.1.tgz",
6945
+
"integrity": "sha512-ja3nSo2UCkVeitltJGkS3pfQHAanHv/DqGatdI39ja6McgABlpsZ5hVgl6wuR8Qx5etY3T5qgDQhOWzc5RReZA==",
6946
+
"dev": true,
6947
+
"license": "MIT",
6948
+
"peerDependencies": {
6949
+
"vite": "^2 || ^3 || ^4 || ^5 || ^6"
6819
6950
}
6820
6951
},
6821
6952
"node_modules/vitefu": {
+7
-1
package.json
+7
-1
package.json
···
1
1
{
2
2
"dependencies": {
3
+
"@automerge/automerge": "^3.0.0-beta.0",
3
4
"@bradenmacdonald/s3-lite-client": "npm:@jsr/bradenmacdonald__s3-lite-client@^0.9.0",
4
5
"@picocss/pico": "^2.1.1",
5
6
"@std/media-types": "npm:@jsr/std__media-types@^1.1.0",
7
+
"@tokenizer/http": "^0.9.2",
8
+
"@tokenizer/range": "^0.13.0",
6
9
"@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",
10
+
"98.css": "^0.1.21",
7
11
"iconoir": "^7.11.0",
8
12
"idb-keyval": "^6.2.1",
9
13
"music-metadata": "^11.2.3",
···
11
15
"query-string": "^9.1.2",
12
16
"spellcaster": "^6.0.0",
13
17
"throttle-debounce": "^5.0.2",
18
+
"uint8arrays": "^5.1.0",
14
19
"uri-js": "^4.4.1",
15
20
"webamp": "^1.5.0",
16
21
"xxh32": "^2.0.5"
···
21
26
"astro-purgecss": "^5.2.2",
22
27
"astro-scope": "^3.0.1",
23
28
"purgecss": "^7.0.2",
24
-
"sass": "^1.87.0"
29
+
"sass": "^1.87.0",
30
+
"vite-plugin-wasm": "^3.4.1"
25
31
}
26
32
}
+17
-18
src/pages/configurator/input/_applet.astro
+17
-18
src/pages/configurator/input/_applet.astro
···
11
11
<i class="iconoir-open-in-window"></i>
12
12
<strong>My device</strong>
13
13
</a>
14
+
<br />
15
+
<a href="../../input/s3/" class="with-icon">
16
+
<i class="iconoir-open-in-window"></i>
17
+
<strong>S3-compatible service</strong>
18
+
</a>
14
19
</p>
15
20
</div>
16
21
<p>
17
-
<small
18
-
><em><strong>More options coming soon!</strong><br />S3-compatible APIs, Dropbox, etc.</em
19
-
></small
20
-
>
22
+
<small><em><strong>More options coming soon!</strong></em></small>
21
23
</p>
22
24
</main>
23
25
24
-
<div id="iframes"></div>
25
-
26
-
<style>
27
-
#iframes {
26
+
<style is:global>
27
+
iframe {
28
28
display: none;
29
29
}
30
30
</style>
31
31
32
32
<script>
33
-
import { applets } from "@web-applets/sdk";
34
-
35
33
import type { Track } from "@applets/core/types.d.ts";
36
-
import { applet } from "@scripts/theme";
34
+
import { applet, register } from "@scripts/applets/common";
37
35
38
36
////////////////////////////////////////////
39
37
// SETUP
40
38
////////////////////////////////////////////
41
-
const container = document.querySelector("#iframes") || undefined;
42
-
43
-
// Register applet
44
-
const context = applets.register<{ ready: boolean }>();
39
+
const context = register<{ ready: boolean }>();
45
40
46
41
// Initial state
47
42
context.data = {
···
50
45
51
46
// Applet connections
52
47
const input = {
53
-
nativeFs: await applet("../../input/native-fs", { container }),
54
-
s3: await applet("../../input/s3", { container }),
48
+
nativeFs: await applet("../../input/native-fs"),
49
+
s3: await applet("../../input/s3"),
55
50
};
56
51
57
52
////////////////////////////////////////////
58
53
// ACTIONS
59
54
////////////////////////////////////////////
55
+
const contextualize = async (tracks: Track[]) => {
56
+
await input.s3.sendAction("contextualize", tracks);
57
+
};
60
58
61
59
const list = async (cachedTracks: Track[] = []) => {
62
60
const groups = cachedTracks.reduce(
63
61
(acc: Record<string, Track[]>, track: Track) => {
64
62
const scheme = track.uri.split(":", 1)[0];
65
-
return { ...acc, [scheme]: [...(acc.scheme || []), track] };
63
+
return { ...acc, [scheme]: [...(acc[scheme] || []), track] };
66
64
},
67
65
{
68
66
[input.nativeFs.manifest.input_properties.scheme]: [],
···
110
108
}
111
109
};
112
110
111
+
context.setActionHandler("contextualize", contextualize);
113
112
context.setActionHandler("list", list);
114
113
context.setActionHandler("resolve", resolve);
115
114
+9
-1
src/pages/configurator/input/_manifest.json
+9
-1
src/pages/configurator/input/_manifest.json
···
3
3
"title": "Diffuse Configurator | Input",
4
4
"entrypoint": "index.html",
5
5
"actions": {
6
+
"contextualize": {
7
+
"title": "Contextualize",
8
+
"params_schema": {
9
+
"type": "array",
10
+
"description": "Array of tracks",
11
+
"items": { "type": "object" }
12
+
}
13
+
},
6
14
"list": {
7
15
"title": "List",
8
16
"description": "List tracks from all inputs.",
···
16
24
},
17
25
"resolve": {
18
26
"title": "Resolve",
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`.",
27
+
"description": "Potentially translates a track uri with a matching scheme into a URL pointing at the audio bytes or an audio stream. If it can be resolved that is, otherwise you'll get `undefined`.",
20
28
"params_schema": {
21
29
"type": "object",
22
30
"properties": {
+2
-3
src/pages/configurator/output/_applet.astro
+2
-3
src/pages/configurator/output/_applet.astro
···
39
39
import scope from "astro:scope";
40
40
import { type Signal, computed, effect, signal } from "spellcaster/spellcaster.js";
41
41
import { type ElementConfigurator, repeat, text } from "spellcaster/hyperscript.js";
42
-
import { applets } from "@web-applets/sdk";
43
42
44
-
import { applet, hs } from "@src/scripts/theme";
43
+
import { applet, hs, register } from "@scripts/applets/common";
45
44
import type { OutputGetter, OutputSetter } from "@applets/core/types.d.ts";
46
45
47
46
const METHODS = ["browser", "custom", "device"] as const;
···
63
62
////////////////////////////////////////////
64
63
// SETUP
65
64
////////////////////////////////////////////
66
-
const context = applets.register<{ ready: boolean }>();
65
+
const context = register<{ ready: boolean }>();
67
66
68
67
// Applets container
69
68
const container = document.createElement("div");
+6
src/pages/core/types.d.ts
+6
src/pages/core/types.d.ts
···
9
9
10
10
/* TRACKS */
11
11
12
+
export type ResolvedUri = undefined | { url: string; expiresAt: number }; // TODO: Streams?
13
+
12
14
export interface Track<Tags = TrackTags, Stats = TrackStats> {
13
15
id: string;
14
16
···
29
31
export interface TrackTags {
30
32
album?: string;
31
33
artist?: string;
34
+
disc: { no: number; of?: number };
35
+
genre?: string;
32
36
title: string;
37
+
track: { no: number; of?: number };
38
+
year?: number;
33
39
}
+12
-13
src/pages/engine/audio/_applet.astro
+12
-13
src/pages/engine/audio/_applet.astro
···
1
1
<script>
2
-
import { applets } from "@web-applets/sdk";
3
-
import { State, Track, TrackState } from "./types";
2
+
import type { State, Track, TrackState } from "./types";
3
+
import { register } from "@scripts/applets/common";
4
4
5
5
////////////////////////////////////////////
6
6
// CONSTANTS
···
11
11
////////////////////////////////////////////
12
12
// SETUP
13
13
////////////////////////////////////////////
14
-
const context = applets.register<State>();
14
+
const context = register<State>();
15
+
16
+
// Audio elements container
15
17
const container = document.createElement("div");
16
-
17
18
container.id = "container";
18
19
document.body.appendChild(container);
19
20
···
40
41
////////////////////////////////////////////
41
42
// ACTIONS
42
43
////////////////////////////////////////////
43
-
context.setActionHandler(
44
-
"render",
45
-
async (args: { play?: { trackId: string; volume?: number }; tracks: Track[] }) => {
46
-
await render(args.tracks);
47
-
if (args.play) play({ trackId: args.play.trackId, volume: args.play.volume });
48
-
},
49
-
);
50
-
51
44
context.setActionHandler("pause", pause);
52
45
context.setActionHandler("play", play);
53
46
context.setActionHandler("reload", reload);
47
+
context.setActionHandler("render", render);
54
48
context.setActionHandler("seek", seek);
55
49
context.setActionHandler("volume", volume);
56
50
···
102
96
});
103
97
}
104
98
99
+
async function render(args: { play?: { trackId: string; volume?: number }; tracks: Track[] }) {
100
+
await renderTracks(args.tracks);
101
+
if (args.play) play({ trackId: args.play.trackId, volume: args.play.volume });
102
+
}
103
+
105
104
function seek({ percentage, trackId }: { percentage: number; trackId: string }) {
106
105
withAudioNode(trackId, (audio) => {
107
106
if (!isNaN(audio.duration)) {
···
122
121
////////////////////////////////////////////
123
122
// RENDER
124
123
////////////////////////////////////////////
125
-
async function render(tracks: Array<Track>) {
124
+
async function renderTracks(tracks: Array<Track>) {
126
125
const ids = tracks.map((e) => e.id);
127
126
const existingNodes: Record<string, HTMLAudioElement> = {};
128
127
+2
-2
src/pages/engine/queue/_applet.astro
+2
-2
src/pages/engine/queue/_applet.astro
···
1
1
<script>
2
-
import { applets } from "@web-applets/sdk";
3
2
import { QueueItem, State } from "./types";
3
+
import { register } from "@scripts/applets/common";
4
4
5
5
////////////////////////////////////////////
6
6
// SETUP
7
7
////////////////////////////////////////////
8
-
const context = applets.register<State>();
8
+
const context = register<State>();
9
9
10
10
// Initial state
11
11
context.data = {
+5
-5
src/pages/index.astro
+5
-5
src/pages/index.astro
···
36
36
37
37
const input = [
38
38
{ url: "input/native-fs/", title: "Native File System" },
39
-
{ url: "input/s3-compatible/", title: "(TODO) S3-Compatible API" },
39
+
{ url: "input/s3/", title: "S3-Compatible API" },
40
40
];
41
41
42
42
const orchestrators = [
···
49
49
{ url: "output/indexed-db/", title: "IndexedDB" },
50
50
{ url: "output/native-fs/", title: "Native File System" },
51
51
{ url: "output/todo/", title: "(TODO) Keyhive/Beelay" },
52
-
{ url: "output/todo/", title: "(TODO) Some local-first sync engine" },
52
+
{ url: "output/todo/", title: "(TODO) Dialog DB" },
53
53
];
54
54
55
55
const processors = [
···
141
141
</Applet>
142
142
143
143
<Applet title="Orchestrators" list={orchestrators}>
144
-
These too are applet compositions. However, unlike themes, these are purely logical, and
145
-
reuse applet instances from the parent context (when available). Mostly exist in order to
146
-
construct sensible defaults to use across themes and abstractions.
144
+
These too are applet compositions. However, unlike themes, these are purely logical.
145
+
Mostly exist in order to construct sensible defaults to use across themes and
146
+
abstractions.
147
147
</Applet>
148
148
149
149
<Applet title="Output" list={output}>
+3
-3
src/pages/input/native-fs/_applet.astro
+3
-3
src/pages/input/native-fs/_applet.astro
···
16
16
</main>
17
17
18
18
<script>
19
-
import { applets } from "@web-applets/sdk";
20
19
import { computed, effect, Signal, signal } from "spellcaster";
21
20
import { repeat, tags, text } from "spellcaster/hyperscript.js";
22
21
import { type FileSystemDirectoryHandle, showDirectoryPicker } from "native-file-system-adapter";
···
26
25
27
26
import type { Track } from "@applets/core/types.d.ts";
28
27
import { isAudioFile } from "@scripts/inputs/common";
28
+
import { register } from "@scripts/applets/common";
29
29
30
30
import manifest from "./_manifest.json";
31
31
···
41
41
const SCHEME = manifest.input_properties.scheme;
42
42
43
43
// Register applet
44
-
const context = applets.register();
44
+
const context = register();
45
45
46
46
////////////////////////////////////////////
47
47
// UI
···
198
198
const file = await fileHandle.getFile();
199
199
const url = URL.createObjectURL(file);
200
200
201
-
return url;
201
+
return { expiresAt: Infinity, url };
202
202
};
203
203
204
204
const mount = async () => {
+1
-1
src/pages/input/native-fs/_manifest.json
+1
-1
src/pages/input/native-fs/_manifest.json
···
27
27
},
28
28
"resolve": {
29
29
"title": "Resolve",
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.",
30
+
"description": "Potentially translates a track uri with a matching scheme into a URL pointing at the audio bytes or an audio stream. 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
32
"type": "object",
33
33
"properties": {
+33
-30
src/pages/input/s3/_applet.astro
+33
-30
src/pages/input/s3/_applet.astro
···
38
38
39
39
<script>
40
40
import { S3Client } from "@bradenmacdonald/s3-lite-client";
41
-
import { type AppletEvent, applets } from "@web-applets/sdk";
42
41
import { computed, effect, Signal, signal } from "spellcaster";
43
42
import { type Props, repeat, tags, text } from "spellcaster/hyperscript.js";
44
43
import * as IDB from "idb-keyval";
45
44
import * as URI from "uri-js";
46
45
import QS from "query-string";
47
46
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
-
47
+
import type { Track } from "@applets/core/types.d.ts";
48
+
import { isAudioFile } from "@scripts/inputs/common";
49
+
import { register } from "@scripts/applets/common";
54
50
import manifest from "./_manifest.json";
55
-
import { isAudioFile } from "@scripts/inputs/common";
56
51
57
52
type Bucket = {
58
53
accessKey: string;
···
90
85
const SCHEME = manifest.input_properties.scheme;
91
86
92
87
// 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
-
});
88
+
const context = register();
106
89
107
90
////////////////////////////////////////////
108
91
// UI
···
221
204
// ACTIONS
222
205
////////////////////////////////////////////
223
206
const consult = async (fileUriOrScheme: string) => {
224
-
if (!navigator.onLine) return false;
207
+
if (!navigator.onLine)
208
+
return { supported: false, reason: "Internet connection is not available" };
225
209
226
210
// TODO: Check if bucket is avail*able + CORS works?
227
-
return true;
211
+
return { supported: true };
212
+
};
213
+
214
+
const contextualize = async (tracks: Track[]) => {
215
+
const b = bucketsFromTracks(tracks);
216
+
setBuckets({ ...buckets(), ...b });
228
217
};
229
218
230
-
const list = async (_cachedTracks: Track[] = []) => {
231
-
// TODO: Do we need to do something with the old tracks here?
219
+
const list = async (cachedTracks: Track[] = []) => {
220
+
const cache = cachedTracks.reduce((acc: Record<string, Track>, t: Track) => {
221
+
const uri = URI.parse(t.uri);
222
+
if (!uri.path) return acc;
223
+
return { ...acc, [URI.unescapeComponent(uri.path)]: t };
224
+
}, {});
232
225
233
226
const promises = Object.values(buckets()).map(async (bucket) => {
234
227
const client = createClient(bucket);
···
242
235
return list
243
236
.filter((l) => isAudioFile(l.key))
244
237
.map((l) => {
238
+
const cachedTrack = cache[`/${l.key}`];
239
+
240
+
const id = cachedTrack?.id || crypto.randomUUID();
241
+
const stats = cachedTrack?.stats;
242
+
const tags = cachedTrack?.tags;
243
+
245
244
const track: Track = {
246
-
id: crypto.randomUUID(),
245
+
id,
246
+
stats,
247
+
tags,
247
248
uri: buildURI(bucket, l.key),
248
249
};
249
250
···
264
265
bucket.path.replace(/\/$/, "") + URI.unescapeComponent(parsedURI.path || "")
265
266
).replace(/^\//, "");
266
267
268
+
const expiresInSeconds = 60 * 60 * 24 * 7; // 7 days
269
+
const expiresAtSeconds = Math.round(Date.now() / 1000) + expiresInSeconds;
267
270
const url = await client.getPresignedUrl(method.toUpperCase() as any, path);
268
-
return url;
271
+
272
+
return { expiresAt: expiresAtSeconds, url };
269
273
};
270
274
271
275
const mount = async () => {};
···
273
277
const unmount = async () => {};
274
278
275
279
context.setActionHandler("consult", consult);
280
+
context.setActionHandler("contextualize", contextualize);
276
281
context.setActionHandler("list", list);
277
282
context.setActionHandler("resolve", resolve);
278
283
context.setActionHandler("mount", mount);
···
331
336
332
337
async function loadBuckets() {
333
338
const i = await IDB.get(IDB_BUCKETS);
334
-
const t = bucketsFromTracks(orchestrator.output.data.tracks);
335
-
336
-
return { ...i, ...t };
339
+
return i ? i : {};
337
340
}
338
341
339
342
function parseURI(uriString: string): Bucket | undefined {
+9
-1
src/pages/input/s3/_manifest.json
+9
-1
src/pages/input/s3/_manifest.json
···
13
13
"description": "The uri to check the availability of."
14
14
}
15
15
},
16
+
"contextualize": {
17
+
"title": "Contextualize",
18
+
"params_schema": {
19
+
"type": "array",
20
+
"description": "Array of tracks",
21
+
"items": { "type": "object" }
22
+
}
23
+
},
16
24
"list": {
17
25
"title": "List",
18
26
"description": "List tracks.",
···
26
34
},
27
35
"resolve": {
28
36
"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.",
37
+
"description": "Potentially translates a track uri with a matching scheme into a URL pointing at the audio bytes or an audio stream. If it can be resolved that is, otherwise you'll get `undefined`. Use the `consult` action to get a more detailed answer.",
30
38
"params_schema": {
31
39
"type": "object",
32
40
"properties": {
+45
-30
src/pages/orchestrator/input-cache/_applet.astro
+45
-30
src/pages/orchestrator/input-cache/_applet.astro
···
1
1
<script>
2
-
import { applets } from "@web-applets/sdk";
2
+
import type { ResolvedUri, Track } from "@applets/core/types.d.ts";
3
3
4
-
import type { Output, Track, TrackStats, TrackTags } from "@applets/core/types.d.ts";
5
-
import { applet, waitUntilAppletIsReady } from "@scripts/theme";
4
+
import {
5
+
applet,
6
+
register,
7
+
waitUntilAppletData,
8
+
waitUntilAppletIsReady,
9
+
} from "@scripts/applets/common";
6
10
7
11
////////////////////////////////////////////
8
12
// SETUP
9
13
////////////////////////////////////////////
10
-
const context = applets.register<{ ready: boolean }>();
14
+
import type * as OutputOrchestrator from "@applets/orchestrator/output-management/types.d.ts";
15
+
16
+
const context = register<{ isProcessing: boolean; ready: boolean }>();
11
17
12
18
// Initial data
13
19
context.data = {
20
+
isProcessing: false,
14
21
ready: false,
15
22
};
16
23
17
24
// Applet connections
18
25
const configurator = {
19
-
input: await applet("../../configurator/input", { context: self.top || self.parent }),
26
+
input: await applet("../../configurator/input"),
20
27
};
21
28
22
29
const orchestrator = {
23
-
output: await applet<Output>("../../orchestrator/output-management", {
24
-
context: self.parent,
25
-
}),
30
+
output: await applet<OutputOrchestrator.State>("../../orchestrator/output-management"),
26
31
};
27
32
28
33
const processor = {
29
-
metadataFetcher: await applet("../../processor/metadata-fetcher", { context: self.parent }),
34
+
metadataFetcher: await applet("../../processor/metadata-fetcher"),
30
35
};
31
36
32
37
// 🚀
33
-
process();
38
+
waitUntilAppletData(orchestrator.output, (d) => !!d?.hasSyncedTracks).then(() => process());
34
39
35
40
////////////////////////////////////////////
36
41
// ACTIONS
···
38
43
context.setActionHandler("process", process);
39
44
40
45
async function process() {
46
+
if (context.data.isProcessing) return;
47
+
context.data = { ...context.data, isProcessing: true };
48
+
console.log("🪵 Processing initiated");
49
+
41
50
await waitUntilAppletIsReady(configurator.input);
42
51
43
-
const cachedTracks = orchestrator.output.data.tracks;
52
+
const cachedTracks = orchestrator.output.data.tracks.collection;
53
+
await configurator.input.sendAction("contextualize", cachedTracks);
54
+
44
55
const tracks = await configurator.input.sendAction<Track[]>("list", cachedTracks, {
45
56
timeoutDuration: 60000 * 60 * 24,
46
57
});
···
50
61
async (promise: Promise<Track[]>, track: Track) => {
51
62
const acc = await promise;
52
63
53
-
if (track.tags) return [...acc, track];
64
+
if (track.tags && track.stats) return [...acc, track];
54
65
55
-
const getURL = await configurator.input.sendAction<string | undefined>(
66
+
const resGet = await configurator.input.sendAction<ResolvedUri>(
56
67
"resolve",
57
68
{ method: "GET", uri: track.uri },
58
69
{
59
-
timeoutDuration: 60000,
70
+
timeoutDuration: 60000 * 5,
60
71
},
61
72
);
62
73
63
-
if (!getURL) return acc;
74
+
const resHead = await configurator.input.sendAction<ResolvedUri>(
75
+
"resolve",
76
+
{ method: "HEAD", uri: track.uri },
77
+
{
78
+
timeoutDuration: 60000 * 5,
79
+
},
80
+
);
64
81
65
-
// TODO: Do we need to pass the HEAD URL too?
66
-
const meta = await processor.metadataFetcher.sendAction("extract", getURL, {
67
-
timeoutDuration: 60000,
68
-
});
82
+
if (!resGet) return acc;
69
83
70
-
const stats: TrackStats = {
71
-
duration: meta.format.duration,
72
-
};
84
+
const { stats, tags } = await processor.metadataFetcher.sendAction(
85
+
"extract",
86
+
{ urls: { get: resGet.url, head: resHead?.url || resGet.url } },
87
+
{
88
+
timeoutDuration: 60000 * 15,
89
+
},
90
+
);
73
91
74
-
const tags: TrackTags = {
75
-
album: meta.common.album,
76
-
artist: meta.common.artist,
77
-
title: meta.common.title,
78
-
};
92
+
console.log(stats, tags);
79
93
80
94
return [...acc, { ...track, stats, tags }];
81
95
},
···
84
98
85
99
// Save
86
100
await orchestrator.output.sendAction("tracks", tracksWithMetadata, {
87
-
timeoutDuration: 60000 * 2,
101
+
timeoutDuration: 60000 * 5,
88
102
});
89
103
90
-
// Log
104
+
// Fin
91
105
console.log("🪵 Processing completed");
106
+
context.data = { ...context.data, isProcessing: false };
92
107
}
93
108
94
109
////////////////////////////////////////////
95
110
// 🚦
96
111
////////////////////////////////////////////
97
-
context.data = { ready: true };
112
+
context.data = { ...context.data, ready: true };
98
113
</script>
+98
-22
src/pages/orchestrator/output-management/_applet.astro
+98
-22
src/pages/orchestrator/output-management/_applet.astro
···
1
1
<!-- TODO: Button to export all user/output data. --><!-- TODO: Button to import data? -->
2
2
<script>
3
-
import { applets } from "@web-applets/sdk";
4
3
import { debounce } from "throttle-debounce";
4
+
import * as Automerge from "@automerge/automerge";
5
+
import * as Uint8 from "uint8arrays";
5
6
6
7
import type { Track } from "@applets/core/types.d.ts";
7
8
import type { State } from "./types.d.ts";
8
-
import { applet, waitUntilAppletIsReady } from "@scripts/theme";
9
+
import { applet, register, waitUntilAppletIsReady } from "@scripts/applets/common";
10
+
11
+
type TracksDoc = { collection: Track[] };
12
+
13
+
const TRACKS_INITIAL_DOC = Automerge.load<TracksDoc>(
14
+
Uint8.fromString(
15
+
"hW9Kg5qsIsEAeAEQkb+c0IkXTSWyGqZ6jXtFxgETwM42fL3CMN78UZ4Qa3a9RfOrJu5qKzlM7IxwAUXelQYBAgMCEwIjBkACVgIHFQwhAiMCNAFCAlYCgAECfwB/AX8Bf+ub7MEGfwB/B38KY29sbGVjdGlvbn8AfwEBfwJ/AH8AAA",
16
+
"base64",
17
+
),
18
+
);
9
19
10
20
////////////////////////////////////////////
11
21
// SETUP
12
22
////////////////////////////////////////////
13
-
const context = applets.register<State>();
23
+
const context = register<State>();
24
+
25
+
// Data codec
26
+
const codec = {
27
+
decode(data: any) {
28
+
return {
29
+
hasSyncedTracks: data.hasSyncedTracks,
30
+
ready: context.data.ready,
31
+
tracks: Automerge.load<TracksDoc>(data.tracks),
32
+
};
33
+
},
34
+
35
+
encode(data: State) {
36
+
return {
37
+
hasSyncedTracks: true,
38
+
ready: context.data.ready,
39
+
tracks: Automerge.save(data.tracks),
40
+
};
41
+
},
42
+
};
43
+
44
+
context.codec = codec;
14
45
15
46
// Initial data
16
47
context.data = {
48
+
// Empty tracks collection, DO NOT CHANGE.
49
+
// (avoids the initial sync problem with Automerge)
50
+
tracks: TRACKS_INITIAL_DOC,
51
+
52
+
hasSyncedTracks: false,
53
+
17
54
ready: false,
18
-
tracks: [],
19
55
};
20
56
21
57
// Applet connections
22
58
const configurator = {
23
-
output: await applet("../../configurator/output", { context: self.top || self.parent }),
59
+
output: await applet("../../configurator/output"),
24
60
};
25
61
26
-
// Load tracks
27
-
loadTracks().then((tracks) => {
28
-
update({ tracks });
29
-
});
62
+
// Load tracks if needed
63
+
if (context.isMainInstance())
64
+
loadTracks().then((doc) => {
65
+
console.log("LOADED DOC", doc);
66
+
67
+
if (doc) {
68
+
const mergedDoc = Automerge.merge(context.data.tracks, doc);
69
+
console.log("MERGED DOC", doc);
70
+
update({ tracks: mergedDoc });
71
+
}
72
+
73
+
update({ hasSyncedTracks: true });
74
+
});
30
75
31
76
// State helpers
32
77
function update(partial: Partial<State>): void {
33
78
context.data = { ...context.data, ...partial };
34
79
}
35
80
81
+
function updateTracks(tracks: Track[]): Automerge.Doc<TracksDoc> {
82
+
console.log(context.data.tracks);
83
+
console.log(context.isMainInstance());
84
+
85
+
const doc = Automerge.change(context.data.tracks, (d) => {
86
+
d.collection = cleanUndefinedValuesForTracks(tracks);
87
+
});
88
+
89
+
update({ tracks: doc });
90
+
91
+
return doc;
92
+
}
93
+
36
94
////////////////////////////////////////////
37
95
// LOADERS
38
96
////////////////////////////////////////////
39
-
async function loadTracks(): Promise<Track[]> {
97
+
async function loadTracks() {
40
98
await waitUntilAppletIsReady(configurator.output);
41
99
42
100
const data = await configurator.output.sendAction(
···
49
107
},
50
108
);
51
109
110
+
console.log("🔮 Loading tracks, got:", data);
111
+
52
112
if (!data) {
53
-
return [];
113
+
return undefined;
54
114
}
55
115
56
-
return decode(data as Uint8Array);
116
+
return Automerge.load<TracksDoc>(data as Uint8Array);
57
117
}
58
118
59
119
////////////////////////////////////////////
60
120
// ACTIONS
61
121
////////////////////////////////////////////
62
122
const tracksHandler = (tracks: Track[]) => {
63
-
update({ tracks });
123
+
const doc = updateTracks(tracks);
124
+
125
+
console.log("🔮 Tracks collection updated in memory");
64
126
65
127
// Save tracks to output, but only the ones that need to be saved.
66
-
// TODO: For each track.uri scheme ask the output configurator if it needs to be cached.
67
-
saveTracksToOutput(tracks);
128
+
// TODO: For each track.uri scheme ask the input configurator if it needs to be cached?
129
+
saveTracksToOutput(doc);
68
130
};
69
131
70
-
const saveTracksToOutput = debounce(5000, async function (tracks: Track[]) {
71
-
const data = encode(tracks);
132
+
const saveTracksToOutput = debounce(5000, async function (doc: Automerge.Doc<TracksDoc>) {
133
+
const data = Automerge.save(doc);
134
+
135
+
console.log("🔮 Saving tracks");
72
136
73
137
await configurator.output.sendAction("put", {
74
138
name: "tracks.json",
75
139
data,
76
140
});
141
+
142
+
console.log("🔮 Tracks saved to output");
77
143
});
78
144
79
145
context.setActionHandler("tracks", tracksHandler);
···
81
147
////////////////////////////////////////////
82
148
// 🛠️
83
149
////////////////////////////////////////////
84
-
function decode(data: Uint8Array) {
85
-
return JSON.parse(new TextDecoder().decode(data));
86
-
}
150
+
function cleanUndefinedValuesForTracks(tracks: Track[]): Track[] {
151
+
return tracks.map((track) => {
152
+
const t = { ...track };
87
153
88
-
function encode(data: Object) {
89
-
return new TextEncoder().encode(JSON.stringify(data));
154
+
if (t.tags) {
155
+
if ("album" in t.tags && t.tags.album === undefined) delete t.tags.album;
156
+
if ("artist" in t.tags && t.tags.artist === undefined) delete t.tags.artist;
157
+
if ("genre" in t.tags && t.tags.genre === undefined) delete t.tags.genre;
158
+
if ("year" in t.tags && t.tags.year === undefined) delete t.tags.year;
159
+
160
+
if ("of" in t.tags.disc && t.tags.disc.of === undefined) delete t.tags.disc.of;
161
+
if ("of" in t.tags.track && t.tags.track.of === undefined) delete t.tags.track.of;
162
+
}
163
+
164
+
return t;
165
+
});
90
166
}
91
167
92
168
////////////////////////////////////////////
+9
-2
src/pages/orchestrator/output-management/types.d.ts
+9
-2
src/pages/orchestrator/output-management/types.d.ts
···
1
-
import { Output } from "@applets/core/types";
1
+
import type { Doc } from "@automerge/automerge";
2
+
import type { Output } from "@applets/core/types";
3
+
4
+
export type State = {
5
+
tracks: Doc<{ collection: Output["tracks"] }>;
6
+
7
+
hasSyncedTracks: boolean;
2
8
3
-
export type State = Output & { ready: boolean };
9
+
ready: boolean;
10
+
};
+37
-27
src/pages/orchestrator/single-queue/_applet.astro
+37
-27
src/pages/orchestrator/single-queue/_applet.astro
···
1
1
<script>
2
-
import { applets } from "@web-applets/sdk";
3
-
4
-
import type { Track, Output } from "@applets/core/types.d.ts";
5
-
import { applet, comparable, reactive } from "@scripts/theme";
2
+
import type { ResolvedUri, Track } from "@applets/core/types.d.ts";
3
+
import { applet, comparable, reactive, register } from "@scripts/applets/common";
6
4
7
5
////////////////////////////////////////////
8
6
// SETUP
9
7
////////////////////////////////////////////
10
8
import type * as AudioEngine from "@applets/engine/audio/types.d.ts";
11
9
import type * as QueueEngine from "@applets/engine/queue/types.d.ts";
10
+
import type * as OutputOrchestrator from "@applets/orchestrator/output-management/types.d.ts";
12
11
13
12
// Register applet
14
-
const context = applets.register<unknown>();
13
+
const context = register<unknown>();
15
14
16
15
// Applet connections
16
+
const configurator = {
17
+
input: await applet("../../configurator/input"),
18
+
};
19
+
17
20
const engine = {
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
-
}),
21
+
audio: await applet<AudioEngine.State>("../../engine/audio"),
22
+
queue: await applet<QueueEngine.State>("../../engine/queue"),
24
23
};
25
24
26
25
const orchestrator = {
27
-
output: await applet<Output>("../../orchestrator/output-management", { context: self.parent }),
26
+
output: await applet<OutputOrchestrator.State>("../../orchestrator/output-management"),
28
27
};
29
28
30
29
////////////////////////////////////////////
···
32
31
////////////////////////////////////////////
33
32
context.setActionHandler("fill", fill);
34
33
35
-
function fill() {
36
-
// TODO: Need to translate the semi-permanent track uri
37
-
// into a usable audio URL.
38
-
engine.queue.sendAction(
39
-
"add",
40
-
orchestrator.output.data.tracks.map((track: Track) => {
41
-
return {
42
-
expiresAt: Infinity,
43
-
id: track.id,
44
-
url: track.uri,
45
-
};
46
-
}),
34
+
async function fill(tracks: Track[]) {
35
+
const queueItems = await tracks.reduce(
36
+
async (promise: Promise<QueueEngine.QueueItem[]>, track: Track) => {
37
+
const acc = await promise;
38
+
const res = await configurator.input.sendAction<ResolvedUri>("resolve", {
39
+
method: "GET",
40
+
uri: track.uri,
41
+
});
42
+
43
+
if (!res) return acc;
44
+
45
+
return [
46
+
...acc,
47
+
{
48
+
expiresAt: res.expiresAt,
49
+
id: track.id,
50
+
url: res.url,
51
+
},
52
+
];
53
+
},
54
+
Promise.resolve([]),
47
55
);
56
+
57
+
await engine.queue.sendAction("add", queueItems);
48
58
}
49
59
50
60
////////////////////////////////////////////
···
96
106
},
97
107
});
98
108
99
-
fill();
109
+
fill(orchestrator.output.data.tracks.collection);
100
110
},
101
111
);
102
112
···
106
116
////////////////////////////////////////////
107
117
reactive(
108
118
orchestrator.output,
109
-
(data) => (data ? comparable(data.tracks) : undefined),
119
+
(data) => (data ? comparable(data.tracks.collection) : undefined),
110
120
(hash) => {
111
-
if (hash) fill();
121
+
if (hash) fill(orchestrator.output.data.tracks.collection);
112
122
},
113
123
);
114
124
</script>
+6
-1
src/pages/orchestrator/single-queue/_manifest.json
+6
-1
src/pages/orchestrator/single-queue/_manifest.json
···
5
5
"actions": {
6
6
"fill": {
7
7
"title": "Fill",
8
-
"description": "Fill up the queue."
8
+
"description": "Fill up the queue.",
9
+
"params_schema": {
10
+
"type": "array",
11
+
"items": { "type": "object" },
12
+
"description": "Array of tracks to be used to fill up the queue."
13
+
}
9
14
}
10
15
}
11
16
}
+2
-2
src/pages/output/indexed-db/_applet.astro
+2
-2
src/pages/output/indexed-db/_applet.astro
···
1
1
<script>
2
2
import * as IDB from "idb-keyval";
3
-
import { applets } from "@web-applets/sdk";
4
3
5
4
import type { OutputGetter, OutputSetter } from "@applets/core/types.d.ts";
5
+
import { register } from "@scripts/applets/common";
6
6
7
7
////////////////////////////////////////////
8
8
// SETUP
9
9
////////////////////////////////////////////
10
10
const IDB_PREFIX = "@applets/output/indexed-db";
11
-
const context = applets.register();
11
+
const context = register();
12
12
13
13
////////////////////////////////////////////
14
14
// ACTIONS
+2
-2
src/pages/output/native-fs/_applet.astro
+2
-2
src/pages/output/native-fs/_applet.astro
···
1
1
<script>
2
2
import * as IDB from "idb-keyval";
3
-
import { applets } from "@web-applets/sdk";
4
3
import { type FileSystemDirectoryHandle, showDirectoryPicker } from "native-file-system-adapter";
5
4
6
5
import type { OutputGetter, OutputSetter } from "@applets/core/types.d.ts";
6
+
import { register } from "@scripts/applets/common";
7
7
8
8
////////////////////////////////////////////
9
9
// SETUP
···
11
11
const IDB_PREFIX = "@applets/output/native-fs";
12
12
const IDB_DEVICE_KEY = `${IDB_PREFIX}/device`;
13
13
14
-
const context = applets.register();
14
+
const context = register();
15
15
16
16
////////////////////////////////////////////
17
17
// ACTIONS
+70
-11
src/pages/processor/metadata-fetcher/_applet.astro
+70
-11
src/pages/processor/metadata-fetcher/_applet.astro
···
1
1
<script>
2
2
import { applets } from "@web-applets/sdk";
3
-
import { parseWebStream } from "music-metadata";
3
+
import { parseFromTokenizer, parseWebStream } from "music-metadata";
4
4
import { contentType } from "@std/media-types";
5
5
import * as URI from "uri-js";
6
+
import * as HTTP_TOKENIZER from "@tokenizer/http";
7
+
import * as RANGE_TOKENIZER from "@tokenizer/range";
8
+
9
+
import { TrackStats, TrackTags } from "@applets/core/types";
6
10
7
11
////////////////////////////////////////////
8
12
// SETUP
9
13
////////////////////////////////////////////
10
14
const context = applets.register();
11
15
16
+
type Extraction = { stats: TrackStats; tags: TrackTags };
17
+
type Urls = { get: string; head: string };
18
+
12
19
////////////////////////////////////////////
13
20
// ACTIONS
14
21
////////////////////////////////////////////
15
22
context.setActionHandler("extract", extract);
16
23
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;
23
-
const resp = await fetch(url);
24
-
const stream = resp.body;
25
-
const metadata = await parseWebStream(stream, { mimeType });
24
+
async function extract(args: { mimeType?: string; stream?: ReadableStream; urls?: Urls }) {
25
+
// Construct records
26
+
// TODO: Use other metadata lib as fallback: https://github.com/buzz/mediainfo.js
27
+
const { stats, tags } = await musicMetadataTags(args, false).catch(() => ({
28
+
stats: undefined,
29
+
tags: undefined,
30
+
}));
31
+
32
+
// Fin
33
+
return { stats, tags };
34
+
}
35
+
36
+
// 🛠️
37
+
async function musicMetadataTags(
38
+
{ mimeType, stream, urls }: { mimeType?: string; stream?: ReadableStream; urls?: Urls },
39
+
covers: boolean = false,
40
+
): Promise<Extraction> {
41
+
const uri = urls ? URI.parse(urls.get) : undefined;
42
+
const pathParts = uri?.path?.split("/");
43
+
const filename = pathParts?.[pathParts.length - 1];
44
+
45
+
let meta;
46
+
47
+
if (urls?.get.startsWith("blob:")) {
48
+
const mimeFallback = filename?.includes(".")
49
+
? contentType(filename.split(".").reverse()[0])
50
+
: undefined;
51
+
52
+
const resp = await fetch(urls.get);
53
+
const stream = resp.body;
54
+
55
+
meta = await parseWebStream(stream, { mimeType: mimeType || mimeFallback });
56
+
} else if (urls) {
57
+
const httpClient = new HTTP_TOKENIZER.HttpClient(urls.head, { resolveUrl: false });
58
+
httpClient.resolvedUrl = urls.get;
26
59
27
-
return metadata;
60
+
const tokenizer = await RANGE_TOKENIZER.tokenizer(httpClient);
61
+
62
+
meta = await parseFromTokenizer(tokenizer, { skipCovers: covers });
63
+
} else if (stream) {
64
+
meta = await parseWebStream(stream, { mimeType });
65
+
} else {
66
+
throw new Error("Missing args, need either some urls or a stream.");
67
+
}
68
+
69
+
const stats: TrackStats = {
70
+
duration: meta.format.duration,
71
+
};
72
+
73
+
const tags: TrackTags = {
74
+
album: meta.common.album,
75
+
artist: meta.common.artist,
76
+
disc: { no: meta.common.disk.no || 1, of: meta.common.disk.of ?? undefined },
77
+
genre: Array.isArray(meta.common.genre) ? meta.common.genre[0] : meta.common.genre,
78
+
title: meta.common.title || filename || urls?.head || "Unknown",
79
+
track: { no: meta.common.track.no || 1, of: meta.common.track.of ?? undefined },
80
+
year: meta.common.year,
81
+
};
82
+
83
+
return {
84
+
stats,
85
+
tags,
86
+
};
28
87
}
29
88
</script>
+14
-2
src/pages/processor/metadata-fetcher/_manifest.json
+14
-2
src/pages/processor/metadata-fetcher/_manifest.json
···
7
7
"title": "Extract",
8
8
"description": "Get the metadata for a given URL.",
9
9
"params_schema": {
10
-
"type": "string",
11
-
"description": "URL"
10
+
"type": "object",
11
+
"properties": {
12
+
"stream": {
13
+
"type": "object"
14
+
},
15
+
"urls": {
16
+
"type": "object",
17
+
"properties": {
18
+
"get": { "type": "string" },
19
+
"head": { "type": "string" }
20
+
},
21
+
"required": ["get", "head"]
22
+
}
23
+
}
12
24
}
13
25
}
14
26
}
+279
src/scripts/applets/common.ts
+279
src/scripts/applets/common.ts
···
1
+
import type { Applet, AppletEvent } from "@web-applets/sdk";
2
+
3
+
import QS from "query-string";
4
+
import { applets } from "@web-applets/sdk";
5
+
import { type ElementConfigurator, h } from "spellcaster/hyperscript.js";
6
+
import { effect, isSignal, Signal, signal } from "spellcaster/spellcaster.js";
7
+
import { xxh32 } from "xxh32";
8
+
9
+
////////////////////////////////////////////
10
+
// 🪟 Applet connector
11
+
////////////////////////////////////////////
12
+
export async function applet<D>(
13
+
src: string,
14
+
opts: {
15
+
addSlashSuffix?: boolean;
16
+
applets?: Record<string, string>;
17
+
container?: HTMLElement | Element;
18
+
id?: string;
19
+
setHeight?: boolean;
20
+
} = {},
21
+
): Promise<Applet<D>> {
22
+
src = `${src}${
23
+
src.endsWith("/")
24
+
? ""
25
+
: opts.addSlashSuffix === undefined || opts.addSlashSuffix === true
26
+
? "/"
27
+
: ""
28
+
}`;
29
+
30
+
if (opts.applets) {
31
+
src = QS.stringifyUrl({ url: src, query: opts.applets });
32
+
}
33
+
34
+
const existingFrame: HTMLIFrameElement | null = window.document.querySelector(`[src="${src}"]`);
35
+
36
+
let frame;
37
+
38
+
if (existingFrame) {
39
+
frame = existingFrame;
40
+
} else {
41
+
frame = document.createElement("iframe");
42
+
frame.src = src;
43
+
if (opts.id) frame.id = opts.id;
44
+
45
+
if (opts.container) {
46
+
opts.container.appendChild(frame);
47
+
} else {
48
+
window.document.body.appendChild(frame);
49
+
}
50
+
}
51
+
52
+
if (frame.contentWindow === null) {
53
+
throw new Error("iframe does not have a contentWindow");
54
+
}
55
+
56
+
const applet = await applets.connect<D>(frame.contentWindow).catch((err) => {
57
+
console.error("Error connecting to " + src, err);
58
+
throw err;
59
+
});
60
+
61
+
if (opts.setHeight) {
62
+
applet.onresize = () => {
63
+
frame.height = `${applet.height}px`;
64
+
frame.classList.add("has-loaded");
65
+
};
66
+
} else {
67
+
if (frame.contentDocument?.readyState === "complete") {
68
+
frame.classList.add("has-loaded");
69
+
}
70
+
71
+
frame.addEventListener("load", () => {
72
+
frame.classList.add("has-loaded");
73
+
});
74
+
}
75
+
76
+
return applet;
77
+
}
78
+
79
+
////////////////////////////////////////////
80
+
// 🪟 Applet registration
81
+
////////////////////////////////////////////
82
+
export function register<DataType = any>() {
83
+
const id = `${location.host}${location.pathname}`;
84
+
const scope = applets.register<DataType>();
85
+
86
+
let isMainInstance = true;
87
+
let waitingForPong = true;
88
+
89
+
// One instance to rule them all
90
+
//
91
+
// Ping other instances to see if there are any.
92
+
// As long as there aren't any, it is considered the main instance.
93
+
//
94
+
// Actions are performed on the main instance,
95
+
// and data is replicated from main to the other instances.
96
+
const channel = new BroadcastChannel(id);
97
+
98
+
channel.addEventListener("message", async (event) => {
99
+
if (event.data === "PING") {
100
+
channel.postMessage("PONG");
101
+
} else if (event.data?.type === "data") {
102
+
scope.data = context.codec.decode(event.data.data);
103
+
} else if (waitingForPong && event.data === "PONG") {
104
+
waitingForPong = false;
105
+
isMainInstance = false;
106
+
} else if (isMainInstance && event.data?.type === "action" && event.data?.actionId) {
107
+
const result = await scope.actionHandlers[event.data.actionId]?.(...event.data.arguments);
108
+
channel.postMessage({
109
+
type: "actioncomplete",
110
+
id: event.data.id,
111
+
result,
112
+
});
113
+
}
114
+
});
115
+
116
+
setTimeout(() => (waitingForPong = false), 1000);
117
+
118
+
channel.postMessage("PING");
119
+
120
+
scope.ondata = (event) => {
121
+
if (isMainInstance) {
122
+
channel.postMessage({
123
+
type: "data",
124
+
data: context.codec.encode(event.data),
125
+
});
126
+
}
127
+
};
128
+
129
+
const context = {
130
+
scope,
131
+
132
+
get id() {
133
+
return id;
134
+
},
135
+
136
+
get data() {
137
+
return scope.data;
138
+
},
139
+
140
+
set data(data: DataType) {
141
+
scope.data = data;
142
+
},
143
+
144
+
codec: {
145
+
decode: (data: any) => data as DataType,
146
+
encode: (data: DataType) => data as any,
147
+
},
148
+
149
+
isMainInstance() {
150
+
return isMainInstance;
151
+
},
152
+
153
+
setActionHandler: <H extends Function>(actionId: string, actionHandler: H) => {
154
+
const handler = (...args: any) => {
155
+
if (isMainInstance) {
156
+
return actionHandler(...args);
157
+
}
158
+
159
+
const actionMessage = {
160
+
id: crypto.randomUUID(),
161
+
type: "action",
162
+
actionId,
163
+
arguments: args,
164
+
};
165
+
166
+
return new Promise((resolve) => {
167
+
const actionCallback = (event: MessageEvent) => {
168
+
if (event.data?.type === "actioncomplete" && event.data?.id === actionMessage.id) {
169
+
channel.removeEventListener("message", actionCallback);
170
+
resolve(event.data.result);
171
+
}
172
+
};
173
+
174
+
channel.addEventListener("message", actionCallback);
175
+
channel.postMessage(actionMessage);
176
+
});
177
+
};
178
+
179
+
scope.setActionHandler(actionId, handler);
180
+
},
181
+
};
182
+
183
+
return context;
184
+
}
185
+
186
+
////////////////////////////////////////////
187
+
// 🔮 Reactive state management
188
+
////////////////////////////////////////////
189
+
export function reactive<D, T>(
190
+
applet: Applet<D>,
191
+
dataFn: (data: D) => T,
192
+
effectFn: (t: T) => void,
193
+
) {
194
+
const [getter, setter] = signal(dataFn(applet.data));
195
+
196
+
effect(() => {
197
+
effectFn(getter());
198
+
return undefined;
199
+
});
200
+
201
+
applet.addEventListener("data", (event: AppletEvent) => {
202
+
setter(dataFn(event.data));
203
+
});
204
+
}
205
+
206
+
////////////////////////////////////////////
207
+
// 🛠️
208
+
////////////////////////////////////////////
209
+
export function addScope<O extends object>(astroScope: string, object: O): O {
210
+
return {
211
+
...object,
212
+
attrs: {
213
+
...((object as any).attrs || {}),
214
+
[`data-astro-cid-${astroScope}`]: "",
215
+
},
216
+
};
217
+
}
218
+
219
+
export function appletScopePort() {
220
+
let port: MessagePort | undefined;
221
+
222
+
function connection(event: AppletEvent) {
223
+
if (event.data?.type === "appletconnect") {
224
+
window.removeEventListener("message", connection);
225
+
port = (event as any).ports[0];
226
+
}
227
+
}
228
+
229
+
window.addEventListener("message", connection);
230
+
231
+
return () => port;
232
+
}
233
+
234
+
export function comparable(value: unknown) {
235
+
return xxh32(JSON.stringify(value));
236
+
}
237
+
238
+
export function hs(
239
+
tag: string,
240
+
astroScope: string,
241
+
props?: Record<string, unknown> | Signal<Record<string, unknown>>,
242
+
configure?: ElementConfigurator,
243
+
) {
244
+
const propsWithScope =
245
+
props && isSignal(props)
246
+
? () => addScope(astroScope, props())
247
+
: addScope(astroScope, props || {});
248
+
249
+
return h(tag, propsWithScope, configure);
250
+
}
251
+
252
+
export function isPrimitive(test: unknown) {
253
+
return test !== Object(test);
254
+
}
255
+
256
+
export function waitUntilAppletData<A>(
257
+
applet: Applet<A>,
258
+
dataFn: (a: A | undefined) => boolean,
259
+
): Promise<void> {
260
+
return new Promise((resolve) => {
261
+
if (dataFn(applet.data) === true) {
262
+
resolve();
263
+
return;
264
+
}
265
+
266
+
const callback = (event: AppletEvent) => {
267
+
if (dataFn(event.data) === true) {
268
+
applet.removeEventListener("data", callback);
269
+
resolve();
270
+
}
271
+
};
272
+
273
+
applet.addEventListener("data", callback);
274
+
});
275
+
}
276
+
277
+
export function waitUntilAppletIsReady(applet: Applet): Promise<void> {
278
+
return waitUntilAppletData(applet, (data) => !!data?.ready);
279
+
}
-151
src/scripts/theme.ts
-151
src/scripts/theme.ts
···
1
-
import type { Applet, AppletEvent } from "@web-applets/sdk";
2
-
3
-
import { applets } from "@web-applets/sdk";
4
-
import { type ElementConfigurator, h } from "spellcaster/hyperscript.js";
5
-
import { effect, isSignal, Signal, signal } from "spellcaster/spellcaster.js";
6
-
import { xxh32 } from "xxh32";
7
-
8
-
////////////////////////////////////////////
9
-
// 🪟 Applet initialiser
10
-
////////////////////////////////////////////
11
-
export async function applet<D>(
12
-
src: string,
13
-
opts: {
14
-
addSlashSuffix?: boolean;
15
-
context?: Window;
16
-
container?: HTMLElement | Element;
17
-
id?: string;
18
-
setHeight?: boolean;
19
-
} = {},
20
-
): Promise<Applet<D>> {
21
-
src = `${src}${
22
-
src.endsWith("/")
23
-
? ""
24
-
: opts.addSlashSuffix === undefined || opts.addSlashSuffix === true
25
-
? "/"
26
-
: ""
27
-
}`;
28
-
29
-
const existingFrame: HTMLIFrameElement | null = (opts.context || window).document.querySelector(
30
-
`[src="${src}"]`,
31
-
);
32
-
33
-
let frame;
34
-
35
-
if (existingFrame) {
36
-
frame = existingFrame;
37
-
} else {
38
-
frame = document.createElement("iframe");
39
-
frame.src = src;
40
-
if (opts.id) frame.id = opts.id;
41
-
42
-
if (opts.container) {
43
-
opts.container.appendChild(frame);
44
-
} else {
45
-
(opts.context || window).document.body.appendChild(frame);
46
-
}
47
-
}
48
-
49
-
if (frame.contentWindow === null) {
50
-
throw new Error("iframe does not have a contentWindow");
51
-
}
52
-
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
-
});
61
-
62
-
if (opts.setHeight) {
63
-
applet.onresize = () => {
64
-
frame.height = `${applet.height}px`;
65
-
frame.classList.add("has-loaded");
66
-
};
67
-
} else {
68
-
if (frame.contentDocument?.readyState === "complete") {
69
-
frame.classList.add("has-loaded");
70
-
}
71
-
72
-
frame.addEventListener("load", () => {
73
-
frame.classList.add("has-loaded");
74
-
});
75
-
}
76
-
77
-
return applet;
78
-
}
79
-
80
-
////////////////////////////////////////////
81
-
// 🔮 Reactive state management
82
-
////////////////////////////////////////////
83
-
export function reactive<D, T>(
84
-
applet: Applet<D>,
85
-
dataFn: (data: D) => T,
86
-
effectFn: (t: T) => void,
87
-
) {
88
-
const [getter, setter] = signal(dataFn(applet.data));
89
-
90
-
effect(() => {
91
-
effectFn(getter());
92
-
return undefined;
93
-
});
94
-
95
-
applet.addEventListener("data", (event: AppletEvent) => {
96
-
setter(dataFn(event.data));
97
-
});
98
-
}
99
-
100
-
////////////////////////////////////////////
101
-
// 🛠️
102
-
////////////////////////////////////////////
103
-
export function addScope<O extends object>(astroScope: string, object: O): O {
104
-
return {
105
-
...object,
106
-
attrs: {
107
-
...((object as any).attrs || {}),
108
-
[`data-astro-cid-${astroScope}`]: "",
109
-
},
110
-
};
111
-
}
112
-
113
-
export function comparable(value: unknown) {
114
-
return xxh32(JSON.stringify(value));
115
-
}
116
-
117
-
export function hs(
118
-
tag: string,
119
-
astroScope: string,
120
-
props?: Record<string, unknown> | Signal<Record<string, unknown>>,
121
-
configure?: ElementConfigurator,
122
-
) {
123
-
const propsWithScope =
124
-
props && isSignal(props)
125
-
? () => addScope(astroScope, props())
126
-
: addScope(astroScope, props || {});
127
-
128
-
return h(tag, propsWithScope, configure);
129
-
}
130
-
131
-
export function isPrimitive(test: unknown) {
132
-
return test !== Object(test);
133
-
}
134
-
135
-
export function waitUntilAppletIsReady(applet: Applet): Promise<void> {
136
-
return new Promise((resolve) => {
137
-
if (applet.data?.ready === true) {
138
-
resolve();
139
-
return;
140
-
}
141
-
142
-
const callback = (event: AppletEvent) => {
143
-
if (event.data?.ready === true) {
144
-
applet.removeEventListener("data", callback);
145
-
resolve();
146
-
}
147
-
};
148
-
149
-
applet.addEventListener("data", callback);
150
-
});
151
-
}
+6
-11
src/scripts/themes/pilot/index.ts
+6
-11
src/scripts/themes/pilot/index.ts
···
1
-
import type { Output, Track } from "@applets/core/types.d.ts";
2
-
import { applet, reactive } from "../../theme.ts";
1
+
import { applet, reactive } from "@scripts/applets/common";
3
2
4
3
////////////////////////////////////////////
5
4
// 🎨 Styles
···
14
13
15
14
import type * as AudioUI from "@applets/themes/pilot/ui/audio/types.d.ts";
16
15
17
-
const _configurator = {
18
-
output: await applet("../../configurator/output"),
19
-
};
16
+
// TODO: Themes
20
17
21
18
const engine = {
22
19
audio: await applet<AudioEngine.State>("../../engine/audio"),
23
20
queue: await applet<QueueEngine.State>("../../engine/queue"),
24
21
};
25
22
26
-
const input = {
27
-
nativeFs: await applet("../../input/native-fs"),
28
-
};
29
-
30
23
const _orchestrator = {
31
-
input: await applet<Output>("../../orchestrator/input-cache"),
32
-
output: await applet<Output>("../../orchestrator/output-management"),
24
+
input: await applet("../../orchestrator/input-cache", {
25
+
applets: { input: "todo" },
26
+
}),
27
+
output: await applet("../../orchestrator/output-management"),
33
28
queue: await applet("../../orchestrator/single-queue"),
34
29
};
35
30
+8
-7
src/scripts/themes/webamp/index.ts
+8
-7
src/scripts/themes/webamp/index.ts
···
1
1
import Webamp from "webamp";
2
2
import { URLTrack } from "webamp";
3
3
4
-
import type { Output, Track } from "@applets/core/types.d.ts";
5
-
import { applet, waitUntilAppletIsReady } from "../../theme.ts";
4
+
import type { ResolvedUri, Track } from "@applets/core/types.d.ts";
5
+
import { applet } from "@scripts/applets/common";
6
6
7
7
////////////////////////////////////////////
8
8
// 🎨 Styles
···
12
12
////////////////////////////////////////////
13
13
// 🗂️ Applets
14
14
////////////////////////////////////////////
15
+
import type * as OutputOrchestrator from "@applets/orchestrator/output-management/types.d.ts";
15
16
16
17
const configurator = {
17
18
input: await applet("../../configurator/input"),
18
19
};
19
20
20
21
const orchestrator = {
21
-
output: await applet<Output>("../../orchestrator/output-management"),
22
+
output: await applet<OutputOrchestrator.State>("../../orchestrator/output-management"),
22
23
23
24
// TODO: Should this be explicitely be ran after the output orchestrator is loaded?
24
25
input: await applet("../../orchestrator/input-cache"),
···
47
48
// 🛠️
48
49
////////////////////////////////////////////
49
50
async function loadTracks(): Promise<URLTrack[]> {
50
-
return await orchestrator.output.data.tracks.reduce(
51
+
return await orchestrator.output.data.tracks.collection.reduce(
51
52
async (promise: Promise<URLTrack[]>, track: Track) => {
52
53
const acc = await promise;
53
54
54
55
// TODO: Ideally the URL should only be resolved when needed,
55
56
// but webamp doesn't allow for that.
56
57
// Maybe you could work around it with a service worker.
57
-
const url = await configurator.input.sendAction<string | undefined>(
58
+
const resGet = await configurator.input.sendAction<ResolvedUri>(
58
59
"resolve",
59
60
{ method: "GET", uri: track.uri },
60
61
{
···
62
63
},
63
64
);
64
65
65
-
if (!url) return acc;
66
+
if (!resGet) return acc;
66
67
67
68
const urlTrack: URLTrack = {
68
-
url,
69
+
url: resGet.url,
69
70
metaData: {
70
71
title: track.tags?.title || "",
71
72
artist: track.tags?.artist || "",