Experiment to rebuild Diffuse using web applets.
1import * as URI from "uri-js";
2
3import type { Consult, ConsultGrouping, GroupConsult, Track } from "@applets/core/types.d.ts";
4import { isAudioFile } from "@scripts/input/common";
5import {
6 bucketId,
7 bucketsFromTracks,
8 buildURI,
9 consultBucket,
10 createClient,
11 groupTracksByBucket,
12 loadBuckets,
13 parseURI,
14} from "./common";
15import { expose } from "@scripts/common";
16import { SCHEME } from "./constants";
17
18////////////////////////////////////////////
19// ACTIONS
20////////////////////////////////////////////
21const actions = expose({
22 consult,
23 contextualize,
24 groupConsult,
25 list,
26 resolve,
27});
28
29export type Actions = typeof actions;
30
31// Actions
32
33async function consult(fileUriOrScheme: string): Promise<Consult> {
34 if (!fileUriOrScheme.includes(":")) return { supported: true, consult: "undetermined" };
35
36 const parsed = parseURI(fileUriOrScheme);
37 if (!parsed) return { supported: true, consult: "undetermined" };
38
39 const consult = await consultBucket(parsed.bucket);
40 return { supported: true, consult };
41}
42
43async function contextualize(tracks: Track[]) {
44 return bucketsFromTracks(tracks);
45}
46
47async function groupConsult(tracks: Track[]): Promise<GroupConsult> {
48 const groups = groupTracksByBucket(tracks);
49
50 const promises = Object.entries(groups).map(async ([bucketId, { bucket, tracks }]) => {
51 const available = await consultBucket(bucket);
52 const grouping: ConsultGrouping = available
53 ? { available, tracks }
54 : { available, reason: "Bucket unavailable" };
55
56 return {
57 key: `${SCHEME}:${bucketId}`,
58 grouping,
59 };
60 });
61
62 const entries = (await Promise.all(promises)).map((entry) => [entry.key, entry.grouping]);
63 return Object.fromEntries(entries);
64}
65
66async function list(cachedTracks: Track[] = []) {
67 const cache = cachedTracks.reduce((acc: Record<string, Record<string, Track>>, t: Track) => {
68 const parsed = parseURI(t.uri);
69 if (!parsed) return acc;
70
71 const bid = bucketId(parsed?.bucket);
72 const trk = { [parsed.path]: t };
73
74 return { ...acc, [bid]: acc[bid] ? { ...acc[bid], ...trk } : trk };
75 }, {});
76
77 const buckets = await loadBuckets();
78 const promises = Object.values(buckets).map(async (bucket) => {
79 const client = createClient(bucket);
80 const bid = bucketId(bucket);
81
82 const list = await Array.fromAsync(
83 client.listObjects({
84 prefix: bucket.path.replace(/^\//, ""),
85 }),
86 );
87
88 return list
89 .filter((l) => isAudioFile(l.key))
90 .map((l) => {
91 const cachedTrack = cache[bid][l.key];
92
93 const id = cachedTrack?.id || crypto.randomUUID();
94 const stats = cachedTrack?.stats;
95 const tags = cachedTrack?.tags;
96
97 const track: Track = {
98 id,
99 stats,
100 tags,
101 uri: buildURI(bucket, l.key),
102 };
103
104 return track;
105 });
106 });
107
108 return (await Promise.all(promises)).flat(1);
109}
110
111async function resolve({ method, uri }: { method: string; uri: string }) {
112 const parsed = parseURI(uri);
113 if (!parsed) return undefined;
114
115 const expiresInSeconds = 60 * 60 * 24 * 7; // 7 days
116 const expiresAtSeconds = Math.round(Date.now() / 1000) + expiresInSeconds;
117
118 const client = createClient(parsed.bucket);
119 const url = await client.getPresignedUrl(method.toUpperCase() as any, parsed.path);
120
121 return { expiresAt: expiresAtSeconds, url };
122}