A music player that connects to your cloud/distributed storage.
at v4 136 lines 3.1 kB view raw
1import { ostiary, rpc } from "~/common/worker.js"; 2import { detach as detachUtil, groupKey } from "~/components/input/common.js"; 3 4import { 5 consultHostCached, 6 groupTracksByHost, 7 groupUrisByHost, 8 parseURI, 9} from "./common.js"; 10import { SCHEME } from "./constants.js"; 11 12/** 13 * @import { InputActions as Actions, ConsultGrouping } from "~/components/input/types.d.ts"; 14 */ 15 16//////////////////////////////////////////// 17// ACTIONS 18//////////////////////////////////////////// 19 20/** 21 * @type {Actions['consult']} 22 */ 23export async function consult(fileUriOrScheme) { 24 if (!fileUriOrScheme.includes(":")) { 25 return { supported: true, consult: "undetermined" }; 26 } 27 28 const parsed = parseURI(fileUriOrScheme); 29 if (!parsed) { 30 return { supported: false, reason: "Invalid HTTPS URL" }; 31 } 32 33 const consult = await consultHostCached(parsed.url); 34 return { supported: true, consult }; 35} 36 37/** 38 * @type {Actions['detach']} 39 */ 40export async function detach(args) { 41 return detachUtil({ 42 ...args, 43 44 inputScheme: SCHEME, 45 handleFileUri: ({ fileURI, tracks }) => { 46 const result = parseURI(fileURI); 47 if (!result) return tracks; 48 49 const did = result.host; 50 const groups = groupTracksByHost(tracks); 51 52 delete groups[did]; 53 54 return Object.values(groups).map((a) => a.tracks).flat(1); 55 }, 56 }); 57} 58 59/** 60 * @type {Actions['groupConsult']} 61 */ 62export async function groupConsult(uris) { 63 const groups = groupUrisByHost(uris); 64 65 const promises = Object.entries(groups).map( 66 async ([_domainId, { host, uris }]) => { 67 const testUri = uris[0]; 68 const available = testUri ? await consultHostCached(testUri) : false; 69 70 /** @type {ConsultGrouping} */ 71 const grouping = available 72 ? { available, scheme: SCHEME, uris } 73 : { available, reason: "Host unreachable", scheme: SCHEME, uris }; 74 75 return { 76 key: groupKey(SCHEME, host), 77 grouping, 78 }; 79 }, 80 ); 81 82 const entries = (await Promise.all(promises)).map(( 83 entry, 84 ) => [entry.key, entry.grouping]); 85 86 return Object.fromEntries(entries); 87} 88 89/** 90 * @type {Actions['list']} 91 */ 92export async function list(cachedTracks = []) { 93 return cachedTracks.map((track) => { 94 const t = { ...track }; 95 96 if (t.kind === "placeholder") { 97 t.kind = undefined; 98 } 99 100 return t; 101 }); 102} 103 104/** 105 * @type {Actions['resolve']} 106 */ 107export async function resolve({ method, uri }) { 108 const parsed = parseURI(uri); 109 if (!parsed) return undefined; 110 111 // HTTPS URLs don't need resolution - they're already accessible. 112 // Just return the URL as-is with a far-future expiration. 113 const expiresInSeconds = 60 * 60 * 24 * 365; // 1 year 114 const expiresAtSeconds = Math.round(Date.now() / 1000) + expiresInSeconds; 115 116 return { 117 url: parsed.url, 118 expiresAt: expiresAtSeconds, 119 }; 120} 121 122//////////////////////////////////////////// 123// ⚡️ 124//////////////////////////////////////////// 125 126ostiary((context) => { 127 // Setup RPC 128 129 rpc(context, { 130 consult, 131 detach, 132 groupConsult, 133 list, 134 resolve, 135 }); 136});