Experiment to rebuild Diffuse using web applets.
0

Configure Feed

Select the types of activity you want to include in your feed.

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}