A music player that connects to your cloud/distributed storage. diffuse.sh
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294import * as TID from "@atcute/tid";import { ostiary, rpc } from "~/common/worker.js";import { bytesFromUrl, detach as detachUtil, groupKey, isAudioFile, isImageFile, pickCoverArt,} from "~/components/input/common.js";import { bucketId, buildURI, consultBucketCached, createClient, groupTracksByBucket, groupUrisByBucket, parseURI,} from "./common.js";import { SCHEME } from "./constants.js";
/** * @import { InputActions as Actions, ConsultGrouping } from "@specs/components/input/types.d.ts"; * @import { Track } from "~/definitions/types.d.ts" * @import { Bucket, Demo } from "@specs/components/input/s3/types.d.ts" */
////////////////////////////////////////////// ACTIONS////////////////////////////////////////////
/** * @type {Actions['artwork']} */export async function artwork(uri) { try { const parsed = parseURI(uri); if (!parsed) return null;
// The audio object key is parsed.path. Its parent directory is // everything before the last `/`, with a trailing `/`, or empty // string if the audio sits at the bucket root. const slash = parsed.path.lastIndexOf("/"); const directoryPrefix = slash === -1 ? "" : parsed.path.slice(0, slash + 1);
const client = createClient(parsed.bucket); const list = await Array.fromAsync( client.listObjects({ prefix: directoryPrefix }), );
// Only images directly in this directory, not nested subdirectories. const images = list.filter((object) => { if (!isImageFile(object.key)) return false; const remainder = object.key.slice(directoryPrefix.length); return remainder !== "" && !remainder.includes("/"); });
const image = pickCoverArt(images, (object) => object.key); if (!image) return null;
const url = await client.getPresignedUrl("GET", image.key); return await bytesFromUrl(url); } catch { return null; }}
/** * @type {Actions['consult']} */export async function consult(fileUriOrScheme) { if (!fileUriOrScheme.includes(":")) { return { supported: true, consult: "undetermined" }; }
const parsed = parseURI(fileUriOrScheme); if (!parsed) return { supported: true, consult: "undetermined" };
const consult = await consultBucketCached(parsed.bucket); return { supported: true, consult };}
/** * @type {Actions['detach']} */export async function detach(args) { return detachUtil({ ...args,
inputScheme: SCHEME, handleFileUri: ({ fileURI, tracks }) => { const result = parseURI(fileURI); if (!result) return tracks;
const bid = bucketId(result.bucket); const groups = groupTracksByBucket(tracks);
delete groups[bid];
return Object.values(groups).map((a) => a.tracks).flat(1); }, });}
/** * @type {Actions['groupConsult']} */export async function groupConsult(uris) { const groups = groupUrisByBucket(uris);
const promises = Object.entries(groups).map( async ([bucketId, { bucket, uris }]) => { const available = await consultBucketCached(bucket);
/** @type {ConsultGrouping} */ const grouping = available === "yes" ? { available, scheme: SCHEME, uris } : { available, reason: "Bucket unavailable", scheme: SCHEME, uris };
return { key: groupKey(SCHEME, bucketId), grouping, }; }, );
const entries = (await Promise.all(promises)).map(( entry, ) => [entry.key, entry.grouping]);
return Object.fromEntries(entries);}
/** * @type {Actions['list']} */export async function list(cachedTracks = []) { /** @type {Record<string, Record<string, Track>>} */ const cache = {};
/** @type {Record<string, Bucket>} */ const buckets = {};
cachedTracks.forEach((t) => { const parsed = parseURI(t.uri); if (!parsed) return;
const bid = bucketId(parsed.bucket); buckets[bid] = parsed.bucket;
if (cache[bid]) { cache[bid][parsed.path] = t; } else { cache[bid] = { [parsed.path]: t }; } });
const promises = Object.values(buckets).map(async (bucket) => { const client = createClient(bucket); const bid = bucketId(bucket);
const list = await Array.fromAsync( client.listObjects({ prefix: bucket.path.replace(/^\//, ""), }), );
let tracks = list .filter((l) => isAudioFile(l.key)) .map((l) => { const cachedTrack = cache[bid]?.[l.key];
const id = cachedTrack?.id || TID.now(); const stats = cachedTrack?.stats; const tags = cachedTrack?.tags; const now = new Date().toISOString();
/** @type {Track} */ const track = { $type: "sh.diffuse.output.track", id, createdAt: cachedTrack?.createdAt ?? now, updatedAt: cachedTrack?.updatedAt ?? now, stats, tags, uri: buildURI(bucket, l.key), };
return track; });
// If a bucket didn't have any tracks, // keep a placeholder track so the bucket gets // picked up as a source. if (!tracks.length) { const now = new Date().toISOString();
tracks = [{ $type: "sh.diffuse.output.track", id: TID.now(), createdAt: now, updatedAt: now, kind: "placeholder", uri: buildURI(bucket), }]; }
return tracks; });
const tracks = (await Promise.all(promises)).flat(1); return tracks;}
/** * @type {Actions['resolve']} */export async function resolve( { method, uri },) { const parsed = parseURI(uri); if (!parsed) return undefined;
const expiresInSeconds = 60 * 60 * 24 * 7; // 7 days const expiresAtSeconds = Math.round(Date.now() / 1000) + expiresInSeconds;
const client = createClient(parsed.bucket); const url = await client.getPresignedUrl( /** @type {any} */ (method?.toUpperCase() ?? "GET"), parsed.path, );
return { expiresAt: expiresAtSeconds, url };}
////////////////////////////////////////////// ADDITIONAL ACTIONS////////////////////////////////////////////
/** * @returns {Demo} */export function demo() { // Credentials are read-only, no worries.
/** @type {Bucket} */ const bucket = { accessKey: atob("QUtJQTZPUTNFVk1BWFZDRFFINkI="), bucketName: "ongaku-ryoho-demo", host: "s3.amazonaws.com", path: "/", region: "us-east-1", secretKey: atob("Z0hPQkdHRzU1aXc0a0RDbjdjWlRJYTVTUDRZWnpERkRzQnFCYWI4Mg=="), };
const uri = buildURI(bucket); const now = new Date().toISOString();
/** @type {Track} */ const track = { $type: "sh.diffuse.output.track", id: TID.now(), createdAt: now, updatedAt: now, kind: "placeholder", uri, };
return { bucket, track, };}
////////////////////////////////////////////// ⚡️////////////////////////////////////////////
ostiary((context) => { // Setup RPC
rpc(context, { artwork, consult, detach, groupConsult, list, resolve,
// Additional actions demo, });});