forked from
tokono.ma/diffuse
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['artwork']}
22 */
23export async function artwork(_uri) {
24 return null;
25}
26
27/**
28 * @type {Actions['consult']}
29 */
30export async function consult(fileUriOrScheme) {
31 if (!fileUriOrScheme.includes(":")) {
32 return { supported: true, consult: "undetermined" };
33 }
34
35 const parsed = parseURI(fileUriOrScheme);
36 if (!parsed) {
37 return { supported: false, reason: "Invalid HTTPS URL" };
38 }
39
40 const consult = await consultHostCached(parsed.url);
41 return { supported: true, consult };
42}
43
44/**
45 * @type {Actions['detach']}
46 */
47export async function detach(args) {
48 return detachUtil({
49 ...args,
50
51 inputScheme: SCHEME,
52 handleFileUri: ({ fileURI, tracks }) => {
53 const result = parseURI(fileURI);
54 if (!result) return tracks;
55
56 const did = result.host;
57 const groups = groupTracksByHost(tracks);
58
59 delete groups[did];
60
61 return Object.values(groups).map((a) => a.tracks).flat(1);
62 },
63 });
64}
65
66/**
67 * @type {Actions['groupConsult']}
68 */
69export async function groupConsult(uris) {
70 const groups = groupUrisByHost(uris);
71
72 const promises = Object.entries(groups).map(
73 async ([_domainId, { host, uris }]) => {
74 const testUri = uris[0];
75 const available = testUri ? await consultHostCached(testUri) : false;
76
77 /** @type {ConsultGrouping} */
78 const grouping = available
79 ? { available, scheme: SCHEME, uris }
80 : { available, reason: "Host unreachable", scheme: SCHEME, uris };
81
82 return {
83 key: groupKey(SCHEME, host),
84 grouping,
85 };
86 },
87 );
88
89 const entries = (await Promise.all(promises)).map((
90 entry,
91 ) => [entry.key, entry.grouping]);
92
93 return Object.fromEntries(entries);
94}
95
96/**
97 * @type {Actions['list']}
98 */
99export async function list(cachedTracks = []) {
100 return cachedTracks.map((track) => {
101 const t = { ...track };
102
103 if (t.kind === "placeholder") {
104 t.kind = undefined;
105 }
106
107 return t;
108 });
109}
110
111/**
112 * @type {Actions['resolve']}
113 */
114export async function resolve({ method, uri }) {
115 if (uri.startsWith("blob:")) {
116 const expiresInSeconds = 60 * 60 * 24 * 365;
117 const expiresAtSeconds = Math.round(Date.now() / 1000) + expiresInSeconds;
118 return { url: uri, expiresAt: expiresAtSeconds };
119 }
120
121 const parsed = parseURI(uri);
122 if (!parsed) return undefined;
123
124 // HTTPS URLs don't need resolution - they're already accessible.
125 // Just return the URL as-is with a far-future expiration.
126 const expiresInSeconds = 60 * 60 * 24 * 365; // 1 year
127 const expiresAtSeconds = Math.round(Date.now() / 1000) + expiresInSeconds;
128
129 return {
130 url: parsed.url,
131 expiresAt: expiresAtSeconds,
132 };
133}
134
135////////////////////////////////////////////
136// ⚡️
137////////////////////////////////////////////
138
139ostiary((context) => {
140 // Setup RPC
141
142 rpc(context, {
143 artwork,
144 consult,
145 detach,
146 groupConsult,
147 list,
148 resolve,
149 });
150});