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 { expose, 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, 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" };
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 = cachedTracks.reduce((acc: Record<string, Record<string, Track>>, t: Track) => {
71 const parsed = parseURI(t.uri);
72 if (!parsed) return acc;
73
74 const bid = bucketId(parsed?.bucket);
75 const trk = { [parsed.path]: t };
76
77 return { ...acc, [bid]: acc[bid] ? { ...acc[bid], ...trk } : trk };
78 }, {});
79
80 const buckets = await loadBuckets();
81 const promises = Object.values(buckets).map(async (bucket) => {
82 const client = createClient(bucket);
83 const bid = bucketId(bucket);
84
85 const list = await Array.fromAsync(
86 client.listObjects({
87 prefix: bucket.path.replace(/^\//, ""),
88 }),
89 );
90
91 return list
92 .filter((l) => isAudioFile(l.key))
93 .map((l) => {
94 const cachedTrack = cache[bid][l.key];
95
96 const id = cachedTrack?.id || crypto.randomUUID();
97 const stats = cachedTrack?.stats;
98 const tags = cachedTrack?.tags;
99
100 const track: Track = {
101 id,
102 stats,
103 tags,
104 uri: buildURI(bucket, l.key),
105 };
106
107 return track;
108 });
109 });
110
111 const tracks = (await Promise.all(promises)).flat(1);
112 return transfer(tracks);
113}
114
115async function resolve({ method, uri }: { method: string; uri: string }) {
116 const parsed = parseURI(uri);
117 if (!parsed) return undefined;
118
119 const expiresInSeconds = 60 * 60 * 24 * 7; // 7 days
120 const expiresAtSeconds = Math.round(Date.now() / 1000) + expiresInSeconds;
121
122 const client = createClient(parsed.bucket);
123 const url = await client.getPresignedUrl(method.toUpperCase() as any, parsed.path);
124
125 return { expiresAt: expiresAtSeconds, url };
126}