Experiment to rebuild Diffuse using web applets.
at main 131 lines 3.5 kB view raw
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}