A music player that connects to your cloud/distributed storage.
0
fork

Configure Feed

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

at v4 261 lines 5.9 kB view raw
1import * as TID from "@atcute/tid"; 2import { ostiary, rpc } from "~/common/worker.js"; 3import { 4 detach as detachUtil, 5 groupKey, 6 isAudioFile, 7} from "~/components/input/common.js"; 8import { 9 bucketId, 10 buildURI, 11 consultBucketCached, 12 createClient, 13 groupTracksByBucket, 14 groupUrisByBucket, 15 parseURI, 16} from "./common.js"; 17import { SCHEME } from "./constants.js"; 18 19/** 20 * @import { InputActions as Actions, ConsultGrouping } from "~/components/input/types.d.ts"; 21 * @import { Track } from "~/definitions/types.d.ts" 22 * @import { Bucket, Demo } from "./types.d.ts" 23 */ 24 25//////////////////////////////////////////// 26// ACTIONS 27//////////////////////////////////////////// 28 29/** 30 * @type {Actions['artwork']} 31 */ 32export async function artwork(_uri) { 33 return null; 34} 35 36/** 37 * @type {Actions['consult']} 38 */ 39export async function consult(fileUriOrScheme) { 40 if (!fileUriOrScheme.includes(":")) { 41 return { supported: true, consult: "undetermined" }; 42 } 43 44 const parsed = parseURI(fileUriOrScheme); 45 if (!parsed) return { supported: true, consult: "undetermined" }; 46 47 const consult = await consultBucketCached(parsed.bucket); 48 return { supported: true, consult }; 49} 50 51/** 52 * @type {Actions['detach']} 53 */ 54export async function detach(args) { 55 return detachUtil({ 56 ...args, 57 58 inputScheme: SCHEME, 59 handleFileUri: ({ fileURI, tracks }) => { 60 const result = parseURI(fileURI); 61 if (!result) return tracks; 62 63 const bid = bucketId(result.bucket); 64 const groups = groupTracksByBucket(tracks); 65 66 delete groups[bid]; 67 68 return Object.values(groups).map((a) => a.tracks).flat(1); 69 }, 70 }); 71} 72 73/** 74 * @type {Actions['groupConsult']} 75 */ 76export async function groupConsult(uris) { 77 const groups = groupUrisByBucket(uris); 78 79 const promises = Object.entries(groups).map( 80 async ([bucketId, { bucket, uris }]) => { 81 const available = await consultBucketCached(bucket); 82 83 /** @type {ConsultGrouping} */ 84 const grouping = available 85 ? { available, scheme: SCHEME, uris } 86 : { available, reason: "Bucket unavailable", scheme: SCHEME, uris }; 87 88 return { 89 key: groupKey(SCHEME, bucketId), 90 grouping, 91 }; 92 }, 93 ); 94 95 const entries = (await Promise.all(promises)).map(( 96 entry, 97 ) => [entry.key, entry.grouping]); 98 99 return Object.fromEntries(entries); 100} 101 102/** 103 * @type {Actions['list']} 104 */ 105export async function list(cachedTracks = []) { 106 /** @type {Record<string, Record<string, Track>>} */ 107 const cache = {}; 108 109 /** @type {Record<string, Bucket>} */ 110 const buckets = {}; 111 112 cachedTracks.forEach((t) => { 113 const parsed = parseURI(t.uri); 114 if (!parsed) return; 115 116 const bid = bucketId(parsed.bucket); 117 buckets[bid] = parsed.bucket; 118 119 if (cache[bid]) { 120 cache[bid][parsed.path] = t; 121 } else { 122 cache[bid] = { [parsed.path]: t }; 123 } 124 }); 125 126 const promises = Object.values(buckets).map(async (bucket) => { 127 const client = createClient(bucket); 128 const bid = bucketId(bucket); 129 130 const list = await Array.fromAsync( 131 client.listObjects({ 132 prefix: bucket.path.replace(/^\//, ""), 133 }), 134 ); 135 136 let tracks = list 137 .filter((l) => isAudioFile(l.key)) 138 .map((l) => { 139 const cachedTrack = cache[bid]?.[l.key]; 140 141 const id = cachedTrack?.id || TID.now(); 142 const stats = cachedTrack?.stats; 143 const tags = cachedTrack?.tags; 144 const now = new Date().toISOString(); 145 146 /** @type {Track} */ 147 const track = { 148 $type: "sh.diffuse.output.track", 149 id, 150 createdAt: cachedTrack?.createdAt ?? now, 151 updatedAt: cachedTrack?.updatedAt ?? now, 152 stats, 153 tags, 154 uri: buildURI(bucket, l.key), 155 }; 156 157 return track; 158 }); 159 160 // If a bucket didn't have any tracks, 161 // keep a placeholder track so the bucket gets 162 // picked up as a source. 163 if (!tracks.length) { 164 const now = new Date().toISOString(); 165 166 tracks = [{ 167 $type: "sh.diffuse.output.track", 168 id: TID.now(), 169 createdAt: now, 170 updatedAt: now, 171 kind: "placeholder", 172 uri: buildURI(bucket), 173 }]; 174 } 175 176 return tracks; 177 }); 178 179 const tracks = (await Promise.all(promises)).flat(1); 180 return tracks; 181} 182 183/** 184 * @type {Actions['resolve']} 185 */ 186export async function resolve( 187 { method, uri }, 188) { 189 const parsed = parseURI(uri); 190 if (!parsed) return undefined; 191 192 const expiresInSeconds = 60 * 60 * 24 * 7; // 7 days 193 const expiresAtSeconds = Math.round(Date.now() / 1000) + expiresInSeconds; 194 195 const client = createClient(parsed.bucket); 196 const url = await client.getPresignedUrl( 197 /** @type {any} */ (method?.toUpperCase() ?? "GET"), 198 parsed.path, 199 ); 200 201 return { expiresAt: expiresAtSeconds, url }; 202} 203 204//////////////////////////////////////////// 205// ADDITIONAL ACTIONS 206//////////////////////////////////////////// 207 208/** 209 * @returns {Demo} 210 */ 211export function demo() { 212 // Credentials are read-only, no worries. 213 214 /** @type {Bucket} */ 215 const bucket = { 216 accessKey: atob("QUtJQTZPUTNFVk1BWFZDRFFINkI="), 217 bucketName: "ongaku-ryoho-demo", 218 host: "s3.amazonaws.com", 219 path: "/", 220 region: "us-east-1", 221 secretKey: atob("Z0hPQkdHRzU1aXc0a0RDbjdjWlRJYTVTUDRZWnpERkRzQnFCYWI4Mg=="), 222 }; 223 224 const uri = buildURI(bucket); 225 const now = new Date().toISOString(); 226 227 /** @type {Track} */ 228 const track = { 229 $type: "sh.diffuse.output.track", 230 id: TID.now(), 231 createdAt: now, 232 updatedAt: now, 233 kind: "placeholder", 234 uri, 235 }; 236 237 return { 238 bucket, 239 track, 240 }; 241} 242 243//////////////////////////////////////////// 244// ⚡️ 245//////////////////////////////////////////// 246 247ostiary((context) => { 248 // Setup RPC 249 250 rpc(context, { 251 artwork, 252 consult, 253 detach, 254 groupConsult, 255 list, 256 resolve, 257 258 // Additional actions 259 demo, 260 }); 261});