A music player that connects to your cloud/distributed storage.
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});