A music player that connects to your cloud/distributed storage.
1import * as IDB from "idb-keyval";
2import * as URI from "fast-uri";
3
4import { isAudioFile } from "~/components/input/common.js";
5import { safeDecodeURIComponent } from "~/common/utils.js";
6import { IDB_HANDLES, SCHEME } from "./constants.js";
7
8/**
9 * @import { Track } from "~/definitions/types.d.ts"
10 */
11
12////////////////////////////////////////////
13// 🛠️
14////////////////////////////////////////////
15
16/**
17 * @param {string} tid
18 * @param {string} [path]
19 */
20export function buildURI(tid, path = "/") {
21 return URI.serialize({
22 scheme: SCHEME,
23 host: tid,
24 path,
25 });
26}
27
28/**
29 * @param {FileSystemDirectoryHandle} dirHandle
30 * @param {string} [basePath]
31 * @returns {Promise<string[]>}
32 */
33export async function enumerateAudioFiles(dirHandle, basePath = "/") {
34 const results = [];
35
36 for await (const [name, handle] of /** @type {any} */ (dirHandle).entries()) {
37 const entryPath = basePath + name;
38
39 if (handle.kind === "directory") {
40 const sub = await enumerateAudioFiles(
41 /** @type {FileSystemDirectoryHandle} */ (handle),
42 entryPath + "/",
43 );
44 results.push(...sub);
45 } else if (isAudioFile(name)) {
46 results.push(entryPath);
47 }
48 }
49
50 return results;
51}
52
53/**
54 * @param {FileSystemHandle} handle
55 * @param {string} path
56 * @returns {Promise<FileSystemFileHandle>}
57 */
58export async function getHandleFile(handle, path) {
59 if (handle.kind === "file") {
60 return /** @type {FileSystemFileHandle} */ (handle);
61 }
62
63 const parts = path.replace(/^\//, "").split("/").filter(Boolean);
64 let current = /** @type {FileSystemDirectoryHandle} */ (handle);
65
66 for (const part of parts.slice(0, -1)) {
67 current = await current.getDirectoryHandle(part);
68 }
69
70 return current.getFileHandle(/** @type {string} */ (parts.at(-1)));
71}
72
73/**
74 * @param {Track[]} tracks
75 * @returns {Record<string, { tid: string; tracks: Track[] }>}
76 */
77export function groupTracksByTid(tracks) {
78 /** @type {Record<string, { tid: string; tracks: Track[] }>} */
79 const acc = {};
80
81 tracks.forEach((track) => {
82 const parsed = parseURI(track.uri);
83 if (!parsed) return;
84
85 const { tid } = parsed;
86 if (acc[tid]) {
87 acc[tid].tracks.push(track);
88 } else {
89 acc[tid] = { tid, tracks: [track] };
90 }
91 });
92
93 return acc;
94}
95
96/**
97 * @param {string[]} uris
98 * @returns {Record<string, { tid: string; uris: string[] }>}
99 */
100export function groupUrisByTid(uris) {
101 /** @type {Record<string, { tid: string; uris: string[] }>} */
102 const acc = {};
103
104 uris.forEach((uri) => {
105 const parsed = parseURI(uri);
106 if (!parsed) return;
107
108 const { tid } = parsed;
109 if (acc[tid]) {
110 acc[tid].uris.push(uri);
111 } else {
112 acc[tid] = { tid, uris: [uri] };
113 }
114 });
115
116 return acc;
117}
118
119export function isSupported() {
120 return typeof (/** @type {any} */ (globalThis).showDirectoryPicker) !==
121 "undefined";
122}
123
124/**
125 * @returns {Promise<Record<string, FileSystemHandle>>}
126 */
127export async function loadHandles() {
128 const i = await IDB.get(IDB_HANDLES);
129 return i ?? {};
130}
131
132/**
133 * @param {string} uriString
134 * @returns {{ tid: string; path: string } | undefined}
135 */
136export function parseURI(uriString) {
137 try {
138 const url = new URL(uriString);
139 if (url.protocol !== `${SCHEME}:`) return undefined;
140 if (!url.host) return undefined;
141
142 return {
143 tid: url.host,
144 path: safeDecodeURIComponent(url.pathname),
145 };
146 } catch {
147 return undefined;
148 }
149}
150
151/**
152 * @param {Record<string, FileSystemHandle>} handles
153 */
154export async function saveHandles(handles) {
155 await IDB.set(IDB_HANDLES, handles);
156}
157
158/**
159 * @param {Track[]} tracks
160 * @returns {Record<string, string>}
161 */
162export function tidsFromTracks(tracks) {
163 /** @type {Record<string, string>} */
164 const acc = {};
165
166 tracks.forEach((track) => {
167 const parsed = parseURI(track.uri);
168 if (!parsed) return;
169 acc[parsed.tid] = parsed.tid;
170 });
171
172 return acc;
173}