forked from
tokono.ma/diffuse
A music player that connects to your cloud/distributed storage.
1import * as TID from "@atcute/tid";
2import { ostiary, rpc } from "~/common/worker.js";
3import { groupKey } from "~/components/input/common.js";
4import {
5 buildURI,
6 enumerateAudioFiles,
7 getHandleFile,
8 groupTracksByTid,
9 groupUrisByTid,
10 isSupported,
11 loadHandles,
12 parseURI,
13 saveHandles,
14} from "./common.js";
15import { SCHEME } from "./constants.js";
16
17/**
18 * @import { InputActions as Actions, ConsultGrouping } from "~/components/input/types.d.ts";
19 * @import { Track } from "~/definitions/types.d.ts"
20 */
21
22////////////////////////////////////////////
23// ACTIONS
24////////////////////////////////////////////
25
26/**
27 * @type {Actions['artwork']}
28 */
29export async function artwork(_uri) {
30 return null;
31}
32
33/**
34 * @type {Actions['consult']}
35 */
36export async function consult(fileUriOrScheme) {
37 if (!isSupported()) {
38 return { supported: false, reason: "No browser support" };
39 }
40
41 if (!fileUriOrScheme.includes(":")) {
42 return { supported: true, consult: "undetermined" };
43 }
44
45 const parsed = parseURI(fileUriOrScheme);
46 if (!parsed) return { supported: false, reason: "Unknown handle" };
47
48 const handles = await loadHandles();
49 const handle = handles[parsed.tid];
50
51 if (!handle) return { supported: false, reason: "Unknown handle" };
52
53 const permission = await /** @type {any} */ (handle).queryPermission({
54 mode: "read",
55 });
56
57 return { supported: true, consult: permission === "granted" };
58}
59
60/**
61 * @type {Actions['detach']}
62 */
63export async function detach({ fileUriOrScheme, tracks }) {
64 if (!fileUriOrScheme.includes("://")) {
65 if (fileUriOrScheme === SCHEME) return [];
66 return tracks;
67 }
68
69 const parsed = parseURI(fileUriOrScheme);
70 if (!parsed) return tracks;
71
72 const { tid } = parsed;
73 const groups = groupTracksByTid(tracks);
74 delete groups[tid];
75
76 const handles = await loadHandles();
77 delete handles[tid];
78 await saveHandles(handles);
79
80 return Object.values(groups).map((g) => g.tracks).flat(1);
81}
82
83/**
84 * @type {Actions['groupConsult']}
85 */
86export async function groupConsult(uris) {
87 const groups = groupUrisByTid(uris);
88 const handles = await loadHandles();
89
90 const promises = Object.entries(groups).flatMap(async ([tid, { uris }]) => {
91 const handle = handles[tid];
92 if (!handle) return [];
93
94 const available =
95 (await /** @type {any} */ (handle).queryPermission({ mode: "read" })) ===
96 "granted";
97
98 /** @type {ConsultGrouping} */
99 const grouping = available ? { available, scheme: SCHEME, uris } : {
100 available: false,
101 reason: "Permission not granted",
102 scheme: SCHEME,
103 uris,
104 };
105
106 return [{ key: groupKey(SCHEME, tid), grouping }];
107 });
108
109 const results = (await Promise.all(promises)).flat(1);
110 return Object.fromEntries(results.map((e) => [e.key, e.grouping]));
111}
112
113/**
114 * @type {Actions['list']}
115 */
116export async function list(cachedTracks = []) {
117 const handles = await loadHandles();
118 const now = new Date().toISOString();
119
120 /** @type {Record<string, Track>} */
121 const cacheByUri = {};
122
123 cachedTracks.forEach((t) => {
124 cacheByUri[t.uri] = t;
125 });
126
127 const trackGroups = groupTracksByTid(cachedTracks);
128
129 const allTids = new Set(Object.keys(trackGroups));
130
131 const promises = [...allTids].map(async (tid) => {
132 const handle = handles[tid];
133 if (!handle) return trackGroups[tid]?.tracks ?? /** @type {Track[]} */ ([]);
134
135 const perm = await /** @type {any} */ (handle).queryPermission({
136 mode: "read",
137 });
138
139 if (perm !== "granted") {
140 const cached = trackGroups[tid]?.tracks[0];
141
142 /** @type {Track} */
143 const placeholder = {
144 $type: "sh.diffuse.output.track",
145 id: cached?.id ?? TID.now(),
146 createdAt: cached?.createdAt ?? now,
147 updatedAt: now,
148 kind: "placeholder",
149 uri: buildURI(tid),
150 };
151
152 return [placeholder];
153 }
154
155 if (handle.kind === "file") {
156 const uri = buildURI(tid);
157 const cached = cacheByUri[uri];
158
159 /** @type {Track} */
160 const track = {
161 $type: "sh.diffuse.output.track",
162 id: cached?.id ?? TID.now(),
163 createdAt: cached?.createdAt ?? now,
164 updatedAt: cached?.updatedAt ?? now,
165 stats: cached?.stats,
166 tags: cached?.tags,
167 uri,
168 };
169
170 return [track];
171 }
172
173 const paths = await enumerateAudioFiles(
174 /** @type {FileSystemDirectoryHandle} */ (handle),
175 );
176
177 if (!paths.length) {
178 /** @type {Track} */
179 const placeholder = {
180 $type: "sh.diffuse.output.track",
181 id: TID.now(),
182 createdAt: now,
183 updatedAt: now,
184 kind: "placeholder",
185 uri: buildURI(tid),
186 };
187
188 return [placeholder];
189 }
190
191 return paths.map((path) => {
192 const uri = buildURI(tid, path);
193 const cached = cacheByUri[uri];
194
195 /** @type {Track} */
196 const track = {
197 $type: "sh.diffuse.output.track",
198 id: cached?.id ?? TID.now(),
199 createdAt: cached?.createdAt ?? now,
200 updatedAt: cached?.updatedAt ?? now,
201 stats: cached?.stats,
202 tags: cached?.tags,
203 uri,
204 };
205
206 return track;
207 });
208 });
209
210 const tracks = (await Promise.all(promises)).flat(1);
211 return tracks;
212}
213
214/**
215 * @type {Actions['resolve']}
216 */
217export async function resolve({ uri }) {
218 const parsed = parseURI(uri);
219 if (!parsed) return undefined;
220
221 const handles = await loadHandles();
222 const handle = handles[parsed.tid];
223 const path = parsed.path.replace(/^\//, "");
224
225 if (!handle) return undefined;
226 if (handle.kind === "directory" && path === "") return undefined;
227
228 const fileHandle = await getHandleFile(handle, path);
229 const file = await fileHandle.getFile();
230
231 const url = URL.createObjectURL(file);
232 return { url, expiresAt: Infinity };
233}
234
235////////////////////////////////////////////
236// ⚡️
237////////////////////////////////////////////
238
239ostiary((context) => {
240 rpc(context, {
241 artwork,
242 consult,
243 detach,
244 groupConsult,
245 list,
246 resolve,
247 });
248});