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